-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwriters_toolkit_v2.py
2705 lines (2236 loc) · 103 KB
/
writers_toolkit_v2.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
# TinyDB will be used for configuration storage
import subprocess
import argparse
import os
import sys
import re
import json
import platform
import time
import uuid
import urllib.parse
import shutil
from copy import deepcopy
from nicegui import ui, run, app
from tinydb import TinyDB, Query, where
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
# TinyDB database connection
DB_PATH = "writers_toolkit.json"
db = TinyDB(DB_PATH)
tools_table = db.table('tools')
settings_table = db.table('settings')
# 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: TinyDB Config handling with Integer Enforcement
###############################################################################
def load_tools_config(force_reload=False):
"""
Load tool configurations from the TinyDB database.
Also loads global settings if available.
Args:
force_reload: If True, bypasses any caching (not needed with TinyDB)
Returns:
Dictionary of tool configurations or empty dict if no tools found
"""
global DEFAULT_SAVE_DIR, CURRENT_PROJECT, CURRENT_PROJECT_PATH
# Create a dictionary with all tools
config = {}
# Get all tools from the tools table
all_tools = tools_table.all()
for tool in all_tools:
tool_name = tool.get('name')
if tool_name:
# Remove the 'name' key as it's not part of the original structure
tool_data = dict(tool)
del tool_data['name']
config[tool_name] = tool_data
# Get global settings
global_settings = settings_table.get(doc_id=1)
if global_settings:
config['_global_settings'] = global_settings
# Update global variables based on 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
def save_global_settings(settings_dict):
"""
Save global application settings to the database.
Args:
settings_dict: Dictionary of global settings to save
Returns:
Boolean indicating success or failure
"""
try:
# Get current settings or create new if none exist
current_settings = settings_table.get(doc_id=1)
if current_settings:
# Update existing settings
updated_settings = {**current_settings, **settings_dict}
settings_table.update(updated_settings, doc_ids=[1])
else:
# Insert new settings with doc_id=1
settings_table.insert(settings_dict)
settings_table.update(lambda _: True, doc_ids=[1])
return True
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 database.
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 database
backup_db_path = f"{DB_PATH}.bak"
try:
shutil.copy2(DB_PATH, backup_db_path)
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)
# Clear the tools table
tools_table.truncate()
# Insert tool configurations
for tool_name, tool_data in integer_config.items():
if not tool_name.startswith('_'): # Skip _global_settings
# Add the tool name to the document
tool_doc = {'name': tool_name, **tool_data}
tools_table.insert(tool_doc)
# Save global settings separately if they exist
if '_global_settings' in integer_config:
save_global_settings(integer_config['_global_settings'])
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 TinyDB operations.
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:
# Query to find the tool
Tool = Query()
tool = tools_table.get(Tool.name == script_name)
if not tool:
ui.notify(f"Tool {script_name} not found in configuration", type="negative")
return False
# MODIFY: Update the preferences
changes_made = False
processed_preferences = {}
# Get the tool options
options = tool.get('options', [])
# 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 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 options with the processed values
for name, new_value in processed_preferences.items():
# Find and update the option
for option in 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 tool:
del tool["user_preferences"]
changes_made = True
if not changes_made:
return True # Not an error, just no changes
# Update the tool in the database
tools_table.update({'options': options}, Tool.name == script_name)
ui.notify(f"Default values updated for {script_name}", type="positive")
return True
except Exception as e:
ui.notify(f"Error updating preferences: {str(e)}", type="negative")
return False
###############################################################################
# FUNCTION: File Picker Integration
###############################################################################
def select_file_or_folder(start_dir=None, multiple=False, dialog_title="Select Files or Folders", folders_only=False, callback=None):
"""
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
callback: Function to call with the selected path(s)
"""
if start_dir is None:
start_dir = DEFAULT_SAVE_DIR
# Ensure the directory exists
os.makedirs(os.path.expanduser(start_dir), exist_ok=True)
# Create a wrapper to handle the async result
def file_picker_wrapper():
try:
# Call the picker in a way that allows us to use the results
local_file_picker(
start_dir,
multiple=multiple,
folders_only=folders_only,
callback=process_selection
)
except Exception as e:
ui.notify(f"Error selecting files: {str(e)}", type="negative")
if callback:
callback([] if multiple else None)
# Process the picker results
def process_selection(result):
try:
if result and not multiple:
# If we're not allowing multiple selections, return the first item
result = result[0]
# Call the callback with the result
if callback:
callback(result)
except Exception as e:
ui.notify(f"Error processing selection: {str(e)}", type="negative")
if callback:
callback([] if multiple else None)
# Start the file picker
file_picker_wrapper()
###############################################################################
# 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
###############################################################################
def get_tool_options(script_name, log_output=None, callback=None):
"""
Get options for a script from the database.
Args:
script_name: The script filename
log_output: Optional log component to display information
callback: Function to call with the options
"""
# Query the database for the tool
Tool = Query()
tool = tools_table.get(Tool.name == script_name)
if not tool:
if log_output:
log_output.push(f"Error: Configuration for {script_name} not found in database")
ui.notify(f"Configuration for {script_name} not found", type="negative")
if callback:
callback([])
return
# Display the help information in the log if provided
if log_output:
log_output.push("Tool information from database:")
log_output.push(f"Name: {tool['title']}")
log_output.push(f"Description: {tool['description']}")
log_output.push(f"Help text: {tool['help_text']}")
log_output.push(f"Options: {len(tool['options'])} found")
# Display options by group for better organization
groups = {}
for option in tool['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'])}")
# Call the callback with the options
if callback:
callback(tool['options'])
return tool['options']
###############################################################################
# FUNCTION: Options Dialog
###############################################################################
def browse_files_handler(input_element, start_path, option_name, option_type):
"""Handler for file browsing that's not affected by loop closures"""
# 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)
# Define the callback to process picker results
def handle_selected_files(selected):
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)
else:
ui.notify("No selection made", type="warning")
# Use file picker
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,
callback=handle_selected_files
)
def build_options_dialog(script_name, options, on_result=None):
"""
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
on_result: Callback for dialog result (option_values, should_save)
"""
# 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 - always True for simplicity
save_preferences = True
# Button row
with ui.row().classes('w-full justify-end gap-2 mt-4'):
# Cancel button
def on_cancel():
dialog.close()
if on_result:
on_result((None, False))
ui.button('Cancel', on_click=on_cancel).props('flat no-caps').classes('text-grey')
# Apply button
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, always True for simplicity
should_save = True
# Save preferences immediately if requested
if should_save and changed_options:
update_tool_preferences(script_name, changed_options)
# Call the result callback
dialog.close()
if on_result:
on_result((option_values, should_save))
ui.button('Apply', on_click=on_submit).props('color=primary no-caps').classes('text-white')
# Show the dialog
dialog.open()
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
"""
# Get the tool configuration to check for required parameters
Tool = Query()
tool = tools_table.get(Tool.name == script_name)
options = tool.get('options', []) if tool else []
# 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))
# 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
###############################################################################
def show_command_preview(script_name, option_values, on_result=None):
"""
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
on_result: Callback for dialog result (True=run, False=cancel, None=edit)
"""
# 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