kapsel-cli 0.3.8__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.8 → kapsel_cli-0.5.2}/PKG-INFO +1 -1
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/__init__.py +1 -1
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/cli.py +8 -14
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/__init__.py +23 -2
- kapsel_cli-0.5.2/kapsel/completion/kps/builtins/dir_nav.py +318 -0
- {kapsel_cli-0.3.8 → 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.8 → kapsel_cli-0.5.2}/kapsel/completion/spec_manager.py +10 -2
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/executor.py +43 -39
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/plugin/catalog.py +18 -3
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/plugin/manager.py +9 -1
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/storage/config.py +13 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/storage/history.py +29 -10
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/ui/banner.py +8 -2
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/ui/prompt.py +80 -4
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/ui/theme.py +15 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/PKG-INFO +1 -1
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/SOURCES.txt +9 -0
- {kapsel_cli-0.3.8 → 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.8 → 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.5.2/tests/test_plugin_lang.py +115 -0
- {kapsel_cli-0.3.8 → 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.8 → 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.8/kapsel/completion/kps/builtins/toggle.py +0 -34
- kapsel_cli-0.3.8/tests/test_history.py +0 -223
- kapsel_cli-0.3.8/tests/test_plugin_autopilot.py +0 -65
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/README.md +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/__main__.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/__init__.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/carapace_engine.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/carapace_installer.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/completer.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/__init__.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/add.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/completion.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/config.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/datadir.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/help.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/language.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/plugin_switch.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/status.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/builtins/upgrade.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/dispatcher.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/completion/kps/registry.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/__init__.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/block/__init__.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/block/model.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/block/registry.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/block/runner.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/detector.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/engine.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/i18n.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/plugin/__init__.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/plugin/base.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/plugin/context.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/plugin/fetcher.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/core/plugin/hooks.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/i18n.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/de/help.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/de/tips.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/en/help.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/en/tips.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/es/help.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/es/tips.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/fr/help.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/fr/tips.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/ja/help.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/ja/tips.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/kapsel.pot +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/ru/help.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/ru/tips.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/zh_CN/help.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/locales/zh_CN/tips.yaml +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/storage/__init__.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/storage/logger.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/storage/migrate.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/ui/__init__.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/ui/card.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/ui/tips.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel/ui/viewport.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/dependency_links.txt +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/entry_points.txt +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/requires.txt +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/kapsel_cli.egg-info/top_level.txt +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/setup.cfg +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/tests/test_block_system.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/tests/test_carapace_installer.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/tests/test_config.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/tests/test_i18n.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/tests/test_plugin_alias.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/tests/test_plugin_fetcher.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/tests/test_plugin_init.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/tests/test_plugin_install.py +0 -0
- {kapsel_cli-0.3.8 → kapsel_cli-0.5.2}/tests/test_spec_manager.py +0 -0
- {kapsel_cli-0.3.8 → 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
|
|
|
@@ -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
|
|
@@ -61,12 +61,19 @@ PLUGIN_METADATA: Dict[str, Dict[str, Any]] = {
|
|
|
61
61
|
"tags": ["ai", "copilot", "chat", "llm", "aichat", "openai", "claude", "gemini", "deepseek", "ollama"],
|
|
62
62
|
"summary": "Terminal AI copilot with guided setup wizard, multi-model dialogue, and code synthesis.",
|
|
63
63
|
},
|
|
64
|
-
"
|
|
65
|
-
"title": "
|
|
64
|
+
"queue": {
|
|
65
|
+
"title": "Queue Manager",
|
|
66
66
|
"tool": "pueue (Rust)",
|
|
67
67
|
"category": "Task Management",
|
|
68
|
-
"tags": ["task", "queue", "daemon", "background", "pueue", "pueued", "async", "worker", "job"],
|
|
69
|
-
"summary": "Autonomous background task queue and
|
|
68
|
+
"tags": ["task", "queue", "daemon", "background", "pueue", "pueued", "async", "worker", "job", "auto"],
|
|
69
|
+
"summary": "Autonomous background task queue and execution supervisor (powered by Pueue).",
|
|
70
|
+
},
|
|
71
|
+
"autopilot": {
|
|
72
|
+
"title": "Autopilot Daemon (PM2)",
|
|
73
|
+
"tool": "pm2 (Node.js)",
|
|
74
|
+
"category": "Process Manager",
|
|
75
|
+
"tags": ["process", "daemon", "pm2", "service", "cluster", "supervisor", "monitor"],
|
|
76
|
+
"summary": "Production process manager and daemon supervisor powered by PM2.",
|
|
70
77
|
},
|
|
71
78
|
"fuck": {
|
|
72
79
|
"title": "Auto-Correction",
|