kapsel-cli 0.1.0__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.
- kapsel/__init__.py +6 -0
- kapsel/cli.py +162 -0
- kapsel/completion/__init__.py +23 -0
- kapsel/completion/carapace_engine.py +236 -0
- kapsel/completion/completer.py +251 -0
- kapsel/completion/fig_engine.py +215 -0
- kapsel/completion/fig_schema.py +135 -0
- kapsel/completion/importer.py +92 -0
- kapsel/completion/kps/__init__.py +18 -0
- kapsel/completion/kps/builtins/__init__.py +92 -0
- kapsel/completion/kps/builtins/add.py +121 -0
- kapsel/completion/kps/builtins/config.py +184 -0
- kapsel/completion/kps/builtins/datadir.py +108 -0
- kapsel/completion/kps/builtins/help.py +270 -0
- kapsel/completion/kps/builtins/language.py +83 -0
- kapsel/completion/kps/builtins/status.py +121 -0
- kapsel/completion/kps/builtins/toggle.py +34 -0
- kapsel/completion/kps/dispatcher.py +65 -0
- kapsel/completion/kps/registry.py +101 -0
- kapsel/core/__init__.py +28 -0
- kapsel/core/detector.py +162 -0
- kapsel/core/engine.py +159 -0
- kapsel/core/executor.py +200 -0
- kapsel/core/i18n.py +31 -0
- kapsel/core/plugin/__init__.py +17 -0
- kapsel/core/plugin/base.py +53 -0
- kapsel/core/plugin/catalog.py +187 -0
- kapsel/core/plugin/context.py +71 -0
- kapsel/core/plugin/hooks.py +34 -0
- kapsel/core/plugin/manager.py +173 -0
- kapsel/i18n.py +196 -0
- kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
- kapsel/locales/de/LC_MESSAGES/kapsel.po +215 -0
- kapsel/locales/de/help.yaml +101 -0
- kapsel/locales/en/help.yaml +101 -0
- kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
- kapsel/locales/es/LC_MESSAGES/kapsel.po +213 -0
- kapsel/locales/es/help.yaml +101 -0
- kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
- kapsel/locales/fr/LC_MESSAGES/kapsel.po +213 -0
- kapsel/locales/fr/help.yaml +101 -0
- kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
- kapsel/locales/ja/LC_MESSAGES/kapsel.po +211 -0
- kapsel/locales/ja/help.yaml +101 -0
- kapsel/locales/kapsel.pot +210 -0
- kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
- kapsel/locales/ru/LC_MESSAGES/kapsel.po +212 -0
- kapsel/locales/ru/help.yaml +101 -0
- kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
- kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +211 -0
- kapsel/locales/zh_CN/help.yaml +101 -0
- kapsel/storage/__init__.py +17 -0
- kapsel/storage/config.py +362 -0
- kapsel/storage/history.py +166 -0
- kapsel/storage/logger.py +75 -0
- kapsel/storage/migrate.py +119 -0
- kapsel/ui/__init__.py +21 -0
- kapsel/ui/banner.py +121 -0
- kapsel/ui/card.py +109 -0
- kapsel/ui/prompt.py +391 -0
- kapsel/ui/theme.py +59 -0
- kapsel_cli-0.1.0.dist-info/METADATA +397 -0
- kapsel_cli-0.1.0.dist-info/RECORD +66 -0
- kapsel_cli-0.1.0.dist-info/WHEEL +5 -0
- kapsel_cli-0.1.0.dist-info/entry_points.txt +3 -0
- kapsel_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kapsel Config Command.
|
|
3
|
+
Handles 'config', 'config path', 'config edit', 'config get', 'config set', 'config reload'.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import platform
|
|
9
|
+
import subprocess
|
|
10
|
+
from typing import Any, List, Optional
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
|
|
17
|
+
from kapsel.storage.config import (
|
|
18
|
+
KapselConfig,
|
|
19
|
+
get_config_path,
|
|
20
|
+
load_config,
|
|
21
|
+
update_config_value,
|
|
22
|
+
)
|
|
23
|
+
from kapsel.ui.banner import ensure_utf8_io
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def handle_config_command(args: List[str], console: Optional[Console] = None) -> int:
|
|
27
|
+
"""Handles 'config [subcommand]' and renders rich output."""
|
|
28
|
+
ensure_utf8_io()
|
|
29
|
+
con = console or Console(legacy_windows=False)
|
|
30
|
+
|
|
31
|
+
config_path = get_config_path()
|
|
32
|
+
cfg = load_config()
|
|
33
|
+
|
|
34
|
+
if not args:
|
|
35
|
+
render_config_dashboard(cfg, config_path, con)
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
sub = args[0].lower()
|
|
39
|
+
|
|
40
|
+
if sub == "path":
|
|
41
|
+
print(str(config_path))
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
if sub == "edit":
|
|
45
|
+
con.print(f"[dim]Opening configuration file:[/] [bold #00f0ff]{config_path}[/]")
|
|
46
|
+
try:
|
|
47
|
+
if platform.system() == "Windows":
|
|
48
|
+
os.startfile(str(config_path))
|
|
49
|
+
elif platform.system() == "Darwin":
|
|
50
|
+
subprocess.run(["open", str(config_path)])
|
|
51
|
+
else:
|
|
52
|
+
subprocess.run(["xdg-open", str(config_path)])
|
|
53
|
+
con.print("[bold #10b981]✔ Opened in external editor[/]")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
con.print(f"[bold #f43f5e]Failed to open editor: {e}[/]")
|
|
56
|
+
con.print(f"[dim]You can edit it manually at: {config_path}[/]")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
if sub == "reload":
|
|
60
|
+
new_cfg = load_config(force_reload=True)
|
|
61
|
+
con.print(f"[bold #10b981]✔ Configuration reloaded.[/] (Theme: {new_cfg.theme.get('name')}, Tap: {new_cfg.interaction.get('autosuggest_tap_mode')})")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
if sub in ("datadir", "migrate"):
|
|
65
|
+
from kapsel.commands.datadir import handle_datadir_command
|
|
66
|
+
return handle_datadir_command(args[1:], con)
|
|
67
|
+
|
|
68
|
+
if sub == "get":
|
|
69
|
+
if len(args) < 2:
|
|
70
|
+
con.print("[bold #f43f5e]Error: Please specify configuration key (e.g. config get interaction.autosuggest_tap_mode)[/]")
|
|
71
|
+
return 1
|
|
72
|
+
key_path = args[1]
|
|
73
|
+
val = get_nested_val(cfg.raw, key_path)
|
|
74
|
+
if val is None:
|
|
75
|
+
con.print(f"[dim]Config key '{key_path}' is not set (using default)[/]")
|
|
76
|
+
else:
|
|
77
|
+
con.print(f"[bold #00f0ff]{key_path}[/] = [bold #10b981]{val}[/]")
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
if sub == "set":
|
|
81
|
+
if len(args) < 3:
|
|
82
|
+
con.print("[bold #f43f5e]Error: Invalid format. Usage: config set <key.path> <value>[/]")
|
|
83
|
+
con.print("[dim]Example: config set interaction.autosuggest_tap_mode full[/]")
|
|
84
|
+
con.print("[dim]Example: config set interaction.autosuggest_sensitivity 0.15[/]")
|
|
85
|
+
return 1
|
|
86
|
+
key_path = args[1]
|
|
87
|
+
val_str = args[2]
|
|
88
|
+
|
|
89
|
+
parsed_val: Any = val_str
|
|
90
|
+
if val_str.lower() in ("true", "yes", "1", "on"):
|
|
91
|
+
parsed_val = True
|
|
92
|
+
elif val_str.lower() in ("false", "no", "0", "off"):
|
|
93
|
+
parsed_val = False
|
|
94
|
+
else:
|
|
95
|
+
try:
|
|
96
|
+
if "." in val_str:
|
|
97
|
+
parsed_val = float(val_str)
|
|
98
|
+
else:
|
|
99
|
+
parsed_val = int(val_str)
|
|
100
|
+
except ValueError:
|
|
101
|
+
parsed_val = val_str
|
|
102
|
+
|
|
103
|
+
success = update_config_value(key_path, parsed_val)
|
|
104
|
+
if success:
|
|
105
|
+
con.print(f"[bold #10b981]✔ Config updated:[/] [bold #00f0ff]{key_path}[/] = [bold #10b981]{parsed_val}[/]")
|
|
106
|
+
con.print("[dim]Saved to ~/.kapsel/config.yaml and reloaded.[/]")
|
|
107
|
+
return 0
|
|
108
|
+
else:
|
|
109
|
+
con.print(f"[bold #f43f5e]✘ Failed to update config. Check key path: '{key_path}'[/]")
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
con.print(f"[bold #f43f5e]Unknown config subcommand: '{sub}'.[/] See 'kapsel config --help'.")
|
|
113
|
+
return 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_nested_val(data: dict, path: str) -> Any:
|
|
117
|
+
keys = path.split(".")
|
|
118
|
+
curr = data
|
|
119
|
+
for k in keys:
|
|
120
|
+
if isinstance(curr, dict) and k in curr:
|
|
121
|
+
curr = curr[k]
|
|
122
|
+
else:
|
|
123
|
+
return None
|
|
124
|
+
return curr
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def render_config_dashboard(cfg: KapselConfig, config_path: Path, console: Console) -> None:
|
|
128
|
+
inter = cfg.interaction
|
|
129
|
+
ui_cfg = cfg.ui
|
|
130
|
+
theme_cfg = cfg.theme
|
|
131
|
+
cloud_cfg = cfg.raw.get("cloud", {})
|
|
132
|
+
|
|
133
|
+
grid = Table.grid(expand=True, padding=(0, 2))
|
|
134
|
+
grid.add_column(style="bold #00f0ff", justify="right", width=22)
|
|
135
|
+
grid.add_column(style="#e4e4e7")
|
|
136
|
+
grid.add_column(style="bold #a855f7", justify="right", width=22)
|
|
137
|
+
grid.add_column(style="#e4e4e7")
|
|
138
|
+
|
|
139
|
+
grid.add_row(
|
|
140
|
+
"🎯 Autosuggest Tap:",
|
|
141
|
+
f"[bold #10b981]{inter.get('autosuggest_tap_mode', 'word')}[/]",
|
|
142
|
+
"🎨 Active Theme:",
|
|
143
|
+
f"[bold #00f0ff]{theme_cfg.get('name', 'cyber_dark')}[/]",
|
|
144
|
+
)
|
|
145
|
+
grid.add_row(
|
|
146
|
+
"⚡ Hold Action:",
|
|
147
|
+
f"[bold #10b981]{inter.get('autosuggest_hold_action', 'full')}[/]",
|
|
148
|
+
"⏱ Card Border:",
|
|
149
|
+
f"[bold #10b981]{'On' if ui_cfg.get('enable_card_border') else 'Off'}[/]",
|
|
150
|
+
)
|
|
151
|
+
grid.add_row(
|
|
152
|
+
"⏲ Sensitivity:",
|
|
153
|
+
f"[bold #10b981]{inter.get('autosuggest_sensitivity', 0.25)}s[/]",
|
|
154
|
+
" Git Badge:",
|
|
155
|
+
f"[bold #10b981]{'On' if ui_cfg.get('show_git_branch') else 'Off'}[/]",
|
|
156
|
+
)
|
|
157
|
+
grid.add_row(
|
|
158
|
+
"🔢 Threshold Count:",
|
|
159
|
+
f"[bold #10b981]{inter.get('consecutive_press_threshold', 2)}[/]",
|
|
160
|
+
"🌐 Cloud Endpoint:",
|
|
161
|
+
f"[dim]{cloud_cfg.get('server_endpoint', 'http://127.0.0.1:8000')}[/]",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
content = Table.grid(expand=True, padding=(1, 0))
|
|
165
|
+
content.add_column()
|
|
166
|
+
|
|
167
|
+
header = Text()
|
|
168
|
+
header.append("⚙️ KAPSEL Configuration Dashboard\n", style="bold #00f0ff")
|
|
169
|
+
header.append(f"Config path: {config_path}\n", style="dim #6b7280")
|
|
170
|
+
header.append("Run 'config edit' to open editor · Run 'config set <key> <val>' to update", style="italic #9ca3af")
|
|
171
|
+
|
|
172
|
+
content.add_row(header)
|
|
173
|
+
content.add_row(grid)
|
|
174
|
+
|
|
175
|
+
panel = Panel(
|
|
176
|
+
content,
|
|
177
|
+
border_style="#0891b2",
|
|
178
|
+
padding=(1, 2),
|
|
179
|
+
expand=False,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
console.print()
|
|
183
|
+
console.print(panel)
|
|
184
|
+
console.print()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kapsel Data Directory (datadir / migrate) Command Handler.
|
|
3
|
+
Allows users to view, customize, and migrate Kapsel's data storage location.
|
|
4
|
+
Automatically moves existing databases, configs, logs, and registry manifests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from kapsel.storage.logger import (
|
|
15
|
+
POINTER_FILE,
|
|
16
|
+
get_default_kapsel_dir,
|
|
17
|
+
get_kapsel_dir,
|
|
18
|
+
)
|
|
19
|
+
from kapsel.storage.migrate import migrate_kapsel_data
|
|
20
|
+
from kapsel.ui.banner import ensure_utf8_io
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def handle_datadir_command(args: List[str], console: Optional[Console] = None) -> int:
|
|
24
|
+
"""Dispatches 'datadir' command: view or migrate storage location."""
|
|
25
|
+
ensure_utf8_io()
|
|
26
|
+
con = console or Console(legacy_windows=False)
|
|
27
|
+
|
|
28
|
+
if not args:
|
|
29
|
+
return show_current_datadir(con)
|
|
30
|
+
|
|
31
|
+
sub = args[0].strip()
|
|
32
|
+
|
|
33
|
+
if sub in ("help", "-h", "--help"):
|
|
34
|
+
render_datadir_help(con)
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
if sub in ("status", "show", "current", "path"):
|
|
38
|
+
return show_current_datadir(con)
|
|
39
|
+
|
|
40
|
+
# Any other argument is treated as the target migration path!
|
|
41
|
+
target_path = " ".join(args).strip().strip('"\'').strip()
|
|
42
|
+
return do_migration(target_path, con)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def show_current_datadir(console: Console) -> int:
|
|
46
|
+
current = get_kapsel_dir()
|
|
47
|
+
default = get_default_kapsel_dir()
|
|
48
|
+
is_custom = current.resolve() != default.resolve()
|
|
49
|
+
|
|
50
|
+
# Calculate directory size
|
|
51
|
+
total_bytes = 0
|
|
52
|
+
file_count = 0
|
|
53
|
+
try:
|
|
54
|
+
for p in current.rglob("*"):
|
|
55
|
+
if p.is_file():
|
|
56
|
+
total_bytes += p.stat().st_size
|
|
57
|
+
file_count += 1
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
size_str = f"{total_bytes / 1024:.1f} KB" if total_bytes < 1024 * 1024 else f"{total_bytes / (1024*1024):.2f} MB"
|
|
62
|
+
|
|
63
|
+
grid = Table.grid(padding=(0, 2))
|
|
64
|
+
grid.add_column(style="bold #00f0ff", width=18)
|
|
65
|
+
grid.add_column()
|
|
66
|
+
|
|
67
|
+
grid.add_row("📂 Data Directory:", f"[bold #10b981]{current}[/]")
|
|
68
|
+
grid.add_row(
|
|
69
|
+
"🏷️ Storage Mode:",
|
|
70
|
+
"[bold #a855f7]Custom Location[/]" if is_custom else "[dim]Default (~/.kapsel)[/]",
|
|
71
|
+
)
|
|
72
|
+
grid.add_row("📦 Storage Size:", f"{size_str} ({file_count} files/databases)")
|
|
73
|
+
if is_custom and POINTER_FILE.exists():
|
|
74
|
+
grid.add_row("📌 Pointer File:", f"[dim]{POINTER_FILE}[/]")
|
|
75
|
+
|
|
76
|
+
panel = Panel(
|
|
77
|
+
grid,
|
|
78
|
+
title="[bold #00f0ff]💊 Kapsel Data Directory[/]",
|
|
79
|
+
border_style="#0891b2",
|
|
80
|
+
padding=(1, 2),
|
|
81
|
+
)
|
|
82
|
+
console.print()
|
|
83
|
+
console.print(panel)
|
|
84
|
+
console.print("\n[dim]Usage:[/]")
|
|
85
|
+
console.print(" • Relocate data sandbox: [bold #00f0ff]kps datadir <new_path>[/] (e.g. kps datadir D:\\KapselData)")
|
|
86
|
+
console.print(" • Restore default: [bold #00f0ff]kps datadir default[/]\n")
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def do_migration(target_path: str, console: Console) -> int:
|
|
91
|
+
console.print(f"\n[dim]Preparing migration to:[/] [bold #00f0ff]{target_path}[/]")
|
|
92
|
+
console.print("[dim]Moving databases, configs, and plugins...[/]")
|
|
93
|
+
|
|
94
|
+
success, msg = migrate_kapsel_data(target_path)
|
|
95
|
+
if success:
|
|
96
|
+
console.print(f"\n[bold #10b981]✔ {msg}[/]\n")
|
|
97
|
+
return 0
|
|
98
|
+
else:
|
|
99
|
+
console.print(f"\n[bold #f43f5e]✘ Migration failed: {msg}[/]\n")
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def render_datadir_help(console: Console) -> None:
|
|
104
|
+
console.print("\n[bold #00f0ff]kps datadir[/] - View or customize Kapsel storage location\n")
|
|
105
|
+
console.print("Usage:")
|
|
106
|
+
console.print(" kps datadir View current storage location and stats")
|
|
107
|
+
console.print(" kps datadir <new_path> Migrate existing data to a new location")
|
|
108
|
+
console.print(" kps datadir default Migrate data back to default (~/.kapsel)\n")
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kapsel Help Command (Data-Driven Renderer).
|
|
3
|
+
Loads comprehensive, structured manual datasets from locale resources (defaults to English),
|
|
4
|
+
formats rich visual tables and panels, dynamically integrates live registered plugins,
|
|
5
|
+
and supports focused topic exploration.
|
|
6
|
+
All comments and descriptions are in English.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from kapsel import __version__
|
|
17
|
+
from kapsel.completion.kps.registry import get_kps_registry
|
|
18
|
+
from kapsel.i18n import get_current_language, load_help_data
|
|
19
|
+
from kapsel.ui.banner import ensure_utf8_io
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HelpRenderer:
|
|
23
|
+
"""
|
|
24
|
+
Data-driven renderer for the Kapsel manual and cheatsheet.
|
|
25
|
+
Decouples documentation content from Python code.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, console: Optional[Console] = None, lang: Optional[str] = None):
|
|
29
|
+
self.console = console or Console(legacy_windows=False)
|
|
30
|
+
self.lang = lang or get_current_language()
|
|
31
|
+
self.data = load_help_data(self.lang)
|
|
32
|
+
self.registry = get_kps_registry()
|
|
33
|
+
|
|
34
|
+
def render_full_manual(self) -> None:
|
|
35
|
+
"""Renders the complete manual: Header, Modes, Quickstart, Commands, Shortcuts, and Tips."""
|
|
36
|
+
self.console.print()
|
|
37
|
+
self.render_header()
|
|
38
|
+
self.render_modes()
|
|
39
|
+
self.render_quickstart()
|
|
40
|
+
self.render_commands()
|
|
41
|
+
self.render_shortcuts()
|
|
42
|
+
self.render_tips()
|
|
43
|
+
self.console.print()
|
|
44
|
+
|
|
45
|
+
def render_header(self) -> None:
|
|
46
|
+
"""Renders the title banner and architecture note."""
|
|
47
|
+
meta = self.data.get("meta", {})
|
|
48
|
+
title = meta.get("title", "KAPSEL")
|
|
49
|
+
subtitle = meta.get("subtitle", "User Manual & Fast Command Cheatsheet")
|
|
50
|
+
architecture = meta.get(
|
|
51
|
+
"architecture",
|
|
52
|
+
"Unified Command Pipeline: 'kapsel <cmd>' and 'kps <cmd>' share the exact same execution engine.",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
header = Text()
|
|
56
|
+
header.append(f"💊 {title} ", style="bold #00f0ff")
|
|
57
|
+
header.append(f"v{__version__} · {subtitle}\n", style="dim #6b7280")
|
|
58
|
+
header.append(f"{architecture}\n", style="italic #9ca3af")
|
|
59
|
+
self.console.print(header)
|
|
60
|
+
|
|
61
|
+
def render_modes(self) -> None:
|
|
62
|
+
"""Renders the Core Interaction Dual Modes table."""
|
|
63
|
+
section = self.data.get("modes", {})
|
|
64
|
+
title = section.get("title", "🚀 Core Interaction Dual Modes")
|
|
65
|
+
cols = section.get("columns", {})
|
|
66
|
+
rows = section.get("rows", [])
|
|
67
|
+
|
|
68
|
+
table = Table(
|
|
69
|
+
title=title,
|
|
70
|
+
box=None,
|
|
71
|
+
title_justify="left",
|
|
72
|
+
padding=(0, 2),
|
|
73
|
+
collapse_padding=True,
|
|
74
|
+
)
|
|
75
|
+
table.add_column(cols.get("mode", "Mode"), style="bold #00f0ff", width=22)
|
|
76
|
+
table.add_column(cols.get("trigger", "Trigger"), style="#e4e4e7", width=26)
|
|
77
|
+
table.add_column(cols.get("description", "Behavior & Capabilities"), style="dim #9ca3af")
|
|
78
|
+
|
|
79
|
+
for r in rows:
|
|
80
|
+
table.add_row(r.get("name", ""), r.get("trigger", ""), r.get("description", ""))
|
|
81
|
+
|
|
82
|
+
self.console.print(table)
|
|
83
|
+
self.console.print()
|
|
84
|
+
|
|
85
|
+
def render_quickstart(self) -> None:
|
|
86
|
+
"""Renders the Quickstart workflow guide."""
|
|
87
|
+
section = self.data.get("quickstart", {})
|
|
88
|
+
title = section.get("title", "⚡ Quick Start Workflow")
|
|
89
|
+
items = section.get("items", [])
|
|
90
|
+
|
|
91
|
+
table = Table(title=title, box=None, title_justify="left", padding=(0, 2))
|
|
92
|
+
table.add_column("Workflow Step", style="bold #a855f7", width=26)
|
|
93
|
+
table.add_column("Command Line", style="bold #38bdf8", width=28)
|
|
94
|
+
table.add_column("Purpose", style="#e4e4e7")
|
|
95
|
+
|
|
96
|
+
for item in items:
|
|
97
|
+
table.add_row(item.get("step", ""), item.get("cmd", ""), item.get("desc", ""))
|
|
98
|
+
|
|
99
|
+
self.console.print(table)
|
|
100
|
+
self.console.print()
|
|
101
|
+
|
|
102
|
+
def render_commands(self) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Renders the Capsule Command Suite.
|
|
105
|
+
Merges documented core commands with dynamically registered plugin commands.
|
|
106
|
+
"""
|
|
107
|
+
section = self.data.get("commands", {})
|
|
108
|
+
title = section.get("title", "⚙️ Capsule Command Suite (kapsel / kps)")
|
|
109
|
+
cols = section.get("columns", {})
|
|
110
|
+
builtin_rows = section.get("builtin_rows", [])
|
|
111
|
+
|
|
112
|
+
table = Table(title=title, box=None, title_justify="left", padding=(0, 2))
|
|
113
|
+
table.add_column(cols.get("command", "Command"), style="bold #00f0ff", width=22)
|
|
114
|
+
table.add_column(cols.get("description", "Description"), style="#e4e4e7")
|
|
115
|
+
table.add_column(cols.get("origin", "Origin"), style="dim #9ca3af", width=14)
|
|
116
|
+
|
|
117
|
+
# Track displayed command names to prevent duplicates
|
|
118
|
+
rendered_names = set()
|
|
119
|
+
|
|
120
|
+
# 1. First render structured localized descriptions for built-in rows
|
|
121
|
+
for r in builtin_rows:
|
|
122
|
+
raw_cmd = r.get("name", "")
|
|
123
|
+
table.add_row(raw_cmd, r.get("desc", ""), r.get("origin", "Core"))
|
|
124
|
+
# Normalize command identifier (e.g. 'kps help' -> 'help')
|
|
125
|
+
parts = raw_cmd.split()
|
|
126
|
+
cmd_id = parts[1] if len(parts) > 1 else parts[0]
|
|
127
|
+
rendered_names.add(cmd_id)
|
|
128
|
+
|
|
129
|
+
# 2. Dynamically append any installed plugin commands not in static docs
|
|
130
|
+
all_registered = self.registry.list_commands()
|
|
131
|
+
for cmd in all_registered:
|
|
132
|
+
if cmd.name not in rendered_names:
|
|
133
|
+
origin = f"[{cmd.plugin_id}]" if cmd.plugin_id else "Core"
|
|
134
|
+
table.add_row(f"kps {cmd.name}", cmd.help_text, origin)
|
|
135
|
+
rendered_names.add(cmd.name)
|
|
136
|
+
|
|
137
|
+
self.console.print(table)
|
|
138
|
+
self.console.print()
|
|
139
|
+
|
|
140
|
+
def render_shortcuts(self) -> None:
|
|
141
|
+
"""Renders interactive keybindings and ergonomic shortcuts."""
|
|
142
|
+
section = self.data.get("shortcuts", {})
|
|
143
|
+
title = section.get("title", "⌨️ Interactive Keybindings & Ergonomics")
|
|
144
|
+
cols = section.get("columns", {})
|
|
145
|
+
items = section.get("items", [])
|
|
146
|
+
|
|
147
|
+
table = Table(title=title, box=None, title_justify="left", padding=(0, 2))
|
|
148
|
+
table.add_column(cols.get("key", "Key"), style="bold #38bdf8", width=22)
|
|
149
|
+
table.add_column(cols.get("description", "Action"), style="#e4e4e7")
|
|
150
|
+
|
|
151
|
+
for item in items:
|
|
152
|
+
table.add_row(item.get("key", ""), item.get("desc", ""))
|
|
153
|
+
|
|
154
|
+
self.console.print(table)
|
|
155
|
+
self.console.print()
|
|
156
|
+
|
|
157
|
+
def render_tips(self) -> None:
|
|
158
|
+
"""Renders pro tips and best practices in an aesthetic panel."""
|
|
159
|
+
section = self.data.get("tips", {})
|
|
160
|
+
title = section.get("title", "💡 Pro Tips & Best Practices")
|
|
161
|
+
items = section.get("items", [])
|
|
162
|
+
|
|
163
|
+
if not items:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
grid = Table.grid(padding=(0, 1))
|
|
167
|
+
grid.add_column(style="bold #00f0ff", width=3)
|
|
168
|
+
grid.add_column(style="#e4e4e7")
|
|
169
|
+
|
|
170
|
+
for tip in items:
|
|
171
|
+
grid.add_row("•", tip)
|
|
172
|
+
|
|
173
|
+
panel = Panel(
|
|
174
|
+
grid,
|
|
175
|
+
title=f"[bold #00f0ff]{title}[/]",
|
|
176
|
+
title_align="left",
|
|
177
|
+
border_style="#0891b2",
|
|
178
|
+
padding=(0, 2),
|
|
179
|
+
expand=False,
|
|
180
|
+
)
|
|
181
|
+
self.console.print(panel)
|
|
182
|
+
|
|
183
|
+
def render_topic(self, topic: str) -> bool:
|
|
184
|
+
"""Renders a single focused topic (e.g. 'keys', 'modes', 'commands', or specific command)."""
|
|
185
|
+
clean = topic.strip().lower()
|
|
186
|
+
|
|
187
|
+
if clean in ("mode", "modes", "dual", "architecture"):
|
|
188
|
+
self.console.print()
|
|
189
|
+
self.render_header()
|
|
190
|
+
self.render_modes()
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
if clean in ("quickstart", "start", "workflow"):
|
|
194
|
+
self.console.print()
|
|
195
|
+
self.render_header()
|
|
196
|
+
self.render_quickstart()
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
if clean in ("commands", "cmd", "cmds", "suite"):
|
|
200
|
+
self.console.print()
|
|
201
|
+
self.render_header()
|
|
202
|
+
self.render_commands()
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
if clean in ("key", "keys", "shortcuts", "bindings"):
|
|
206
|
+
self.console.print()
|
|
207
|
+
self.render_header()
|
|
208
|
+
self.render_shortcuts()
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
if clean in ("tip", "tips", "protips"):
|
|
212
|
+
self.console.print()
|
|
213
|
+
self.render_header()
|
|
214
|
+
self.render_tips()
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
# Check if user requested help for a specific registered command (e.g. 'kps help config')
|
|
218
|
+
cmd = self.registry.get(clean)
|
|
219
|
+
if cmd:
|
|
220
|
+
self.console.print()
|
|
221
|
+
grid = Table.grid(padding=(0, 2))
|
|
222
|
+
grid.add_column(style="bold #00f0ff", width=14)
|
|
223
|
+
grid.add_column(style="#e4e4e7")
|
|
224
|
+
|
|
225
|
+
origin_str = f"[{cmd.plugin_id}] Plugin" if cmd.plugin_id else "Core Built-in"
|
|
226
|
+
grid.add_row("Command:", f"[bold white]kapsel {cmd.name}[/] [dim](or: kps {cmd.name})[/]")
|
|
227
|
+
grid.add_row("Description:", cmd.help_text)
|
|
228
|
+
grid.add_row("Usage:", f"[bold #a855f7]{cmd.usage or f'kps {cmd.name} [options]'}[/]")
|
|
229
|
+
grid.add_row("Origin:", origin_str)
|
|
230
|
+
|
|
231
|
+
if cmd.subcommands:
|
|
232
|
+
grid.add_row("Subcommands:", "")
|
|
233
|
+
for sub, desc in cmd.subcommands.items():
|
|
234
|
+
grid.add_row("", f" [bold #38bdf8]{sub:<16}[/] [dim]{desc}[/]")
|
|
235
|
+
|
|
236
|
+
panel = Panel(
|
|
237
|
+
grid,
|
|
238
|
+
title=f"[bold #00f0ff]📖 Command Guide: {cmd.name}[/]",
|
|
239
|
+
border_style="#0891b2",
|
|
240
|
+
padding=(1, 2),
|
|
241
|
+
expand=False,
|
|
242
|
+
)
|
|
243
|
+
self.console.print(panel)
|
|
244
|
+
self.console.print()
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def handle_help(args: Optional[List[str]] = None, console: Optional[Console] = None) -> int:
|
|
251
|
+
"""
|
|
252
|
+
Entrypoint for the 'help' command.
|
|
253
|
+
Dispatches to HelpRenderer for complete manual or focused sub-topic query.
|
|
254
|
+
"""
|
|
255
|
+
ensure_utf8_io()
|
|
256
|
+
con = console or Console(legacy_windows=False)
|
|
257
|
+
renderer = HelpRenderer(con)
|
|
258
|
+
|
|
259
|
+
if not args:
|
|
260
|
+
renderer.render_full_manual()
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
target_topic = args[0]
|
|
264
|
+
handled = renderer.render_topic(target_topic)
|
|
265
|
+
if not handled:
|
|
266
|
+
con.print(f"[yellow]Notice:[/] Topic or command '[white]{target_topic}[/]' not found in help manual.")
|
|
267
|
+
con.print("[dim]Showing full manual instead:[/]")
|
|
268
|
+
renderer.render_full_manual()
|
|
269
|
+
|
|
270
|
+
return 0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kapsel Language Command: 'kapsel language [lang]'.
|
|
3
|
+
Displays active language or switches between supported world languages:
|
|
4
|
+
- en: English (default)
|
|
5
|
+
- zh_CN: Simplified Chinese
|
|
6
|
+
- ja: Japanese
|
|
7
|
+
- es: Spanish
|
|
8
|
+
- fr: French
|
|
9
|
+
- de: German
|
|
10
|
+
- ru: Russian
|
|
11
|
+
All comments and descriptions are in English.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from kapsel.i18n import (
|
|
20
|
+
SUPPORTED_LANGUAGES,
|
|
21
|
+
_,
|
|
22
|
+
get_current_language,
|
|
23
|
+
normalize_language_code,
|
|
24
|
+
set_current_language,
|
|
25
|
+
)
|
|
26
|
+
from kapsel.ui.banner import ensure_utf8_io
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def handle_language_command(args: Optional[List[str]] = None, console: Optional[Console] = None) -> int:
|
|
30
|
+
"""
|
|
31
|
+
Handles 'kapsel language' and 'kps language'.
|
|
32
|
+
Without arguments: displays supported languages table and active language badge.
|
|
33
|
+
With arguments: switches current language and persists setting to ~/.kapsel/config.yaml.
|
|
34
|
+
"""
|
|
35
|
+
ensure_utf8_io()
|
|
36
|
+
con = console or Console(legacy_windows=False)
|
|
37
|
+
current_code = get_current_language()
|
|
38
|
+
|
|
39
|
+
if not args:
|
|
40
|
+
# Render language selection table
|
|
41
|
+
curr_info = SUPPORTED_LANGUAGES.get(current_code, {"name": current_code, "native": current_code})
|
|
42
|
+
|
|
43
|
+
grid = Table(title=f"🌐 {_('Available Languages')}", box=None, title_justify="left", padding=(0, 2))
|
|
44
|
+
grid.add_column(_("Code"), style="bold #00f0ff", width=12)
|
|
45
|
+
grid.add_column(_("Language"), style="#e4e4e7", width=22)
|
|
46
|
+
grid.add_column(_("Native Name"), style="bold #a855f7", width=20)
|
|
47
|
+
grid.add_column(_("Aliases"), style="dim #9ca3af")
|
|
48
|
+
grid.add_column(_("Status"), width=12)
|
|
49
|
+
|
|
50
|
+
for code, meta in SUPPORTED_LANGUAGES.items():
|
|
51
|
+
aliases_str = ", ".join(meta.get("aliases", []))
|
|
52
|
+
is_curr = (code == current_code)
|
|
53
|
+
status_str = f"[bold #10b981]✔ {_('Active')}[/]" if is_curr else ""
|
|
54
|
+
grid.add_row(code, meta["name"], meta["native"], aliases_str, status_str)
|
|
55
|
+
|
|
56
|
+
con.print()
|
|
57
|
+
con.print(grid)
|
|
58
|
+
con.print()
|
|
59
|
+
con.print(f"[dim]{_('Usage:')} [/][bold #38bdf8]kps language <code/alias>[/] [dim](e.g. 'kps language zh' or 'kps language ja')[/]\n")
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
target_raw = args[0].strip()
|
|
63
|
+
target_code = normalize_language_code(target_raw)
|
|
64
|
+
|
|
65
|
+
if not target_code or target_code not in SUPPORTED_LANGUAGES:
|
|
66
|
+
available = ", ".join(SUPPORTED_LANGUAGES.keys())
|
|
67
|
+
err_msg = _("Error: Unsupported language '{input}'. Available codes: {available}").format(
|
|
68
|
+
input=target_raw, available=available
|
|
69
|
+
)
|
|
70
|
+
con.print(f"[bold #f43f5e]{err_msg}[/]")
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
success = set_current_language(target_code, persist=True)
|
|
74
|
+
if success:
|
|
75
|
+
meta = SUPPORTED_LANGUAGES[target_code]
|
|
76
|
+
msg = _("✔ Language successfully switched to: {name} ({code})").format(
|
|
77
|
+
name=meta["native"], code=target_code
|
|
78
|
+
)
|
|
79
|
+
con.print(f"[bold #10b981]{msg}[/]")
|
|
80
|
+
return 0
|
|
81
|
+
else:
|
|
82
|
+
con.print(f"[bold #f43f5e]Failed to update language setting.[/]")
|
|
83
|
+
return 1
|