qwik 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.
- qwik/__init__.py +3 -0
- qwik/__main__.py +6 -0
- qwik/cli.py +142 -0
- qwik/commands/add.py +110 -0
- qwik/commands/doctor.py +153 -0
- qwik/commands/edit.py +110 -0
- qwik/commands/enable_disable.py +50 -0
- qwik/commands/exporter.py +53 -0
- qwik/commands/importer.py +79 -0
- qwik/commands/init_shell.py +120 -0
- qwik/commands/list.py +32 -0
- qwik/commands/pick.py +76 -0
- qwik/commands/remove.py +36 -0
- qwik/commands/rename.py +52 -0
- qwik/commands/run.py +69 -0
- qwik/commands/search.py +40 -0
- qwik/commands/show.py +29 -0
- qwik/commands/tag.py +57 -0
- qwik/config.py +110 -0
- qwik/core/conflicts.py +186 -0
- qwik/core/models.py +196 -0
- qwik/core/search.py +86 -0
- qwik/core/store.py +154 -0
- qwik/core/substitute.py +158 -0
- qwik/shells/base.py +122 -0
- qwik/shells/bash.py +44 -0
- qwik/shells/cmd.py +51 -0
- qwik/shells/fish.py +43 -0
- qwik/shells/pwsh.py +42 -0
- qwik/shells/zsh.py +40 -0
- qwik/ui/picker.py +188 -0
- qwik/ui/prompts.py +123 -0
- qwik/ui/tables.py +97 -0
- qwik/ui/theme.py +58 -0
- qwik-0.1.0.dist-info/METADATA +468 -0
- qwik-0.1.0.dist-info/RECORD +39 -0
- qwik-0.1.0.dist-info/WHEEL +4 -0
- qwik-0.1.0.dist-info/entry_points.txt +2 -0
- qwik-0.1.0.dist-info/licenses/LICENSE +21 -0
qwik/__init__.py
ADDED
qwik/__main__.py
ADDED
qwik/cli.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Main Typer CLI application wiring all subcommands together."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from qwik import __version__
|
|
9
|
+
from qwik.commands.add import add_command
|
|
10
|
+
from qwik.commands.doctor import doctor_command
|
|
11
|
+
from qwik.commands.edit import edit_command
|
|
12
|
+
from qwik.commands.enable_disable import disable_command, enable_command
|
|
13
|
+
from qwik.commands.exporter import export_command
|
|
14
|
+
from qwik.commands.importer import import_command
|
|
15
|
+
from qwik.commands.init_shell import init_shell_command
|
|
16
|
+
from qwik.commands.list import list_command
|
|
17
|
+
from qwik.commands.pick import pick_command
|
|
18
|
+
from qwik.commands.remove import remove_command
|
|
19
|
+
from qwik.commands.rename import rename_command
|
|
20
|
+
from qwik.commands.run import run_command
|
|
21
|
+
from qwik.commands.search import search_command
|
|
22
|
+
from qwik.commands.show import show_command
|
|
23
|
+
from qwik.commands.tag import tag_command, untag_command
|
|
24
|
+
|
|
25
|
+
__all__ = ["app"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _version_callback(value: bool) -> None:
|
|
29
|
+
"""Print version and exit."""
|
|
30
|
+
if value:
|
|
31
|
+
Console().print(f"qwik {__version__}")
|
|
32
|
+
raise typer.Exit()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
app = typer.Typer(
|
|
36
|
+
name="qwik",
|
|
37
|
+
help="A friendly CLI alias manager.",
|
|
38
|
+
no_args_is_help=False,
|
|
39
|
+
add_completion=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Register subcommands
|
|
43
|
+
app.command("add")(add_command)
|
|
44
|
+
app.command("list")(list_command)
|
|
45
|
+
app.command("show")(show_command)
|
|
46
|
+
app.command("edit")(edit_command)
|
|
47
|
+
app.command("rename")(rename_command)
|
|
48
|
+
app.command("rm")(remove_command)
|
|
49
|
+
app.command("enable")(enable_command)
|
|
50
|
+
app.command("disable")(disable_command)
|
|
51
|
+
app.command("run")(run_command)
|
|
52
|
+
app.command("search")(search_command)
|
|
53
|
+
app.command("pick")(pick_command)
|
|
54
|
+
app.command("tag")(tag_command)
|
|
55
|
+
app.command("untag")(untag_command)
|
|
56
|
+
app.command("export")(export_command)
|
|
57
|
+
app.command("import")(import_command)
|
|
58
|
+
app.command("init")(init_shell_command)
|
|
59
|
+
app.command("doctor")(doctor_command)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.callback(invoke_without_command=True)
|
|
63
|
+
def main(
|
|
64
|
+
ctx: typer.Context,
|
|
65
|
+
run_alias: str | None = typer.Option(
|
|
66
|
+
None,
|
|
67
|
+
"--run",
|
|
68
|
+
"-r",
|
|
69
|
+
help="Run an alias (qwik -r <name> [args...]).",
|
|
70
|
+
),
|
|
71
|
+
list_flag: bool = typer.Option(False, "--list", "-l", help="Quick list."),
|
|
72
|
+
search_query: str | None = typer.Option(
|
|
73
|
+
None,
|
|
74
|
+
"--search",
|
|
75
|
+
"-s",
|
|
76
|
+
help="Quick search.",
|
|
77
|
+
),
|
|
78
|
+
version: bool = typer.Option(
|
|
79
|
+
False,
|
|
80
|
+
"--version",
|
|
81
|
+
"-v",
|
|
82
|
+
help="Show version and exit.",
|
|
83
|
+
callback=_version_callback,
|
|
84
|
+
is_eager=True,
|
|
85
|
+
),
|
|
86
|
+
help_flag: bool = typer.Option(
|
|
87
|
+
False,
|
|
88
|
+
"--help",
|
|
89
|
+
"-h",
|
|
90
|
+
help="Show this message and exit.",
|
|
91
|
+
is_eager=True,
|
|
92
|
+
),
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Top-level callback implementing shortcut flags and bare invocation.
|
|
95
|
+
|
|
96
|
+
When invoked with no subcommand and no flags, the fuzzy picker opens.
|
|
97
|
+
"""
|
|
98
|
+
# If a subcommand is already being handled, do nothing.
|
|
99
|
+
if ctx.invoked_subcommand is not None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
if help_flag:
|
|
103
|
+
Console().print(ctx.get_help())
|
|
104
|
+
raise typer.Exit()
|
|
105
|
+
|
|
106
|
+
if list_flag:
|
|
107
|
+
list_command()
|
|
108
|
+
raise typer.Exit()
|
|
109
|
+
|
|
110
|
+
if search_query is not None:
|
|
111
|
+
search_command(search_query)
|
|
112
|
+
raise typer.Exit()
|
|
113
|
+
|
|
114
|
+
if run_alias is not None:
|
|
115
|
+
# Need to capture remaining args after -r ... tricky in Typer callback.
|
|
116
|
+
# We use sys.argv but only look at the FIRST occurrence (index 0/1 are
|
|
117
|
+
# the Python/qwik executable), so alias names containing "-r" are safe
|
|
118
|
+
# unless they appear before the flag.
|
|
119
|
+
import sys
|
|
120
|
+
|
|
121
|
+
# Find first occurrence in argv (after the program name)
|
|
122
|
+
idx = None
|
|
123
|
+
for i, arg in enumerate(sys.argv[1:], start=1):
|
|
124
|
+
if arg == "-r" or arg == "--run":
|
|
125
|
+
idx = i
|
|
126
|
+
break
|
|
127
|
+
if idx is None:
|
|
128
|
+
Console().print(
|
|
129
|
+
"[qwik.error]Could not locate -r in arguments.[/qwik.error]"
|
|
130
|
+
)
|
|
131
|
+
raise typer.Exit(1)
|
|
132
|
+
remainder = sys.argv[idx + 1 :]
|
|
133
|
+
if not remainder:
|
|
134
|
+
Console().print("[qwik.error]Usage: qwik -r <name> [args...][/qwik.error]")
|
|
135
|
+
raise typer.Exit(1)
|
|
136
|
+
name = remainder[0]
|
|
137
|
+
args = remainder[1:]
|
|
138
|
+
run_command(name, args)
|
|
139
|
+
raise typer.Exit()
|
|
140
|
+
|
|
141
|
+
# Bare invocation → fuzzy picker
|
|
142
|
+
pick_command()
|
qwik/commands/add.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""``qwik add`` — create a new alias."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from qwik.core.conflicts import ConflictChecker
|
|
12
|
+
from qwik.core.models import Alias
|
|
13
|
+
from qwik.core.store import get_store
|
|
14
|
+
from qwik.ui.prompts import (
|
|
15
|
+
print_error,
|
|
16
|
+
print_info,
|
|
17
|
+
print_success,
|
|
18
|
+
print_warning,
|
|
19
|
+
prompt_confirm,
|
|
20
|
+
prompt_text,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = ["add_command"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def add_command(
|
|
27
|
+
name: Optional[str] = typer.Argument(None, help="Alias name."),
|
|
28
|
+
command: list[str] = typer.Argument(None, help="Command the alias expands to."),
|
|
29
|
+
tag: Optional[str] = typer.Option(
|
|
30
|
+
None, "--tag", "-t", help="Comma-separated tags."
|
|
31
|
+
),
|
|
32
|
+
description: Optional[str] = typer.Option(
|
|
33
|
+
None, "--description", "-d", help="Optional description."
|
|
34
|
+
),
|
|
35
|
+
force: bool = typer.Option(
|
|
36
|
+
False, "--force", "-f", help="Overwrite if alias already exists."
|
|
37
|
+
),
|
|
38
|
+
global_install: bool = typer.Option(
|
|
39
|
+
False, "--global", "-g", help="(Reserved) install for all users."
|
|
40
|
+
),
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Create a new alias.
|
|
43
|
+
|
|
44
|
+
When *name* or *command* are omitted, the command runs interactively.
|
|
45
|
+
"""
|
|
46
|
+
del global_install # reserved for future use
|
|
47
|
+
store = get_store()
|
|
48
|
+
store_data = store.load()
|
|
49
|
+
console = Console()
|
|
50
|
+
|
|
51
|
+
# Interactive mode if name missing
|
|
52
|
+
if name is None:
|
|
53
|
+
name = prompt_text("? Alias name", console=console, allow_empty=False)
|
|
54
|
+
if not command:
|
|
55
|
+
cmd_input = prompt_text("? Command", console=console, allow_empty=False)
|
|
56
|
+
command = [cmd_input]
|
|
57
|
+
|
|
58
|
+
full_command = " ".join(command)
|
|
59
|
+
|
|
60
|
+
# Conflict checks
|
|
61
|
+
checker = ConflictChecker(store_data)
|
|
62
|
+
result = checker.check(name)
|
|
63
|
+
|
|
64
|
+
if not result.valid_syntax:
|
|
65
|
+
print_error(
|
|
66
|
+
result.name, suggestion="Names must match ^[A-Za-z_][A-Za-z0-9_-]*$"
|
|
67
|
+
)
|
|
68
|
+
raise typer.Exit(1)
|
|
69
|
+
|
|
70
|
+
if result.existing_alias and not force:
|
|
71
|
+
print_error(
|
|
72
|
+
f'Alias "{name}" already exists.',
|
|
73
|
+
suggestion=f"qwik edit {name} to change the command\n"
|
|
74
|
+
f"qwik rm {name} to remove it\n"
|
|
75
|
+
f"--force to overwrite",
|
|
76
|
+
)
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
|
|
79
|
+
if result.is_builtin and not force:
|
|
80
|
+
print_error(
|
|
81
|
+
f'"{name}" is a shell builtin. Shadowing it can break your shell.',
|
|
82
|
+
suggestion="Use --force only if you know what you're doing.",
|
|
83
|
+
)
|
|
84
|
+
raise typer.Exit(1)
|
|
85
|
+
|
|
86
|
+
if result.needs_warning:
|
|
87
|
+
print_warning(
|
|
88
|
+
f'"{name}" shadows {result.path_location}.',
|
|
89
|
+
console=console,
|
|
90
|
+
)
|
|
91
|
+
if not prompt_confirm("Continue?", default=False, console=console):
|
|
92
|
+
raise typer.Exit(0)
|
|
93
|
+
|
|
94
|
+
alias = Alias(
|
|
95
|
+
command=full_command,
|
|
96
|
+
tag=tag or "",
|
|
97
|
+
description=description or "",
|
|
98
|
+
created_at=datetime.now(timezone.utc),
|
|
99
|
+
updated_at=datetime.now(timezone.utc),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
store_data.add(name, alias, force=force)
|
|
104
|
+
except KeyError as exc:
|
|
105
|
+
print_error(str(exc))
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
|
|
108
|
+
store.save_with_backup(store_data)
|
|
109
|
+
print_success(f'Added "{name}" → {full_command!r}', console=console)
|
|
110
|
+
print_info("Run `source <rc>` or open a new terminal to use it.", console=console)
|
qwik/commands/doctor.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""``qwik doctor`` — diagnose environment and store health."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from qwik.core.store import get_store
|
|
13
|
+
from qwik.shells.base import SUPPORTED_SHELLS
|
|
14
|
+
from qwik.ui.prompts import print_error, print_success, print_warning
|
|
15
|
+
|
|
16
|
+
__all__ = ["doctor_command"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def doctor_command() -> None:
|
|
20
|
+
"""Diagnose shell, hook status, store readability, and conflicts."""
|
|
21
|
+
console = Console()
|
|
22
|
+
store = get_store()
|
|
23
|
+
|
|
24
|
+
checks_ok = 0
|
|
25
|
+
checks_warn = 0
|
|
26
|
+
checks_err = 0
|
|
27
|
+
|
|
28
|
+
# Detect shell
|
|
29
|
+
shell = _detect_shell()
|
|
30
|
+
console.print(f"[bold]Shell detected:[/bold] {shell or 'unknown'}")
|
|
31
|
+
if shell in SUPPORTED_SHELLS:
|
|
32
|
+
print_success(f"{shell} is supported.", console=console)
|
|
33
|
+
checks_ok += 1
|
|
34
|
+
elif shell:
|
|
35
|
+
print_warning(f"{shell} support is best-effort.", console=console)
|
|
36
|
+
checks_warn += 1
|
|
37
|
+
|
|
38
|
+
# Hook installed?
|
|
39
|
+
hook_installed = _hook_installed(shell)
|
|
40
|
+
if hook_installed:
|
|
41
|
+
print_success("Shell hook appears installed.", console=console)
|
|
42
|
+
checks_ok += 1
|
|
43
|
+
else:
|
|
44
|
+
print_warning(
|
|
45
|
+
"Shell hook not detected. Run `qwik init <shell> --install`.",
|
|
46
|
+
console=console,
|
|
47
|
+
)
|
|
48
|
+
checks_warn += 1
|
|
49
|
+
|
|
50
|
+
# Store readable?
|
|
51
|
+
data = None
|
|
52
|
+
try:
|
|
53
|
+
data = store.load()
|
|
54
|
+
print_success(f"Store readable ({len(data.aliases)} aliases).", console=console)
|
|
55
|
+
checks_ok += 1
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
print_error(f"Store unreadable: {exc}", console=console)
|
|
58
|
+
checks_err += 1
|
|
59
|
+
|
|
60
|
+
# Conflicts with system commands
|
|
61
|
+
if data is not None:
|
|
62
|
+
if data.aliases:
|
|
63
|
+
conflicts = [n for n in data.aliases if shutil.which(n)]
|
|
64
|
+
if conflicts:
|
|
65
|
+
print_warning(
|
|
66
|
+
f"Aliases shadowing PATH binaries: {', '.join(conflicts)}",
|
|
67
|
+
console=console,
|
|
68
|
+
)
|
|
69
|
+
checks_warn += 1
|
|
70
|
+
else:
|
|
71
|
+
print_success("No alias shadows a system binary.", console=console)
|
|
72
|
+
checks_ok += 1
|
|
73
|
+
else:
|
|
74
|
+
print_success("No aliases — nothing to shadow.", console=console)
|
|
75
|
+
checks_ok += 1
|
|
76
|
+
|
|
77
|
+
# Summary
|
|
78
|
+
console.print()
|
|
79
|
+
total = checks_ok + checks_warn + checks_err
|
|
80
|
+
console.print(
|
|
81
|
+
f"[bold]Summary:[/bold] {checks_ok}/{total} passed, "
|
|
82
|
+
f"{checks_warn} warning(s), {checks_err} error(s)"
|
|
83
|
+
)
|
|
84
|
+
if checks_err:
|
|
85
|
+
raise typer.Exit(1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _detect_shell() -> str | None:
|
|
89
|
+
"""Attempt to detect the current user's shell.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A lowercase shell name (e.g. ``bash``, ``zsh``), or ``None``.
|
|
93
|
+
"""
|
|
94
|
+
# Prefer the $SHELL environment variable.
|
|
95
|
+
shell_env = os.environ.get("SHELL", "").lower()
|
|
96
|
+
if "bash" in shell_env:
|
|
97
|
+
return "bash"
|
|
98
|
+
if "zsh" in shell_env:
|
|
99
|
+
return "zsh"
|
|
100
|
+
if "fish" in shell_env:
|
|
101
|
+
return "fish"
|
|
102
|
+
if "pwsh" in shell_env or "powershell" in shell_env:
|
|
103
|
+
return "pwsh"
|
|
104
|
+
|
|
105
|
+
# Fallback: inspect parent process via /proc on Linux.
|
|
106
|
+
try:
|
|
107
|
+
with Path("/proc/self/status").open(encoding="utf-8") as f:
|
|
108
|
+
for line in f:
|
|
109
|
+
if line.startswith("PPid:"):
|
|
110
|
+
ppid = line.split()[1]
|
|
111
|
+
exe_link = Path(f"/proc/{ppid}/exe")
|
|
112
|
+
if exe_link.exists():
|
|
113
|
+
name = exe_link.resolve().name.lower()
|
|
114
|
+
if "bash" in name:
|
|
115
|
+
return "bash"
|
|
116
|
+
if "zsh" in name:
|
|
117
|
+
return "zsh"
|
|
118
|
+
if "fish" in name:
|
|
119
|
+
return "fish"
|
|
120
|
+
if "pwsh" in name or "powershell" in name:
|
|
121
|
+
return "pwsh"
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _hook_installed(shell: str | None) -> bool:
|
|
128
|
+
"""Crude heuristic: grep rc file for 'qwik init'.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
shell: Detected shell name.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
``True`` if the rc file contains an ``qwik init`` invocation.
|
|
135
|
+
"""
|
|
136
|
+
if shell is None:
|
|
137
|
+
return False
|
|
138
|
+
rc_map = {
|
|
139
|
+
"bash": Path.home() / ".bashrc",
|
|
140
|
+
"zsh": Path.home() / ".zshrc",
|
|
141
|
+
"fish": Path.home() / ".config" / "fish" / "config.fish",
|
|
142
|
+
"pwsh": Path.home()
|
|
143
|
+
/ ".config"
|
|
144
|
+
/ "powershell"
|
|
145
|
+
/ "Microsoft.PowerShell_profile.ps1",
|
|
146
|
+
}
|
|
147
|
+
rc = rc_map.get(shell)
|
|
148
|
+
if rc is None or not rc.exists():
|
|
149
|
+
return False
|
|
150
|
+
try:
|
|
151
|
+
return "qwik init" in rc.read_text(encoding="utf-8")
|
|
152
|
+
except Exception:
|
|
153
|
+
return False
|
qwik/commands/edit.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""``qwik edit`` — open alias in ``$EDITOR``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from qwik.core.models import Alias
|
|
15
|
+
from qwik.core.store import get_store
|
|
16
|
+
from qwik.ui.prompts import print_error, print_success
|
|
17
|
+
|
|
18
|
+
__all__ = ["edit_command"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def edit_command(
|
|
22
|
+
name: str = typer.Argument(..., help="Alias name to edit."),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Open the alias entry in ``$EDITOR`` as an editable TOML snippet."""
|
|
25
|
+
store = get_store()
|
|
26
|
+
data = store.load()
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
alias = data.get(name)
|
|
30
|
+
if alias is None:
|
|
31
|
+
print_error(f'Alias "{name}" does not exist.', console=console)
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
|
|
34
|
+
editor = os.environ.get("EDITOR", "vi")
|
|
35
|
+
|
|
36
|
+
snippet = (
|
|
37
|
+
f'# Edit the fields below and save/quit to apply changes to "{name}"\n'
|
|
38
|
+
f"command = {alias.command!r}\n"
|
|
39
|
+
f"tag = {alias.tag!r}\n"
|
|
40
|
+
f"description = {alias.description!r}\n"
|
|
41
|
+
f"enabled = {alias.enabled!r}\n"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
with tempfile.NamedTemporaryFile(
|
|
45
|
+
mode="w+", suffix=".toml", delete=False, encoding="utf-8"
|
|
46
|
+
) as tmp:
|
|
47
|
+
tmp.write(snippet)
|
|
48
|
+
tmp_path = Path(tmp.name)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
subprocess.run([editor, str(tmp_path)], check=True)
|
|
52
|
+
edited_text = tmp_path.read_text(encoding="utf-8")
|
|
53
|
+
# Very simple parser: extract key = value lines
|
|
54
|
+
new_fields: dict[str, object] = {}
|
|
55
|
+
for line in edited_text.splitlines():
|
|
56
|
+
line = line.strip()
|
|
57
|
+
if not line or line.startswith("#"):
|
|
58
|
+
continue
|
|
59
|
+
# Strip inline comments (first unquoted #)
|
|
60
|
+
comment_idx = -1
|
|
61
|
+
in_quote: str | None = None
|
|
62
|
+
for i, ch in enumerate(line):
|
|
63
|
+
if ch in ('"', "'") and (i == 0 or line[i - 1] != "\\"):
|
|
64
|
+
if in_quote == ch:
|
|
65
|
+
in_quote = None
|
|
66
|
+
elif in_quote is None:
|
|
67
|
+
in_quote = ch
|
|
68
|
+
elif ch == "#" and in_quote is None:
|
|
69
|
+
comment_idx = i
|
|
70
|
+
break
|
|
71
|
+
if comment_idx >= 0:
|
|
72
|
+
line = line[:comment_idx].rstrip()
|
|
73
|
+
if " = " not in line:
|
|
74
|
+
continue
|
|
75
|
+
key, raw_val = line.split(" = ", 1)
|
|
76
|
+
key = key.strip()
|
|
77
|
+
raw_val = raw_val.strip()
|
|
78
|
+
low = raw_val.lower()
|
|
79
|
+
if low == "true":
|
|
80
|
+
new_fields[key] = True
|
|
81
|
+
elif low == "false":
|
|
82
|
+
new_fields[key] = False
|
|
83
|
+
elif raw_val.startswith("[") and raw_val.endswith("]"):
|
|
84
|
+
# list of strings
|
|
85
|
+
inner = raw_val[1:-1]
|
|
86
|
+
new_fields[key] = [
|
|
87
|
+
v.strip().strip("'\"").strip()
|
|
88
|
+
for v in inner.split(",")
|
|
89
|
+
if v.strip()
|
|
90
|
+
]
|
|
91
|
+
else:
|
|
92
|
+
new_fields[key] = raw_val.strip("'\"").strip()
|
|
93
|
+
|
|
94
|
+
data.aliases[name] = Alias(
|
|
95
|
+
command=str(new_fields.get("command", alias.command)),
|
|
96
|
+
tag=new_fields.get("tag", alias.tag) or [],
|
|
97
|
+
description=str(new_fields.get("description", alias.description)),
|
|
98
|
+
enabled=new_fields.get("enabled", alias.enabled),
|
|
99
|
+
created_at=alias.created_at,
|
|
100
|
+
updated_at=datetime.now(timezone.utc),
|
|
101
|
+
last_used=alias.last_used,
|
|
102
|
+
run_count=alias.run_count,
|
|
103
|
+
)
|
|
104
|
+
store.save_with_backup(data)
|
|
105
|
+
print_success(f'Updated "{name}".', console=console)
|
|
106
|
+
except subprocess.CalledProcessError as exc:
|
|
107
|
+
print_error(f"Editor exited with code {exc.returncode}.", console=console)
|
|
108
|
+
raise typer.Exit(1)
|
|
109
|
+
finally:
|
|
110
|
+
tmp_path.unlink(missing_ok=True)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""``qwik enable`` / ``qwik disable`` — toggle alias state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from qwik.core.store import get_store
|
|
11
|
+
from qwik.ui.prompts import print_error, print_success
|
|
12
|
+
|
|
13
|
+
__all__ = ["enable_command", "disable_command"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _toggle(name: str, enabled: bool) -> None:
|
|
17
|
+
"""Set the enabled flag for *name* and persist.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
name: Alias identifier.
|
|
21
|
+
enabled: Desired state.
|
|
22
|
+
"""
|
|
23
|
+
store = get_store()
|
|
24
|
+
data = store.load()
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
alias = data.get(name)
|
|
28
|
+
if alias is None:
|
|
29
|
+
print_error(f'Alias "{name}" does not exist.', console=console)
|
|
30
|
+
raise typer.Exit(1)
|
|
31
|
+
|
|
32
|
+
alias.enabled = enabled
|
|
33
|
+
alias.updated_at = datetime.now(timezone.utc)
|
|
34
|
+
store.save_with_backup(data)
|
|
35
|
+
state = "enabled" if enabled else "disabled"
|
|
36
|
+
print_success(f'"{name}" is now {state}.', console=console)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def enable_command(
|
|
40
|
+
name: str = typer.Argument(..., help="Alias name to enable."),
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Re-enable a previously disabled alias."""
|
|
43
|
+
_toggle(name, True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def disable_command(
|
|
47
|
+
name: str = typer.Argument(..., help="Alias name to disable."),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Temporarily disable an alias without deleting it."""
|
|
50
|
+
_toggle(name, False)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""``qwik export`` — write aliases to TOML/JSON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from qwik.core.store import get_store
|
|
12
|
+
from qwik.ui.prompts import print_error, print_success
|
|
13
|
+
|
|
14
|
+
__all__ = ["export_command"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def export_command(
|
|
18
|
+
path: Path = typer.Argument(..., help="Destination file path."),
|
|
19
|
+
format: Optional[str] = typer.Option(
|
|
20
|
+
None,
|
|
21
|
+
"--format",
|
|
22
|
+
"-f",
|
|
23
|
+
help="toml or json (inferred from extension if omitted).",
|
|
24
|
+
),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Export aliases to a file."""
|
|
27
|
+
store = get_store()
|
|
28
|
+
data = store.load()
|
|
29
|
+
console = Console()
|
|
30
|
+
|
|
31
|
+
if not data.aliases:
|
|
32
|
+
print_error("No aliases to export.", console=console)
|
|
33
|
+
raise typer.Exit(1)
|
|
34
|
+
|
|
35
|
+
fmt = (format or path.suffix.lstrip(".")).lower()
|
|
36
|
+
if fmt not in ("toml", "json"):
|
|
37
|
+
print_error(f"Unknown format '{fmt}'. Use toml or json.", console=console)
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
if fmt == "toml":
|
|
41
|
+
import tomlkit
|
|
42
|
+
|
|
43
|
+
doc = store._store_to_document(data)
|
|
44
|
+
path.write_text(tomlkit.dumps(doc), encoding="utf-8")
|
|
45
|
+
else:
|
|
46
|
+
import json
|
|
47
|
+
|
|
48
|
+
path.write_text(
|
|
49
|
+
json.dumps(data.model_dump(mode="json"), indent=2),
|
|
50
|
+
encoding="utf-8",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
print_success(f"Exported {len(data.aliases)} aliases to {path}", console=console)
|