-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwriters_toolkit_original.py
1826 lines (1505 loc) · 80.2 KB
/
writers_toolkit_original.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# python -B writers_toolkit.json
# tools_config.json file must exist
import subprocess
import argparse
import os
import sys
import re
import json
import asyncio
import platform
import time
import uuid
import urllib.parse
from copy import deepcopy
from nicegui import ui, run, app
from file_folder_local_picker import local_file_picker
import editor_module
HOST = "127.0.0.1"
PORT = 8081
# Default projects directory - all projects must be in this folder
PROJECTS_DIR = os.path.expanduser("~/writing")
# Ensure the projects directory exists
os.makedirs(PROJECTS_DIR, exist_ok=True)
# Default save directory - initially set to projects directory
DEFAULT_SAVE_DIR = PROJECTS_DIR
# Current project name and path
CURRENT_PROJECT = None
CURRENT_PROJECT_PATH = None
# Original:
# Default save directory
# DEFAULT_SAVE_DIR = os.path.expanduser("~")
# Default JSON file path for tool configurations
TOOLS_JSON_PATH = "tools_config.json"
# Add a global variable for the timer task
timer_task = None
# A simple global to mark which tool is running (or None if none).
tool_in_progress = None
# We'll collect references to all "Run" buttons here so we can disable/enable them.
run_buttons = []
def open_file_in_editor(file_path):
"""Open a file in the integrated text editor in a new tab."""
try:
encoded_path = urllib.parse.quote(os.path.abspath(file_path))
ui.navigate.to(f"/editor?file={encoded_path}", new_tab=True)
except Exception as e:
ui.notify(f"Error opening file: {str(e)}", type="negative")
###############################################################################
# FUNCTION: Simplified JSON Config handling with Integer Enforcement
###############################################################################
def load_tools_config(force_reload=False):
"""
Load tool configurations from the JSON file.
Also loads global settings if available.
Args:
force_reload: If True, bypasses any caching and reloads directly from disk
Returns:
Dictionary of tool configurations or empty dict if file not found/invalid
"""
global DEFAULT_SAVE_DIR, CURRENT_PROJECT, CURRENT_PROJECT_PATH # Ensure we can modify the global variables
if not os.path.exists(TOOLS_JSON_PATH):
ui.notify(f"Error: Configuration file not found at {TOOLS_JSON_PATH}", type="negative")
return {}
try:
with open(TOOLS_JSON_PATH, 'r') as f:
config = json.load(f)
# Check for global settings and update DEFAULT_SAVE_DIR if available
if "_global_settings" in config:
global_settings = config["_global_settings"]
# Load project settings
if "current_project" in global_settings:
CURRENT_PROJECT = global_settings["current_project"]
if "current_project_path" in global_settings:
loaded_path = os.path.expanduser(global_settings["current_project_path"])
# Validate the path is within PROJECTS_DIR
if os.path.exists(loaded_path) and os.path.commonpath([loaded_path, PROJECTS_DIR]) == PROJECTS_DIR:
CURRENT_PROJECT_PATH = loaded_path
else:
# Path is invalid or outside PROJECTS_DIR
ui.notify(f"Warning: Saved project path is not within {PROJECTS_DIR}", type="warning")
CURRENT_PROJECT = None
CURRENT_PROJECT_PATH = None
# Prefer to use project path for save dir if available
if "default_save_dir" in global_settings:
saved_dir = os.path.expanduser(global_settings["default_save_dir"])
# Validate the save directory is within PROJECTS_DIR
if os.path.exists(saved_dir) and os.path.commonpath([saved_dir, PROJECTS_DIR]) == PROJECTS_DIR:
DEFAULT_SAVE_DIR = saved_dir
else:
# Path is outside PROJECTS_DIR, fallback to PROJECTS_DIR
ui.notify(f"Warning: Saved directory is not within {PROJECTS_DIR}", type="warning")
DEFAULT_SAVE_DIR = PROJECTS_DIR
elif CURRENT_PROJECT_PATH:
# If no save dir but we have a project path, use that
DEFAULT_SAVE_DIR = CURRENT_PROJECT_PATH
else:
# Fallback to projects directory
DEFAULT_SAVE_DIR = PROJECTS_DIR
return config
except Exception as e:
print(f"\nError loading JSON config:\n{str(e)}\n")
ui.notify(f"Error loading JSON config: {str(e)}", type="negative")
return {}
def save_global_settings(settings_dict):
"""
Save global application settings to the config file.
Args:
settings_dict: Dictionary of global settings to save
Returns:
Boolean indicating success or failure
"""
try:
# Read the current config - FORCE RELOAD to get the latest version
config = load_tools_config(force_reload=True)
# If we got an empty config, something went wrong - don't save it
if not config:
ui.notify("Error: Cannot save settings because config file couldn't be loaded", type="negative")
return False
# Create _global_settings section if it doesn't exist
if "_global_settings" not in config:
config["_global_settings"] = {}
# Update the global settings with the provided values
config["_global_settings"].update(settings_dict)
# Sanity check: make sure we're not overwriting with just global settings
# This ensures we're not accidentally removing tool configurations
if len(config.keys()) <= 1: # Only _global_settings or empty
backup_path = f"{TOOLS_JSON_PATH}.before_error"
try:
with open(TOOLS_JSON_PATH, 'r') as src, open(backup_path, 'w') as dst:
dst.write(src.read())
ui.notify(f"Error: Configuration would be invalid. Created safety backup at {backup_path}",
type="negative")
except Exception:
pass
return False
result = save_tools_config(config)
return result
except Exception as e:
ui.notify(f"Error saving global settings: {str(e)}", type="negative")
return False
def ensure_integer_values(obj):
"""
Recursively ensure all numeric values are integers, not floats.
Args:
obj: Any Python object (dict, list, etc.)
Returns:
The same object with all float values converted to integers
"""
if isinstance(obj, float):
# Convert all floats to integers
return int(obj)
elif isinstance(obj, dict):
return {k: ensure_integer_values(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [ensure_integer_values(item) for item in obj]
else:
return obj
def save_tools_config(config):
"""
Save tool configurations to the JSON file.
Converts all numeric values to integers before saving.
Args:
config: Dictionary of tool configurations
Returns:
Boolean indicating success or failure
"""
try:
# SAFETY CHECK - never save an empty configuration
if not config or len(config) == 0:
ui.notify("Error: Refusing to save empty configuration", type="negative")
return False
# Ensure we're not just saving _global_settings
has_real_tools = False
for key in config.keys():
if not key.startswith('_'):
has_real_tools = True
break
if not has_real_tools:
ui.notify("Error: Configuration contains no tool definitions", type="negative")
return False
# Create a backup of the current file if it exists
if os.path.exists(TOOLS_JSON_PATH):
backup_path = f"{TOOLS_JSON_PATH}.bak"
try:
with open(TOOLS_JSON_PATH, 'r') as src, open(backup_path, 'w') as dst:
dst.write(src.read())
except Exception as e:
ui.notify(f"Warning: Failed to create backup file: {str(e)}", type="warning")
# Convert all floats to integers
integer_config = ensure_integer_values(config)
# Write the entire configuration to the file
with open(TOOLS_JSON_PATH, 'w') as f:
json.dump(integer_config, f, indent=4)
return True
except Exception as e:
ui.notify(f"Error saving configuration: {str(e)}", type="negative")
return False
def update_tool_preferences(script_name, new_preferences):
"""
Update tool preferences using a simple load-modify-save approach.
Ensures all numeric values are integers.
Args:
script_name: The script filename
new_preferences: Dictionary of preference name-value pairs to update
Returns:
Boolean indicating success or failure
"""
try:
# LOAD: Read the entire configuration - force reload to get latest version
config = load_tools_config(force_reload=True)
# Check if the script exists in the configuration
if script_name not in config:
ui.notify(f"Tool {script_name} not found in configuration", type="negative")
return False
# MODIFY: Update the preferences
changes_made = False
processed_preferences = {}
# First process all preferences, converting floats to ints where needed
for name, value in new_preferences.items():
# Convert all floating-point values to integers if option type is int
option_found = False
for option in config[script_name]["options"]:
if option["name"] == name:
option_found = True
option_type = option.get("type", "str")
if option_type == "int" and isinstance(value, float):
processed_preferences[name] = int(value)
else:
processed_preferences[name] = value
break
if not option_found:
# If option not found, just pass the value as is
processed_preferences[name] = value
# Now update the configuration with the processed values
for name, new_value in processed_preferences.items():
# Find and update the option in the configuration
for option in config[script_name]["options"]:
if option["name"] == name:
# Ensure there's a default property
if "default" not in option:
option["default"] = ""
# Update the default value
option["default"] = new_value
changes_made = True
break
# Remove any legacy user_preferences section if it exists
if "user_preferences" in config[script_name]:
del config[script_name]["user_preferences"]
changes_made = True
if not changes_made:
# ui.notify("No changes were made to the configuration", type="info")
return True # Not an error, just no changes
# SAVE: Write the entire configuration back to the file
result = save_tools_config(config)
if result:
ui.notify(f"Default values updated for {script_name}", type="positive")
return result
except Exception as e:
ui.notify(f"Error updating preferences: {str(e)}", type="negative")
return False
###############################################################################
# FUNCTION: File Picker Integration
###############################################################################
async def select_file_or_folder(start_dir=None, multiple=False, dialog_title="Select Files or Folders", folders_only=False):
"""
Display a file/folder picker dialog.
Args:
start_dir: Initial directory to display
multiple: Allow multiple selections
dialog_title: Title for the dialog
folders_only: Only allow folder selection
Returns:
Selected file/folder path(s) or None/[] if cancelled
"""
if start_dir is None:
start_dir = DEFAULT_SAVE_DIR
# Ensure the directory exists
os.makedirs(os.path.expanduser(start_dir), exist_ok=True)
try:
# Pass the folders_only parameter to the local_file_picker
result = await local_file_picker(
start_dir,
multiple=multiple,
folders_only=folders_only
)
if result and not multiple:
# If we're not allowing multiple selections, return the first item
return result[0]
return result
except Exception as e:
ui.notify(f"Error selecting files: {str(e)}", type="negative")
return [] if multiple else None
###############################################################################
# FUNCTION: Argument Parsing and Script Runner
###############################################################################
def create_parser_for_tool(script_name, options):
"""
Create an argparse parser for a specific tool based on its options.
Uses the explicit type information from the JSON config.
Args:
script_name: The name of the script
options: List of option dictionaries from the JSON config
Returns:
An argparse.ArgumentParser object configured for the tool
"""
# Create a parser for the tool
parser = argparse.ArgumentParser(
prog=f"python {script_name}",
description=f"Run {script_name} with specified options"
)
# Add arguments for each option
for option in options:
name = option["name"]
description = option["description"]
required = option.get("required", False)
default = option.get("default", None)
arg_name = option.get("arg_name", "")
option_type = option.get("type", "str") # Get the explicit type, default to "str"
choices = option.get("choices", None) # Optional list of choices
# Handle different types of arguments based on the explicit type
if option_type == "bool":
# Boolean flags
parser.add_argument(
name,
action="store_true",
help=description
)
elif option_type == "int":
# Integer values
parser.add_argument(
name,
metavar=arg_name,
type=int,
help=description,
required=required,
default=default,
choices=choices
)
elif option_type == "float":
# Float values
parser.add_argument(
name,
metavar=arg_name,
type=float,
help=description,
required=required,
default=default,
choices=choices
)
elif option_type == "choices":
# Choice from a list of values
parser.add_argument(
name,
metavar=arg_name,
help=description,
required=required,
default=default,
choices=choices
)
else:
# Default to string for all other types
parser.add_argument(
name,
metavar=arg_name,
help=description,
required=required,
default=default,
choices=choices
)
return parser
def run_tool(script_name, args_dict, log_output=None):
"""
Run a tool script with the provided arguments and track output files.
Args:
script_name: The script filename to run
args_dict: Dictionary of argument name-value pairs
log_output: A ui.log component to output to in real-time
Returns:
Tuple of (stdout, stderr, created_files) from the subprocess
"""
# Get the tool configuration to determine types
config = load_tools_config()
options = config.get(script_name, {}).get("options", [])
# Create a mapping of option names to their types
option_types = {opt["name"]: opt.get("type", "str") for opt in options}
# Generate a unique ID for this tool run
run_uuid = str(uuid.uuid4())
# Create a path for the output tracking file with absolute path
current_dir = os.path.abspath('.')
tracking_file = os.path.join(current_dir, f"{run_uuid}.txt")
tracking_file = os.path.abspath(tracking_file)
if log_output:
log_output.push(f"DEBUG: Creating tracking file: {tracking_file}")
log_output.push(f"DEBUG: Directory exists: {os.path.exists(os.path.dirname(tracking_file))}")
log_output.push(f"DEBUG: Directory is writable: {os.access(os.path.dirname(tracking_file), os.W_OK)}")
# Add the tracking file parameter to the tool arguments
args_dict["--output_tracking"] = tracking_file
# Convert the arguments dictionary into a command-line argument list
args_list = []
for name, value in args_dict.items():
# Get the option type, default to "str"
option_type = option_types.get(name, "str")
# Handle different types
if option_type == "int" and isinstance(value, float):
value = int(value)
elif option_type == "bool":
if value:
# Just add the flag for boolean options
args_list.append(name)
continue # Skip adding value for boolean options
# For all non-boolean options, add the name and value
if value is not None:
args_list.append(name)
args_list.append(str(value))
# Determine the Python executable based on platform
if platform.system() == 'Windows':
python_exe = 'python' # Using python directly; change to 'py' if needed
else:
python_exe = 'python'
# Construct the full command: python script_name [args]
cmd = [python_exe, "-u", script_name] + args_list
if log_output:
# Log the command
log_output.push(f"Running command: {' '.join(cmd)}")
log_output.push("Working...")
# Run the command and capture output
result = subprocess.run(cmd, capture_output=True, text=True)
if log_output:
if result.stdout:
log_output.push(result.stdout)
if result.stderr:
log_output.push(f"ERROR: {result.stderr}")
log_output.push(f"\nProcess finished with return code {result.returncode}")
log_output.push("Done!")
# Check for the output tracking file
created_files = []
if os.path.exists(tracking_file):
try:
with open(tracking_file, 'r', encoding='utf-8') as f:
file_content = f.read()
with open(tracking_file, 'r', encoding='utf-8') as f:
for line in f:
file_path = line.strip()
abs_file_path = os.path.abspath(file_path)
if abs_file_path and os.path.exists(abs_file_path):
if log_output:
log_output.push(f"DEBUG: Valid file found: {abs_file_path}")
created_files.append(abs_file_path)
elif abs_file_path:
if log_output:
log_output.push(f"DEBUG: Listed file doesn't exist: {abs_file_path}")
except Exception as e:
if log_output:
log_output.push(f"Error reading output files list: {e}")
else:
if log_output:
log_output.push(f"DEBUG ERROR: Tracking file not found after tool execution")
return result.stdout, result.stderr, created_files
###############################################################################
# FUNCTION: Tool Options Retrieval
###############################################################################
async def get_tool_options(script_name, log_output=None):
"""
Get options for a script from the JSON configuration.
Always loads directly from the file to get the latest version.
Args:
script_name: The script filename
log_output: Optional log component to display information
Returns:
List of option dictionaries with name, arg_name, description, required, default, type
"""
# Always load the tool configurations fresh from disk
config = load_tools_config(force_reload=True)
if script_name not in config:
if log_output:
log_output.push(f"Error: Configuration for {script_name} not found in JSON config")
ui.notify(f"Configuration for {script_name} not found", type="negative")
return []
tool_config = config[script_name]
# Display the help information in the log if provided
if log_output:
log_output.push("Tool information from JSON config:")
log_output.push(f"Name: {tool_config['title']}")
log_output.push(f"Description: {tool_config['description']}")
log_output.push(f"Help text: {tool_config['help_text']}")
log_output.push(f"Options: {len(tool_config['options'])} found")
# Display options by group for better organization
groups = {}
for option in tool_config['options']:
group = option.get('group', 'Other')
if group not in groups:
groups[group] = []
groups[group].append(option)
for group_name, group_options in groups.items():
log_output.push(f"\n{group_name}:")
for option in group_options:
required_mark = " (required)" if option.get('required', False) else ""
option_type = option.get('type', 'str')
log_output.push(f" {option['name']} {option.get('arg_name', '')}{required_mark} [Type: {option_type}]")
log_output.push(f" {option['description']}")
log_output.push(f" Default: {option.get('default', 'None')}")
if option.get('choices'):
log_output.push(f" Choices: {', '.join(str(c) for c in option['choices'])}")
return tool_config['options']
###############################################################################
# FUNCTION: Options Dialog
###############################################################################
async def browse_files_handler(input_element, start_path, option_name, option_type):
"""Global handler for file browsing that's not affected by loop closures"""
try:
# Determine parameters from option name
is_dir_selection = "dir" in option_name.lower() or "directory" in option_name.lower() or "folder" in option_name.lower()
allow_multiple = "multiple" in option_name.lower() or "files" in option_name.lower()
# Get starting directory
start_dir = input_element.value if input_element.value else start_path
if os.path.isfile(os.path.expanduser(start_dir)):
start_dir = os.path.dirname(start_dir)
# Use file picker
selected = await select_file_or_folder(
start_dir=start_dir,
multiple=allow_multiple,
dialog_title=f"Select {'Folder' if is_dir_selection else 'File'}",
folders_only=is_dir_selection
)
# Process selection
if selected:
# Format selection appropriately
if isinstance(selected, list) and selected and allow_multiple:
formatted_value = ", ".join(selected)
else:
formatted_value = selected[0] if isinstance(selected, list) and len(selected) == 1 else selected
# Direct approach to update the input
input_element.value = formatted_value
input_element.set_value(formatted_value)
# Force UI updating with a direct JavaScript approach
element_id = input_element.id
js_code = f"""
setTimeout(function() {{
document.querySelectorAll('input').forEach(function(el) {{
if (el.id && el.id.includes('{element_id}')) {{
el.value = '{formatted_value}';
el.dispatchEvent(new Event('change'));
}}
}});
}}, 100);
"""
ui.run_javascript(js_code)
return True
else:
ui.notify("No selection made", type="warning")
return False
except Exception as e:
ui.notify(f"Error: {str(e)}", type="negative")
return False
async def build_options_dialog(script_name, options):
"""
Create a dialog to collect options for a script.
Uses explicit type information from the JSON config.
Args:
script_name: The name of the script
options: List of option dictionaries with name, arg_name, description, required, default, type
Returns:
Dictionary mapping option names to their values
"""
# Create an async result that we'll resolve when the form is submitted
result_future = asyncio.Future()
# Dictionary to store the input elements and their values
input_elements = {}
# Create the dialog
dialog = ui.dialog()
dialog.props('persistent')
with dialog, ui.card().classes('w-full max-w-3xl p-4'):
ui.label(f'Options for {script_name}').classes('text-h6 mb-4')
# Group options by their group
grouped_options = {}
for option in options:
group = option.get('group', 'Other')
if group not in grouped_options:
grouped_options[group] = []
grouped_options[group].append(option)
# Create the options container with sections by group
with ui.column().classes('w-full gap-2'):
# For each group, create a section
for group_name, group_options in grouped_options.items():
with ui.expansion(group_name, value=True).classes('w-full q-mb-md'):
# For each option in this group, create an appropriate input field
for option in group_options:
name = option["name"]
description = option["description"]
required = option.get("required", False)
arg_name = option.get("arg_name", "")
default_value = option.get("default")
option_type = option.get("type", "str") # Get the explicit type
choices = option.get("choices", None)
# Format the label
label = f"{name}"
if arg_name:
label += f" <{arg_name}>"
if required:
label += " *"
# Create a card for each option for better organization
with ui.card().classes('w-full q-pa-sm q-mb-sm'):
ui.label(label).classes('text-bold')
ui.label(description).classes('text-caption text-grey-7')
# Show current default value in the description
if default_value is not None:
ui.label(f"Default: {default_value}").classes('text-caption text-grey-7')
# Create appropriate input fields based on the explicit type
if option_type == "bool":
# Boolean options (checkboxes)
value_to_use = default_value if default_value is not None else False
checkbox = ui.checkbox("Enable this option", value=value_to_use)
input_elements[name] = checkbox
elif option_type == "int":
# Integer input fields
value_to_use = default_value if default_value is not None else None
# If the option has choices, create a dropdown
if choices:
dropdown = ui.select(
options=choices,
value=value_to_use,
label=f"Select value for {name}"
)
input_elements[name] = dropdown
else:
# Regular number input for integers
input_field = ui.number(
placeholder="Enter number...",
value=value_to_use,
precision=0 # Force integer values only
)
input_elements[name] = input_field
elif option_type == "float":
# Float input fields
value_to_use = default_value if default_value is not None else None
# If the option has choices, create a dropdown
if choices:
dropdown = ui.select(
options=choices,
value=value_to_use,
label=f"Select value for {name}"
)
input_elements[name] = dropdown
else:
# Regular number input for floats
input_field = ui.number(
placeholder="Enter number...",
value=value_to_use
)
input_elements[name] = input_field
elif option_type == "choices":
# Dropdown for choices
if choices:
dropdown = ui.select(
options=choices,
value=default_value,
label=f"Select value for {name}"
)
input_elements[name] = dropdown
else:
# Fallback to text input if no choices provided
input_field = ui.input(
placeholder="Enter value...",
value=default_value
)
input_elements[name] = input_field
elif option_type == "file" or option_type == "path" or any(kw in name.lower() for kw in ["file", "path", "dir", "directory"]):
# File/directory paths with integrated file picker
with ui.row().classes('w-full items-center'):
input_field = ui.input(
placeholder="Enter path...",
value=default_value
).classes('w-full')
input_elements[name] = input_field
# Set a default path based on option name
if "manuscript" in name.lower():
default_path = os.path.join(DEFAULT_SAVE_DIR, "manuscript.txt")
elif "outline" in name.lower():
default_path = os.path.join(DEFAULT_SAVE_DIR, "outline.txt")
elif "world" in name.lower():
default_path = os.path.join(DEFAULT_SAVE_DIR, "world.txt")
elif "save_dir" in name.lower():
default_path = DEFAULT_SAVE_DIR
else:
default_path = DEFAULT_SAVE_DIR
# Default button sets the default path without opening file picker
ui.button("Default", icon='description').props('flat dense no-caps').on('click',
lambda i=input_field, p=default_path: i.set_value(p))
# Store the needed variables for the current iteration in the closure
current_name = name
current_option_type = option_type
current_default_path = default_path
current_input = input_field # Create a stable reference
# Define the button click handler to explicitly pass the captured references
ui.button("Browse", icon='folder_open').props('flat dense no-caps').on('click',
lambda e, input=current_input, path=current_default_path, n=current_name, t=current_option_type:
browse_files_handler(input, path, n, t))
else:
# Default to text input for all other types
input_field = ui.input(
placeholder="Enter value...",
value=default_value
)
input_elements[name] = input_field
# Add save preferences checkbox
save_preferences_checkbox = True # ui.checkbox("Save these settings as defaults", value=True)
# Button row
with ui.row().classes('w-full justify-end gap-2 mt-4'):
ui.button('Cancel', on_click=lambda: [dialog.close(), result_future.set_result((None, False))]) \
.props('flat no-caps').classes('text-grey')
def on_submit():
# Collect values from input elements
option_values = {} # For running the tool
changed_options = {} # For saving as preferences
for name, input_element in input_elements.items():
if hasattr(input_element, 'value'):
# Find the original option to get its default value and type
original_option = next((opt for opt in options if opt['name'] == name), None)
if original_option is None:
continue
current_value = input_element.value
default_value = original_option.get('default')
option_type = original_option.get('type', 'str')
is_required = original_option.get('required', False)
# Convert values to correct type if needed
if option_type == "int" and isinstance(current_value, float):
current_value = int(current_value)
# Add to the command execution values if it has a value
if current_value is not None:
is_empty_string = isinstance(current_value, str) and current_value.strip() == ""
if not is_empty_string or is_required:
option_values[name] = current_value
# Track changed values for preference saving
if current_value != default_value:
changed_options[name] = current_value
# Get the save preferences checkbox value, no, always save:
should_save = True
# Save preferences immediately if requested
if should_save and changed_options:
update_tool_preferences(script_name, changed_options)
# Resolve the future with the option values and save flag
result_future.set_result((option_values, should_save))
dialog.close()
ui.button('Apply', on_click=on_submit).props('color=primary no-caps').classes('text-white')
# Show the dialog and wait for it to be resolved
dialog.open()
return await result_future
def build_command_string(script_name, option_values):
"""
Build a command-line argument string from option values.
Include ALL parameters in the command string.
Args:
script_name: The script filename
option_values: Dictionary mapping option names to their values
Returns:
Tuple of (full command string, args_list) for display and execution
"""
# Load the tool configurations to check for required parameters - always force reload
config = load_tools_config(force_reload=True)
tool_config = config.get(script_name, {})
options = tool_config.get('options', [])
# Get all required option names
required_options = [opt['name'] for opt in options if opt.get('required', False)]
# Create a mapping of option names to their types
option_types = {opt['name']: opt.get('type', 'str') for opt in options}
# Check if all required options are provided
missing_required = [opt for opt in required_options if opt not in option_values]
if missing_required:
ui.notify(f"Missing required options: {', '.join(missing_required)}", type="negative")
# Add the missing required options with empty values to prompt user to fix
for opt in missing_required:
option_values[opt] = ""
# Create a properly formatted argument list
args_list = []
# Simply include ALL parameters - don't check against defaults
for name, value in option_values.items():
# Get the option type
option_type = option_types.get(name, 'str')
# Convert values to correct type if needed
if option_type == "int" and isinstance(value, float):
value = int(value)
if option_type == "bool":
if value:
# Just add the flag for boolean options
args_list.append(name)
else:
# Handle empty strings
is_empty_string = isinstance(value, str) and value.strip() == ""
is_required = name in required_options
# Include the parameter if it's not an empty string or if it's required
if not is_empty_string or is_required:
args_list.append(name)
args_list.append(str(value))
# Add default save_dir if not specified
# if not any(arg.startswith('--save_dir') for arg in args_list):
# args_list.extend(['--save_dir', DEFAULT_SAVE_DIR])
# Determine the Python executable based on platform
python_exe = 'python' # Using python directly for all platforms
# Build the full command for display
# Use proper platform-specific quoting for display purposes
quoted_args = []
for arg in args_list:
if platform.system() == 'Windows':
# On Windows, quote if arg contains spaces
if ' ' in arg:
quoted_args.append(f'"{arg}"')
else:
quoted_args.append(arg)
else:
# On Unix, use shell-style quoting (simple version)
if ' ' in arg or any(c in arg for c in '*?[]{}():,&|;<>~`!$'):
quoted_args.append(f"'{arg}'")
else:
quoted_args.append(arg)
full_command = f"{python_exe} -u {script_name} {' '.join(quoted_args)}"
return full_command, args_list
###############################################################################
# FUNCTION: Command Preview Dialog
###############################################################################
async def show_command_preview(script_name, option_values):
"""
Show a dialog with the preview of the command to be executed.
Args:
script_name: The script filename
option_values: Dictionary mapping option names to their values
Returns:
Boolean indicating whether to run the command
"""
# Build the command string for display
full_command, args_list = build_command_string(script_name, option_values)
# Create a display-friendly version of the arguments
args_display = []
i = 0
while i < len(args_list):
if i+1 < len(args_list) and args_list[i].startswith('--'):
# Combine argument name and value
args_display.append(f"{args_list[i]} {args_list[i+1]}")
i += 2
else:
# Just a flag
args_display.append(args_list[i])