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.
Files changed (66) hide show
  1. kapsel/__init__.py +6 -0
  2. kapsel/cli.py +162 -0
  3. kapsel/completion/__init__.py +23 -0
  4. kapsel/completion/carapace_engine.py +236 -0
  5. kapsel/completion/completer.py +251 -0
  6. kapsel/completion/fig_engine.py +215 -0
  7. kapsel/completion/fig_schema.py +135 -0
  8. kapsel/completion/importer.py +92 -0
  9. kapsel/completion/kps/__init__.py +18 -0
  10. kapsel/completion/kps/builtins/__init__.py +92 -0
  11. kapsel/completion/kps/builtins/add.py +121 -0
  12. kapsel/completion/kps/builtins/config.py +184 -0
  13. kapsel/completion/kps/builtins/datadir.py +108 -0
  14. kapsel/completion/kps/builtins/help.py +270 -0
  15. kapsel/completion/kps/builtins/language.py +83 -0
  16. kapsel/completion/kps/builtins/status.py +121 -0
  17. kapsel/completion/kps/builtins/toggle.py +34 -0
  18. kapsel/completion/kps/dispatcher.py +65 -0
  19. kapsel/completion/kps/registry.py +101 -0
  20. kapsel/core/__init__.py +28 -0
  21. kapsel/core/detector.py +162 -0
  22. kapsel/core/engine.py +159 -0
  23. kapsel/core/executor.py +200 -0
  24. kapsel/core/i18n.py +31 -0
  25. kapsel/core/plugin/__init__.py +17 -0
  26. kapsel/core/plugin/base.py +53 -0
  27. kapsel/core/plugin/catalog.py +187 -0
  28. kapsel/core/plugin/context.py +71 -0
  29. kapsel/core/plugin/hooks.py +34 -0
  30. kapsel/core/plugin/manager.py +173 -0
  31. kapsel/i18n.py +196 -0
  32. kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
  33. kapsel/locales/de/LC_MESSAGES/kapsel.po +215 -0
  34. kapsel/locales/de/help.yaml +101 -0
  35. kapsel/locales/en/help.yaml +101 -0
  36. kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
  37. kapsel/locales/es/LC_MESSAGES/kapsel.po +213 -0
  38. kapsel/locales/es/help.yaml +101 -0
  39. kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
  40. kapsel/locales/fr/LC_MESSAGES/kapsel.po +213 -0
  41. kapsel/locales/fr/help.yaml +101 -0
  42. kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
  43. kapsel/locales/ja/LC_MESSAGES/kapsel.po +211 -0
  44. kapsel/locales/ja/help.yaml +101 -0
  45. kapsel/locales/kapsel.pot +210 -0
  46. kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
  47. kapsel/locales/ru/LC_MESSAGES/kapsel.po +212 -0
  48. kapsel/locales/ru/help.yaml +101 -0
  49. kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
  50. kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +211 -0
  51. kapsel/locales/zh_CN/help.yaml +101 -0
  52. kapsel/storage/__init__.py +17 -0
  53. kapsel/storage/config.py +362 -0
  54. kapsel/storage/history.py +166 -0
  55. kapsel/storage/logger.py +75 -0
  56. kapsel/storage/migrate.py +119 -0
  57. kapsel/ui/__init__.py +21 -0
  58. kapsel/ui/banner.py +121 -0
  59. kapsel/ui/card.py +109 -0
  60. kapsel/ui/prompt.py +391 -0
  61. kapsel/ui/theme.py +59 -0
  62. kapsel_cli-0.1.0.dist-info/METADATA +397 -0
  63. kapsel_cli-0.1.0.dist-info/RECORD +66 -0
  64. kapsel_cli-0.1.0.dist-info/WHEEL +5 -0
  65. kapsel_cli-0.1.0.dist-info/entry_points.txt +3 -0
  66. 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