copa-cli 0.3.0__tar.gz → 0.4.0__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.
- {copa_cli-0.3.0/copa_cli.egg-info → copa_cli-0.4.0}/PKG-INFO +59 -7
- {copa_cli-0.3.0 → copa_cli-0.4.0}/README.md +58 -6
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/cli_internal.py +6 -6
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/config.py +8 -1
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/copa.zsh +31 -13
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/fzf.py +5 -6
- {copa_cli-0.3.0 → copa_cli-0.4.0/copa_cli.egg-info}/PKG-INFO +59 -7
- {copa_cli-0.3.0 → copa_cli-0.4.0}/pyproject.toml +1 -1
- {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_modal.py +43 -5
- {copa_cli-0.3.0 → copa_cli-0.4.0}/LICENSE +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/__init__.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/__main__.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/cli.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/cli_common.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/cli_llm.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/cli_share.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/db.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/evolve.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/history.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/llm.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/mcp_server.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/models.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/scanner.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/scoring.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/sharing.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa_cli.egg-info/SOURCES.txt +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa_cli.egg-info/dependency_links.txt +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa_cli.egg-info/entry_points.txt +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa_cli.egg-info/requires.txt +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/copa_cli.egg-info/top_level.txt +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/setup.cfg +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_cli_and_sharing.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_db.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_fzf.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_models.py +0 -0
- {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: copa-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Command Palette — smart command tracking, ranking, and sharing for your shell
|
|
5
5
|
Author: Mark Stanford
|
|
6
6
|
License-Expression: MIT
|
|
@@ -94,7 +94,7 @@ Then restart your shell or run `source ~/.zshrc`. This does three things:
|
|
|
94
94
|
|
|
95
95
|
1. **Records every command you run** — a `precmd` hook silently calls `copa _record` in the background after each command, building up frequency and recency data with zero latency impact.
|
|
96
96
|
2. **Replaces Ctrl+R** — the default zsh reverse-history-search is replaced with Copa's fzf-powered command palette (see below).
|
|
97
|
-
3. **Supplements tab completion** — Copa registers as a
|
|
97
|
+
3. **Supplements tab completion** — Copa registers as a completer so that any command gets completion candidates from your Copa database. The behavior is configurable (`fallback`, `hybrid`, `always`, or `never`) — see [Tab Completion](#tab-completion).
|
|
98
98
|
|
|
99
99
|
Initialize the database:
|
|
100
100
|
|
|
@@ -143,11 +143,15 @@ While the fzf palette is open, these keys are available:
|
|
|
143
143
|
| **Ctrl+T** | Append `>` | Redirect output |
|
|
144
144
|
| **Ctrl+A** | Append `&&` | Chain with next command |
|
|
145
145
|
| **Ctrl+/** | Append `2>/dev/null` | Suppress stderr |
|
|
146
|
-
| **Ctrl+
|
|
147
|
-
| **Ctrl+
|
|
146
|
+
| **Ctrl+S** | Scope by group | Opens inline group list — Enter filters to that group, ESC returns to all |
|
|
147
|
+
| **Ctrl+G** | Assign group | Opens inline group list — Enter assigns the group to the highlighted command |
|
|
148
|
+
| **Ctrl+N** | Cycle group | Cycles through groups: (all) → group1 → group2 → ... → (all) |
|
|
149
|
+
| **Ctrl+D** | Describe | Generate/edit a description using LLM (with tty-aware input) |
|
|
148
150
|
| **Ctrl+F** | Edit flags | Add flag documentation to the highlighted command |
|
|
151
|
+
| **Ctrl+H** | Toggle header | Show/hide the key hints for more screen space |
|
|
152
|
+
| **ESC** | Cancel/back | In scope/group mode: returns to command list. Otherwise: closes fzf |
|
|
149
153
|
|
|
150
|
-
Keybindings are configurable via `~/.copa/config.toml`. See
|
|
154
|
+
Keybindings are configurable via `~/.copa/config.toml`. See [Configuration](#configuration).
|
|
151
155
|
|
|
152
156
|
### Preview pane
|
|
153
157
|
|
|
@@ -159,11 +163,29 @@ Selecting a command places it directly into your shell prompt (without executing
|
|
|
159
163
|
|
|
160
164
|
## Tab Completion
|
|
161
165
|
|
|
162
|
-
Copa supplements zsh's built-in tab completion for **any** command — not just Copa's own CLI. Once `copa.zsh` is sourced, Copa registers as a
|
|
166
|
+
Copa supplements zsh's built-in tab completion for **any** command — not just Copa's own CLI. Once `copa.zsh` is sourced, Copa registers as a completer in zsh's completion system.
|
|
167
|
+
|
|
168
|
+
### Completion modes
|
|
169
|
+
|
|
170
|
+
Copa supports four completion modes, configured via `~/.copa/config.toml`:
|
|
171
|
+
|
|
172
|
+
| Mode | Behavior |
|
|
173
|
+
|------|----------|
|
|
174
|
+
| `fallback` | **(default)** Only show Copa completions when native completers found nothing |
|
|
175
|
+
| `hybrid` | Show Copa completions alongside native completions (in a separate group) |
|
|
176
|
+
| `always` | Copa completions replace native completions |
|
|
177
|
+
| `never` | Disable Copa tab completion entirely |
|
|
178
|
+
|
|
179
|
+
```toml
|
|
180
|
+
# ~/.copa/config.toml
|
|
181
|
+
[completion]
|
|
182
|
+
mode = "fallback" # fallback | hybrid | always | never
|
|
183
|
+
branding = true # show "Copa history" group header
|
|
184
|
+
```
|
|
163
185
|
|
|
164
186
|
### How it works
|
|
165
187
|
|
|
166
|
-
When you press Tab,
|
|
188
|
+
When you press Tab, Copa queries its database for commands matching what you've typed so far and suggests the next word(s):
|
|
167
189
|
|
|
168
190
|
```
|
|
169
191
|
$ adb shell dump<TAB>
|
|
@@ -430,6 +452,36 @@ Available MCP tools:
|
|
|
430
452
|
- `copa_create_group` — create a group with commands
|
|
431
453
|
- `copa_bulk_add` — bulk add commands
|
|
432
454
|
|
|
455
|
+
## Configuration
|
|
456
|
+
|
|
457
|
+
Copa is configured via `~/.copa/config.toml`. All settings are optional — Copa uses sensible defaults.
|
|
458
|
+
|
|
459
|
+
```toml
|
|
460
|
+
# ~/.copa/config.toml
|
|
461
|
+
|
|
462
|
+
# Keybindings for the Ctrl+R fzf palette
|
|
463
|
+
# Values are fzf key names: ctrl-<letter>, alt-<letter>, ctrl-/
|
|
464
|
+
# ctrl-r and enter are reserved and cannot be reassigned
|
|
465
|
+
[keys]
|
|
466
|
+
background = "ctrl-v" # append &
|
|
467
|
+
merge_output = "ctrl-o" # append 2>&1
|
|
468
|
+
pipe = "ctrl-x" # append |
|
|
469
|
+
redirect = "ctrl-t" # append >
|
|
470
|
+
chain = "ctrl-a" # append &&
|
|
471
|
+
suppress = "ctrl-/" # append 2>/dev/null
|
|
472
|
+
describe = "ctrl-d" # LLM describe
|
|
473
|
+
group = "ctrl-g" # assign group (inline modal)
|
|
474
|
+
flags = "ctrl-f" # edit flags
|
|
475
|
+
filter_group = "ctrl-s" # scope by group (inline modal)
|
|
476
|
+
cycle_group = "ctrl-n" # cycle through groups
|
|
477
|
+
toggle_header = "ctrl-h" # show/hide key hints
|
|
478
|
+
|
|
479
|
+
# Tab completion behavior
|
|
480
|
+
[completion]
|
|
481
|
+
mode = "fallback" # fallback | hybrid | always | never
|
|
482
|
+
branding = true # show "Copa history" group header
|
|
483
|
+
```
|
|
484
|
+
|
|
433
485
|
## CLI Reference
|
|
434
486
|
|
|
435
487
|
| Command | Purpose |
|
|
@@ -64,7 +64,7 @@ Then restart your shell or run `source ~/.zshrc`. This does three things:
|
|
|
64
64
|
|
|
65
65
|
1. **Records every command you run** — a `precmd` hook silently calls `copa _record` in the background after each command, building up frequency and recency data with zero latency impact.
|
|
66
66
|
2. **Replaces Ctrl+R** — the default zsh reverse-history-search is replaced with Copa's fzf-powered command palette (see below).
|
|
67
|
-
3. **Supplements tab completion** — Copa registers as a
|
|
67
|
+
3. **Supplements tab completion** — Copa registers as a completer so that any command gets completion candidates from your Copa database. The behavior is configurable (`fallback`, `hybrid`, `always`, or `never`) — see [Tab Completion](#tab-completion).
|
|
68
68
|
|
|
69
69
|
Initialize the database:
|
|
70
70
|
|
|
@@ -113,11 +113,15 @@ While the fzf palette is open, these keys are available:
|
|
|
113
113
|
| **Ctrl+T** | Append `>` | Redirect output |
|
|
114
114
|
| **Ctrl+A** | Append `&&` | Chain with next command |
|
|
115
115
|
| **Ctrl+/** | Append `2>/dev/null` | Suppress stderr |
|
|
116
|
-
| **Ctrl+
|
|
117
|
-
| **Ctrl+
|
|
116
|
+
| **Ctrl+S** | Scope by group | Opens inline group list — Enter filters to that group, ESC returns to all |
|
|
117
|
+
| **Ctrl+G** | Assign group | Opens inline group list — Enter assigns the group to the highlighted command |
|
|
118
|
+
| **Ctrl+N** | Cycle group | Cycles through groups: (all) → group1 → group2 → ... → (all) |
|
|
119
|
+
| **Ctrl+D** | Describe | Generate/edit a description using LLM (with tty-aware input) |
|
|
118
120
|
| **Ctrl+F** | Edit flags | Add flag documentation to the highlighted command |
|
|
121
|
+
| **Ctrl+H** | Toggle header | Show/hide the key hints for more screen space |
|
|
122
|
+
| **ESC** | Cancel/back | In scope/group mode: returns to command list. Otherwise: closes fzf |
|
|
119
123
|
|
|
120
|
-
Keybindings are configurable via `~/.copa/config.toml`. See
|
|
124
|
+
Keybindings are configurable via `~/.copa/config.toml`. See [Configuration](#configuration).
|
|
121
125
|
|
|
122
126
|
### Preview pane
|
|
123
127
|
|
|
@@ -129,11 +133,29 @@ Selecting a command places it directly into your shell prompt (without executing
|
|
|
129
133
|
|
|
130
134
|
## Tab Completion
|
|
131
135
|
|
|
132
|
-
Copa supplements zsh's built-in tab completion for **any** command — not just Copa's own CLI. Once `copa.zsh` is sourced, Copa registers as a
|
|
136
|
+
Copa supplements zsh's built-in tab completion for **any** command — not just Copa's own CLI. Once `copa.zsh` is sourced, Copa registers as a completer in zsh's completion system.
|
|
137
|
+
|
|
138
|
+
### Completion modes
|
|
139
|
+
|
|
140
|
+
Copa supports four completion modes, configured via `~/.copa/config.toml`:
|
|
141
|
+
|
|
142
|
+
| Mode | Behavior |
|
|
143
|
+
|------|----------|
|
|
144
|
+
| `fallback` | **(default)** Only show Copa completions when native completers found nothing |
|
|
145
|
+
| `hybrid` | Show Copa completions alongside native completions (in a separate group) |
|
|
146
|
+
| `always` | Copa completions replace native completions |
|
|
147
|
+
| `never` | Disable Copa tab completion entirely |
|
|
148
|
+
|
|
149
|
+
```toml
|
|
150
|
+
# ~/.copa/config.toml
|
|
151
|
+
[completion]
|
|
152
|
+
mode = "fallback" # fallback | hybrid | always | never
|
|
153
|
+
branding = true # show "Copa history" group header
|
|
154
|
+
```
|
|
133
155
|
|
|
134
156
|
### How it works
|
|
135
157
|
|
|
136
|
-
When you press Tab,
|
|
158
|
+
When you press Tab, Copa queries its database for commands matching what you've typed so far and suggests the next word(s):
|
|
137
159
|
|
|
138
160
|
```
|
|
139
161
|
$ adb shell dump<TAB>
|
|
@@ -400,6 +422,36 @@ Available MCP tools:
|
|
|
400
422
|
- `copa_create_group` — create a group with commands
|
|
401
423
|
- `copa_bulk_add` — bulk add commands
|
|
402
424
|
|
|
425
|
+
## Configuration
|
|
426
|
+
|
|
427
|
+
Copa is configured via `~/.copa/config.toml`. All settings are optional — Copa uses sensible defaults.
|
|
428
|
+
|
|
429
|
+
```toml
|
|
430
|
+
# ~/.copa/config.toml
|
|
431
|
+
|
|
432
|
+
# Keybindings for the Ctrl+R fzf palette
|
|
433
|
+
# Values are fzf key names: ctrl-<letter>, alt-<letter>, ctrl-/
|
|
434
|
+
# ctrl-r and enter are reserved and cannot be reassigned
|
|
435
|
+
[keys]
|
|
436
|
+
background = "ctrl-v" # append &
|
|
437
|
+
merge_output = "ctrl-o" # append 2>&1
|
|
438
|
+
pipe = "ctrl-x" # append |
|
|
439
|
+
redirect = "ctrl-t" # append >
|
|
440
|
+
chain = "ctrl-a" # append &&
|
|
441
|
+
suppress = "ctrl-/" # append 2>/dev/null
|
|
442
|
+
describe = "ctrl-d" # LLM describe
|
|
443
|
+
group = "ctrl-g" # assign group (inline modal)
|
|
444
|
+
flags = "ctrl-f" # edit flags
|
|
445
|
+
filter_group = "ctrl-s" # scope by group (inline modal)
|
|
446
|
+
cycle_group = "ctrl-n" # cycle through groups
|
|
447
|
+
toggle_header = "ctrl-h" # show/hide key hints
|
|
448
|
+
|
|
449
|
+
# Tab completion behavior
|
|
450
|
+
[completion]
|
|
451
|
+
mode = "fallback" # fallback | hybrid | always | never
|
|
452
|
+
branding = true # show "Copa history" group header
|
|
453
|
+
```
|
|
454
|
+
|
|
403
455
|
## CLI Reference
|
|
404
456
|
|
|
405
457
|
| Command | Purpose |
|
|
@@ -175,20 +175,20 @@ def set_flags(cmd_id: int):
|
|
|
175
175
|
|
|
176
176
|
@click.command("_list-groups", hidden=True)
|
|
177
177
|
def list_groups():
|
|
178
|
-
"""Output group names for fzf group picker."""
|
|
178
|
+
"""Output group names for fzf group picker (delimited for --with-nth)."""
|
|
179
179
|
db = get_db()
|
|
180
|
-
click.echo("(all)")
|
|
180
|
+
click.echo("0┃(all)┃")
|
|
181
181
|
for g in db.get_groups():
|
|
182
|
-
click.echo(g)
|
|
182
|
+
click.echo(f"0┃{g}┃")
|
|
183
183
|
|
|
184
184
|
|
|
185
185
|
@click.command("_list-groups-for-assign", hidden=True)
|
|
186
186
|
def list_groups_for_assign():
|
|
187
|
-
"""Output group names for group-assign modal."""
|
|
187
|
+
"""Output group names for group-assign modal (delimited for --with-nth)."""
|
|
188
188
|
db = get_db()
|
|
189
|
-
click.echo("(none)")
|
|
189
|
+
click.echo("0┃(none)┃")
|
|
190
190
|
for g in db.get_groups():
|
|
191
|
-
click.echo(g)
|
|
191
|
+
click.echo(f"0┃{g}┃")
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
@click.command("_set-group-direct", hidden=True)
|
|
@@ -71,12 +71,14 @@ def load_config(path: Path | None = None) -> dict:
|
|
|
71
71
|
Returns a dict with:
|
|
72
72
|
- All action_name -> fzf_key entries (keybindings)
|
|
73
73
|
- "_completion_branding" -> bool (whether to show Copa branding on tab completions)
|
|
74
|
+
- "_completion_mode" -> str (fallback|always|hybrid|never)
|
|
74
75
|
|
|
75
76
|
Silently ignores: unknown actions, invalid key names, reserved keys,
|
|
76
77
|
duplicate assignments. Falls back to all defaults on malformed TOML.
|
|
77
78
|
"""
|
|
78
79
|
config: dict = dict(DEFAULT_KEYS)
|
|
79
80
|
config["_completion_branding"] = True # default: show branding
|
|
81
|
+
config["_completion_mode"] = "fallback" # default: only when native completers found nothing
|
|
80
82
|
|
|
81
83
|
if path is None:
|
|
82
84
|
path = Path.home() / ".copa" / "config.toml"
|
|
@@ -97,6 +99,9 @@ def load_config(path: Path | None = None) -> dict:
|
|
|
97
99
|
branding = completion_section.get("branding")
|
|
98
100
|
if isinstance(branding, bool):
|
|
99
101
|
config["_completion_branding"] = branding
|
|
102
|
+
mode = completion_section.get("mode")
|
|
103
|
+
if isinstance(mode, str) and mode in ("fallback", "always", "hybrid", "never"):
|
|
104
|
+
config["_completion_mode"] = mode
|
|
100
105
|
|
|
101
106
|
keys_section = data.get("keys")
|
|
102
107
|
if not isinstance(keys_section, dict):
|
|
@@ -177,9 +182,11 @@ def emit_zsh_config(config: dict[str, str]) -> str:
|
|
|
177
182
|
# Use $'...\n...' quoting so zsh interprets the newline
|
|
178
183
|
lines.append(f"_COPA_HEADER=$'{row1}\\n{row2}'")
|
|
179
184
|
|
|
180
|
-
# Completion
|
|
185
|
+
# Completion config
|
|
181
186
|
branding = config.get("_completion_branding", True)
|
|
182
187
|
lines.append(f"_COPA_COMPLETION_BRANDING='{'true' if branding else 'false'}'")
|
|
188
|
+
completion_mode = config.get("_completion_mode", "fallback")
|
|
189
|
+
lines.append(f"_COPA_COMPLETION_MODE='{completion_mode}'")
|
|
183
190
|
|
|
184
191
|
# Suffix associative array
|
|
185
192
|
lines.append("typeset -gA _COPA_SUFFIXES")
|
|
@@ -19,6 +19,7 @@ eval "$(copa _fzf-config 2>/dev/null)" || {
|
|
|
19
19
|
_COPA_CYCLE_GROUP_KEY='ctrl-n'
|
|
20
20
|
_COPA_TOGGLE_HEADER_KEY='ctrl-h'
|
|
21
21
|
_COPA_COMPLETION_BRANDING='true'
|
|
22
|
+
_COPA_COMPLETION_MODE='fallback'
|
|
22
23
|
_COPA_HEADER=$'Copa | ^R:cycle | ^V:& | ^O:2>&1 | ^X:| | ^T:> | ^A:&& | ^/:quiet | ^H:keys\n^G:grp | ^D:desc | ^F:flag | ^S:scope | ^N:↻grp'
|
|
23
24
|
typeset -gA _COPA_SUFFIXES
|
|
24
25
|
_COPA_SUFFIXES[ctrl-v]=' &'
|
|
@@ -73,10 +74,10 @@ _copa_fzf_widget() {
|
|
|
73
74
|
--expect "$_COPA_EXPECT" \
|
|
74
75
|
--bind "${_COPA_DESCRIBE_KEY}:execute($copa_bin describe {1})+refresh-preview" \
|
|
75
76
|
--bind "${_COPA_FLAGS_KEY}:execute($copa_bin _set-flags {1})+reload($copa_bin fzf-list)+refresh-preview" \
|
|
76
|
-
--bind "${_COPA_FILTER_GROUP_KEY}:reload($copa_bin _list-groups)+change-prompt(scope> )+clear-query" \
|
|
77
|
+
--bind "${_COPA_FILTER_GROUP_KEY}:reload($copa_bin _list-groups)+change-prompt(scope> )+clear-query+hide-preview" \
|
|
77
78
|
--bind "${_COPA_GROUP_KEY}:transform:
|
|
78
79
|
echo {1} > ${_copa_modal_file};
|
|
79
|
-
echo \"reload(${copa_bin} _list-groups-for-assign)+change-prompt(group> )+clear-query\"" \
|
|
80
|
+
echo \"reload(${copa_bin} _list-groups-for-assign)+change-prompt(group> )+clear-query+hide-preview\"" \
|
|
80
81
|
--bind "${_COPA_CYCLE_GROUP_KEY}:transform:
|
|
81
82
|
cur_group='(all)';
|
|
82
83
|
if [[ \$FZF_PROMPT =~ 'copa \\[(.+)\\]> ' ]]; then
|
|
@@ -99,23 +100,23 @@ _copa_fzf_widget() {
|
|
|
99
100
|
--bind "${_COPA_TOGGLE_HEADER_KEY}:toggle-header" \
|
|
100
101
|
--bind 'enter:transform:
|
|
101
102
|
if [[ $FZF_PROMPT == "scope> " ]]; then
|
|
102
|
-
selected={};
|
|
103
|
+
selected={2};
|
|
103
104
|
if [[ $selected == "(all)" ]]; then
|
|
104
|
-
echo "reload('"$copa_bin"' fzf-list --mode all)+change-prompt(copa> )+clear-query"
|
|
105
|
+
echo "reload('"$copa_bin"' fzf-list --mode all)+change-prompt(copa> )+clear-query+show-preview"
|
|
105
106
|
else
|
|
106
|
-
echo "reload('"$copa_bin"' fzf-list --mode group --group $selected)+change-prompt(copa [$selected]> )+clear-query"
|
|
107
|
+
echo "reload('"$copa_bin"' fzf-list --mode group --group $selected)+change-prompt(copa [$selected]> )+clear-query+show-preview"
|
|
107
108
|
fi
|
|
108
109
|
elif [[ $FZF_PROMPT == "group> " ]]; then
|
|
109
110
|
cmd_id=$(cat '"${_copa_modal_file}"');
|
|
110
|
-
selected={};
|
|
111
|
+
selected={2};
|
|
111
112
|
'"$copa_bin"' _set-group-direct $cmd_id $selected;
|
|
112
|
-
echo "reload('"$copa_bin"' fzf-list)+change-prompt(copa> )+clear-query"
|
|
113
|
+
echo "reload('"$copa_bin"' fzf-list)+change-prompt(copa> )+clear-query+show-preview"
|
|
113
114
|
else
|
|
114
115
|
echo "accept"
|
|
115
116
|
fi' \
|
|
116
117
|
--bind 'esc:transform:
|
|
117
118
|
if [[ $FZF_PROMPT == "scope> " || $FZF_PROMPT == "group> " ]]; then
|
|
118
|
-
echo "reload('"$copa_bin"' fzf-list)+change-prompt(copa> )+clear-query"
|
|
119
|
+
echo "reload('"$copa_bin"' fzf-list)+change-prompt(copa> )+clear-query+show-preview"
|
|
119
120
|
else
|
|
120
121
|
echo "abort"
|
|
121
122
|
fi' \
|
|
@@ -151,18 +152,33 @@ fi
|
|
|
151
152
|
eval "$(copa completion zsh)"
|
|
152
153
|
|
|
153
154
|
# --- Supplemental tab completion from Copa database ---
|
|
154
|
-
#
|
|
155
|
-
#
|
|
155
|
+
# Mode is controlled by _COPA_COMPLETION_MODE (set via copa _fzf-config):
|
|
156
|
+
# fallback — only when native completers found nothing (default)
|
|
157
|
+
# always — Copa completions replace native completions
|
|
158
|
+
# hybrid — Copa completions shown alongside native completions
|
|
159
|
+
# never — disable Copa tab completion entirely
|
|
160
|
+
if [[ "$_COPA_COMPLETION_MODE" != 'never' ]]; then
|
|
161
|
+
|
|
156
162
|
_copa_history_complete() {
|
|
157
|
-
#
|
|
158
|
-
|
|
163
|
+
# In fallback mode, only show when native completers found nothing
|
|
164
|
+
if [[ "$_COPA_COMPLETION_MODE" == 'fallback' ]]; then
|
|
165
|
+
(( compstate[nmatches] > 0 )) && return
|
|
166
|
+
fi
|
|
159
167
|
# Skip empty tokens (bare <TAB> with no partial input)
|
|
160
168
|
[[ -z "${words[CURRENT]}" ]] && return
|
|
161
169
|
# Skip internal copa commands
|
|
162
170
|
[[ "${words[CURRENT]}" == _copa_* ]] && return
|
|
163
171
|
local -a results
|
|
164
172
|
results=("${(@f)$(copa _complete-word "${(@)words[1,CURRENT]}" 2>/dev/null)}")
|
|
165
|
-
(( ${#results} ))
|
|
173
|
+
if (( ${#results} )); then
|
|
174
|
+
if [[ "$_COPA_COMPLETION_MODE" == 'always' ]]; then
|
|
175
|
+
# Replace: clear native matches, add only Copa results
|
|
176
|
+
compadd -U -V 'copa-history' -o nosort -- "${results[@]}"
|
|
177
|
+
else
|
|
178
|
+
# fallback & hybrid: add Copa results as a separate group
|
|
179
|
+
compadd -V 'copa-history' -o nosort -- "${results[@]}"
|
|
180
|
+
fi
|
|
181
|
+
fi
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
# Append to existing completers without clobbering user config
|
|
@@ -177,3 +193,5 @@ _copa_history_complete() {
|
|
|
177
193
|
zstyle ':completion:*:*:*:copa-history' group-header '%F{magenta}-- Copa history --%f'
|
|
178
194
|
fi
|
|
179
195
|
}
|
|
196
|
+
|
|
197
|
+
fi # end _COPA_COMPLETION_MODE != 'never'
|
|
@@ -37,7 +37,7 @@ def format_lines(commands: list[Command]) -> list[str]:
|
|
|
37
37
|
return []
|
|
38
38
|
|
|
39
39
|
# Compute column widths from the full list
|
|
40
|
-
max_cmd =
|
|
40
|
+
max_cmd = max(len(c.command) for c in commands)
|
|
41
41
|
max_grp = max(
|
|
42
42
|
(len(f"[{c.group_name}]") for c in commands if c.group_name),
|
|
43
43
|
default=0,
|
|
@@ -48,11 +48,10 @@ def format_lines(commands: list[Command]) -> list[str]:
|
|
|
48
48
|
# Field 1: hidden ID
|
|
49
49
|
id_field = f"{cmd.id:>5}"
|
|
50
50
|
|
|
51
|
-
# Field 2: command text
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
cmd_field = f" {cmd_text:<{max_cmd}} "
|
|
51
|
+
# Field 2: command text — full text, never truncated.
|
|
52
|
+
# fzf handles overflow with horizontal scrolling.
|
|
53
|
+
# Padding uses max_cmd for column alignment on shorter commands.
|
|
54
|
+
cmd_field = f" {cmd.command:<{max_cmd}} "
|
|
56
55
|
|
|
57
56
|
# Field 3: metadata — pin indicator, group badge, frequency
|
|
58
57
|
pin = f"{_YELLOW}*{_RESET} " if cmd.is_pinned else " "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: copa-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Command Palette — smart command tracking, ranking, and sharing for your shell
|
|
5
5
|
Author: Mark Stanford
|
|
6
6
|
License-Expression: MIT
|
|
@@ -94,7 +94,7 @@ Then restart your shell or run `source ~/.zshrc`. This does three things:
|
|
|
94
94
|
|
|
95
95
|
1. **Records every command you run** — a `precmd` hook silently calls `copa _record` in the background after each command, building up frequency and recency data with zero latency impact.
|
|
96
96
|
2. **Replaces Ctrl+R** — the default zsh reverse-history-search is replaced with Copa's fzf-powered command palette (see below).
|
|
97
|
-
3. **Supplements tab completion** — Copa registers as a
|
|
97
|
+
3. **Supplements tab completion** — Copa registers as a completer so that any command gets completion candidates from your Copa database. The behavior is configurable (`fallback`, `hybrid`, `always`, or `never`) — see [Tab Completion](#tab-completion).
|
|
98
98
|
|
|
99
99
|
Initialize the database:
|
|
100
100
|
|
|
@@ -143,11 +143,15 @@ While the fzf palette is open, these keys are available:
|
|
|
143
143
|
| **Ctrl+T** | Append `>` | Redirect output |
|
|
144
144
|
| **Ctrl+A** | Append `&&` | Chain with next command |
|
|
145
145
|
| **Ctrl+/** | Append `2>/dev/null` | Suppress stderr |
|
|
146
|
-
| **Ctrl+
|
|
147
|
-
| **Ctrl+
|
|
146
|
+
| **Ctrl+S** | Scope by group | Opens inline group list — Enter filters to that group, ESC returns to all |
|
|
147
|
+
| **Ctrl+G** | Assign group | Opens inline group list — Enter assigns the group to the highlighted command |
|
|
148
|
+
| **Ctrl+N** | Cycle group | Cycles through groups: (all) → group1 → group2 → ... → (all) |
|
|
149
|
+
| **Ctrl+D** | Describe | Generate/edit a description using LLM (with tty-aware input) |
|
|
148
150
|
| **Ctrl+F** | Edit flags | Add flag documentation to the highlighted command |
|
|
151
|
+
| **Ctrl+H** | Toggle header | Show/hide the key hints for more screen space |
|
|
152
|
+
| **ESC** | Cancel/back | In scope/group mode: returns to command list. Otherwise: closes fzf |
|
|
149
153
|
|
|
150
|
-
Keybindings are configurable via `~/.copa/config.toml`. See
|
|
154
|
+
Keybindings are configurable via `~/.copa/config.toml`. See [Configuration](#configuration).
|
|
151
155
|
|
|
152
156
|
### Preview pane
|
|
153
157
|
|
|
@@ -159,11 +163,29 @@ Selecting a command places it directly into your shell prompt (without executing
|
|
|
159
163
|
|
|
160
164
|
## Tab Completion
|
|
161
165
|
|
|
162
|
-
Copa supplements zsh's built-in tab completion for **any** command — not just Copa's own CLI. Once `copa.zsh` is sourced, Copa registers as a
|
|
166
|
+
Copa supplements zsh's built-in tab completion for **any** command — not just Copa's own CLI. Once `copa.zsh` is sourced, Copa registers as a completer in zsh's completion system.
|
|
167
|
+
|
|
168
|
+
### Completion modes
|
|
169
|
+
|
|
170
|
+
Copa supports four completion modes, configured via `~/.copa/config.toml`:
|
|
171
|
+
|
|
172
|
+
| Mode | Behavior |
|
|
173
|
+
|------|----------|
|
|
174
|
+
| `fallback` | **(default)** Only show Copa completions when native completers found nothing |
|
|
175
|
+
| `hybrid` | Show Copa completions alongside native completions (in a separate group) |
|
|
176
|
+
| `always` | Copa completions replace native completions |
|
|
177
|
+
| `never` | Disable Copa tab completion entirely |
|
|
178
|
+
|
|
179
|
+
```toml
|
|
180
|
+
# ~/.copa/config.toml
|
|
181
|
+
[completion]
|
|
182
|
+
mode = "fallback" # fallback | hybrid | always | never
|
|
183
|
+
branding = true # show "Copa history" group header
|
|
184
|
+
```
|
|
163
185
|
|
|
164
186
|
### How it works
|
|
165
187
|
|
|
166
|
-
When you press Tab,
|
|
188
|
+
When you press Tab, Copa queries its database for commands matching what you've typed so far and suggests the next word(s):
|
|
167
189
|
|
|
168
190
|
```
|
|
169
191
|
$ adb shell dump<TAB>
|
|
@@ -430,6 +452,36 @@ Available MCP tools:
|
|
|
430
452
|
- `copa_create_group` — create a group with commands
|
|
431
453
|
- `copa_bulk_add` — bulk add commands
|
|
432
454
|
|
|
455
|
+
## Configuration
|
|
456
|
+
|
|
457
|
+
Copa is configured via `~/.copa/config.toml`. All settings are optional — Copa uses sensible defaults.
|
|
458
|
+
|
|
459
|
+
```toml
|
|
460
|
+
# ~/.copa/config.toml
|
|
461
|
+
|
|
462
|
+
# Keybindings for the Ctrl+R fzf palette
|
|
463
|
+
# Values are fzf key names: ctrl-<letter>, alt-<letter>, ctrl-/
|
|
464
|
+
# ctrl-r and enter are reserved and cannot be reassigned
|
|
465
|
+
[keys]
|
|
466
|
+
background = "ctrl-v" # append &
|
|
467
|
+
merge_output = "ctrl-o" # append 2>&1
|
|
468
|
+
pipe = "ctrl-x" # append |
|
|
469
|
+
redirect = "ctrl-t" # append >
|
|
470
|
+
chain = "ctrl-a" # append &&
|
|
471
|
+
suppress = "ctrl-/" # append 2>/dev/null
|
|
472
|
+
describe = "ctrl-d" # LLM describe
|
|
473
|
+
group = "ctrl-g" # assign group (inline modal)
|
|
474
|
+
flags = "ctrl-f" # edit flags
|
|
475
|
+
filter_group = "ctrl-s" # scope by group (inline modal)
|
|
476
|
+
cycle_group = "ctrl-n" # cycle through groups
|
|
477
|
+
toggle_header = "ctrl-h" # show/hide key hints
|
|
478
|
+
|
|
479
|
+
# Tab completion behavior
|
|
480
|
+
[completion]
|
|
481
|
+
mode = "fallback" # fallback | hybrid | always | never
|
|
482
|
+
branding = true # show "Copa history" group header
|
|
483
|
+
```
|
|
484
|
+
|
|
433
485
|
## CLI Reference
|
|
434
486
|
|
|
435
487
|
| Command | Purpose |
|
|
@@ -27,7 +27,9 @@ class TestListGroupsForAssign:
|
|
|
27
27
|
result = runner.invoke(cli, ["_list-groups-for-assign"])
|
|
28
28
|
assert result.exit_code == 0
|
|
29
29
|
lines = result.output.strip().split("\n")
|
|
30
|
-
|
|
30
|
+
# Delimited format: 0┃(none)┃
|
|
31
|
+
assert "(none)" in lines[0]
|
|
32
|
+
assert "┃" in lines[0]
|
|
31
33
|
|
|
32
34
|
def test_outputs_groups(self, tmp_path, monkeypatch):
|
|
33
35
|
db = self._make_db(tmp_path, monkeypatch)
|
|
@@ -37,16 +39,19 @@ class TestListGroupsForAssign:
|
|
|
37
39
|
result = runner.invoke(cli, ["_list-groups-for-assign"])
|
|
38
40
|
assert result.exit_code == 0
|
|
39
41
|
lines = result.output.strip().split("\n")
|
|
40
|
-
assert
|
|
41
|
-
|
|
42
|
-
assert "
|
|
42
|
+
assert "(none)" in lines[0]
|
|
43
|
+
group_names = [line.split("┃")[1] for line in lines]
|
|
44
|
+
assert "alpha" in group_names
|
|
45
|
+
assert "beta" in group_names
|
|
43
46
|
|
|
44
47
|
def test_no_groups_just_none(self, tmp_path, monkeypatch):
|
|
45
48
|
self._make_db(tmp_path, monkeypatch)
|
|
46
49
|
runner = CliRunner()
|
|
47
50
|
result = runner.invoke(cli, ["_list-groups-for-assign"])
|
|
48
51
|
assert result.exit_code == 0
|
|
49
|
-
|
|
52
|
+
lines = result.output.strip().split("\n")
|
|
53
|
+
assert len(lines) == 1
|
|
54
|
+
assert "(none)" in lines[0]
|
|
50
55
|
|
|
51
56
|
|
|
52
57
|
class TestSetGroupDirect:
|
|
@@ -144,6 +149,39 @@ class TestConfigToggleHeader:
|
|
|
144
149
|
break
|
|
145
150
|
|
|
146
151
|
|
|
152
|
+
class TestCompletionMode:
|
|
153
|
+
"""Test completion mode config."""
|
|
154
|
+
|
|
155
|
+
def test_default_mode_is_fallback(self):
|
|
156
|
+
config = load_config()
|
|
157
|
+
assert config["_completion_mode"] == "fallback"
|
|
158
|
+
|
|
159
|
+
def test_emit_zsh_config_has_completion_mode(self):
|
|
160
|
+
config = load_config()
|
|
161
|
+
output = emit_zsh_config(config)
|
|
162
|
+
assert "_COPA_COMPLETION_MODE='fallback'" in output
|
|
163
|
+
|
|
164
|
+
def test_load_config_accepts_valid_modes(self, tmp_path):
|
|
165
|
+
for mode in ("fallback", "always", "hybrid", "never"):
|
|
166
|
+
config_file = tmp_path / f"config_{mode}.toml"
|
|
167
|
+
config_file.write_text(f'[completion]\nmode = "{mode}"\n')
|
|
168
|
+
config = load_config(config_file)
|
|
169
|
+
assert config["_completion_mode"] == mode
|
|
170
|
+
|
|
171
|
+
def test_load_config_rejects_invalid_mode(self, tmp_path):
|
|
172
|
+
config_file = tmp_path / "config.toml"
|
|
173
|
+
config_file.write_text('[completion]\nmode = "bogus"\n')
|
|
174
|
+
config = load_config(config_file)
|
|
175
|
+
assert config["_completion_mode"] == "fallback"
|
|
176
|
+
|
|
177
|
+
def test_emit_modes_in_output(self):
|
|
178
|
+
config = load_config()
|
|
179
|
+
for mode in ("fallback", "always", "hybrid", "never"):
|
|
180
|
+
config["_completion_mode"] = mode
|
|
181
|
+
output = emit_zsh_config(config)
|
|
182
|
+
assert f"_COPA_COMPLETION_MODE='{mode}'" in output
|
|
183
|
+
|
|
184
|
+
|
|
147
185
|
class TestTtyHelpersInCommon:
|
|
148
186
|
"""Test that tty helpers are accessible from cli_common."""
|
|
149
187
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|