1+ # !/usr/bin/python
2+
3+ from lib import (
4+ cerr , cout , StatusCode , die , working_dir , change_dir_to ,
5+ gen_or_read_options
6+ )
7+ from pathlib import Path
8+ from typing import Callable
9+ from sys import stdout
10+ from shutil import copy2
11+
12+ def format_table (headers : list [str ], rows : list [list [str ]]) -> str :
13+ all_data = [headers ] + rows
14+ column_widths = [max (len (str (cell )) for cell in column ) for column in zip (* all_data )]
15+ padding = 2
16+ separator = "+" + "+" .join ("-" * (width + padding ) for width in column_widths ) + "+"
17+
18+ def format_row (rows : list [str ]) -> str :
19+ cells = [str (cell ).ljust (width ) for cell , width in zip (rows , column_widths )]
20+ return "|" + "|" .join (f"{ cell } " for cell in cells ) + "|"
21+
22+ table_lines = [
23+ separator ,
24+ format_row (headers ),
25+ * [format_row (row ) for row in rows ],
26+ separator ,
27+ ]
28+
29+ return "\n " .join (table_lines )
30+
31+ def send_help (extra : str ) -> None :
32+ cout ("List of available commands:" )
33+
34+ headers = ["name" , "aliases" , "description" ]
35+ rows : list [list [str ]] = []
36+ seen_commands : set [str ] = set ()
37+ unique_commands : list [CommandInfo ] = []
38+
39+ for command in COMMAND_REGISTRY .values ():
40+ if command .name not in seen_commands :
41+ seen_commands .add (command .name )
42+ unique_commands .append (command )
43+
44+ for command in unique_commands :
45+ aliaeses_display = f"[{ ', ' .join (command .aliases )} ]" if command .aliases else "-"
46+
47+ rows .append ([
48+ command .name ,
49+ aliaeses_display ,
50+ command .description
51+ ])
52+
53+ rows .sort (key = lambda x : x [0 ])
54+ cout (format_table (headers , rows ))
55+
56+ def copy_compiler_commands (extra : str ) -> None :
57+ compiler_commands_file = Path ("../compile_commands.json" ) if working_dir () == "scripts" else Path ("compile_commands.json" )
58+ build_dir = Path ("../build" ) if working_dir () == "scripts" else Path ("build" )
59+ root_dir = Path (".." ) if working_dir () == "scripts" else Path ("." )
60+
61+ if not build_dir .exists ():
62+ cerr (f"Build directory not found for find & copy: { build_dir .absolute ()} " )
63+ return
64+
65+ compiler_commands_path = Path (f"{ build_dir } /{ compiler_commands_file } " )
66+ try :
67+ copy2 (compiler_commands_path .absolute (), f"{ compiler_commands_file .absolute ()} " )
68+ cout (f"Successfully copied { compiler_commands_file .absolute ()} to { root_dir .absolute ()} " )
69+
70+ except Exception as error :
71+ cerr (f"Cannot copy { compiler_commands_path .absolute ()} to { compiler_commands_file .absolute ()} , error: { error } " )
72+
73+ def find_config (extra : str ) -> None :
74+ if working_dir () == "scripts" :
75+ change_dir_to (".." )
76+
77+ config_path = Path ("cfg_2.json" )
78+ if not config_path .exists ():
79+ cerr (f"configuration file { config_path } not found, generate now? Y/N" )
80+
81+ while True :
82+ prompt = input ("[y/n]: " )
83+ if len (prompt .strip ()) == 0 :
84+ continue
85+
86+ match prompt .lower ():
87+ case "n" | "no" :
88+ cout ("Canceled." )
89+ break
90+
91+ case "y" | "yes" :
92+ try :
93+ gen_or_read_options (config_path )
94+ cout (f"Generate configuration success, as: { config_path .absolute ()} " )
95+ break
96+
97+ except Exception as error :
98+ cerr (f"Cannot generate configuration file, error: { error } " )
99+ break
100+
101+ case _:
102+ cerr (f"Option { prompt } is invalid" )
103+ break
104+ return
105+
106+ cout (f"Found configuration path as { config_path .absolute ()} " )
107+
108+ def clear_console (extra : str ) -> None :
109+ print ("\033 [2J\033 [H" , end = "" )
110+ stdout .flush ()
111+
112+ def exit (extra : str ) -> None :
113+ die (StatusCode .SUCCESS )
114+
115+ class CommandInfo :
116+ def __init__ (self , name : str , aliases : list [str ],
117+ description : str , handler : Callable [[str ], None ]) -> None :
118+ self .name = name
119+ self .description = description
120+ self .aliases = aliases
121+ self .handler = handler
122+
123+ @property
124+ def all_commands (self ) -> list [str ]:
125+ return [self .name ] + self .aliases
126+
127+ COMMAND_REGISTRY : dict [str , CommandInfo ] = {}
128+ def register_command (command_info : CommandInfo ) -> None :
129+ for alias in command_info .all_commands :
130+ COMMAND_REGISTRY [alias .lower ()] = command_info
131+
132+ register_command (CommandInfo (
133+ name = "help" ,
134+ aliases = ["help" , "h" , "?" ],
135+ description = "show the help message" ,
136+ handler = send_help
137+ ))
138+ register_command (CommandInfo (
139+ name = "quit" ,
140+ aliases = ["quit" , "exit" , "q" ],
141+ description = "exit interactive mode" ,
142+ handler = exit
143+ ))
144+ register_command (CommandInfo (
145+ name = "config" ,
146+ aliases = ["config" , "cfg" ],
147+ description = "find or generate the configuration" ,
148+ handler = find_config
149+ ))
150+ register_command (CommandInfo (
151+ name = "clear" ,
152+ aliases = ["clear" , "cls" ],
153+ description = "clear interactive console" ,
154+ handler = clear_console
155+ ))
156+ register_command (CommandInfo (
157+ name = "copy" ,
158+ aliases = ["copy-compiler-commands" , "ccc" , "cpc" ],
159+ description = "copy compiler_commands.json to root directory" ,
160+ handler = copy_compiler_commands
161+ ))
162+
163+ def main_loop () -> None :
164+ while True :
165+ command = input (">>> " )
166+ if len (command .strip ()) == 0 :
167+ continue
168+
169+ parts = command .split (maxsplit = 1 )
170+ command = parts [0 ].lower ()
171+ args = parts [1 ] if len (parts ) > 1 else ""
172+
173+ command_object = COMMAND_REGISTRY .get (command )
174+
175+ if command_object :
176+ command_object .handler (args )
177+
178+ else :
179+ cerr (f"command not found: { command } " )
180+
181+ try :
182+ main_loop ()
183+
184+ except KeyboardInterrupt :
185+ cerr ()
186+ die (StatusCode .CANCELED )
0 commit comments