kapsel-cli 0.3.6__tar.gz → 0.5.2__tar.gz
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_cli-0.3.6 → kapsel_cli-0.5.2}/PKG-INFO +1 -1
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/__init__.py +1 -1
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/cli.py +8 -14
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/__init__.py +23 -2
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/add.py +132 -51
- kapsel_cli-0.5.2/kapsel/completion/kps/builtins/dir_nav.py +318 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/search.py +11 -4
- kapsel_cli-0.5.2/kapsel/completion/kps/builtins/toggle.py +281 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/upgrade.py +11 -9
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/spec_manager.py +15 -5
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/executor.py +43 -39
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/plugin/catalog.py +18 -3
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/plugin/manager.py +9 -1
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/storage/config.py +13 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/storage/history.py +29 -10
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/ui/banner.py +8 -2
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/ui/prompt.py +80 -4
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/ui/theme.py +15 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/PKG-INFO +1 -1
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/SOURCES.txt +9 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/pyproject.toml +1 -1
- kapsel_cli-0.5.2/tests/test_dir_nav.py +241 -0
- kapsel_cli-0.5.2/tests/test_history.py +78 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_plugin_ai.py +2 -3
- kapsel_cli-0.5.2/tests/test_plugin_autopilot.py +175 -0
- kapsel_cli-0.5.2/tests/test_plugin_commit.py +165 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_plugin_fetcher.py +10 -0
- kapsel_cli-0.5.2/tests/test_plugin_lang.py +115 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_plugin_portal.py +85 -1
- kapsel_cli-0.5.2/tests/test_plugin_preview.py +508 -0
- kapsel_cli-0.5.2/tests/test_plugin_queue.py +78 -0
- kapsel_cli-0.5.2/tests/test_plugin_template.py +67 -0
- kapsel_cli-0.5.2/tests/test_plugin_trans.py +151 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_search_and_completions.py +1 -0
- kapsel_cli-0.5.2/tests/test_toggle.py +115 -0
- kapsel_cli-0.3.6/kapsel/completion/kps/builtins/toggle.py +0 -34
- kapsel_cli-0.3.6/tests/test_history.py +0 -223
- kapsel_cli-0.3.6/tests/test_plugin_autopilot.py +0 -65
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/README.md +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/__main__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/carapace_engine.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/carapace_installer.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/completer.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/completion.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/config.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/datadir.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/help.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/language.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/plugin_switch.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/status.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/dispatcher.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/completion/kps/registry.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/block/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/block/model.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/block/registry.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/block/runner.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/detector.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/engine.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/i18n.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/plugin/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/plugin/base.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/plugin/context.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/plugin/fetcher.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/core/plugin/hooks.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/i18n.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/de/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/de/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/en/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/en/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/es/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/es/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/fr/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/fr/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/ja/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/ja/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/kapsel.pot +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/ru/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/ru/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/zh_CN/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/locales/zh_CN/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/storage/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/storage/logger.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/storage/migrate.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/ui/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/ui/card.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/ui/tips.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel/ui/viewport.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/dependency_links.txt +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/entry_points.txt +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/requires.txt +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/top_level.txt +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/setup.cfg +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_block_system.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_carapace_installer.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_config.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_i18n.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_plugin_alias.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_plugin_init.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_plugin_install.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_spec_manager.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.5.2}/tests/test_upgrade.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kapsel-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Next-generation cross-platform terminal capsule & ergonomic shell multiplexer with context-aware command translation and 1,000+ CLI completions.
|
|
5
5
|
Author-email: MrEiu <k648888@vip.qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -48,13 +48,8 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
48
48
|
)
|
|
49
49
|
return result.execution.exit_code
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
if clean_args
|
|
53
|
-
is_toggle_start = True
|
|
54
|
-
clean_args = clean_args[1:]
|
|
55
|
-
|
|
56
|
-
# If other positional subcommands passed (e.g. kapsel status, kapsel help, kapsel add, kapsel config, kapsel datadir)
|
|
57
|
-
if clean_args and not is_toggle_start:
|
|
51
|
+
# If positional subcommands passed (e.g. kapsel toggle, kapsel status, kapsel help, kapsel add, kapsel config, kapsel datadir)
|
|
52
|
+
if clean_args:
|
|
58
53
|
engine = DualStateEngine()
|
|
59
54
|
full_cmd = "kapsel " + " ".join(clean_args)
|
|
60
55
|
result = engine.dispatch(full_cmd)
|
|
@@ -66,6 +61,10 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
66
61
|
)
|
|
67
62
|
return result.execution.exit_code
|
|
68
63
|
|
|
64
|
+
# Do not start interactive shell in non-interactive / piped environments
|
|
65
|
+
if not sys.stdin.isatty() and not os.environ.get("KAPSEL_FORCE_TTY"):
|
|
66
|
+
return 0
|
|
67
|
+
|
|
69
68
|
# Launch interactive shell (Kapsel Default Mode)
|
|
70
69
|
engine = DualStateEngine()
|
|
71
70
|
console = Console(legacy_windows=False)
|
|
@@ -76,9 +75,7 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
76
75
|
|
|
77
76
|
os.environ["KAPSEL_ACTIVE"] = "1"
|
|
78
77
|
|
|
79
|
-
if
|
|
80
|
-
console.print("[bold #10b981]✔ Kapsel active.[/] [dim]Type 'toggle' or 'exit' to quit.[/]\n")
|
|
81
|
-
elif engine.config.enable_banner and not no_banner:
|
|
78
|
+
if engine.config.enable_banner and not no_banner:
|
|
82
79
|
render_banner()
|
|
83
80
|
|
|
84
81
|
prompt_session = KapselPrompt(engine)
|
|
@@ -101,7 +98,7 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
101
98
|
continue
|
|
102
99
|
|
|
103
100
|
normalized = " ".join(stripped.lower().split())
|
|
104
|
-
if normalized in ("exit", "quit", "
|
|
101
|
+
if normalized in ("exit", "quit", "bye"):
|
|
105
102
|
os.environ.pop("KAPSEL_ACTIVE", None)
|
|
106
103
|
console.print("[dim]Exited Kapsel.[/]")
|
|
107
104
|
break
|
|
@@ -143,9 +140,6 @@ def kps_cli() -> int:
|
|
|
143
140
|
print(f"Kapsel v{__version__}")
|
|
144
141
|
return 0
|
|
145
142
|
|
|
146
|
-
if argv[0] == "toggle":
|
|
147
|
-
return main(["toggle"])
|
|
148
|
-
|
|
149
143
|
# 1. Fast dispatch to registered built-in or plugin kps commands
|
|
150
144
|
cmd_line = " ".join(argv)
|
|
151
145
|
builtin_exit = dispatch_kps(cmd_line)
|
|
@@ -114,8 +114,13 @@ def register_builtins(registry: "KpsCommandRegistry") -> None:
|
|
|
114
114
|
registry.register(
|
|
115
115
|
name="toggle",
|
|
116
116
|
handler=handle_toggle_command,
|
|
117
|
-
help_text="Toggle Kapsel as default terminal
|
|
118
|
-
|
|
117
|
+
help_text="Toggle Kapsel as default terminal startup shell (auto-launch on opening terminal)",
|
|
118
|
+
subcommands={
|
|
119
|
+
"on": "Enable Kapsel as default terminal startup shell in shell profile",
|
|
120
|
+
"off": "Disable Kapsel default startup and restore host shell",
|
|
121
|
+
"status": "Check current default terminal startup configuration",
|
|
122
|
+
},
|
|
123
|
+
usage="kapsel toggle [on|off|status]",
|
|
119
124
|
scope="system",
|
|
120
125
|
)
|
|
121
126
|
|
|
@@ -187,4 +192,20 @@ def register_builtins(registry: "KpsCommandRegistry") -> None:
|
|
|
187
192
|
scope="system",
|
|
188
193
|
)
|
|
189
194
|
|
|
195
|
+
# 11. dir & rm (Directional navigation & file management)
|
|
196
|
+
from kapsel.completion.kps.builtins.dir_nav import handle_dir_command, handle_rm_command
|
|
197
|
+
registry.register(
|
|
198
|
+
name="dir",
|
|
199
|
+
handler=handle_dir_command,
|
|
200
|
+
help_text="Preview directories and view Ctrl+Arrow navigation shortcuts",
|
|
201
|
+
usage="kapsel dir",
|
|
202
|
+
scope="system",
|
|
203
|
+
)
|
|
204
|
+
registry.register(
|
|
205
|
+
name="rm",
|
|
206
|
+
handler=handle_rm_command,
|
|
207
|
+
help_text="Safely permanently delete a file or directory with explicit confirmation",
|
|
208
|
+
usage="kapsel rm <file_or_directory>",
|
|
209
|
+
scope="system",
|
|
210
|
+
)
|
|
190
211
|
|
|
@@ -9,93 +9,72 @@ import importlib.util
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
import shutil
|
|
11
11
|
import sys
|
|
12
|
-
from typing import List, Optional
|
|
12
|
+
from typing import List, Optional, Tuple
|
|
13
13
|
from rich.console import Console
|
|
14
14
|
from rich.panel import Panel
|
|
15
|
+
from rich.table import Table
|
|
15
16
|
|
|
16
17
|
from kapsel.storage.config import get_kapsel_dir, load_config, update_config_value
|
|
17
18
|
from kapsel.ui.banner import ensure_utf8_io
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
def
|
|
21
|
+
def _install_single_plugin(
|
|
22
|
+
target_raw: str,
|
|
23
|
+
con: Console,
|
|
24
|
+
is_dev: bool = False,
|
|
25
|
+
is_batch: bool = False,
|
|
26
|
+
) -> Tuple[bool, str]:
|
|
21
27
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
Installs a single plugin by name or path:
|
|
29
|
+
1. Resolves source directory (dev local, user global, or remote fetch).
|
|
30
|
+
2. Copies to user global plugins directory.
|
|
31
|
+
3. Adds to config.yaml enabled plugins list.
|
|
32
|
+
4. Triggers standalone install.py if present.
|
|
33
|
+
Returns (success: bool, message: str).
|
|
24
34
|
"""
|
|
25
|
-
ensure_utf8_io()
|
|
26
|
-
con = console or Console(legacy_windows=False)
|
|
27
|
-
|
|
28
|
-
if not args:
|
|
29
|
-
con.print("[bold #f43f5e]Error:[/] Please specify a plugin name to add.")
|
|
30
|
-
con.print("[dim]Usage: kapsel add <plugin_name> (e.g. kapsel add install)[/]\n")
|
|
31
|
-
return 1
|
|
32
|
-
|
|
33
|
-
target_raw = args[0].strip()
|
|
34
|
-
|
|
35
|
-
# Handle 'kapsel add update' to scan and refresh catalog subcommands dictionary
|
|
36
|
-
if target_raw.lower() == "update":
|
|
37
|
-
from kapsel.core.plugin.catalog import update_plugin_catalog, render_catalog_table
|
|
38
|
-
con.print("[bold #00f0ff]🔄 Scanning repository plugins and updating catalog dictionary...[/]")
|
|
39
|
-
updated_catalog = update_plugin_catalog(con)
|
|
40
|
-
render_catalog_table(updated_catalog, con)
|
|
41
|
-
con.print("[bold #10b981]✔ Plugin catalog and subcommands completion dictionary successfully updated![/]\n")
|
|
42
|
-
return 0
|
|
43
|
-
|
|
44
|
-
cfg = load_config()
|
|
45
|
-
is_dev = getattr(cfg, "is_dev", False)
|
|
46
|
-
|
|
47
|
-
source_path = Path(target_raw).expanduser()
|
|
48
|
-
|
|
49
35
|
global_plugins_dir = get_kapsel_dir() / "plugins"
|
|
50
36
|
global_plugins_dir.mkdir(parents=True, exist_ok=True)
|
|
51
37
|
|
|
52
|
-
|
|
53
|
-
# In release mode, bare names like 'install' will never be hijacked by local folders.
|
|
54
|
-
# Only treat as a local directory if explicit path syntax is used (./, ../, /, \) or in dev mode.
|
|
38
|
+
source_path = Path(target_raw).expanduser()
|
|
55
39
|
has_path_qualifier = any(sep in target_raw for sep in ("/", "\\")) or target_raw.startswith(".")
|
|
56
40
|
|
|
57
41
|
if (has_path_qualifier or is_dev) and source_path.exists() and source_path.is_dir():
|
|
58
|
-
# User specified an explicit directory path (or in dev mode where local directory takes priority)
|
|
59
42
|
target_name = source_path.name.lower()
|
|
60
43
|
dest_plugin_dir = global_plugins_dir / target_name
|
|
61
44
|
|
|
62
|
-
# Verify source has __init__.py
|
|
63
45
|
if not (source_path / "__init__.py").exists():
|
|
64
|
-
|
|
65
|
-
|
|
46
|
+
err = f"Source directory '{source_path}' does not contain '__init__.py'."
|
|
47
|
+
con.print(f"[bold #f43f5e]Error:[/] {err}")
|
|
48
|
+
return False, err
|
|
66
49
|
|
|
67
|
-
# Install / sync to user global plugin directory
|
|
68
50
|
shutil.copytree(source_path, dest_plugin_dir, dirs_exist_ok=True)
|
|
69
51
|
plugin_dir = dest_plugin_dir
|
|
70
|
-
|
|
71
52
|
else:
|
|
72
|
-
# User specified a plugin name (e.g. kapsel add install)
|
|
73
53
|
target_name = target_raw.lower()
|
|
74
54
|
dest_plugin_dir = global_plugins_dir / target_name
|
|
75
55
|
dev_plugin_dir = Path.cwd() / "plugins" / target_name
|
|
76
56
|
|
|
77
57
|
if is_dev and dev_plugin_dir.exists() and dev_plugin_dir.is_dir() and (dev_plugin_dir / "__init__.py").exists():
|
|
78
|
-
# If present in local dev directory (dev mode only), sync to global user directory
|
|
79
58
|
shutil.copytree(dev_plugin_dir, dest_plugin_dir, dirs_exist_ok=True)
|
|
80
59
|
plugin_dir = dest_plugin_dir
|
|
81
60
|
elif dest_plugin_dir.exists() and dest_plugin_dir.is_dir():
|
|
82
61
|
plugin_dir = dest_plugin_dir
|
|
83
62
|
else:
|
|
84
|
-
# 3. Not found locally: Automatically fetch from official remote repository
|
|
85
63
|
from kapsel.core.plugin.fetcher import fetch_plugin_from_remote
|
|
86
|
-
|
|
87
64
|
if fetch_plugin_from_remote(target_name, dest_plugin_dir, con):
|
|
88
65
|
plugin_dir = dest_plugin_dir
|
|
89
66
|
else:
|
|
90
|
-
|
|
91
|
-
con.print(f"[
|
|
92
|
-
|
|
93
|
-
|
|
67
|
+
err = f"Plugin '{target_name}' could not be installed."
|
|
68
|
+
con.print(f"[bold #f43f5e]Error:[/] {err}")
|
|
69
|
+
if not is_batch:
|
|
70
|
+
con.print(f"[dim]Expected location:[/] {dest_plugin_dir}")
|
|
71
|
+
con.print("[dim]To install a custom plugin, specify its local path or place it into your global plugins directory.[/]\n")
|
|
72
|
+
return False, err
|
|
94
73
|
|
|
95
|
-
# Verify that the final plugin directory contains __init__.py
|
|
96
74
|
if not (plugin_dir / "__init__.py").exists():
|
|
97
|
-
|
|
98
|
-
|
|
75
|
+
err = f"Plugin directory '{plugin_dir}' does not contain a valid '__init__.py'."
|
|
76
|
+
con.print(f"[bold #f43f5e]Error:[/] {err}")
|
|
77
|
+
return False, err
|
|
99
78
|
|
|
100
79
|
# Add to config.yaml enabled plugins list
|
|
101
80
|
cfg = load_config()
|
|
@@ -105,7 +84,6 @@ def handle_add_command(args: List[str], console: Optional[Console] = None) -> in
|
|
|
105
84
|
update_config_value("plugins", "enabled", new_enabled)
|
|
106
85
|
|
|
107
86
|
# Decoupled plugin dependency installation:
|
|
108
|
-
# Trigger standalone install.py if provided by the plugin
|
|
109
87
|
install_script = plugin_dir / "install.py"
|
|
110
88
|
if install_script.exists():
|
|
111
89
|
try:
|
|
@@ -132,6 +110,109 @@ def handle_add_command(args: List[str], console: Optional[Console] = None) -> in
|
|
|
132
110
|
|
|
133
111
|
msg = f"[bold #10b981]✔ Plugin '[#00f0ff]{target_name}[/]' (v{ver}) successfully added and enabled![/]\n\n"
|
|
134
112
|
msg += f"[dim]Location: {plugin_dir}[/]"
|
|
113
|
+
return True, msg
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _handle_add_all(con: Console, is_dev: bool = False) -> int:
|
|
117
|
+
"""
|
|
118
|
+
Sequentially installs and runs installation scripts for all official Kapsel plugins.
|
|
119
|
+
"""
|
|
120
|
+
from kapsel.core.plugin.catalog import load_plugin_catalog_rich, DEFAULT_CATALOG
|
|
121
|
+
catalog = load_plugin_catalog_rich()
|
|
122
|
+
if not catalog:
|
|
123
|
+
catalog = {k: {"description": v} for k, v in DEFAULT_CATALOG.items()}
|
|
124
|
+
|
|
125
|
+
plugin_names = set()
|
|
126
|
+
for k in catalog.keys():
|
|
127
|
+
if k not in ("update", "all") and not k.startswith(("_", "$")):
|
|
128
|
+
plugin_names.add(k)
|
|
129
|
+
|
|
130
|
+
dev_plugins_dir = Path.cwd() / "plugins"
|
|
131
|
+
if is_dev and dev_plugins_dir.is_dir():
|
|
132
|
+
for item in dev_plugins_dir.iterdir():
|
|
133
|
+
if item.is_dir() and (item / "__init__.py").exists():
|
|
134
|
+
if item.name.lower() not in ("update", "all"):
|
|
135
|
+
plugin_names.add(item.name.lower())
|
|
136
|
+
|
|
137
|
+
ordered_plugins = sorted(plugin_names)
|
|
138
|
+
total = len(ordered_plugins)
|
|
139
|
+
|
|
140
|
+
con.print(f"\n[bold #00f0ff]============================================================[/]")
|
|
141
|
+
con.print(f"[bold #00f0ff] 📦 Kapsel Batch Plugin Installer: {total} Plugins Found [/]")
|
|
142
|
+
con.print(f"[bold #00f0ff]============================================================[/]\n")
|
|
143
|
+
|
|
144
|
+
succeeded: List[str] = []
|
|
145
|
+
failed: List[str] = []
|
|
146
|
+
|
|
147
|
+
for idx, name in enumerate(ordered_plugins, 1):
|
|
148
|
+
con.print(f"[bold #00f0ff]━━━ [{idx}/{total}] Adding & Installing Plugin: [white]{name}[/] ━━━[/]")
|
|
149
|
+
try:
|
|
150
|
+
ok, msg = _install_single_plugin(name, con, is_dev=is_dev, is_batch=True)
|
|
151
|
+
if ok:
|
|
152
|
+
succeeded.append(name)
|
|
153
|
+
con.print(f" [bold #10b981]✔ Added & configured successfully[/]\n")
|
|
154
|
+
else:
|
|
155
|
+
failed.append(name)
|
|
156
|
+
con.print(f" [bold #f43f5e]✘ Installation failed[/]\n")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
failed.append(name)
|
|
159
|
+
con.print(f" [bold #f43f5e]✘ Unexpected error during '{name}' install:[/] {e}\n")
|
|
160
|
+
|
|
161
|
+
# Summary table
|
|
162
|
+
table = Table(title="[bold #00f0ff]📊 Batch Installation Summary[/]", border_style="#0891b2")
|
|
163
|
+
table.add_column("Plugin", style="#00f0ff")
|
|
164
|
+
table.add_column("Status", style="white")
|
|
165
|
+
|
|
166
|
+
for name in ordered_plugins:
|
|
167
|
+
if name in succeeded:
|
|
168
|
+
table.add_row(name, "[bold #10b981]✔ Installed & Enabled[/]")
|
|
169
|
+
else:
|
|
170
|
+
table.add_row(name, "[bold #f43f5e]✘ Failed[/]")
|
|
171
|
+
|
|
172
|
+
con.print(table)
|
|
173
|
+
summary_color = "#10b981" if not failed else "#f59e0b"
|
|
174
|
+
con.print(f"\n[{summary_color}]Batch Completed:[/] [white]{len(succeeded)} succeeded[/], "
|
|
175
|
+
f"[{'#f43f5e' if failed else 'white'}]{len(failed)} failed[/].\n")
|
|
176
|
+
return 0 if succeeded else 1
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def handle_add_command(args: List[str], console: Optional[Console] = None) -> int:
|
|
180
|
+
"""
|
|
181
|
+
Handles 'kapsel add <plugin_name>' system command.
|
|
182
|
+
Enables local plugins, triggers their standalone install.py, and registers them into config.yaml.
|
|
183
|
+
Supports 'kapsel add all' to sequentially install all available plugins.
|
|
184
|
+
"""
|
|
185
|
+
ensure_utf8_io()
|
|
186
|
+
con = console or Console(legacy_windows=False)
|
|
187
|
+
|
|
188
|
+
if not args:
|
|
189
|
+
con.print("[bold #f43f5e]Error:[/] Please specify a plugin name to add.")
|
|
190
|
+
con.print("[dim]Usage: kapsel add <plugin_name> | all | update (e.g. kapsel add all)[/]\n")
|
|
191
|
+
return 1
|
|
192
|
+
|
|
193
|
+
target_raw = args[0].strip()
|
|
194
|
+
|
|
195
|
+
# Handle 'kapsel add update' to scan and refresh catalog subcommands dictionary
|
|
196
|
+
if target_raw.lower() == "update":
|
|
197
|
+
from kapsel.core.plugin.catalog import update_plugin_catalog, render_catalog_table
|
|
198
|
+
con.print("[bold #00f0ff]🔄 Scanning repository plugins and updating catalog dictionary...[/]")
|
|
199
|
+
updated_catalog = update_plugin_catalog(con)
|
|
200
|
+
render_catalog_table(updated_catalog, con)
|
|
201
|
+
con.print("[bold #10b981]✔ Plugin catalog and subcommands completion dictionary successfully updated![/]\n")
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
cfg = load_config()
|
|
205
|
+
is_dev = getattr(cfg, "is_dev", False)
|
|
206
|
+
|
|
207
|
+
# Handle 'kapsel add all' to sequentially install all available plugins
|
|
208
|
+
if target_raw.lower() == "all":
|
|
209
|
+
return _handle_add_all(con, is_dev=is_dev)
|
|
210
|
+
|
|
211
|
+
# Handle single plugin installation
|
|
212
|
+
success, msg = _install_single_plugin(target_raw, con, is_dev=is_dev, is_batch=False)
|
|
213
|
+
if success:
|
|
214
|
+
con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
|
|
215
|
+
return 0
|
|
216
|
+
else:
|
|
217
|
+
return 1
|
|
135
218
|
|
|
136
|
-
con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
|
|
137
|
-
return 0
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kapsel Directional Navigation & Native File Management Suite (方向键原生交互与文件管理).
|
|
3
|
+
Completely avoids bottom_toolbar for maximum terminal compatibility.
|
|
4
|
+
Uses native floating completion menus and prompt-line interactions:
|
|
5
|
+
- Ctrl+Right: Opens native floating completion dropdown for subdirectories (cd <dir>/).
|
|
6
|
+
- Ctrl+Left: Double-tap returns to parent directory ('cd ..').
|
|
7
|
+
- Ctrl+Up: Populates 'touch ' on prompt line for cross-platform empty file creation.
|
|
8
|
+
- Ctrl+Down: Double-tap populates 'kapsel rm ' with native completion dropdown + explicit terminal confirmation.
|
|
9
|
+
All comments and docstrings are in English.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import time
|
|
16
|
+
from typing import List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
from prompt_toolkit.application.run_in_terminal import run_in_terminal
|
|
19
|
+
from prompt_toolkit.buffer import CompletionState
|
|
20
|
+
from prompt_toolkit.completion import Completion
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Double-tap timers
|
|
26
|
+
_LAST_CTRL_LEFT_TIME: float = 0.0
|
|
27
|
+
_LAST_CTRL_DOWN_TIME: float = 0.0
|
|
28
|
+
DOUBLE_TAP_TIMEOUT: float = 2.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def list_subdirectories(cwd: Optional[Path] = None, show_hidden: bool = False) -> List[Path]:
|
|
32
|
+
"""Lists subdirectories within cwd, sorted alphabetically case-insensitively."""
|
|
33
|
+
target = cwd or Path.cwd()
|
|
34
|
+
try:
|
|
35
|
+
dirs = [
|
|
36
|
+
p for p in target.iterdir()
|
|
37
|
+
if p.is_dir() and (show_hidden or not p.name.startswith("."))
|
|
38
|
+
]
|
|
39
|
+
dirs.sort(key=lambda p: p.name.lower())
|
|
40
|
+
return dirs
|
|
41
|
+
except Exception:
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def list_all_items(cwd: Optional[Path] = None, show_hidden: bool = False) -> List[Path]:
|
|
46
|
+
"""Lists all files and subdirectories within cwd for deletion selection."""
|
|
47
|
+
target = cwd or Path.cwd()
|
|
48
|
+
try:
|
|
49
|
+
items = [
|
|
50
|
+
p for p in target.iterdir()
|
|
51
|
+
if (show_hidden or not p.name.startswith("."))
|
|
52
|
+
]
|
|
53
|
+
items.sort(key=lambda p: (not p.is_dir(), p.name.lower()))
|
|
54
|
+
return items
|
|
55
|
+
except Exception:
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_empty_file(cwd: Path, filename: str) -> Tuple[bool, str]:
|
|
60
|
+
"""Safely creates a new empty file in the given directory."""
|
|
61
|
+
clean_name = filename.strip().strip("\"'")
|
|
62
|
+
if not clean_name:
|
|
63
|
+
return False, "文件名不能为空 (Filename cannot be empty)"
|
|
64
|
+
|
|
65
|
+
target = cwd / clean_name
|
|
66
|
+
try:
|
|
67
|
+
if target.exists():
|
|
68
|
+
return False, f"目标已存在 (Item already exists): {clean_name}"
|
|
69
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
target.touch()
|
|
71
|
+
return True, f"✔ 空文件已创建 (Created): {clean_name}"
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return False, f"✖ 创建失败 (Creation error): {e}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def delete_item_permanently(target_path: Path) -> Tuple[bool, str]:
|
|
77
|
+
"""Permanently deletes a file or directory without backup."""
|
|
78
|
+
try:
|
|
79
|
+
if not target_path.exists():
|
|
80
|
+
return False, f"目标不存在 (Item not found): {target_path.name}"
|
|
81
|
+
if target_path.is_dir():
|
|
82
|
+
shutil.rmtree(target_path)
|
|
83
|
+
else:
|
|
84
|
+
target_path.unlink()
|
|
85
|
+
return True, f"✔ 已永久删除 (Permanently deleted): {target_path.name}"
|
|
86
|
+
except Exception as e:
|
|
87
|
+
return False, f"✖ 删除失败 (Deletion error): {e}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# -----------------------------------------------------------------------------
|
|
91
|
+
# 1. Ctrl+Right: Native Floating Completion Dropdown for Subdirectories
|
|
92
|
+
# -----------------------------------------------------------------------------
|
|
93
|
+
def trigger_subfolder_completion(buffer, cwd: Optional[Path] = None) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Populates 'cd ' into buffer and immediately activates prompt_toolkit's
|
|
96
|
+
native floating completion dropdown populated with subdirectories.
|
|
97
|
+
"""
|
|
98
|
+
target_cwd = cwd or Path.cwd()
|
|
99
|
+
dirs = list_subdirectories(target_cwd)
|
|
100
|
+
|
|
101
|
+
if not dirs:
|
|
102
|
+
run_in_terminal(lambda: print("\n\x1b[38;5;245m[📁 提示: 当前目录下暂无子文件夹]\x1b[0m"))
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
# Ensure buffer has 'cd ' prefix
|
|
106
|
+
raw = buffer.text.strip()
|
|
107
|
+
if not raw or raw == "cd":
|
|
108
|
+
buffer.text = "cd "
|
|
109
|
+
buffer.cursor_position = 3
|
|
110
|
+
elif not buffer.text.endswith(" "):
|
|
111
|
+
buffer.text += " "
|
|
112
|
+
buffer.cursor_position = len(buffer.text)
|
|
113
|
+
|
|
114
|
+
completions = []
|
|
115
|
+
for d in dirs:
|
|
116
|
+
name = d.name
|
|
117
|
+
text_insert = f'"{name}"' if " " in name else name
|
|
118
|
+
completions.append(Completion(
|
|
119
|
+
text=text_insert,
|
|
120
|
+
start_position=0,
|
|
121
|
+
display=f"📁 {name}/",
|
|
122
|
+
display_meta="[目录 - Enter选入,再按Enter进入]",
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
buffer.complete_state = CompletionState(
|
|
126
|
+
original_document=buffer.document,
|
|
127
|
+
completions=completions,
|
|
128
|
+
complete_index=0,
|
|
129
|
+
)
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# -----------------------------------------------------------------------------
|
|
134
|
+
# 2. Ctrl+Left: Double-Tap Parent Directory Return (cd ..)
|
|
135
|
+
# -----------------------------------------------------------------------------
|
|
136
|
+
def handle_ctrl_left_double_tap(buffer, on_cd_parent=None) -> bool:
|
|
137
|
+
"""
|
|
138
|
+
Double-tap check for returning to parent directory (cd ..).
|
|
139
|
+
1st tap: Shows light inline notice.
|
|
140
|
+
2nd tap (< 2.0s): Executes 'cd ..'.
|
|
141
|
+
"""
|
|
142
|
+
global _LAST_CTRL_LEFT_TIME
|
|
143
|
+
now = time.time()
|
|
144
|
+
if now - _LAST_CTRL_LEFT_TIME <= DOUBLE_TAP_TIMEOUT:
|
|
145
|
+
_LAST_CTRL_LEFT_TIME = 0.0
|
|
146
|
+
if on_cd_parent is not None:
|
|
147
|
+
on_cd_parent()
|
|
148
|
+
else:
|
|
149
|
+
buffer.text = "cd .."
|
|
150
|
+
buffer.cursor_position = len(buffer.text)
|
|
151
|
+
buffer.validate_and_handle()
|
|
152
|
+
return True
|
|
153
|
+
else:
|
|
154
|
+
_LAST_CTRL_LEFT_TIME = now
|
|
155
|
+
run_in_terminal(lambda: print("\n\x1b[38;5;245m[📁 提示: 再按一次 Ctrl+Left 返回父目录 (..)]\x1b[0m"))
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# -----------------------------------------------------------------------------
|
|
160
|
+
# 3. Ctrl+Up: Create New Empty File Prompt Line
|
|
161
|
+
# -----------------------------------------------------------------------------
|
|
162
|
+
def trigger_create_file(buffer) -> None:
|
|
163
|
+
"""Populates 'touch ' into prompt line for easy cross-platform file creation."""
|
|
164
|
+
buffer.text = "touch "
|
|
165
|
+
buffer.cursor_position = 6
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# -----------------------------------------------------------------------------
|
|
169
|
+
# 4. Ctrl+Down: Double-Tap Item Deletion with Native Completion & Confirmation
|
|
170
|
+
# -----------------------------------------------------------------------------
|
|
171
|
+
def handle_ctrl_down_double_tap(buffer, cwd: Optional[Path] = None) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Double-tap check for item deletion.
|
|
174
|
+
1st tap: Shows caution notice.
|
|
175
|
+
2nd tap (< 2.0s): Populates 'kapsel rm ' and triggers native floating completions.
|
|
176
|
+
"""
|
|
177
|
+
global _LAST_CTRL_DOWN_TIME
|
|
178
|
+
now = time.time()
|
|
179
|
+
if now - _LAST_CTRL_DOWN_TIME <= DOUBLE_TAP_TIMEOUT:
|
|
180
|
+
_LAST_CTRL_DOWN_TIME = 0.0
|
|
181
|
+
target_cwd = cwd or Path.cwd()
|
|
182
|
+
items = list_all_items(target_cwd)
|
|
183
|
+
|
|
184
|
+
if not items:
|
|
185
|
+
run_in_terminal(lambda: print("\n\x1b[38;5;203m[⚠️ 提示: 当前目录下无任何文件或文件夹]\x1b[0m"))
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
buffer.text = "kapsel rm "
|
|
189
|
+
buffer.cursor_position = len(buffer.text)
|
|
190
|
+
|
|
191
|
+
completions = []
|
|
192
|
+
for it in items:
|
|
193
|
+
name = it.name
|
|
194
|
+
icon = "📁 " if it.is_dir() else "📄 "
|
|
195
|
+
type_label = "[目录]" if it.is_dir() else "[文件]"
|
|
196
|
+
text_insert = f'"{name}"' if " " in name else name
|
|
197
|
+
completions.append(Completion(
|
|
198
|
+
text=text_insert,
|
|
199
|
+
start_position=0,
|
|
200
|
+
display=f"{icon}{name}",
|
|
201
|
+
display_meta=f"{type_label} 永久删除",
|
|
202
|
+
))
|
|
203
|
+
|
|
204
|
+
buffer.complete_state = CompletionState(
|
|
205
|
+
original_document=buffer.document,
|
|
206
|
+
completions=completions,
|
|
207
|
+
complete_index=0,
|
|
208
|
+
)
|
|
209
|
+
return True
|
|
210
|
+
else:
|
|
211
|
+
_LAST_CTRL_DOWN_TIME = now
|
|
212
|
+
run_in_terminal(lambda: print("\n\x1b[38;5;203m[⚠️ 谨慎操作: 再按一次 Ctrl+Down 进入删除选择]\x1b[0m"))
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# -----------------------------------------------------------------------------
|
|
217
|
+
# CLI Handlers: 'kapsel rm' (strict confirmation) and 'kapsel dir'
|
|
218
|
+
# -----------------------------------------------------------------------------
|
|
219
|
+
def handle_rm_command(args: List[str], console: Optional[Console] = None) -> int:
|
|
220
|
+
"""CLI handler for 'kapsel rm' with strict interactive double confirmation."""
|
|
221
|
+
con = console or Console(legacy_windows=False)
|
|
222
|
+
if not args or args[0] in ("-h", "--help", "help"):
|
|
223
|
+
con.print("[dim]Usage: kapsel rm <file_or_directory>[/]")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
raw_path = " ".join(args).strip().strip("\"'")
|
|
227
|
+
target = Path(raw_path)
|
|
228
|
+
if not target.is_absolute():
|
|
229
|
+
target = (Path.cwd() / target).resolve()
|
|
230
|
+
|
|
231
|
+
if not target.exists():
|
|
232
|
+
con.print(f"[bold #f43f5e]✖ 目标不存在: {raw_path}[/]")
|
|
233
|
+
return 1
|
|
234
|
+
|
|
235
|
+
item_type = "目录 (及其中所有内容)" if target.is_dir() else "文件"
|
|
236
|
+
con.print(f"\n[bold #f43f5e]🚨 永久删除警告[/]: 即将永久彻底删除 {item_type} [bold white]{target.name}[/]!")
|
|
237
|
+
con.print("[dim]该操作不可逆,且不保留回收站备份。[/]")
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
reply = input("👉 请输入 y 确认删除 (输入其他键放弃): ").strip().lower()
|
|
241
|
+
except (KeyboardInterrupt, EOFError):
|
|
242
|
+
con.print("\n[dim]操作已取消。[/]")
|
|
243
|
+
return 0
|
|
244
|
+
|
|
245
|
+
if reply in ("y", "yes"):
|
|
246
|
+
ok, msg = delete_item_permanently(target)
|
|
247
|
+
if ok:
|
|
248
|
+
con.print(f"[bold #10b981]{msg}[/]\n")
|
|
249
|
+
return 0
|
|
250
|
+
else:
|
|
251
|
+
con.print(f"[bold #f43f5e]{msg}[/]\n")
|
|
252
|
+
return 1
|
|
253
|
+
else:
|
|
254
|
+
con.print("[dim]操作已放弃,未做任何更改。[/]\n")
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def handle_dir_command(args: List[str], console: Optional[Console] = None) -> int:
|
|
259
|
+
"""CLI entry point for 'kapsel dir' / 'kps dir'."""
|
|
260
|
+
con = console or Console(legacy_windows=False)
|
|
261
|
+
|
|
262
|
+
if args and args[0] in ("-h", "--help", "help"):
|
|
263
|
+
con.print("\n[bold #00f0ff]📁 Kapsel Directional Navigation & File Management (`kapsel dir`)[/]")
|
|
264
|
+
con.print("[dim]Inspect directories and view keyboard shortcuts for rapid prompt navigation.[/]\n")
|
|
265
|
+
table = Table(border_style="#0891b2", title="[bold #38bdf8]⚡ Directional Hotkeys (Ctrl + Arrows)[/]")
|
|
266
|
+
table.add_column("Shortcut", style="bold #00f0ff", width=16)
|
|
267
|
+
table.add_column("Action", style="white", width=24)
|
|
268
|
+
table.add_column("Details & Safety Mechanism", style="dim", overflow="fold")
|
|
269
|
+
|
|
270
|
+
table.add_row(
|
|
271
|
+
"Ctrl + Right (→)",
|
|
272
|
+
"Browse & Enter Subfolder",
|
|
273
|
+
"Opens native floating dropdown; ↑/↓ select; 1st Enter loads 'cd', 2nd Enter enters folder.",
|
|
274
|
+
)
|
|
275
|
+
table.add_row(
|
|
276
|
+
"Ctrl + Left (←)",
|
|
277
|
+
"Return to Parent (cd ..)",
|
|
278
|
+
"Double-tap safety (按两下): 1st tap warns, 2nd tap within 2s returns to parent.",
|
|
279
|
+
)
|
|
280
|
+
table.add_row(
|
|
281
|
+
"Ctrl + Up (↑)",
|
|
282
|
+
"Create Empty File",
|
|
283
|
+
"Single press populates 'touch '; type filename and Enter to create.",
|
|
284
|
+
)
|
|
285
|
+
table.add_row(
|
|
286
|
+
"Ctrl + Down (↓)",
|
|
287
|
+
"Permanent Deletion",
|
|
288
|
+
"Double-tap opens delete dropdown; terminal prompts explicit (y/N) confirmation.",
|
|
289
|
+
)
|
|
290
|
+
con.print(table)
|
|
291
|
+
con.print()
|
|
292
|
+
return 0
|
|
293
|
+
|
|
294
|
+
cwd = Path.cwd()
|
|
295
|
+
dirs = list_subdirectories(cwd)
|
|
296
|
+
all_items = list_all_items(cwd)
|
|
297
|
+
files = [p for p in all_items if not p.is_dir()]
|
|
298
|
+
|
|
299
|
+
con.print(f"\n[bold #00f0ff]📂 Current Directory:[/] [bold white]{cwd}[/]")
|
|
300
|
+
con.print(f"[dim]Contains {len(dirs)} subdirectories and {len(files)} files.[/]\n")
|
|
301
|
+
|
|
302
|
+
table = Table(border_style="#0891b2")
|
|
303
|
+
table.add_column("Type", justify="center", width=8)
|
|
304
|
+
table.add_column("Name", style="bold white")
|
|
305
|
+
table.add_column("Size / Info", style="dim", justify="right", width=14)
|
|
306
|
+
|
|
307
|
+
for d in dirs:
|
|
308
|
+
table.add_row("[bold #38bdf8]📁 DIR[/]", f"[bold #00f0ff]{d.name}/[/]", "-")
|
|
309
|
+
for f in files[:25]:
|
|
310
|
+
size = f"{f.stat().st_size} B" if f.exists() else "-"
|
|
311
|
+
table.add_row("[dim]📄 FILE[/]", f.name, size)
|
|
312
|
+
|
|
313
|
+
if len(files) > 25:
|
|
314
|
+
table.add_row("[dim]...[/]", f"[dim]({len(files) - 25} more files not shown)[/]", "")
|
|
315
|
+
|
|
316
|
+
con.print(table)
|
|
317
|
+
con.print("[dim]💡 Tip: Press [bold #00f0ff]Ctrl+Right[/] in prompt to open the floating subfolder dropdown![/]\n")
|
|
318
|
+
return 0
|