Skip to content

Commit f3c9737

Browse files
committed
add win32 copy richtext support
1 parent 648a539 commit f3c9737

File tree

4 files changed

+114
-4
lines changed

4 files changed

+114
-4
lines changed

AgentCrew/modules/gui/utils/__init__.py

Whitespace-only changes.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import sys
2+
3+
4+
def copy_html_to_clipboard(html_content, text_fallback=None):
5+
"""
6+
Copy HTML content to clipboard as formatted text with fallback to plain text
7+
8+
Args:
9+
html_content (str): HTML content to copy
10+
text_fallback (str): Plain text fallback (optional)
11+
"""
12+
if sys.platform != "win32":
13+
raise NotImplementedError("This function is only implemented for Windows.")
14+
15+
import win32clipboard
16+
import win32con
17+
18+
# If no text fallback provided, strip HTML tags
19+
if text_fallback is None:
20+
import re
21+
22+
text_fallback = re.sub("<[^<]+?>", "", html_content)
23+
24+
# Prepare HTML clipboard format
25+
html_clipboard_data = prepare_html_clipboard_data(html_content)
26+
27+
win32clipboard.OpenClipboard()
28+
try:
29+
win32clipboard.EmptyClipboard()
30+
31+
# Set HTML format (for rich text paste)
32+
html_format = win32clipboard.RegisterClipboardFormat("HTML Format")
33+
win32clipboard.SetClipboardData(
34+
html_format, html_clipboard_data.encode("utf-8")
35+
)
36+
37+
# Set plain text format (for fallback)
38+
win32clipboard.SetClipboardData(win32con.CF_TEXT, text_fallback.encode("utf-8"))
39+
40+
# Set Unicode text format
41+
win32clipboard.SetClipboardData(win32con.CF_UNICODETEXT, text_fallback)
42+
43+
finally:
44+
win32clipboard.CloseClipboard()
45+
46+
47+
def prepare_html_clipboard_data(html_content):
48+
"""
49+
Prepare HTML content for Windows clipboard format
50+
"""
51+
# HTML clipboard format requires specific headers
52+
html_prefix = """Version:0.9
53+
StartHTML:000000000
54+
EndHTML:000000000
55+
StartFragment:000000000
56+
EndFragment:000000000
57+
<!DOCTYPE html>
58+
<html>
59+
<head>
60+
<meta charset="utf-8">
61+
</head>
62+
<body>
63+
<!--StartFragment-->"""
64+
65+
html_suffix = """<!--EndFragment-->
66+
</body>
67+
</html>"""
68+
69+
# Calculate offsets
70+
start_html = len(html_prefix.split("\n")[0]) + 1 # After version line
71+
start_fragment = len(html_prefix)
72+
end_fragment = start_fragment + len(html_content)
73+
end_html = end_fragment + len(html_suffix)
74+
75+
# Format the header with correct offsets
76+
header = f"""Version:0.9
77+
StartHTML:{start_html:09d}
78+
EndHTML:{end_html:09d}
79+
StartFragment:{start_fragment:09d}
80+
EndFragment:{end_fragment:09d}"""
81+
82+
full_html = f"""{header}
83+
<!DOCTYPE html>
84+
<html>
85+
<head>
86+
<meta charset="utf-8">
87+
</head>
88+
<body>
89+
<!--StartFragment-->{html_content}<!--EndFragment-->
90+
</body>
91+
</html>"""
92+
93+
return full_html

AgentCrew/modules/gui/widgets/message_bubble.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Optional
22
import markdown
33
import os
4+
import sys
45
import mimetypes
56

67
from PySide6.QtWidgets import (
@@ -353,14 +354,22 @@ def show_context_menu(self, position):
353354
copy_markdown_action.triggered.connect(self.copy_as_markdown)
354355

355356
copy_all_html_action = menu.addAction("Copy all as Html")
356-
copy_all_html_action.triggered.connect(
357-
lambda: pyperclip.copy(self.message_label.text())
358-
)
357+
copy_all_html_action.triggered.connect(self._copy_as_html)
359358

360359
menu.setStyleSheet(self.style_provider.get_context_menu_style())
361360

362361
menu.exec_(self.message_label.mapToGlobal(position))
363362

363+
def _copy_as_html(self):
364+
if sys.platform == "win32":
365+
from AgentCrew.modules.gui.utils.wins_clipboard import (
366+
copy_html_to_clipboard,
367+
)
368+
369+
copy_html_to_clipboard(self.message_label.text(), self.raw_text)
370+
else:
371+
pyperclip.copy(self.message_label.text())
372+
364373
def _select_all_text(self):
365374
"""Select all text in the message label."""
366375
try:
@@ -409,7 +418,14 @@ def _copy_selected_html(self):
409418
fragment = cursor.selection()
410419
selected_html = fragment.toHtml()
411420

412-
pyperclip.copy(selected_html)
421+
if sys.platform == "win32":
422+
from AgentCrew.modules.gui.utils.wins_clipboard import (
423+
copy_html_to_clipboard,
424+
)
425+
426+
copy_html_to_clipboard(selected_html, selected_plain_text)
427+
else:
428+
pyperclip.copy(selected_html)
413429
except Exception:
414430
try:
415431
selected_text = self.message_label.selectedText()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies = [
4242
"nest-asyncio>=1.6.0",
4343
"voyageai>=0.3.2",
4444
"numpy>=1.24.4,<2; python_version < '3.13' and sys_platform == 'darwin'",
45+
"pywin32; sys_platform == 'win32'",
4546
"rapidocr-onnxruntime>=1.4.4",
4647
"a2a-sdk>=0.2.9",
4748
"qtawesome>=1.4.0",

0 commit comments

Comments
 (0)