py2gui 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py2gui/game_example.py +659 -0
- py2gui/py2gui.py +875 -0
- py2gui/test.py +110 -0
- py2gui-0.1.0.dist-info/METADATA +9 -0
- py2gui-0.1.0.dist-info/RECORD +8 -0
- py2gui-0.1.0.dist-info/WHEEL +5 -0
- py2gui-0.1.0.dist-info/licenses/LICENSE +21 -0
- py2gui-0.1.0.dist-info/top_level.txt +1 -0
py2gui/py2gui.py
ADDED
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import scrolledtext, simpledialog, Menu, Frame, Entry, Button, StringVar, font
|
|
3
|
+
import queue
|
|
4
|
+
import threading
|
|
5
|
+
import traceback
|
|
6
|
+
import re
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from typing import Callable, Any, Optional, List, Tuple, Dict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Py2GUI:
|
|
13
|
+
def __init__(self, title: str = "Py2GUI", width: int = 80, height: int = 20, config_file: str = "config.json"):
|
|
14
|
+
self.root = tk.Tk()
|
|
15
|
+
self.root.title(title)
|
|
16
|
+
self.root.resizable(True, True)
|
|
17
|
+
self.width = width
|
|
18
|
+
self.height = height
|
|
19
|
+
self.running = True
|
|
20
|
+
self.config_file = config_file
|
|
21
|
+
|
|
22
|
+
# Load configuration
|
|
23
|
+
self.config = self._load_config()
|
|
24
|
+
|
|
25
|
+
# Extended ANSI color configuration with full 256 colors
|
|
26
|
+
self.ansi_colors = {
|
|
27
|
+
# Basic colors
|
|
28
|
+
'30': '#000000', # Black
|
|
29
|
+
'31': '#ff0000', # Red
|
|
30
|
+
'32': '#00ff00', # Green
|
|
31
|
+
'33': '#ffff00', # Yellow
|
|
32
|
+
'34': '#0000ff', # Blue
|
|
33
|
+
'35': '#ff00ff', # Magenta
|
|
34
|
+
'36': '#00ffff', # Cyan
|
|
35
|
+
'37': '#ffffff', # White
|
|
36
|
+
|
|
37
|
+
# Bright colors
|
|
38
|
+
'90': '#808080', # Gray
|
|
39
|
+
'91': '#ff8080', # Bright red
|
|
40
|
+
'92': '#80ff80', # Bright green
|
|
41
|
+
'93': '#ffff80', # Bright yellow
|
|
42
|
+
'94': '#8080ff', # Bright blue
|
|
43
|
+
'95': '#ff80ff', # Bright magenta
|
|
44
|
+
'96': '#80ffff', # Bright cyan
|
|
45
|
+
'97': '#ffffff', # Bright white
|
|
46
|
+
|
|
47
|
+
# Background colors
|
|
48
|
+
'40': '#1a1a1a', # Black background
|
|
49
|
+
'41': '#ff0000', # Red background
|
|
50
|
+
'42': '#00ff00', # Green background
|
|
51
|
+
'43': '#ffff00', # Yellow background
|
|
52
|
+
'44': '#0000ff', # Blue background
|
|
53
|
+
'45': '#ff00ff', # Magenta background
|
|
54
|
+
'46': '#00ffff', # Cyan background
|
|
55
|
+
'47': '#ffffff', # White background
|
|
56
|
+
|
|
57
|
+
# Extended 256 colors (common ones)
|
|
58
|
+
'38;5;0': '#000000', # Black
|
|
59
|
+
'38;5;1': '#800000', # Dark red
|
|
60
|
+
'38;5;2': '#008000', # Dark green
|
|
61
|
+
'38;5;3': '#808000', # Dark yellow
|
|
62
|
+
'38;5;4': '#000080', # Dark blue
|
|
63
|
+
'38;5;5': '#800080', # Dark magenta
|
|
64
|
+
'38;5;6': '#008080', # Dark cyan
|
|
65
|
+
'38;5;7': '#c0c0c0', # Light gray
|
|
66
|
+
'38;5;8': '#808080', # Dark gray
|
|
67
|
+
'38;5;9': '#ff0000', # Red
|
|
68
|
+
'38;5;10': '#00ff00', # Green
|
|
69
|
+
'38;5;11': '#ffff00', # Yellow
|
|
70
|
+
'38;5;12': '#0000ff', # Blue
|
|
71
|
+
'38;5;13': '#ff00ff', # Magenta
|
|
72
|
+
'38;5;14': '#00ffff', # Cyan
|
|
73
|
+
'38;5;15': '#ffffff', # White
|
|
74
|
+
|
|
75
|
+
# Extended background colors
|
|
76
|
+
'48;5;0': '#000000', # Black background
|
|
77
|
+
'48;5;1': '#800000', # Dark red background
|
|
78
|
+
'48;5;2': '#008000', # Dark green background
|
|
79
|
+
'48;5;3': '#808000', # Dark yellow background
|
|
80
|
+
'48;5;4': '#000080', # Dark blue background
|
|
81
|
+
'48;5;5': '#800080', # Dark magenta background
|
|
82
|
+
'48;5;6': '#008080', # Dark cyan background
|
|
83
|
+
'48;5;7': '#c0c0c0', # Light gray background
|
|
84
|
+
|
|
85
|
+
# True color support (RGB)
|
|
86
|
+
'38;2;0;0;0': '#000000', # Black
|
|
87
|
+
'38;2;255;0;0': '#ff0000', # Red
|
|
88
|
+
'38;2;0;255;0': '#00ff00', # Green
|
|
89
|
+
'38;2;0;0;255': '#0000ff', # Blue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Color name to hex mapping for display_colored method
|
|
93
|
+
self.color_name_to_hex = {
|
|
94
|
+
'black': '#000000',
|
|
95
|
+
'red': '#ff0000',
|
|
96
|
+
'green': '#00ff00',
|
|
97
|
+
'yellow': '#ffff00',
|
|
98
|
+
'blue': '#0000ff',
|
|
99
|
+
'magenta': '#ff00ff',
|
|
100
|
+
'cyan': '#00ffff',
|
|
101
|
+
'white': '#ffffff',
|
|
102
|
+
'gray': '#808080',
|
|
103
|
+
'bright red': '#ff8080',
|
|
104
|
+
'bright green': '#80ff80',
|
|
105
|
+
'bright yellow': '#ffff80',
|
|
106
|
+
'bright blue': '#8080ff',
|
|
107
|
+
'bright magenta': '#ff80ff',
|
|
108
|
+
'bright cyan': '#80ffff',
|
|
109
|
+
'bright white': '#ffffff',
|
|
110
|
+
'orange': '#ff8000',
|
|
111
|
+
'purple': '#8000ff',
|
|
112
|
+
'pink': '#ff80ff',
|
|
113
|
+
'brown': '#804000',
|
|
114
|
+
'dark gray': '#404040',
|
|
115
|
+
'light gray': '#c0c0c0',
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Available fonts
|
|
119
|
+
self.available_fonts = font.families()
|
|
120
|
+
|
|
121
|
+
# ANSI style configuration
|
|
122
|
+
self.ansi_styles = {
|
|
123
|
+
'0': {'font': ('Courier', 10, 'normal'), 'fg': 'white', 'bg': 'black'}, # Reset
|
|
124
|
+
'1': {'font': ('Courier', 10, 'bold')}, # Bold
|
|
125
|
+
'3': {'font': ('Courier', 10, 'italic')}, # Italic
|
|
126
|
+
'4': {'underline': True}, # Underline
|
|
127
|
+
'7': {'fg': 'black', 'bg': 'white'}, # Reverse video
|
|
128
|
+
'9': {'strikethrough': True}, # Strikethrough
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Current text style state
|
|
132
|
+
self.current_style = {
|
|
133
|
+
'font': ('Courier', 10, 'normal'),
|
|
134
|
+
'foreground': 'white',
|
|
135
|
+
'background': 'black',
|
|
136
|
+
'underline': False,
|
|
137
|
+
'strikethrough': False
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Defined text tags
|
|
141
|
+
self.tag_names = set()
|
|
142
|
+
|
|
143
|
+
# Handle window closure
|
|
144
|
+
self.root.protocol("WM_DELETE_WINDOW", self.exit)
|
|
145
|
+
|
|
146
|
+
# Main frame to hold everything
|
|
147
|
+
self.main_frame = Frame(self.root)
|
|
148
|
+
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
149
|
+
|
|
150
|
+
# Output area
|
|
151
|
+
self.text_area = scrolledtext.ScrolledText(
|
|
152
|
+
self.main_frame,
|
|
153
|
+
wrap=tk.WORD,
|
|
154
|
+
width=width,
|
|
155
|
+
height=height,
|
|
156
|
+
font=("Courier", 10),
|
|
157
|
+
bg="black",
|
|
158
|
+
fg="white",
|
|
159
|
+
insertbackground="white"
|
|
160
|
+
)
|
|
161
|
+
self.text_area.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
|
|
162
|
+
self.text_area.config(state=tk.DISABLED)
|
|
163
|
+
|
|
164
|
+
# Configure default tag
|
|
165
|
+
self.text_area.tag_configure("default",
|
|
166
|
+
font=("Courier", 10, "normal"),
|
|
167
|
+
foreground="white",
|
|
168
|
+
background="black"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Configure color tags with hex color codes (excluding disabled colors)
|
|
172
|
+
for code, color_hex in self.ansi_colors.items():
|
|
173
|
+
# Skip disabled colors
|
|
174
|
+
if 'disabled_colors' in self.config and code in self.config['disabled_colors']:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
tag_name = f"ansi_{code}"
|
|
178
|
+
if (code.startswith('3') and ';' not in code) or code.startswith('38'):
|
|
179
|
+
# Foreground colors
|
|
180
|
+
self.text_area.tag_configure(tag_name, foreground=color_hex)
|
|
181
|
+
elif code.startswith('4') or code.startswith('48'):
|
|
182
|
+
# Background colors
|
|
183
|
+
self.text_area.tag_configure(tag_name, background=color_hex)
|
|
184
|
+
|
|
185
|
+
# Configure style tags
|
|
186
|
+
self.text_area.tag_configure("bold", font=("Courier", 10, "bold"))
|
|
187
|
+
self.text_area.tag_configure("italic", font=("Courier", 10, "italic"))
|
|
188
|
+
self.text_area.tag_configure("underline", underline=True)
|
|
189
|
+
self.text_area.tag_configure("strikethrough", overstrike=True)
|
|
190
|
+
self.text_area.tag_configure("reverse", foreground="black", background="white")
|
|
191
|
+
|
|
192
|
+
# Terminal-style input area frame
|
|
193
|
+
self.input_frame = Frame(self.main_frame)
|
|
194
|
+
self.input_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
195
|
+
|
|
196
|
+
# Input label
|
|
197
|
+
self.input_label = tk.Label(self.input_frame, text=">> ", font=("Courier", 10), fg="white", bg="black")
|
|
198
|
+
self.input_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
199
|
+
|
|
200
|
+
# Terminal-style input entry
|
|
201
|
+
self.input_var = StringVar()
|
|
202
|
+
self.input_entry = Entry(
|
|
203
|
+
self.input_frame,
|
|
204
|
+
textvariable=self.input_var,
|
|
205
|
+
font=("Courier", 10),
|
|
206
|
+
bg="black",
|
|
207
|
+
fg="white",
|
|
208
|
+
insertbackground="white"
|
|
209
|
+
)
|
|
210
|
+
self.input_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
|
|
211
|
+
|
|
212
|
+
# Send button
|
|
213
|
+
self.send_button = Button(
|
|
214
|
+
self.input_frame,
|
|
215
|
+
text="Send",
|
|
216
|
+
command=self._on_send_input,
|
|
217
|
+
font=("Courier", 9)
|
|
218
|
+
)
|
|
219
|
+
self.send_button.pack(side=tk.LEFT)
|
|
220
|
+
|
|
221
|
+
# Input queue for user_write
|
|
222
|
+
self.input_queue = queue.Queue()
|
|
223
|
+
|
|
224
|
+
# Input queue for user_type_in
|
|
225
|
+
self.type_in_queue = queue.Queue()
|
|
226
|
+
|
|
227
|
+
# Bind Enter key to send input
|
|
228
|
+
self.input_entry.bind('<Return>', self._on_enter_pressed)
|
|
229
|
+
|
|
230
|
+
# Menus
|
|
231
|
+
self._setup_menus()
|
|
232
|
+
|
|
233
|
+
def _load_config(self) -> Dict:
|
|
234
|
+
"""Load configuration from JSON file"""
|
|
235
|
+
default_config = {
|
|
236
|
+
"disabled_menus": [],
|
|
237
|
+
"disabled_views": [],
|
|
238
|
+
"disabled_colors": [],
|
|
239
|
+
"show_clear_button": True,
|
|
240
|
+
"show_demo_button": True
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
if os.path.exists(self.config_file):
|
|
245
|
+
with open(self.config_file, 'r') as f:
|
|
246
|
+
config = json.load(f)
|
|
247
|
+
# Merge with default config
|
|
248
|
+
for key, value in default_config.items():
|
|
249
|
+
if key not in config:
|
|
250
|
+
config[key] = value
|
|
251
|
+
return config
|
|
252
|
+
except Exception as e:
|
|
253
|
+
print(f"Error loading config file: {e}")
|
|
254
|
+
|
|
255
|
+
return default_config
|
|
256
|
+
|
|
257
|
+
def _setup_menus(self) -> None:
|
|
258
|
+
menubar = Menu(self.root)
|
|
259
|
+
self.root.config(menu=menubar)
|
|
260
|
+
|
|
261
|
+
# File menu
|
|
262
|
+
if 'disabled_menus' not in self.config or 'File' not in self.config['disabled_menus']:
|
|
263
|
+
file_menu = Menu(menubar, tearoff=0)
|
|
264
|
+
menubar.add_cascade(label="File", menu=file_menu)
|
|
265
|
+
file_menu.add_command(label="Exit", command=self.exit)
|
|
266
|
+
|
|
267
|
+
# Edit menu
|
|
268
|
+
if 'disabled_menus' not in self.config or 'Edit' not in self.config['disabled_menus']:
|
|
269
|
+
edit_menu = Menu(menubar, tearoff=0)
|
|
270
|
+
menubar.add_cascade(label="Edit", menu=edit_menu)
|
|
271
|
+
edit_menu.add_command(label="Copy", command=self.copy_text)
|
|
272
|
+
edit_menu.add_command(label="Select All", command=self.select_all)
|
|
273
|
+
|
|
274
|
+
# View menu
|
|
275
|
+
if 'disabled_menus' not in self.config or 'View' not in self.config['disabled_menus']:
|
|
276
|
+
view_menu = Menu(menubar, tearoff=0)
|
|
277
|
+
menubar.add_cascade(label="View", menu=view_menu)
|
|
278
|
+
|
|
279
|
+
# Add view menu items based on configuration
|
|
280
|
+
if 'disabled_views' not in self.config or 'Focus Input' not in self.config['disabled_views']:
|
|
281
|
+
view_menu.add_command(label="Focus Input", command=self.focus_input)
|
|
282
|
+
|
|
283
|
+
if 'disabled_views' not in self.config or 'Clear Output' not in self.config['disabled_views']:
|
|
284
|
+
view_menu.add_command(label="Clear Output", command=self.clear)
|
|
285
|
+
|
|
286
|
+
if 'disabled_views' not in self.config or 'Demo ANSI Colors' not in self.config['disabled_views']:
|
|
287
|
+
view_menu.add_command(label="Demo ANSI Colors", command=self._demo_colors)
|
|
288
|
+
|
|
289
|
+
# Colors menu
|
|
290
|
+
if 'disabled_menus' not in self.config or 'Colors' not in self.config['disabled_menus']:
|
|
291
|
+
colors_menu = Menu(menubar, tearoff=0)
|
|
292
|
+
menubar.add_cascade(label="Colors", menu=colors_menu)
|
|
293
|
+
colors_menu.add_command(label="Default Theme", command=lambda: self.set_theme("default"))
|
|
294
|
+
colors_menu.add_command(label="Dark Theme", command=lambda: self.set_theme("dark"))
|
|
295
|
+
colors_menu.add_command(label="Light Theme", command=lambda: self.set_theme("light"))
|
|
296
|
+
colors_menu.add_command(label="Green on Black", command=lambda: self.set_theme("matrix"))
|
|
297
|
+
|
|
298
|
+
def _parse_ansi_codes(self, text: str) -> List[Tuple[str, List[str]]]:
|
|
299
|
+
"""Parse ANSI escape sequences in text"""
|
|
300
|
+
# ANSI escape sequence regex
|
|
301
|
+
ansi_pattern = re.compile(r'(\033\[[\d;]*m)')
|
|
302
|
+
|
|
303
|
+
parts = []
|
|
304
|
+
last_end = 0
|
|
305
|
+
current_codes = []
|
|
306
|
+
|
|
307
|
+
for match in ansi_pattern.finditer(text):
|
|
308
|
+
# Add normal text
|
|
309
|
+
if match.start() > last_end:
|
|
310
|
+
normal_text = text[last_end:match.start()]
|
|
311
|
+
if normal_text:
|
|
312
|
+
parts.append((normal_text, current_codes.copy()))
|
|
313
|
+
|
|
314
|
+
# Parse ANSI code
|
|
315
|
+
ansi_code = match.group(0)
|
|
316
|
+
code_str = ansi_code[2:-1] # Remove \033[ and m
|
|
317
|
+
|
|
318
|
+
if code_str == '':
|
|
319
|
+
# Reset all attributes
|
|
320
|
+
current_codes = ['0']
|
|
321
|
+
else:
|
|
322
|
+
codes = code_str.split(';')
|
|
323
|
+
for code in codes:
|
|
324
|
+
if code == '0':
|
|
325
|
+
# Reset
|
|
326
|
+
current_codes = ['0']
|
|
327
|
+
elif code in ['1', '3', '4', '7', '9']:
|
|
328
|
+
# Style codes
|
|
329
|
+
if code in current_codes:
|
|
330
|
+
# Remove duplicate style codes
|
|
331
|
+
pass
|
|
332
|
+
else:
|
|
333
|
+
if code == '1' and '22' in current_codes:
|
|
334
|
+
current_codes.remove('22')
|
|
335
|
+
current_codes.append(code)
|
|
336
|
+
elif code in ['22', '23', '24', '27', '29']:
|
|
337
|
+
# Reset specific styles
|
|
338
|
+
reset_map = {'22': '1', '23': '3', '24': '4', '27': '7', '29': '9'}
|
|
339
|
+
if reset_map[code] in current_codes:
|
|
340
|
+
current_codes.remove(reset_map[code])
|
|
341
|
+
elif code in self.ansi_colors or code.startswith('38;') or code.startswith('48;'):
|
|
342
|
+
# Color codes
|
|
343
|
+
# Skip disabled colors
|
|
344
|
+
if 'disabled_colors' in self.config and code in self.config['disabled_colors']:
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
# Remove same type of color codes
|
|
348
|
+
if code in ['30', '31', '32', '33', '34', '35', '36', '37',
|
|
349
|
+
'90', '91', '92', '93', '94', '95', '96', '97']:
|
|
350
|
+
# Remove other basic foreground colors
|
|
351
|
+
for c in list(current_codes):
|
|
352
|
+
if c in ['30', '31', '32', '33', '34', '35', '36', '37',
|
|
353
|
+
'90', '91', '92', '93', '94', '95', '96', '97']:
|
|
354
|
+
current_codes.remove(c)
|
|
355
|
+
elif code in ['40', '41', '42', '43', '44', '45', '46', '47']:
|
|
356
|
+
# Remove other basic background colors
|
|
357
|
+
for c in list(current_codes):
|
|
358
|
+
if c in ['40', '41', '42', '43', '44', '45', '46', '47']:
|
|
359
|
+
current_codes.remove(c)
|
|
360
|
+
elif code.startswith('38;'):
|
|
361
|
+
# Remove other foreground colors
|
|
362
|
+
for c in list(current_codes):
|
|
363
|
+
if c.startswith('38;'):
|
|
364
|
+
current_codes.remove(c)
|
|
365
|
+
elif code.startswith('48;'):
|
|
366
|
+
# Remove other background colors
|
|
367
|
+
for c in list(current_codes):
|
|
368
|
+
if c.startswith('48;'):
|
|
369
|
+
current_codes.remove(c)
|
|
370
|
+
current_codes.append(code)
|
|
371
|
+
|
|
372
|
+
# Add the last part of text
|
|
373
|
+
if last_end < len(text):
|
|
374
|
+
remaining_text = text[last_end:]
|
|
375
|
+
if remaining_text:
|
|
376
|
+
parts.append((remaining_text, current_codes.copy()))
|
|
377
|
+
|
|
378
|
+
return parts
|
|
379
|
+
|
|
380
|
+
def _get_tags_for_codes(self, codes: List[str]) -> List[str]:
|
|
381
|
+
"""Get corresponding tag list based on ANSI codes"""
|
|
382
|
+
tags = []
|
|
383
|
+
|
|
384
|
+
for code in codes:
|
|
385
|
+
if code == '0':
|
|
386
|
+
# Reset all styles
|
|
387
|
+
tags = ['default']
|
|
388
|
+
break
|
|
389
|
+
elif code in ['1', '3', '4', '7', '9']:
|
|
390
|
+
# Style tags
|
|
391
|
+
style_map = {'1': 'bold', '3': 'italic', '4': 'underline',
|
|
392
|
+
'7': 'reverse', '9': 'strikethrough'}
|
|
393
|
+
if code in style_map:
|
|
394
|
+
tags.append(style_map[code])
|
|
395
|
+
elif code in self.ansi_colors or code.startswith('38;') or code.startswith('48;'):
|
|
396
|
+
# Skip disabled colors
|
|
397
|
+
if 'disabled_colors' in self.config and code in self.config['disabled_colors']:
|
|
398
|
+
continue
|
|
399
|
+
# Color tags
|
|
400
|
+
tags.append(f"ansi_{code}")
|
|
401
|
+
|
|
402
|
+
return tags
|
|
403
|
+
|
|
404
|
+
def _process_escape_sequences(self, text: str) -> str:
|
|
405
|
+
"""Process escape sequences like \n, \t, etc."""
|
|
406
|
+
# Replace common escape sequences
|
|
407
|
+
replacements = {
|
|
408
|
+
'\\n': '\n',
|
|
409
|
+
'\\t': '\t',
|
|
410
|
+
'\\r': '\r',
|
|
411
|
+
'\\b': '\b',
|
|
412
|
+
'\\f': '\f',
|
|
413
|
+
'\\v': '\v',
|
|
414
|
+
'\\\\': '\\',
|
|
415
|
+
'\\"': '"',
|
|
416
|
+
"\\'": "'"
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for esc_seq, char in replacements.items():
|
|
420
|
+
text = text.replace(esc_seq, char)
|
|
421
|
+
|
|
422
|
+
return text
|
|
423
|
+
|
|
424
|
+
def display_paragraph(self, text: str, parse_ansi: bool = True, font_family: Optional[str] = None,
|
|
425
|
+
font_size: Optional[int] = None, font_style: Optional[str] = None) -> None:
|
|
426
|
+
"""Thread-safe display of paragraphs with escape sequence processing and no auto newline"""
|
|
427
|
+
def _update():
|
|
428
|
+
self.text_area.config(state=tk.NORMAL)
|
|
429
|
+
|
|
430
|
+
# Process escape sequences
|
|
431
|
+
text_processed = self._process_escape_sequences(text)
|
|
432
|
+
|
|
433
|
+
# Check for custom font settings
|
|
434
|
+
font_tags = []
|
|
435
|
+
if font_family or font_size or font_style:
|
|
436
|
+
# Create a unique tag for this font combination
|
|
437
|
+
font_family_val = font_family or "Courier"
|
|
438
|
+
font_size_val = font_size or 10
|
|
439
|
+
font_style_val = font_style or "normal"
|
|
440
|
+
font_key = f"font_{font_family_val}_{font_size_val}_{font_style_val}"
|
|
441
|
+
|
|
442
|
+
if font_key not in self.tag_names:
|
|
443
|
+
self.text_area.tag_configure(font_key, font=(font_family_val, font_size_val, font_style_val))
|
|
444
|
+
self.tag_names.add(font_key)
|
|
445
|
+
font_tags.append(font_key)
|
|
446
|
+
|
|
447
|
+
if parse_ansi and ('\033[' in text_processed or '\x1b[' in text_processed):
|
|
448
|
+
# Parse and apply ANSI colors
|
|
449
|
+
parts = self._parse_ansi_codes(text_processed)
|
|
450
|
+
|
|
451
|
+
for part_text, codes in parts:
|
|
452
|
+
tags = self._get_tags_for_codes(codes)
|
|
453
|
+
if not tags:
|
|
454
|
+
tags = ['default']
|
|
455
|
+
|
|
456
|
+
# Add font tags if specified
|
|
457
|
+
if font_tags:
|
|
458
|
+
tags = font_tags + tags
|
|
459
|
+
|
|
460
|
+
# Insert text and apply tags
|
|
461
|
+
self.text_area.insert(tk.END, part_text, tuple(tags))
|
|
462
|
+
else:
|
|
463
|
+
# Plain text
|
|
464
|
+
tags = ['default']
|
|
465
|
+
if font_tags:
|
|
466
|
+
tags = font_tags
|
|
467
|
+
self.text_area.insert(tk.END, text_processed, tuple(tags))
|
|
468
|
+
|
|
469
|
+
self.text_area.config(state=tk.DISABLED)
|
|
470
|
+
self.text_area.see(tk.END)
|
|
471
|
+
|
|
472
|
+
self.root.after(0, _update)
|
|
473
|
+
|
|
474
|
+
def display(self, text: str, parse_ansi: bool = True, font_family: Optional[str] = None,
|
|
475
|
+
font_size: Optional[int] = None, font_style: Optional[str] = None) -> None:
|
|
476
|
+
"""Thread-safe display with ANSI color support and font customization"""
|
|
477
|
+
def _update():
|
|
478
|
+
self.text_area.config(state=tk.NORMAL)
|
|
479
|
+
|
|
480
|
+
# Check for custom font settings
|
|
481
|
+
font_tags = []
|
|
482
|
+
if font_family or font_size or font_style:
|
|
483
|
+
# Create a unique tag for this font combination
|
|
484
|
+
font_family_val = font_family or "Courier"
|
|
485
|
+
font_size_val = font_size or 10
|
|
486
|
+
font_style_val = font_style or "normal"
|
|
487
|
+
font_key = f"font_{font_family_val}_{font_size_val}_{font_style_val}"
|
|
488
|
+
|
|
489
|
+
if font_key not in self.tag_names:
|
|
490
|
+
self.text_area.tag_configure(font_key, font=(font_family_val, font_size_val, font_style_val))
|
|
491
|
+
self.tag_names.add(font_key)
|
|
492
|
+
font_tags.append(font_key)
|
|
493
|
+
|
|
494
|
+
if parse_ansi and ('\033[' in text or '\x1b[' in text):
|
|
495
|
+
# Parse and apply ANSI colors
|
|
496
|
+
parts = self._parse_ansi_codes(text)
|
|
497
|
+
|
|
498
|
+
for part_text, codes in parts:
|
|
499
|
+
tags = self._get_tags_for_codes(codes)
|
|
500
|
+
if not tags:
|
|
501
|
+
tags = ['default']
|
|
502
|
+
|
|
503
|
+
# Add font tags if specified
|
|
504
|
+
if font_tags:
|
|
505
|
+
tags = font_tags + tags
|
|
506
|
+
|
|
507
|
+
# Insert text and apply tags
|
|
508
|
+
self.text_area.insert(tk.END, part_text, tuple(tags))
|
|
509
|
+
|
|
510
|
+
# Add newline
|
|
511
|
+
if font_tags:
|
|
512
|
+
self.text_area.insert(tk.END, "\n", tuple(font_tags))
|
|
513
|
+
else:
|
|
514
|
+
self.text_area.insert(tk.END, "\n", 'default')
|
|
515
|
+
else:
|
|
516
|
+
# Plain text
|
|
517
|
+
tags = ['default']
|
|
518
|
+
if font_tags:
|
|
519
|
+
tags = font_tags
|
|
520
|
+
self.text_area.insert(tk.END, str(text) + "\n", tuple(tags))
|
|
521
|
+
|
|
522
|
+
self.text_area.config(state=tk.DISABLED)
|
|
523
|
+
self.text_area.see(tk.END)
|
|
524
|
+
|
|
525
|
+
self.root.after(0, _update)
|
|
526
|
+
|
|
527
|
+
def display_colored(self, text: str, fg_color: Optional[str] = None, bg_color: Optional[str] = None,
|
|
528
|
+
bold: bool = False, underline: bool = False, italic: bool = False,
|
|
529
|
+
strikethrough: bool = False, reverse: bool = False,
|
|
530
|
+
font_family: Optional[str] = None, font_size: Optional[int] = None,
|
|
531
|
+
font_style: Optional[str] = None) -> None:
|
|
532
|
+
"""Directly display colored text with full color and font support"""
|
|
533
|
+
def _update():
|
|
534
|
+
self.text_area.config(state=tk.NORMAL)
|
|
535
|
+
|
|
536
|
+
tags = ['default']
|
|
537
|
+
|
|
538
|
+
# Handle foreground color - FIXED: Check for empty strings
|
|
539
|
+
if fg_color is not None and fg_color != "":
|
|
540
|
+
# Check if it's a color name that needs conversion
|
|
541
|
+
if fg_color.lower() in self.color_name_to_hex:
|
|
542
|
+
color_value = self.color_name_to_hex[fg_color.lower()]
|
|
543
|
+
elif fg_color.isdigit():
|
|
544
|
+
# It's an ANSI code
|
|
545
|
+
# Skip disabled colors
|
|
546
|
+
if 'disabled_colors' in self.config and fg_color in self.config['disabled_colors']:
|
|
547
|
+
# Use default color for disabled colors
|
|
548
|
+
pass
|
|
549
|
+
elif fg_color in self.ansi_colors:
|
|
550
|
+
tags.append(f"ansi_{fg_color}")
|
|
551
|
+
else:
|
|
552
|
+
# Default to white if unknown ANSI code
|
|
553
|
+
color_value = '#ffffff'
|
|
554
|
+
custom_fg_tag = f"custom_fg_{color_value}"
|
|
555
|
+
tags.append(custom_fg_tag)
|
|
556
|
+
if custom_fg_tag not in self.tag_names:
|
|
557
|
+
self.text_area.tag_configure(custom_fg_tag, foreground=color_value)
|
|
558
|
+
self.tag_names.add(custom_fg_tag)
|
|
559
|
+
elif fg_color.startswith('#') and len(fg_color) in [4, 5, 7, 9]:
|
|
560
|
+
# It's a hex color
|
|
561
|
+
color_value = fg_color
|
|
562
|
+
custom_fg_tag = f"custom_fg_{color_value}"
|
|
563
|
+
tags.append(custom_fg_tag)
|
|
564
|
+
if custom_fg_tag not in self.tag_names:
|
|
565
|
+
self.text_area.tag_configure(custom_fg_tag, foreground=color_value)
|
|
566
|
+
self.tag_names.add(custom_fg_tag)
|
|
567
|
+
elif ';' in fg_color and fg_color.startswith('38;'):
|
|
568
|
+
# Extended ANSI color code
|
|
569
|
+
if fg_color in self.ansi_colors:
|
|
570
|
+
tags.append(f"ansi_{fg_color}")
|
|
571
|
+
else:
|
|
572
|
+
# Try to use as a named color, but only if it's not empty
|
|
573
|
+
try:
|
|
574
|
+
if fg_color.strip(): # Check it's not just whitespace
|
|
575
|
+
self.text_area.tag_configure(f"custom_fg_{fg_color}", foreground=fg_color)
|
|
576
|
+
tags.append(f"custom_fg_{fg_color}")
|
|
577
|
+
self.tag_names.add(f"custom_fg_{fg_color}")
|
|
578
|
+
except tk.TclError:
|
|
579
|
+
# Fall back to default
|
|
580
|
+
pass
|
|
581
|
+
|
|
582
|
+
# Handle background color - FIXED: Check for empty strings
|
|
583
|
+
if bg_color is not None and bg_color != "":
|
|
584
|
+
# Check if it's a color name that needs conversion
|
|
585
|
+
if bg_color.lower() in self.color_name_to_hex:
|
|
586
|
+
color_value = self.color_name_to_hex[bg_color.lower()]
|
|
587
|
+
elif bg_color.isdigit():
|
|
588
|
+
# It's an ANSI code
|
|
589
|
+
# Skip disabled colors
|
|
590
|
+
if 'disabled_colors' in self.config and bg_color in self.config['disabled_colors']:
|
|
591
|
+
# Use default color for disabled colors
|
|
592
|
+
pass
|
|
593
|
+
elif bg_color in self.ansi_colors:
|
|
594
|
+
tags.append(f"ansi_{bg_color}")
|
|
595
|
+
else:
|
|
596
|
+
# Default to black if unknown ANSI code
|
|
597
|
+
color_value = '#000000'
|
|
598
|
+
custom_bg_tag = f"custom_bg_{color_value}"
|
|
599
|
+
tags.append(custom_bg_tag)
|
|
600
|
+
if custom_bg_tag not in self.tag_names:
|
|
601
|
+
self.text_area.tag_configure(custom_bg_tag, background=color_value)
|
|
602
|
+
self.tag_names.add(custom_bg_tag)
|
|
603
|
+
elif bg_color.startswith('#') and len(bg_color) in [4, 5, 7, 9]:
|
|
604
|
+
# It's a hex color
|
|
605
|
+
color_value = bg_color
|
|
606
|
+
custom_bg_tag = f"custom_bg_{color_value}"
|
|
607
|
+
tags.append(custom_bg_tag)
|
|
608
|
+
if custom_bg_tag not in self.tag_names:
|
|
609
|
+
self.text_area.tag_configure(custom_bg_tag, background=color_value)
|
|
610
|
+
self.tag_names.add(custom_bg_tag)
|
|
611
|
+
elif ';' in bg_color and bg_color.startswith('48;'):
|
|
612
|
+
# Extended ANSI color code
|
|
613
|
+
if bg_color in self.ansi_colors:
|
|
614
|
+
tags.append(f"ansi_{bg_color}")
|
|
615
|
+
else:
|
|
616
|
+
# Try to use as a named color, but only if it's not empty
|
|
617
|
+
try:
|
|
618
|
+
if bg_color.strip(): # Check it's not just whitespace
|
|
619
|
+
self.text_area.tag_configure(f"custom_bg_{bg_color}", background=bg_color)
|
|
620
|
+
tags.append(f"custom_bg_{bg_color}")
|
|
621
|
+
self.tag_names.add(f"custom_bg_{bg_color}")
|
|
622
|
+
except tk.TclError:
|
|
623
|
+
# Fall back to default
|
|
624
|
+
pass
|
|
625
|
+
|
|
626
|
+
# Handle font
|
|
627
|
+
if font_family or font_size or font_style:
|
|
628
|
+
# Create a unique tag for this font combination
|
|
629
|
+
font_family_val = font_family or "Courier"
|
|
630
|
+
font_size_val = font_size or 10
|
|
631
|
+
font_style_val = font_style or "normal"
|
|
632
|
+
font_key = f"font_{font_family_val}_{font_size_val}_{font_style_val}"
|
|
633
|
+
|
|
634
|
+
if font_key not in self.tag_names:
|
|
635
|
+
self.text_area.tag_configure(font_key, font=(font_family_val, font_size_val, font_style_val))
|
|
636
|
+
self.tag_names.add(font_key)
|
|
637
|
+
tags.append(font_key)
|
|
638
|
+
|
|
639
|
+
# Handle styles
|
|
640
|
+
if bold:
|
|
641
|
+
tags.append('bold')
|
|
642
|
+
if underline:
|
|
643
|
+
tags.append('underline')
|
|
644
|
+
if italic:
|
|
645
|
+
tags.append('italic')
|
|
646
|
+
if strikethrough:
|
|
647
|
+
tags.append('strikethrough')
|
|
648
|
+
if reverse:
|
|
649
|
+
tags.append('reverse')
|
|
650
|
+
|
|
651
|
+
self.text_area.insert(tk.END, str(text) + "\n", tuple(tags))
|
|
652
|
+
self.text_area.config(state=tk.DISABLED)
|
|
653
|
+
self.text_area.see(tk.END)
|
|
654
|
+
|
|
655
|
+
self.root.after(0, _update)
|
|
656
|
+
|
|
657
|
+
def _demo_colors(self) -> None:
|
|
658
|
+
"""Display ANSI color demo"""
|
|
659
|
+
demo_texts = [
|
|
660
|
+
("\033[1mANSI Color Demo\033[0m\n", False), # Don't parse ANSI
|
|
661
|
+
("\033[1;37mBasic Colors:\033[0m\n", True),
|
|
662
|
+
(" \033[30mBlack\033[0m \033[31mRed\033[0m \033[32mGreen\033[0m \033[33mYellow\033[0m\n", True),
|
|
663
|
+
(" \033[34mBlue\033[0m \033[35mMagenta\033[0m \033[36mCyan\033[0m \033[37mWhite\033[0m\n", True),
|
|
664
|
+
("\n\033[1;37mBright Colors:\033[0m\n", True),
|
|
665
|
+
(" \033[90mGray\033[0m \033[91mBright Red\033[0m \033[92mBright Green\033[0m\n", True),
|
|
666
|
+
(" \033[93mBright Yellow\033[0m \033[94mBright Blue\033[0m \033[95mBright Magenta\033[0m\n", True),
|
|
667
|
+
("\n\033[1;37mBackground Colors:\033[0m\n", True),
|
|
668
|
+
(" \033[40;37mBlack BG\033[0m \033[41mRed BG\033[0m \033[42mGreen BG\033[0m\n", True),
|
|
669
|
+
(" \033[43mYellow BG\033[0m \033[44mBlue BG\033[0m \033[45mMagenta BG\033[0m\n", True),
|
|
670
|
+
("\n\033[1;37mExtended Colors:\033[0m\n", True),
|
|
671
|
+
(" \033[38;5;1mDark Red\033[0m \033[38;5;9mRed\033[0m \033[38;5;10mGreen\033[0m \033[38;5;12mBlue\033[0m\n", True),
|
|
672
|
+
(" \033[48;5;1mDark Red BG\033[0m \033[48;5;9mRed BG\033[0m\n", True),
|
|
673
|
+
("\n\033[1;37mTrue Colors (RGB):\033[0m\n", True),
|
|
674
|
+
(" \033[38;2;255;0;0mRed\033[0m \033[38;2;0;255;0mGreen\033[0m \033[38;2;0;0;255mBlue\033[0m\n", True),
|
|
675
|
+
("\n\033[1;37mText Styles:\033[0m\n", True),
|
|
676
|
+
(" \033[1mBold\033[0m \033[3mItalic\033[0m \033[4mUnderline\033[0m \033[9mStrikethrough\033[0m\n", True),
|
|
677
|
+
("\n\033[1;37mCombined Styles:\033[0m\n", True),
|
|
678
|
+
(" \033[1;31mBold Red\033[0m \033[1;4;32mBold Underlined Green\033[0m\n", True),
|
|
679
|
+
(" \033[1;33;44mBold Yellow on Blue\033[0m \033[1;37;41mBold White on Red\033[0m\n", True),
|
|
680
|
+
]
|
|
681
|
+
|
|
682
|
+
for text, parse_ansi in demo_texts:
|
|
683
|
+
self.display(text, parse_ansi)
|
|
684
|
+
|
|
685
|
+
def set_theme(self, theme_name: str) -> None:
|
|
686
|
+
"""Set theme"""
|
|
687
|
+
def _set_theme():
|
|
688
|
+
if theme_name == "dark":
|
|
689
|
+
self.text_area.config(bg="black", fg="white", insertbackground="white")
|
|
690
|
+
self.text_area.tag_configure("default", foreground="white", background="black")
|
|
691
|
+
elif theme_name == "light":
|
|
692
|
+
self.text_area.config(bg="white", fg="black", insertbackground="black")
|
|
693
|
+
self.text_area.tag_configure("default", foreground="black", background="white")
|
|
694
|
+
elif theme_name == "matrix":
|
|
695
|
+
self.text_area.config(bg="black", fg="#00ff00", insertbackground="#00ff00")
|
|
696
|
+
self.text_area.tag_configure("default", foreground="#00ff00", background="black")
|
|
697
|
+
else: # default
|
|
698
|
+
self.text_area.config(bg="black", fg="white", insertbackground="white")
|
|
699
|
+
self.text_area.tag_configure("default", foreground="white", background="black")
|
|
700
|
+
|
|
701
|
+
self.text_area.see(tk.END)
|
|
702
|
+
|
|
703
|
+
self.root.after(0, _set_theme)
|
|
704
|
+
|
|
705
|
+
def user_write(self, prompt: str = "Input:") -> Optional[str]:
|
|
706
|
+
"""Thread-safe input dialog (opens in a new window)"""
|
|
707
|
+
if not self.running:
|
|
708
|
+
return None
|
|
709
|
+
def _ask():
|
|
710
|
+
try:
|
|
711
|
+
result = simpledialog.askstring("Input", prompt, parent=self.root)
|
|
712
|
+
self.input_queue.put(result)
|
|
713
|
+
except tk.TclError:
|
|
714
|
+
self.input_queue.put(None)
|
|
715
|
+
self.root.after(0, _ask)
|
|
716
|
+
|
|
717
|
+
# Wait for input with timeout to check if still running
|
|
718
|
+
while self.running:
|
|
719
|
+
try:
|
|
720
|
+
return self.input_queue.get(timeout=0.1)
|
|
721
|
+
except queue.Empty:
|
|
722
|
+
continue
|
|
723
|
+
return None
|
|
724
|
+
|
|
725
|
+
def user_type_in(self, prompt: str = ">> ") -> Optional[str]:
|
|
726
|
+
"""
|
|
727
|
+
Thread-safe terminal-style input (embedded in the main window)
|
|
728
|
+
User types in the terminal-like input field and presses Enter to send
|
|
729
|
+
"""
|
|
730
|
+
if not self.running:
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
def _prepare_input():
|
|
734
|
+
try:
|
|
735
|
+
# Update the prompt in the label
|
|
736
|
+
self.input_label.config(text=prompt)
|
|
737
|
+
|
|
738
|
+
# Clear any previous input
|
|
739
|
+
self.input_var.set("")
|
|
740
|
+
|
|
741
|
+
# Focus the input field
|
|
742
|
+
self.input_entry.focus_set()
|
|
743
|
+
|
|
744
|
+
# Enable the input field
|
|
745
|
+
self.input_entry.config(state=tk.NORMAL)
|
|
746
|
+
except tk.TclError:
|
|
747
|
+
pass
|
|
748
|
+
|
|
749
|
+
# Clear the queue in case there are stale values
|
|
750
|
+
while not self.type_in_queue.empty():
|
|
751
|
+
try:
|
|
752
|
+
self.type_in_queue.get_nowait()
|
|
753
|
+
except queue.Empty:
|
|
754
|
+
break
|
|
755
|
+
|
|
756
|
+
# Prepare the input field
|
|
757
|
+
try:
|
|
758
|
+
self.root.after(0, _prepare_input)
|
|
759
|
+
except tk.TclError:
|
|
760
|
+
return None
|
|
761
|
+
|
|
762
|
+
# Block and wait for user input
|
|
763
|
+
while self.running:
|
|
764
|
+
try:
|
|
765
|
+
return self.type_in_queue.get(timeout=0.1)
|
|
766
|
+
except queue.Empty:
|
|
767
|
+
continue
|
|
768
|
+
return None
|
|
769
|
+
|
|
770
|
+
def _on_enter_pressed(self, event: Optional[tk.Event] = None) -> str:
|
|
771
|
+
"""Handle Enter key press in the input field"""
|
|
772
|
+
self._on_send_input()
|
|
773
|
+
return "break" # Prevent default behavior
|
|
774
|
+
|
|
775
|
+
def _on_send_input(self) -> None:
|
|
776
|
+
"""Send the input from the terminal-style input field"""
|
|
777
|
+
user_input = self.input_var.get().strip()
|
|
778
|
+
|
|
779
|
+
if user_input:
|
|
780
|
+
# Put the input in the queue
|
|
781
|
+
self.type_in_queue.put(user_input)
|
|
782
|
+
|
|
783
|
+
# Display the user input in the output area
|
|
784
|
+
self.text_area.config(state=tk.NORMAL)
|
|
785
|
+
self.text_area.insert(tk.END, f"{self.input_label.cget('text')}{user_input}\n", 'default')
|
|
786
|
+
self.text_area.config(state=tk.DISABLED)
|
|
787
|
+
self.text_area.see(tk.END)
|
|
788
|
+
|
|
789
|
+
# Clear the input field
|
|
790
|
+
self.input_var.set("")
|
|
791
|
+
|
|
792
|
+
def _clear_input(self) -> None:
|
|
793
|
+
"""Clear the terminal-style input field"""
|
|
794
|
+
self.input_var.set("")
|
|
795
|
+
self.input_entry.focus_set()
|
|
796
|
+
|
|
797
|
+
def focus_input(self) -> None:
|
|
798
|
+
"""Set focus to the terminal-style input field"""
|
|
799
|
+
self.input_entry.focus_set()
|
|
800
|
+
|
|
801
|
+
def clear(self) -> None:
|
|
802
|
+
self.text_area.config(state=tk.NORMAL)
|
|
803
|
+
self.text_area.delete(1.0, tk.END)
|
|
804
|
+
self.text_area.config(state=tk.DISABLED)
|
|
805
|
+
|
|
806
|
+
def copy_text(self) -> None:
|
|
807
|
+
try:
|
|
808
|
+
selected = self.text_area.selection_get()
|
|
809
|
+
self.root.clipboard_clear()
|
|
810
|
+
self.root.clipboard_append(selected)
|
|
811
|
+
except tk.TclError:
|
|
812
|
+
pass
|
|
813
|
+
|
|
814
|
+
def select_all(self) -> None:
|
|
815
|
+
self.text_area.config(state=tk.NORMAL)
|
|
816
|
+
self.text_area.tag_add(tk.SEL, "1.0", tk.END)
|
|
817
|
+
self.text_area.config(state=tk.DISABLED)
|
|
818
|
+
self.text_area.mark_set(tk.INSERT, "1.0")
|
|
819
|
+
self.text_area.see(tk.INSERT)
|
|
820
|
+
|
|
821
|
+
def exit(self) -> None:
|
|
822
|
+
self.running = False
|
|
823
|
+
try:
|
|
824
|
+
self.root.quit()
|
|
825
|
+
self.root.destroy()
|
|
826
|
+
except tk.TclError:
|
|
827
|
+
pass # Already destroyed
|
|
828
|
+
|
|
829
|
+
def run(self, func: Optional[Callable] = None, *args, **kwargs) -> Any:
|
|
830
|
+
"""
|
|
831
|
+
Run the GUI and optionally a worker function.
|
|
832
|
+
All Tkinter operations stay on main thread; your logic runs in a thread.
|
|
833
|
+
"""
|
|
834
|
+
result_queue = queue.Queue()
|
|
835
|
+
|
|
836
|
+
if func:
|
|
837
|
+
def worker():
|
|
838
|
+
try:
|
|
839
|
+
result = func(*args, **kwargs)
|
|
840
|
+
result_queue.put(result)
|
|
841
|
+
except Exception as e:
|
|
842
|
+
self.display(f"Error: {e}\n{traceback.format_exc()}")
|
|
843
|
+
result_queue.put(e)
|
|
844
|
+
threading.Thread(target=worker, daemon=True).start()
|
|
845
|
+
|
|
846
|
+
try:
|
|
847
|
+
self.root.mainloop()
|
|
848
|
+
except KeyboardInterrupt:
|
|
849
|
+
self.exit()
|
|
850
|
+
|
|
851
|
+
if func:
|
|
852
|
+
# Wait for function to complete, but make it interruptible
|
|
853
|
+
while self.running:
|
|
854
|
+
try:
|
|
855
|
+
return result_queue.get(timeout=0.1)
|
|
856
|
+
except queue.Empty:
|
|
857
|
+
continue
|
|
858
|
+
return None
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
# Global instance and helper functions
|
|
862
|
+
_gui_instance = Py2GUI()
|
|
863
|
+
|
|
864
|
+
display = _gui_instance.display
|
|
865
|
+
display_colored = _gui_instance.display_colored
|
|
866
|
+
display_paragraph = _gui_instance.display_paragraph
|
|
867
|
+
user_write = _gui_instance.user_write
|
|
868
|
+
user_type_in = _gui_instance.user_type_in
|
|
869
|
+
clear = _gui_instance.clear
|
|
870
|
+
copy_text = _gui_instance.copy_text
|
|
871
|
+
select_all = _gui_instance.select_all
|
|
872
|
+
exit_gui = _gui_instance.exit
|
|
873
|
+
run = _gui_instance.run
|
|
874
|
+
focus_input = _gui_instance.focus_input
|
|
875
|
+
set_theme = _gui_instance.set_theme
|