py2gui 0.1.0__py3-none-any.whl → 0.1.1.1__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/py2gui.py +458 -341
- {py2gui-0.1.0.dist-info → py2gui-0.1.1.1.dist-info}/METADATA +2 -2
- py2gui-0.1.1.1.dist-info/RECORD +8 -0
- py2gui-0.1.0.dist-info/RECORD +0 -8
- {py2gui-0.1.0.dist-info → py2gui-0.1.1.1.dist-info}/WHEEL +0 -0
- {py2gui-0.1.0.dist-info → py2gui-0.1.1.1.dist-info}/licenses/LICENSE +0 -0
- {py2gui-0.1.0.dist-info → py2gui-0.1.1.1.dist-info}/top_level.txt +0 -0
py2gui/py2gui.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Py2GUI - Enhanced Python Terminal-style GUI
|
|
3
|
+
Fixed version: Fixed potential errors and issues
|
|
4
|
+
"""
|
|
1
5
|
import tkinter as tk
|
|
2
6
|
from tkinter import scrolledtext, simpledialog, Menu, Frame, Entry, Button, StringVar, font
|
|
3
7
|
import queue
|
|
@@ -6,11 +10,14 @@ import traceback
|
|
|
6
10
|
import re
|
|
7
11
|
import json
|
|
8
12
|
import os
|
|
9
|
-
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Callable, Any, Optional, List, Tuple, Dict, Set
|
|
15
|
+
import warnings
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
class Py2GUI:
|
|
13
19
|
def __init__(self, title: str = "Py2GUI", width: int = 80, height: int = 20, config_file: str = "config.json"):
|
|
20
|
+
"""Initialize Py2GUI instance"""
|
|
14
21
|
self.root = tk.Tk()
|
|
15
22
|
self.root.title(title)
|
|
16
23
|
self.root.resizable(True, True)
|
|
@@ -22,7 +29,7 @@ class Py2GUI:
|
|
|
22
29
|
# Load configuration
|
|
23
30
|
self.config = self._load_config()
|
|
24
31
|
|
|
25
|
-
# Extended ANSI color configuration
|
|
32
|
+
# Extended ANSI color configuration
|
|
26
33
|
self.ansi_colors = {
|
|
27
34
|
# Basic colors
|
|
28
35
|
'30': '#000000', # Black
|
|
@@ -54,7 +61,7 @@ class Py2GUI:
|
|
|
54
61
|
'46': '#00ffff', # Cyan background
|
|
55
62
|
'47': '#ffffff', # White background
|
|
56
63
|
|
|
57
|
-
# Extended 256 colors
|
|
64
|
+
# Extended 256 colors
|
|
58
65
|
'38;5;0': '#000000', # Black
|
|
59
66
|
'38;5;1': '#800000', # Dark red
|
|
60
67
|
'38;5;2': '#008000', # Dark green
|
|
@@ -82,14 +89,14 @@ class Py2GUI:
|
|
|
82
89
|
'48;5;6': '#008080', # Dark cyan background
|
|
83
90
|
'48;5;7': '#c0c0c0', # Light gray background
|
|
84
91
|
|
|
85
|
-
# True
|
|
92
|
+
# True Color support
|
|
86
93
|
'38;2;0;0;0': '#000000', # Black
|
|
87
94
|
'38;2;255;0;0': '#ff0000', # Red
|
|
88
95
|
'38;2;0;255;0': '#00ff00', # Green
|
|
89
96
|
'38;2;0;0;255': '#0000ff', # Blue
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
# Color name to hex mapping
|
|
99
|
+
# Color name to hex mapping
|
|
93
100
|
self.color_name_to_hex = {
|
|
94
101
|
'black': '#000000',
|
|
95
102
|
'red': '#ff0000',
|
|
@@ -116,17 +123,11 @@ class Py2GUI:
|
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
# Available fonts
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
}
|
|
126
|
+
try:
|
|
127
|
+
self.available_fonts = font.families()
|
|
128
|
+
except Exception:
|
|
129
|
+
self.available_fonts = ["Courier", "Consolas", "Monaco", "Menlo"]
|
|
130
|
+
warnings.warn(f"Could not load font families, using fallback fonts: {self.available_fonts}")
|
|
130
131
|
|
|
131
132
|
# Current text style state
|
|
132
133
|
self.current_style = {
|
|
@@ -138,12 +139,12 @@ class Py2GUI:
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
# Defined text tags
|
|
141
|
-
self.tag_names = set()
|
|
142
|
+
self.tag_names: Set[str] = set()
|
|
142
143
|
|
|
143
|
-
# Handle window
|
|
144
|
+
# Handle window close
|
|
144
145
|
self.root.protocol("WM_DELETE_WINDOW", self.exit)
|
|
145
146
|
|
|
146
|
-
# Main frame
|
|
147
|
+
# Main frame
|
|
147
148
|
self.main_frame = Frame(self.root)
|
|
148
149
|
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
149
150
|
|
|
@@ -168,7 +169,7 @@ class Py2GUI:
|
|
|
168
169
|
background="black"
|
|
169
170
|
)
|
|
170
171
|
|
|
171
|
-
# Configure color tags
|
|
172
|
+
# Configure color tags
|
|
172
173
|
for code, color_hex in self.ansi_colors.items():
|
|
173
174
|
# Skip disabled colors
|
|
174
175
|
if 'disabled_colors' in self.config and code in self.config['disabled_colors']:
|
|
@@ -176,11 +177,12 @@ class Py2GUI:
|
|
|
176
177
|
|
|
177
178
|
tag_name = f"ansi_{code}"
|
|
178
179
|
if (code.startswith('3') and ';' not in code) or code.startswith('38'):
|
|
179
|
-
# Foreground
|
|
180
|
+
# Foreground color
|
|
180
181
|
self.text_area.tag_configure(tag_name, foreground=color_hex)
|
|
181
182
|
elif code.startswith('4') or code.startswith('48'):
|
|
182
|
-
# Background
|
|
183
|
+
# Background color
|
|
183
184
|
self.text_area.tag_configure(tag_name, background=color_hex)
|
|
185
|
+
self.tag_names.add(tag_name)
|
|
184
186
|
|
|
185
187
|
# Configure style tags
|
|
186
188
|
self.text_area.tag_configure("bold", font=("Courier", 10, "bold"))
|
|
@@ -189,7 +191,11 @@ class Py2GUI:
|
|
|
189
191
|
self.text_area.tag_configure("strikethrough", overstrike=True)
|
|
190
192
|
self.text_area.tag_configure("reverse", foreground="black", background="white")
|
|
191
193
|
|
|
192
|
-
#
|
|
194
|
+
# Add style tags to tag set
|
|
195
|
+
for tag in ["bold", "italic", "underline", "strikethrough", "reverse"]:
|
|
196
|
+
self.tag_names.add(tag)
|
|
197
|
+
|
|
198
|
+
# Terminal style input area frame
|
|
193
199
|
self.input_frame = Frame(self.main_frame)
|
|
194
200
|
self.input_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
195
201
|
|
|
@@ -197,7 +203,7 @@ class Py2GUI:
|
|
|
197
203
|
self.input_label = tk.Label(self.input_frame, text=">> ", font=("Courier", 10), fg="white", bg="black")
|
|
198
204
|
self.input_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
199
205
|
|
|
200
|
-
# Terminal
|
|
206
|
+
# Terminal style input field
|
|
201
207
|
self.input_var = StringVar()
|
|
202
208
|
self.input_entry = Entry(
|
|
203
209
|
self.input_frame,
|
|
@@ -218,19 +224,17 @@ class Py2GUI:
|
|
|
218
224
|
)
|
|
219
225
|
self.send_button.pack(side=tk.LEFT)
|
|
220
226
|
|
|
221
|
-
# Input
|
|
227
|
+
# Input queues
|
|
222
228
|
self.input_queue = queue.Queue()
|
|
223
|
-
|
|
224
|
-
# Input queue for user_type_in
|
|
225
229
|
self.type_in_queue = queue.Queue()
|
|
226
230
|
|
|
227
|
-
# Bind Enter key
|
|
231
|
+
# Bind Enter key
|
|
228
232
|
self.input_entry.bind('<Return>', self._on_enter_pressed)
|
|
229
233
|
|
|
230
|
-
#
|
|
234
|
+
# Create menus
|
|
231
235
|
self._setup_menus()
|
|
232
236
|
|
|
233
|
-
def _load_config(self) -> Dict:
|
|
237
|
+
def _load_config(self) -> Dict[str, Any]:
|
|
234
238
|
"""Load configuration from JSON file"""
|
|
235
239
|
default_config = {
|
|
236
240
|
"disabled_menus": [],
|
|
@@ -242,19 +246,29 @@ class Py2GUI:
|
|
|
242
246
|
|
|
243
247
|
try:
|
|
244
248
|
if os.path.exists(self.config_file):
|
|
245
|
-
with open(self.config_file, 'r') as f:
|
|
249
|
+
with open(self.config_file, 'r', encoding='utf-8') as f:
|
|
246
250
|
config = json.load(f)
|
|
247
251
|
# Merge with default config
|
|
248
252
|
for key, value in default_config.items():
|
|
249
253
|
if key not in config:
|
|
250
254
|
config[key] = value
|
|
251
255
|
return config
|
|
256
|
+
except (json.JSONDecodeError, IOError, OSError) as e:
|
|
257
|
+
self._safe_print(f"Error loading config file: {e}")
|
|
252
258
|
except Exception as e:
|
|
253
|
-
|
|
259
|
+
self._safe_print(f"Unexpected error loading config: {e}")
|
|
254
260
|
|
|
255
261
|
return default_config
|
|
256
262
|
|
|
263
|
+
def _safe_print(self, message: str) -> None:
|
|
264
|
+
"""Safely print message (for initialization and error handling)"""
|
|
265
|
+
try:
|
|
266
|
+
print(message, file=sys.stderr)
|
|
267
|
+
except Exception:
|
|
268
|
+
pass # Ignore print errors
|
|
269
|
+
|
|
257
270
|
def _setup_menus(self) -> None:
|
|
271
|
+
"""Set up menu system"""
|
|
258
272
|
menubar = Menu(self.root)
|
|
259
273
|
self.root.config(menu=menubar)
|
|
260
274
|
|
|
@@ -276,7 +290,6 @@ class Py2GUI:
|
|
|
276
290
|
view_menu = Menu(menubar, tearoff=0)
|
|
277
291
|
menubar.add_cascade(label="View", menu=view_menu)
|
|
278
292
|
|
|
279
|
-
# Add view menu items based on configuration
|
|
280
293
|
if 'disabled_views' not in self.config or 'Focus Input' not in self.config['disabled_views']:
|
|
281
294
|
view_menu.add_command(label="Focus Input", command=self.focus_input)
|
|
282
295
|
|
|
@@ -297,12 +310,12 @@ class Py2GUI:
|
|
|
297
310
|
|
|
298
311
|
def _parse_ansi_codes(self, text: str) -> List[Tuple[str, List[str]]]:
|
|
299
312
|
"""Parse ANSI escape sequences in text"""
|
|
300
|
-
# ANSI escape sequence regex
|
|
301
|
-
ansi_pattern = re.compile(r'(\
|
|
313
|
+
# ANSI escape sequence regex pattern
|
|
314
|
+
ansi_pattern = re.compile(r'(\x1b\[[\d;]*m)')
|
|
302
315
|
|
|
303
316
|
parts = []
|
|
304
317
|
last_end = 0
|
|
305
|
-
current_codes = []
|
|
318
|
+
current_codes: List[str] = []
|
|
306
319
|
|
|
307
320
|
for match in ansi_pattern.finditer(text):
|
|
308
321
|
# Add normal text
|
|
@@ -313,7 +326,7 @@ class Py2GUI:
|
|
|
313
326
|
|
|
314
327
|
# Parse ANSI code
|
|
315
328
|
ansi_code = match.group(0)
|
|
316
|
-
code_str = ansi_code[2:-1] # Remove \
|
|
329
|
+
code_str = ansi_code[2:-1] # Remove \x1b[ and m
|
|
317
330
|
|
|
318
331
|
if code_str == '':
|
|
319
332
|
# Reset all attributes
|
|
@@ -326,10 +339,7 @@ class Py2GUI:
|
|
|
326
339
|
current_codes = ['0']
|
|
327
340
|
elif code in ['1', '3', '4', '7', '9']:
|
|
328
341
|
# Style codes
|
|
329
|
-
if code in current_codes:
|
|
330
|
-
# Remove duplicate style codes
|
|
331
|
-
pass
|
|
332
|
-
else:
|
|
342
|
+
if code not in current_codes:
|
|
333
343
|
if code == '1' and '22' in current_codes:
|
|
334
344
|
current_codes.remove('22')
|
|
335
345
|
current_codes.append(code)
|
|
@@ -340,11 +350,10 @@ class Py2GUI:
|
|
|
340
350
|
current_codes.remove(reset_map[code])
|
|
341
351
|
elif code in self.ansi_colors or code.startswith('38;') or code.startswith('48;'):
|
|
342
352
|
# Color codes
|
|
343
|
-
# Skip disabled colors
|
|
344
353
|
if 'disabled_colors' in self.config and code in self.config['disabled_colors']:
|
|
345
354
|
continue
|
|
346
355
|
|
|
347
|
-
# Remove same type
|
|
356
|
+
# Remove same type color codes
|
|
348
357
|
if code in ['30', '31', '32', '33', '34', '35', '36', '37',
|
|
349
358
|
'90', '91', '92', '93', '94', '95', '96', '97']:
|
|
350
359
|
# Remove other basic foreground colors
|
|
@@ -369,7 +378,7 @@ class Py2GUI:
|
|
|
369
378
|
current_codes.remove(c)
|
|
370
379
|
current_codes.append(code)
|
|
371
380
|
|
|
372
|
-
# Add
|
|
381
|
+
# Add remaining text
|
|
373
382
|
if last_end < len(text):
|
|
374
383
|
remaining_text = text[last_end:]
|
|
375
384
|
if remaining_text:
|
|
@@ -397,9 +406,11 @@ class Py2GUI:
|
|
|
397
406
|
if 'disabled_colors' in self.config and code in self.config['disabled_colors']:
|
|
398
407
|
continue
|
|
399
408
|
# Color tags
|
|
400
|
-
|
|
409
|
+
tag_name = f"ansi_{code}"
|
|
410
|
+
if tag_name in self.tag_names:
|
|
411
|
+
tags.append(tag_name)
|
|
401
412
|
|
|
402
|
-
return tags
|
|
413
|
+
return tags if tags else ['default']
|
|
403
414
|
|
|
404
415
|
def _process_escape_sequences(self, text: str) -> str:
|
|
405
416
|
"""Process escape sequences like \n, \t, etc."""
|
|
@@ -422,261 +433,271 @@ class Py2GUI:
|
|
|
422
433
|
return text
|
|
423
434
|
|
|
424
435
|
def display_paragraph(self, text: str, parse_ansi: bool = True, font_family: Optional[str] = None,
|
|
425
|
-
|
|
426
|
-
"""Thread-safe display
|
|
436
|
+
font_size: Optional[int] = None, font_style: Optional[str] = None) -> None:
|
|
437
|
+
"""Thread-safe display paragraph (no auto newline)"""
|
|
427
438
|
def _update():
|
|
428
|
-
|
|
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}"
|
|
439
|
+
try:
|
|
440
|
+
self.text_area.config(state=tk.NORMAL)
|
|
441
441
|
|
|
442
|
-
|
|
443
|
-
|
|
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)
|
|
442
|
+
# Process escape sequences
|
|
443
|
+
text_processed = self._process_escape_sequences(text)
|
|
450
444
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
445
|
+
# Check custom font settings
|
|
446
|
+
font_tags = []
|
|
447
|
+
if font_family or font_size or font_style:
|
|
448
|
+
# Create unique tag for font combination
|
|
449
|
+
font_family_val = font_family or "Courier"
|
|
450
|
+
font_size_val = font_size or 10
|
|
451
|
+
font_style_val = font_style or "normal"
|
|
452
|
+
font_key = f"font_{font_family_val}_{font_size_val}_{font_style_val}"
|
|
455
453
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
454
|
+
if font_key not in self.tag_names:
|
|
455
|
+
self.text_area.tag_configure(font_key,
|
|
456
|
+
font=(font_family_val, font_size_val, font_style_val))
|
|
457
|
+
self.tag_names.add(font_key)
|
|
458
|
+
font_tags.append(font_key)
|
|
459
|
+
|
|
460
|
+
if parse_ansi and ('\x1b[' in text_processed or '\033[' in text_processed):
|
|
461
|
+
# Parse and apply ANSI colors
|
|
462
|
+
parts = self._parse_ansi_codes(text_processed)
|
|
459
463
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
464
|
+
for part_text, codes in parts:
|
|
465
|
+
tags = self._get_tags_for_codes(codes)
|
|
466
|
+
|
|
467
|
+
# Add font tags (if specified)
|
|
468
|
+
if font_tags:
|
|
469
|
+
tags = font_tags + tags
|
|
470
|
+
|
|
471
|
+
# Insert text and apply tags
|
|
472
|
+
self.text_area.insert(tk.END, part_text, tuple(tags))
|
|
473
|
+
else:
|
|
474
|
+
# Normal text
|
|
475
|
+
tags = ['default']
|
|
476
|
+
if font_tags:
|
|
477
|
+
tags = font_tags
|
|
478
|
+
self.text_area.insert(tk.END, text_processed, tuple(tags))
|
|
479
|
+
|
|
480
|
+
self.text_area.config(state=tk.DISABLED)
|
|
481
|
+
self.text_area.see(tk.END)
|
|
482
|
+
except tk.TclError as e:
|
|
483
|
+
if self.running:
|
|
484
|
+
self._safe_print(f"Tkinter error in display_paragraph: {e}")
|
|
485
|
+
except Exception as e:
|
|
486
|
+
if self.running:
|
|
487
|
+
self._safe_print(f"Error in display_paragraph: {e}")
|
|
488
|
+
|
|
489
|
+
if self.running:
|
|
490
|
+
self.root.after(0, _update)
|
|
473
491
|
|
|
474
492
|
def display(self, text: str, parse_ansi: bool = True, font_family: Optional[str] = None,
|
|
475
|
-
|
|
476
|
-
"""Thread-safe display
|
|
493
|
+
font_size: Optional[int] = None, font_style: Optional[str] = None) -> None:
|
|
494
|
+
"""Thread-safe display text (auto newline)"""
|
|
477
495
|
def _update():
|
|
478
|
-
|
|
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}"
|
|
496
|
+
try:
|
|
497
|
+
self.text_area.config(state=tk.NORMAL)
|
|
488
498
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
499
|
+
# Check custom font settings
|
|
500
|
+
font_tags = []
|
|
501
|
+
if font_family or font_size or font_style:
|
|
502
|
+
# Create unique tag for font combination
|
|
503
|
+
font_family_val = font_family or "Courier"
|
|
504
|
+
font_size_val = font_size or 10
|
|
505
|
+
font_style_val = font_style or "normal"
|
|
506
|
+
font_key = f"font_{font_family_val}_{font_size_val}_{font_style_val}"
|
|
507
|
+
|
|
508
|
+
if font_key not in self.tag_names:
|
|
509
|
+
self.text_area.tag_configure(font_key,
|
|
510
|
+
font=(font_family_val, font_size_val, font_style_val))
|
|
511
|
+
self.tag_names.add(font_key)
|
|
512
|
+
font_tags.append(font_key)
|
|
497
513
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
tags = ['default']
|
|
514
|
+
if parse_ansi and ('\x1b[' in str(text) or '\033[' in str(text)):
|
|
515
|
+
# Parse and apply ANSI colors
|
|
516
|
+
parts = self._parse_ansi_codes(str(text))
|
|
502
517
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
518
|
+
for part_text, codes in parts:
|
|
519
|
+
tags = self._get_tags_for_codes(codes)
|
|
520
|
+
|
|
521
|
+
# Add font tags (if specified)
|
|
522
|
+
if font_tags:
|
|
523
|
+
tags = font_tags + tags
|
|
524
|
+
|
|
525
|
+
# Insert text and apply tags
|
|
526
|
+
self.text_area.insert(tk.END, part_text, tuple(tags))
|
|
506
527
|
|
|
507
|
-
#
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
self.text_area.insert(tk.END, "\n", tuple(font_tags))
|
|
528
|
+
# Add newline
|
|
529
|
+
if font_tags:
|
|
530
|
+
self.text_area.insert(tk.END, "\n", tuple(font_tags))
|
|
531
|
+
else:
|
|
532
|
+
self.text_area.insert(tk.END, "\n", 'default')
|
|
513
533
|
else:
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
self.text_area.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
534
|
+
# Normal text
|
|
535
|
+
tags = ['default']
|
|
536
|
+
if font_tags:
|
|
537
|
+
tags = font_tags
|
|
538
|
+
self.text_area.insert(tk.END, str(text) + "\n", tuple(tags))
|
|
539
|
+
|
|
540
|
+
self.text_area.config(state=tk.DISABLED)
|
|
541
|
+
self.text_area.see(tk.END)
|
|
542
|
+
except tk.TclError as e:
|
|
543
|
+
if self.running:
|
|
544
|
+
self._safe_print(f"Tkinter error in display: {e}")
|
|
545
|
+
except Exception as e:
|
|
546
|
+
if self.running:
|
|
547
|
+
self._safe_print(f"Error in display: {e}")
|
|
548
|
+
|
|
549
|
+
if self.running:
|
|
550
|
+
self.root.after(0, _update)
|
|
526
551
|
|
|
527
552
|
def display_colored(self, text: str, fg_color: Optional[str] = None, bg_color: Optional[str] = None,
|
|
528
553
|
bold: bool = False, underline: bool = False, italic: bool = False,
|
|
529
554
|
strikethrough: bool = False, reverse: bool = False,
|
|
530
555
|
font_family: Optional[str] = None, font_size: Optional[int] = None,
|
|
531
556
|
font_style: Optional[str] = None) -> None:
|
|
532
|
-
"""Directly display colored text
|
|
557
|
+
"""Directly display colored text"""
|
|
533
558
|
def _update():
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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'
|
|
559
|
+
try:
|
|
560
|
+
self.text_area.config(state=tk.NORMAL)
|
|
561
|
+
|
|
562
|
+
tags = ['default']
|
|
563
|
+
|
|
564
|
+
# Process foreground color
|
|
565
|
+
if fg_color is not None and fg_color != "":
|
|
566
|
+
fg_color_lower = fg_color.lower()
|
|
567
|
+
if fg_color_lower in self.color_name_to_hex:
|
|
568
|
+
color_value = self.color_name_to_hex[fg_color_lower]
|
|
554
569
|
custom_fg_tag = f"custom_fg_{color_value}"
|
|
555
570
|
tags.append(custom_fg_tag)
|
|
556
571
|
if custom_fg_tag not in self.tag_names:
|
|
557
572
|
self.text_area.tag_configure(custom_fg_tag, foreground=color_value)
|
|
558
573
|
self.tag_names.add(custom_fg_tag)
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
tags.append(f"
|
|
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}")
|
|
574
|
+
elif fg_color.isdigit():
|
|
575
|
+
# ANSI code
|
|
576
|
+
if ('disabled_colors' not in self.config or
|
|
577
|
+
fg_color not in self.config.get('disabled_colors', [])):
|
|
578
|
+
if fg_color in self.ansi_colors:
|
|
579
|
+
tags.append(f"ansi_{fg_color}")
|
|
580
|
+
elif fg_color.startswith('#') and len(fg_color) in [4, 5, 7, 9]:
|
|
581
|
+
# Hex color
|
|
582
|
+
color_value = fg_color
|
|
583
|
+
custom_fg_tag = f"custom_fg_{color_value}"
|
|
584
|
+
tags.append(custom_fg_tag)
|
|
585
|
+
if custom_fg_tag not in self.tag_names:
|
|
586
|
+
self.text_area.tag_configure(custom_fg_tag, foreground=color_value)
|
|
587
|
+
self.tag_names.add(custom_fg_tag)
|
|
588
|
+
elif ';' in fg_color and fg_color.startswith('38;'):
|
|
589
|
+
# Extended ANSI color codes
|
|
590
|
+
if fg_color in self.ansi_colors:
|
|
591
|
+
tags.append(f"ansi_{fg_color}")
|
|
595
592
|
else:
|
|
596
|
-
#
|
|
597
|
-
|
|
593
|
+
# Try named color
|
|
594
|
+
try:
|
|
595
|
+
if fg_color.strip():
|
|
596
|
+
self.text_area.tag_configure(f"custom_fg_{fg_color}", foreground=fg_color)
|
|
597
|
+
tags.append(f"custom_fg_{fg_color}")
|
|
598
|
+
self.tag_names.add(f"custom_fg_{fg_color}")
|
|
599
|
+
except tk.TclError:
|
|
600
|
+
pass
|
|
601
|
+
|
|
602
|
+
# Process background color
|
|
603
|
+
if bg_color is not None and bg_color != "":
|
|
604
|
+
bg_color_lower = bg_color.lower()
|
|
605
|
+
if bg_color_lower in self.color_name_to_hex:
|
|
606
|
+
color_value = self.color_name_to_hex[bg_color_lower]
|
|
598
607
|
custom_bg_tag = f"custom_bg_{color_value}"
|
|
599
608
|
tags.append(custom_bg_tag)
|
|
600
609
|
if custom_bg_tag not in self.tag_names:
|
|
601
610
|
self.text_area.tag_configure(custom_bg_tag, background=color_value)
|
|
602
611
|
self.tag_names.add(custom_bg_tag)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
tags.append(f"
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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}"
|
|
612
|
+
elif bg_color.isdigit():
|
|
613
|
+
# ANSI code
|
|
614
|
+
if ('disabled_colors' not in self.config or
|
|
615
|
+
bg_color not in self.config.get('disabled_colors', [])):
|
|
616
|
+
if bg_color in self.ansi_colors:
|
|
617
|
+
tags.append(f"ansi_{bg_color}")
|
|
618
|
+
elif bg_color.startswith('#') and len(bg_color) in [4, 5, 7, 9]:
|
|
619
|
+
# Hex color
|
|
620
|
+
color_value = bg_color
|
|
621
|
+
custom_bg_tag = f"custom_bg_{color_value}"
|
|
622
|
+
tags.append(custom_bg_tag)
|
|
623
|
+
if custom_bg_tag not in self.tag_names:
|
|
624
|
+
self.text_area.tag_configure(custom_bg_tag, background=color_value)
|
|
625
|
+
self.tag_names.add(custom_bg_tag)
|
|
626
|
+
elif ';' in bg_color and bg_color.startswith('48;'):
|
|
627
|
+
# Extended ANSI color codes
|
|
628
|
+
if bg_color in self.ansi_colors:
|
|
629
|
+
tags.append(f"ansi_{bg_color}")
|
|
630
|
+
else:
|
|
631
|
+
# Try named color
|
|
632
|
+
try:
|
|
633
|
+
if bg_color.strip():
|
|
634
|
+
self.text_area.tag_configure(f"custom_bg_{bg_color}", background=bg_color)
|
|
635
|
+
tags.append(f"custom_bg_{bg_color}")
|
|
636
|
+
self.tag_names.add(f"custom_bg_{bg_color}")
|
|
637
|
+
except tk.TclError:
|
|
638
|
+
pass
|
|
633
639
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
640
|
+
# Process font
|
|
641
|
+
if font_family or font_size or font_style:
|
|
642
|
+
font_family_val = font_family or "Courier"
|
|
643
|
+
font_size_val = font_size or 10
|
|
644
|
+
font_style_val = font_style or "normal"
|
|
645
|
+
font_key = f"font_{font_family_val}_{font_size_val}_{font_style_val}"
|
|
646
|
+
|
|
647
|
+
if font_key not in self.tag_names:
|
|
648
|
+
self.text_area.tag_configure(font_key,
|
|
649
|
+
font=(font_family_val, font_size_val, font_style_val))
|
|
650
|
+
self.tag_names.add(font_key)
|
|
651
|
+
tags.append(font_key)
|
|
652
|
+
|
|
653
|
+
# Process styles
|
|
654
|
+
if bold:
|
|
655
|
+
tags.append('bold')
|
|
656
|
+
if underline:
|
|
657
|
+
tags.append('underline')
|
|
658
|
+
if italic:
|
|
659
|
+
tags.append('italic')
|
|
660
|
+
if strikethrough:
|
|
661
|
+
tags.append('strikethrough')
|
|
662
|
+
if reverse:
|
|
663
|
+
tags.append('reverse')
|
|
664
|
+
|
|
665
|
+
self.text_area.insert(tk.END, str(text) + "\n", tuple(tags))
|
|
666
|
+
self.text_area.config(state=tk.DISABLED)
|
|
667
|
+
self.text_area.see(tk.END)
|
|
668
|
+
except tk.TclError as e:
|
|
669
|
+
if self.running:
|
|
670
|
+
self._safe_print(f"Tkinter error in display_colored: {e}")
|
|
671
|
+
except Exception as e:
|
|
672
|
+
if self.running:
|
|
673
|
+
self._safe_print(f"Error in display_colored: {e}")
|
|
674
|
+
|
|
675
|
+
if self.running:
|
|
676
|
+
self.root.after(0, _update)
|
|
656
677
|
|
|
657
678
|
def _demo_colors(self) -> None:
|
|
658
679
|
"""Display ANSI color demo"""
|
|
659
680
|
demo_texts = [
|
|
660
|
-
("\
|
|
661
|
-
("\
|
|
662
|
-
(" \
|
|
663
|
-
(" \
|
|
664
|
-
("\n\
|
|
665
|
-
(" \
|
|
666
|
-
(" \
|
|
667
|
-
("\n\
|
|
668
|
-
(" \
|
|
669
|
-
(" \
|
|
670
|
-
("\n\
|
|
671
|
-
(" \
|
|
672
|
-
(" \
|
|
673
|
-
("\n\
|
|
674
|
-
(" \
|
|
675
|
-
("\n\
|
|
676
|
-
(" \
|
|
677
|
-
("\n\
|
|
678
|
-
(" \
|
|
679
|
-
(" \
|
|
681
|
+
("\x1b[1mANSI Color Demo\x1b[0m\n", False),
|
|
682
|
+
("\x1b[1;37mBasic Colors:\x1b[0m\n", True),
|
|
683
|
+
(" \x1b[30mBlack\x1b[0m \x1b[31mRed\x1b[0m \x1b[32mGreen\x1b[0m \x1b[33mYellow\x1b[0m\n", True),
|
|
684
|
+
(" \x1b[34mBlue\x1b[0m \x1b[35mMagenta\x1b[0m \x1b[36mCyan\x1b[0m \x1b[37mWhite\x1b[0m\n", True),
|
|
685
|
+
("\n\x1b[1;37mBright Colors:\x1b[0m\n", True),
|
|
686
|
+
(" \x1b[90mGray\x1b[0m \x1b[91mBright Red\x1b[0m \x1b[92mBright Green\x1b[0m\n", True),
|
|
687
|
+
(" \x1b[93mBright Yellow\x1b[0m \x1b[94mBright Blue\x1b[0m \x1b[95mBright Magenta\x1b[0m\n", True),
|
|
688
|
+
("\n\x1b[1;37mBackground Colors:\x1b[0m\n", True),
|
|
689
|
+
(" \x1b[40;37mBlack BG\x1b[0m \x1b[41mRed BG\x1b[0m \x1b[42mGreen BG\x1b[0m\n", True),
|
|
690
|
+
(" \x1b[43mYellow BG\x1b[0m \x1b[44mBlue BG\x1b[0m \x1b[45mMagenta BG\x1b[0m\n", True),
|
|
691
|
+
("\n\x1b[1;37mExtended Colors:\x1b[0m\n", True),
|
|
692
|
+
(" \x1b[38;5;1mDark Red\x1b[0m \x1b[38;5;9mRed\x1b[0m \x1b[38;5;10mGreen\x1b[0m \x1b[38;5;12mBlue\x1b[0m\n", True),
|
|
693
|
+
(" \x1b[48;5;1mDark Red BG\x1b[0m \x1b[48;5;9mRed BG\x1b[0m\n", True),
|
|
694
|
+
("\n\x1b[1;37mTrue Colors (RGB):\x1b[0m\n", True),
|
|
695
|
+
(" \x1b[38;2;255;0;0mRed\x1b[0m \x1b[38;2;0;255;0mGreen\x1b[0m \x1b[38;2;0;0;255mBlue\x1b[0m\n", True),
|
|
696
|
+
("\n\x1b[1;37mText Styles:\x1b[0m\n", True),
|
|
697
|
+
(" \x1b[1mBold\x1b[0m \x1b[3mItalic\x1b[0m \x1b[4mUnderline\x1b[0m \x1b[9mStrikethrough\x1b[0m\n", True),
|
|
698
|
+
("\n\x1b[1;37mCombined Styles:\x1b[0m\n", True),
|
|
699
|
+
(" \x1b[1;31mBold Red\x1b[0m \x1b[1;4;32mBold Underlined Green\x1b[0m\n", True),
|
|
700
|
+
(" \x1b[1;33;44mBold Yellow on Blue\x1b[0m \x1b[1;37;41mBold White on Red\x1b[0m\n", True),
|
|
680
701
|
]
|
|
681
702
|
|
|
682
703
|
for text, parse_ansi in demo_texts:
|
|
@@ -685,36 +706,46 @@ class Py2GUI:
|
|
|
685
706
|
def set_theme(self, theme_name: str) -> None:
|
|
686
707
|
"""Set theme"""
|
|
687
708
|
def _set_theme():
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
709
|
+
try:
|
|
710
|
+
if theme_name == "dark":
|
|
711
|
+
self.text_area.config(bg="black", fg="white", insertbackground="white")
|
|
712
|
+
self.text_area.tag_configure("default", foreground="white", background="black")
|
|
713
|
+
elif theme_name == "light":
|
|
714
|
+
self.text_area.config(bg="white", fg="black", insertbackground="black")
|
|
715
|
+
self.text_area.tag_configure("default", foreground="black", background="white")
|
|
716
|
+
elif theme_name == "matrix":
|
|
717
|
+
self.text_area.config(bg="black", fg="#00ff00", insertbackground="#00ff00")
|
|
718
|
+
self.text_area.tag_configure("default", foreground="#00ff00", background="black")
|
|
719
|
+
else: # Default theme
|
|
720
|
+
self.text_area.config(bg="black", fg="white", insertbackground="white")
|
|
721
|
+
self.text_area.tag_configure("default", foreground="white", background="black")
|
|
722
|
+
|
|
723
|
+
self.text_area.see(tk.END)
|
|
724
|
+
except tk.TclError as e:
|
|
725
|
+
if self.running:
|
|
726
|
+
self._safe_print(f"Tkinter error setting theme: {e}")
|
|
702
727
|
|
|
703
|
-
self.
|
|
728
|
+
if self.running:
|
|
729
|
+
self.root.after(0, _set_theme)
|
|
704
730
|
|
|
705
731
|
def user_write(self, prompt: str = "Input:") -> Optional[str]:
|
|
706
|
-
"""Thread-safe input dialog (opens in
|
|
732
|
+
"""Thread-safe input dialog (opens in new window)"""
|
|
707
733
|
if not self.running:
|
|
708
734
|
return None
|
|
735
|
+
|
|
709
736
|
def _ask():
|
|
710
737
|
try:
|
|
711
738
|
result = simpledialog.askstring("Input", prompt, parent=self.root)
|
|
712
739
|
self.input_queue.put(result)
|
|
713
740
|
except tk.TclError:
|
|
714
741
|
self.input_queue.put(None)
|
|
742
|
+
except Exception as e:
|
|
743
|
+
self._safe_print(f"Error in user_write dialog: {e}")
|
|
744
|
+
self.input_queue.put(None)
|
|
745
|
+
|
|
715
746
|
self.root.after(0, _ask)
|
|
716
747
|
|
|
717
|
-
# Wait for input
|
|
748
|
+
# Wait for input, but check if still running
|
|
718
749
|
while self.running:
|
|
719
750
|
try:
|
|
720
751
|
return self.input_queue.get(timeout=0.1)
|
|
@@ -723,40 +754,40 @@ class Py2GUI:
|
|
|
723
754
|
return None
|
|
724
755
|
|
|
725
756
|
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
|
-
"""
|
|
757
|
+
"""Thread-safe terminal-style input (embedded in main window)"""
|
|
730
758
|
if not self.running:
|
|
731
759
|
return None
|
|
732
760
|
|
|
733
761
|
def _prepare_input():
|
|
734
762
|
try:
|
|
735
|
-
# Update
|
|
763
|
+
# Update prompt in label
|
|
736
764
|
self.input_label.config(text=prompt)
|
|
737
765
|
|
|
738
|
-
# Clear
|
|
766
|
+
# Clear previous input
|
|
739
767
|
self.input_var.set("")
|
|
740
768
|
|
|
741
|
-
# Focus
|
|
769
|
+
# Focus input field
|
|
742
770
|
self.input_entry.focus_set()
|
|
743
771
|
|
|
744
|
-
# Enable
|
|
772
|
+
# Enable input field
|
|
745
773
|
self.input_entry.config(state=tk.NORMAL)
|
|
746
|
-
except tk.TclError:
|
|
747
|
-
|
|
774
|
+
except tk.TclError as e:
|
|
775
|
+
if self.running:
|
|
776
|
+
self._safe_print(f"Tkinter error preparing input: {e}")
|
|
748
777
|
|
|
749
|
-
# Clear
|
|
778
|
+
# Clear old values from queue
|
|
750
779
|
while not self.type_in_queue.empty():
|
|
751
780
|
try:
|
|
752
781
|
self.type_in_queue.get_nowait()
|
|
753
782
|
except queue.Empty:
|
|
754
783
|
break
|
|
755
784
|
|
|
756
|
-
# Prepare
|
|
785
|
+
# Prepare input field
|
|
757
786
|
try:
|
|
758
787
|
self.root.after(0, _prepare_input)
|
|
759
|
-
except tk.TclError:
|
|
788
|
+
except tk.TclError as e:
|
|
789
|
+
if self.running:
|
|
790
|
+
self._safe_print(f"Tkinter error scheduling input preparation: {e}")
|
|
760
791
|
return None
|
|
761
792
|
|
|
762
793
|
# Block and wait for user input
|
|
@@ -768,68 +799,100 @@ class Py2GUI:
|
|
|
768
799
|
return None
|
|
769
800
|
|
|
770
801
|
def _on_enter_pressed(self, event: Optional[tk.Event] = None) -> str:
|
|
771
|
-
"""Handle Enter key press in
|
|
802
|
+
"""Handle Enter key press in input field"""
|
|
772
803
|
self._on_send_input()
|
|
773
804
|
return "break" # Prevent default behavior
|
|
774
805
|
|
|
775
806
|
def _on_send_input(self) -> None:
|
|
776
|
-
"""Send
|
|
777
|
-
|
|
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)
|
|
807
|
+
"""Send input from terminal-style input field"""
|
|
808
|
+
try:
|
|
809
|
+
user_input = self.input_var.get().strip()
|
|
788
810
|
|
|
789
|
-
|
|
790
|
-
|
|
811
|
+
if user_input:
|
|
812
|
+
# Put input in queue
|
|
813
|
+
self.type_in_queue.put(user_input)
|
|
814
|
+
|
|
815
|
+
# Display user input in output area
|
|
816
|
+
self.text_area.config(state=tk.NORMAL)
|
|
817
|
+
self.text_area.insert(tk.END, f"{self.input_label.cget('text')}{user_input}\n", 'default')
|
|
818
|
+
self.text_area.config(state=tk.DISABLED)
|
|
819
|
+
self.text_area.see(tk.END)
|
|
820
|
+
|
|
821
|
+
# Clear input field
|
|
822
|
+
self.input_var.set("")
|
|
823
|
+
except tk.TclError as e:
|
|
824
|
+
if self.running:
|
|
825
|
+
self._safe_print(f"Tkinter error sending input: {e}")
|
|
826
|
+
except Exception as e:
|
|
827
|
+
if self.running:
|
|
828
|
+
self._safe_print(f"Error sending input: {e}")
|
|
791
829
|
|
|
792
830
|
def _clear_input(self) -> None:
|
|
793
|
-
"""Clear
|
|
794
|
-
|
|
795
|
-
|
|
831
|
+
"""Clear terminal-style input field"""
|
|
832
|
+
try:
|
|
833
|
+
self.input_var.set("")
|
|
834
|
+
self.input_entry.focus_set()
|
|
835
|
+
except tk.TclError as e:
|
|
836
|
+
if self.running:
|
|
837
|
+
self._safe_print(f"Tkinter error clearing input: {e}")
|
|
796
838
|
|
|
797
839
|
def focus_input(self) -> None:
|
|
798
|
-
"""Set focus to
|
|
799
|
-
|
|
840
|
+
"""Set focus to terminal-style input field"""
|
|
841
|
+
try:
|
|
842
|
+
self.input_entry.focus_set()
|
|
843
|
+
except tk.TclError as e:
|
|
844
|
+
if self.running:
|
|
845
|
+
self._safe_print(f"Tkinter error focusing input: {e}")
|
|
800
846
|
|
|
801
847
|
def clear(self) -> None:
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
848
|
+
"""Clear output area"""
|
|
849
|
+
try:
|
|
850
|
+
self.text_area.config(state=tk.NORMAL)
|
|
851
|
+
self.text_area.delete(1.0, tk.END)
|
|
852
|
+
self.text_area.config(state=tk.DISABLED)
|
|
853
|
+
except tk.TclError as e:
|
|
854
|
+
if self.running:
|
|
855
|
+
self._safe_print(f"Tkinter error clearing text: {e}")
|
|
805
856
|
|
|
806
857
|
def copy_text(self) -> None:
|
|
858
|
+
"""Copy selected text"""
|
|
807
859
|
try:
|
|
808
860
|
selected = self.text_area.selection_get()
|
|
809
861
|
self.root.clipboard_clear()
|
|
810
862
|
self.root.clipboard_append(selected)
|
|
811
863
|
except tk.TclError:
|
|
812
|
-
pass
|
|
864
|
+
pass # No text selected
|
|
865
|
+
except Exception as e:
|
|
866
|
+
if self.running:
|
|
867
|
+
self._safe_print(f"Error copying text: {e}")
|
|
813
868
|
|
|
814
869
|
def select_all(self) -> None:
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
870
|
+
"""Select all text"""
|
|
871
|
+
try:
|
|
872
|
+
self.text_area.config(state=tk.NORMAL)
|
|
873
|
+
self.text_area.tag_add(tk.SEL, "1.0", tk.END)
|
|
874
|
+
self.text_area.config(state=tk.DISABLED)
|
|
875
|
+
self.text_area.mark_set(tk.INSERT, "1.0")
|
|
876
|
+
self.text_area.see(tk.INSERT)
|
|
877
|
+
except tk.TclError as e:
|
|
878
|
+
if self.running:
|
|
879
|
+
self._safe_print(f"Tkinter error selecting all: {e}")
|
|
820
880
|
|
|
821
881
|
def exit(self) -> None:
|
|
882
|
+
"""Exit GUI"""
|
|
822
883
|
self.running = False
|
|
823
884
|
try:
|
|
824
885
|
self.root.quit()
|
|
825
886
|
self.root.destroy()
|
|
826
887
|
except tk.TclError:
|
|
827
888
|
pass # Already destroyed
|
|
889
|
+
except Exception as e:
|
|
890
|
+
self._safe_print(f"Error exiting GUI: {e}")
|
|
828
891
|
|
|
829
892
|
def run(self, func: Optional[Callable] = None, *args, **kwargs) -> Any:
|
|
830
893
|
"""
|
|
831
|
-
Run
|
|
832
|
-
|
|
894
|
+
Run GUI and optional worker function
|
|
895
|
+
Tkinter operations stay in main thread; logic runs in another thread
|
|
833
896
|
"""
|
|
834
897
|
result_queue = queue.Queue()
|
|
835
898
|
|
|
@@ -841,13 +904,17 @@ class Py2GUI:
|
|
|
841
904
|
except Exception as e:
|
|
842
905
|
self.display(f"Error: {e}\n{traceback.format_exc()}")
|
|
843
906
|
result_queue.put(e)
|
|
844
|
-
threading.Thread(target=worker, daemon=True)
|
|
907
|
+
thread = threading.Thread(target=worker, daemon=True)
|
|
908
|
+
thread.start()
|
|
845
909
|
|
|
846
910
|
try:
|
|
847
911
|
self.root.mainloop()
|
|
848
912
|
except KeyboardInterrupt:
|
|
849
913
|
self.exit()
|
|
850
|
-
|
|
914
|
+
except Exception as e:
|
|
915
|
+
self._safe_print(f"Error in mainloop: {e}")
|
|
916
|
+
self.exit()
|
|
917
|
+
|
|
851
918
|
if func:
|
|
852
919
|
# Wait for function to complete, but make it interruptible
|
|
853
920
|
while self.running:
|
|
@@ -859,17 +926,67 @@ class Py2GUI:
|
|
|
859
926
|
|
|
860
927
|
|
|
861
928
|
# Global instance and helper functions
|
|
862
|
-
_gui_instance =
|
|
929
|
+
_gui_instance = None
|
|
930
|
+
|
|
931
|
+
def _get_instance() -> Py2GUI:
|
|
932
|
+
"""Get or create global instance"""
|
|
933
|
+
global _gui_instance
|
|
934
|
+
if _gui_instance is None or not _gui_instance.running:
|
|
935
|
+
_gui_instance = Py2GUI()
|
|
936
|
+
return _gui_instance
|
|
937
|
+
|
|
938
|
+
def display(text: str, parse_ansi: bool = True, font_family: Optional[str] = None,
|
|
939
|
+
font_size: Optional[int] = None, font_style: Optional[str] = None) -> None:
|
|
940
|
+
"""Display text (auto newline)"""
|
|
941
|
+
_get_instance().display(text, parse_ansi, font_family, font_size, font_style)
|
|
942
|
+
|
|
943
|
+
def display_colored(text: str, fg_color: Optional[str] = None, bg_color: Optional[str] = None,
|
|
944
|
+
bold: bool = False, underline: bool = False, italic: bool = False,
|
|
945
|
+
strikethrough: bool = False, reverse: bool = False,
|
|
946
|
+
font_family: Optional[str] = None, font_size: Optional[int] = None,
|
|
947
|
+
font_style: Optional[str] = None) -> None:
|
|
948
|
+
"""Directly display colored text"""
|
|
949
|
+
_get_instance().display_colored(text, fg_color, bg_color, bold, underline,
|
|
950
|
+
italic, strikethrough, reverse, font_family,
|
|
951
|
+
font_size, font_style)
|
|
952
|
+
|
|
953
|
+
def display_paragraph(text: str, parse_ansi: bool = True, font_family: Optional[str] = None,
|
|
954
|
+
font_size: Optional[int] = None, font_style: Optional[str] = None) -> None:
|
|
955
|
+
"""Display paragraph (no auto newline)"""
|
|
956
|
+
_get_instance().display_paragraph(text, parse_ansi, font_family, font_size, font_style)
|
|
957
|
+
|
|
958
|
+
def user_write(prompt: str = "Input:") -> Optional[str]:
|
|
959
|
+
"""User input (dialog)"""
|
|
960
|
+
return _get_instance().user_write(prompt)
|
|
961
|
+
|
|
962
|
+
def user_type_in(prompt: str = ">> ") -> Optional[str]:
|
|
963
|
+
"""User input (terminal-style)"""
|
|
964
|
+
return _get_instance().user_type_in(prompt)
|
|
965
|
+
|
|
966
|
+
def clear() -> None:
|
|
967
|
+
"""Clear output"""
|
|
968
|
+
_get_instance().clear()
|
|
969
|
+
|
|
970
|
+
def copy_text() -> None:
|
|
971
|
+
"""Copy text"""
|
|
972
|
+
_get_instance().copy_text()
|
|
973
|
+
|
|
974
|
+
def select_all() -> None:
|
|
975
|
+
"""Select all"""
|
|
976
|
+
_get_instance().select_all()
|
|
977
|
+
|
|
978
|
+
def exit_gui() -> None:
|
|
979
|
+
"""Exit GUI"""
|
|
980
|
+
_get_instance().exit()
|
|
981
|
+
|
|
982
|
+
def run(func: Optional[Callable] = None, *args, **kwargs) -> Any:
|
|
983
|
+
"""Run GUI"""
|
|
984
|
+
return _get_instance().run(func, *args, **kwargs)
|
|
985
|
+
|
|
986
|
+
def focus_input() -> None:
|
|
987
|
+
"""Focus on input field"""
|
|
988
|
+
_get_instance().focus_input()
|
|
863
989
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
|
990
|
+
def set_theme(theme_name: str) -> None:
|
|
991
|
+
"""Set theme"""
|
|
992
|
+
_get_instance().set_theme(theme_name)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
py2gui/game_example.py,sha256=jl_Ko5_UcYxjEpDpWrf-K8mYERgXII431xlhIaOvvrA,26666
|
|
2
|
+
py2gui/py2gui.py,sha256=sJmGoRCfI-N_64eTplvuyXEzduB-B6A4uGtBZXxmd2E,42964
|
|
3
|
+
py2gui/test.py,sha256=eWdoQUIKF_VVyaw6fckkN6jZu5-D8IkcqBQzDkBlKXM,4353
|
|
4
|
+
py2gui-0.1.1.1.dist-info/licenses/LICENSE,sha256=9SfUknNU1t6kFscrCqKPf3vPbcxbyq-jPtX6J_mpgoo,1065
|
|
5
|
+
py2gui-0.1.1.1.dist-info/METADATA,sha256=PShgpKtfh1zmC_hlx-pGh7dvQOpIF_oOgonh_7vwT-8,224
|
|
6
|
+
py2gui-0.1.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
+
py2gui-0.1.1.1.dist-info/top_level.txt,sha256=NFgf8RRbdgAbdKXmuKobDL9dUeIc-911JuRuzzbR-7w,7
|
|
8
|
+
py2gui-0.1.1.1.dist-info/RECORD,,
|
py2gui-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
py2gui/game_example.py,sha256=jl_Ko5_UcYxjEpDpWrf-K8mYERgXII431xlhIaOvvrA,26666
|
|
2
|
-
py2gui/py2gui.py,sha256=wtGvNzCPccDFXI_9YpXfhuxF7NB1TcCt3Qc4naZmNAQ,38182
|
|
3
|
-
py2gui/test.py,sha256=eWdoQUIKF_VVyaw6fckkN6jZu5-D8IkcqBQzDkBlKXM,4353
|
|
4
|
-
py2gui-0.1.0.dist-info/licenses/LICENSE,sha256=9SfUknNU1t6kFscrCqKPf3vPbcxbyq-jPtX6J_mpgoo,1065
|
|
5
|
-
py2gui-0.1.0.dist-info/METADATA,sha256=egNc7muDEgGXQyyqX6K5_lOEGcyZy-NsaTWAgsJR9gw,215
|
|
6
|
-
py2gui-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
-
py2gui-0.1.0.dist-info/top_level.txt,sha256=NFgf8RRbdgAbdKXmuKobDL9dUeIc-911JuRuzzbR-7w,7
|
|
8
|
-
py2gui-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|