Skip to content

Commit 1a292b6

Browse files
feat(scripts): add interactive command-line interface
Add interactive.py with command registry system supporting help, quit, config, clear, and copy-compiler-commands operations.
1 parent 112f7fa commit 1a292b6

1 file changed

Lines changed: 186 additions & 0 deletions

File tree

scripts/interactive.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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

Comments
 (0)