copa-cli 0.7.0__tar.gz → 0.8.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.7.0 → copa_cli-0.8.0}/PKG-INFO +38 -3
- {copa_cli-0.7.0 → copa_cli-0.8.0}/README.md +37 -2
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/copa.zsh +52 -27
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/db.py +2 -1
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/mcp_server.py +105 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/PKG-INFO +38 -3
- {copa_cli-0.7.0 → copa_cli-0.8.0}/pyproject.toml +1 -1
- {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_modal.py +54 -23
- {copa_cli-0.7.0 → copa_cli-0.8.0}/LICENSE +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/__init__.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/__main__.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/cli.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/cli_common.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/cli_internal.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/cli_llm.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/cli_share.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/config.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/evolve.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/fzf.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/history.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/llm.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/models.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/scanner.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/scoring.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/sharing.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/SOURCES.txt +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/dependency_links.txt +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/entry_points.txt +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/requires.txt +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/top_level.txt +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/setup.cfg +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_cli_and_sharing.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_db.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_fzf.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_models.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_polish.py +0 -0
- {copa_cli-0.7.0 → copa_cli-0.8.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.8.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
|
|
@@ -36,6 +36,8 @@ Dynamic: license-file
|
|
|
36
36
|
|
|
37
37
|
Copa tracks the commands you run, ranks them by frequency and recency, and gives you instant fuzzy search via fzf. Think of it as a smart, searchable, shareable upgrade to shell history.
|
|
38
38
|
|
|
39
|
+

|
|
40
|
+
|
|
39
41
|
## Features
|
|
40
42
|
|
|
41
43
|
- **Smart ranking** — commands scored by `2*log(1+freq) + 8*0.5^(age/3d)`, so frequent *and* recent commands float to the top
|
|
@@ -134,6 +136,8 @@ copa sync
|
|
|
134
136
|
|
|
135
137
|
Once shell integration is sourced, pressing **Ctrl+R** opens an fzf-powered command palette instead of the default zsh reverse search. This is Copa's primary interface.
|
|
136
138
|
|
|
139
|
+

|
|
140
|
+
|
|
137
141
|
### What you see
|
|
138
142
|
|
|
139
143
|
Copa pipes every tracked command into fzf with aligned columns:
|
|
@@ -173,6 +177,8 @@ While the fzf palette is open, these keys are available:
|
|
|
173
177
|
| **Ctrl+/** | Append `2>/dev/null` | Suppress stderr |
|
|
174
178
|
| **Ctrl+S** | Scope by group | Opens inline group list — Enter filters to that group, ESC returns to all |
|
|
175
179
|
| **Ctrl+G** | Assign group | Opens inline group list — Enter assigns the group to the highlighted command |
|
|
180
|
+
|
|
181
|
+

|
|
176
182
|
| **Ctrl+N** | Cycle group | Cycles through groups: (all) → group1 → group2 → ... → (all) |
|
|
177
183
|
| **Ctrl+D** | Describe | Generate/edit a description using LLM (with tty-aware input) |
|
|
178
184
|
| **Ctrl+F** | Edit flags | Add flag documentation to the highlighted command |
|
|
@@ -184,6 +190,8 @@ Keybindings are configurable via `~/.copa/config.toml`. See [Configuration](#con
|
|
|
184
190
|
|
|
185
191
|
### Select mode (bulk operations)
|
|
186
192
|
|
|
193
|
+

|
|
194
|
+
|
|
187
195
|
Press **Ctrl+B** from the Ctrl+R palette to enter **select mode**. This opens a new fzf view with multi-select enabled:
|
|
188
196
|
|
|
189
197
|
- **Tab** toggles selection on individual commands
|
|
@@ -220,10 +228,14 @@ Selecting a command places it directly into your shell prompt (without executing
|
|
|
220
228
|
|
|
221
229
|
## Tab Completion
|
|
222
230
|
|
|
231
|
+

|
|
232
|
+
|
|
223
233
|
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.
|
|
224
234
|
|
|
225
235
|
### Completion modes
|
|
226
236
|
|
|
237
|
+

|
|
238
|
+
|
|
227
239
|
Copa supports four completion modes, configured via `~/.copa/config.toml`:
|
|
228
240
|
|
|
229
241
|
| Mode | Behavior |
|
|
@@ -269,6 +281,10 @@ Copa's own CLI completions (`copa li<TAB>` → `list`) continue to work as befor
|
|
|
269
281
|
|
|
270
282
|
## Inline Suggestions (Ghost Text)
|
|
271
283
|
|
|
284
|
+
| Tab Mode 1 (direct accept) | Tab Mode 2 (menu select, default) |
|
|
285
|
+
|---|---|
|
|
286
|
+
|  |  |
|
|
287
|
+
|
|
272
288
|
Copa shows grey ghost text after your cursor as you type — the best matching command from your database, ranked by frequency and recency. This works like fish shell's autosuggestions or zsh-autosuggestions, with zero plugin dependencies.
|
|
273
289
|
|
|
274
290
|
### How it works
|
|
@@ -285,9 +301,10 @@ $ git pu█sh origin main ← grey ghost text
|
|
|
285
301
|
|-----|-------------------|---------------|
|
|
286
302
|
| Type chars | Insert char, re-fetch suggestion | Insert char, fetch suggestion |
|
|
287
303
|
| Backspace | Delete char, **latch** (suppress suggestions) | Delete char normally |
|
|
288
|
-
| **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default):
|
|
289
|
-
| **Down** |
|
|
304
|
+
| **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default): open completion menu with suggestion highlighted at top, native completions below | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
|
|
305
|
+
| **Down** | Open completion menu with suggestion highlighted at top | History navigation |
|
|
290
306
|
| **Right arrow** | Accept one word, re-fetch | Move cursor right |
|
|
307
|
+
| **Cmd+Right / End** | Accept full suggestion | Move to end of line |
|
|
291
308
|
| Enter | Clear suggestion, execute | Execute |
|
|
292
309
|
| Esc | Dismiss suggestion | Normal Esc |
|
|
293
310
|
| Up | Clear suggestion, navigate history | History navigation |
|
|
@@ -441,6 +458,8 @@ copa describe 42
|
|
|
441
458
|
|
|
442
459
|
Generates a description for a specific command by ID. Same accept/edit flow as `fix --auto`.
|
|
443
460
|
|
|
461
|
+

|
|
462
|
+
|
|
444
463
|
## Script Metadata Protocol
|
|
445
464
|
|
|
446
465
|
Copa recognizes `#@` headers in script files (checked in the first 50 lines):
|
|
@@ -488,6 +507,8 @@ copa scan --dir ~/bin # scan a specific directory
|
|
|
488
507
|
|
|
489
508
|
## Sharing
|
|
490
509
|
|
|
510
|
+

|
|
511
|
+
|
|
491
512
|
Export a group as a `.copa` file:
|
|
492
513
|
|
|
493
514
|
```bash
|
|
@@ -622,8 +643,16 @@ Available MCP tools:
|
|
|
622
643
|
- `copa_get_stats` — usage statistics
|
|
623
644
|
- `copa_add_command` — add a command
|
|
624
645
|
- `copa_update_description` — update a description
|
|
646
|
+
- `copa_delete_command` — delete a command by ID
|
|
647
|
+
- `copa_set_group` — set or change a command's group
|
|
648
|
+
- `copa_update_flags` — update flag documentation for a command
|
|
649
|
+
- `copa_pin_command` — pin or unpin a command
|
|
625
650
|
- `copa_create_group` — create a group with commands
|
|
626
651
|
- `copa_bulk_add` — bulk add commands
|
|
652
|
+
- `copa_share_load` — load a .copa file as a shared set
|
|
653
|
+
- `copa_share_list` — list all loaded shared sets
|
|
654
|
+
- `copa_share_remove` — remove a shared set
|
|
655
|
+
- `copa_export_group` — export a group as a .copa file
|
|
627
656
|
|
|
628
657
|
## Configuration
|
|
629
658
|
|
|
@@ -671,6 +700,8 @@ continue = ["pipe", "chain", "redirect"] # default: |, &&, > re-open fzf
|
|
|
671
700
|
|
|
672
701
|
### Composition key behavior
|
|
673
702
|
|
|
703
|
+

|
|
704
|
+
|
|
674
705
|
When you press a composition key (like Ctrl-A for `&&`), Copa can either **close fzf** (placing the command + operator in your prompt) or **continue** (appending the operator and re-opening fzf so you can select the next command to chain).
|
|
675
706
|
|
|
676
707
|
By default, "connector" operators re-open fzf:
|
|
@@ -692,6 +723,10 @@ To revert to the old behavior (all keys close immediately), set:
|
|
|
692
723
|
continue = []
|
|
693
724
|
```
|
|
694
725
|
|
|
726
|
+
## End-to-End Workflow
|
|
727
|
+
|
|
728
|
+

|
|
729
|
+
|
|
695
730
|
## CLI Reference
|
|
696
731
|
|
|
697
732
|
| Command | Purpose |
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
Copa tracks the commands you run, ranks them by frequency and recency, and gives you instant fuzzy search via fzf. Think of it as a smart, searchable, shareable upgrade to shell history.
|
|
8
8
|
|
|
9
|
+

|
|
10
|
+
|
|
9
11
|
## Features
|
|
10
12
|
|
|
11
13
|
- **Smart ranking** — commands scored by `2*log(1+freq) + 8*0.5^(age/3d)`, so frequent *and* recent commands float to the top
|
|
@@ -104,6 +106,8 @@ copa sync
|
|
|
104
106
|
|
|
105
107
|
Once shell integration is sourced, pressing **Ctrl+R** opens an fzf-powered command palette instead of the default zsh reverse search. This is Copa's primary interface.
|
|
106
108
|
|
|
109
|
+

|
|
110
|
+
|
|
107
111
|
### What you see
|
|
108
112
|
|
|
109
113
|
Copa pipes every tracked command into fzf with aligned columns:
|
|
@@ -143,6 +147,8 @@ While the fzf palette is open, these keys are available:
|
|
|
143
147
|
| **Ctrl+/** | Append `2>/dev/null` | Suppress stderr |
|
|
144
148
|
| **Ctrl+S** | Scope by group | Opens inline group list — Enter filters to that group, ESC returns to all |
|
|
145
149
|
| **Ctrl+G** | Assign group | Opens inline group list — Enter assigns the group to the highlighted command |
|
|
150
|
+
|
|
151
|
+

|
|
146
152
|
| **Ctrl+N** | Cycle group | Cycles through groups: (all) → group1 → group2 → ... → (all) |
|
|
147
153
|
| **Ctrl+D** | Describe | Generate/edit a description using LLM (with tty-aware input) |
|
|
148
154
|
| **Ctrl+F** | Edit flags | Add flag documentation to the highlighted command |
|
|
@@ -154,6 +160,8 @@ Keybindings are configurable via `~/.copa/config.toml`. See [Configuration](#con
|
|
|
154
160
|
|
|
155
161
|
### Select mode (bulk operations)
|
|
156
162
|
|
|
163
|
+

|
|
164
|
+
|
|
157
165
|
Press **Ctrl+B** from the Ctrl+R palette to enter **select mode**. This opens a new fzf view with multi-select enabled:
|
|
158
166
|
|
|
159
167
|
- **Tab** toggles selection on individual commands
|
|
@@ -190,10 +198,14 @@ Selecting a command places it directly into your shell prompt (without executing
|
|
|
190
198
|
|
|
191
199
|
## Tab Completion
|
|
192
200
|
|
|
201
|
+

|
|
202
|
+
|
|
193
203
|
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.
|
|
194
204
|
|
|
195
205
|
### Completion modes
|
|
196
206
|
|
|
207
|
+

|
|
208
|
+
|
|
197
209
|
Copa supports four completion modes, configured via `~/.copa/config.toml`:
|
|
198
210
|
|
|
199
211
|
| Mode | Behavior |
|
|
@@ -239,6 +251,10 @@ Copa's own CLI completions (`copa li<TAB>` → `list`) continue to work as befor
|
|
|
239
251
|
|
|
240
252
|
## Inline Suggestions (Ghost Text)
|
|
241
253
|
|
|
254
|
+
| Tab Mode 1 (direct accept) | Tab Mode 2 (menu select, default) |
|
|
255
|
+
|---|---|
|
|
256
|
+
|  |  |
|
|
257
|
+
|
|
242
258
|
Copa shows grey ghost text after your cursor as you type — the best matching command from your database, ranked by frequency and recency. This works like fish shell's autosuggestions or zsh-autosuggestions, with zero plugin dependencies.
|
|
243
259
|
|
|
244
260
|
### How it works
|
|
@@ -255,9 +271,10 @@ $ git pu█sh origin main ← grey ghost text
|
|
|
255
271
|
|-----|-------------------|---------------|
|
|
256
272
|
| Type chars | Insert char, re-fetch suggestion | Insert char, fetch suggestion |
|
|
257
273
|
| Backspace | Delete char, **latch** (suppress suggestions) | Delete char normally |
|
|
258
|
-
| **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default):
|
|
259
|
-
| **Down** |
|
|
274
|
+
| **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default): open completion menu with suggestion highlighted at top, native completions below | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
|
|
275
|
+
| **Down** | Open completion menu with suggestion highlighted at top | History navigation |
|
|
260
276
|
| **Right arrow** | Accept one word, re-fetch | Move cursor right |
|
|
277
|
+
| **Cmd+Right / End** | Accept full suggestion | Move to end of line |
|
|
261
278
|
| Enter | Clear suggestion, execute | Execute |
|
|
262
279
|
| Esc | Dismiss suggestion | Normal Esc |
|
|
263
280
|
| Up | Clear suggestion, navigate history | History navigation |
|
|
@@ -411,6 +428,8 @@ copa describe 42
|
|
|
411
428
|
|
|
412
429
|
Generates a description for a specific command by ID. Same accept/edit flow as `fix --auto`.
|
|
413
430
|
|
|
431
|
+

|
|
432
|
+
|
|
414
433
|
## Script Metadata Protocol
|
|
415
434
|
|
|
416
435
|
Copa recognizes `#@` headers in script files (checked in the first 50 lines):
|
|
@@ -458,6 +477,8 @@ copa scan --dir ~/bin # scan a specific directory
|
|
|
458
477
|
|
|
459
478
|
## Sharing
|
|
460
479
|
|
|
480
|
+

|
|
481
|
+
|
|
461
482
|
Export a group as a `.copa` file:
|
|
462
483
|
|
|
463
484
|
```bash
|
|
@@ -592,8 +613,16 @@ Available MCP tools:
|
|
|
592
613
|
- `copa_get_stats` — usage statistics
|
|
593
614
|
- `copa_add_command` — add a command
|
|
594
615
|
- `copa_update_description` — update a description
|
|
616
|
+
- `copa_delete_command` — delete a command by ID
|
|
617
|
+
- `copa_set_group` — set or change a command's group
|
|
618
|
+
- `copa_update_flags` — update flag documentation for a command
|
|
619
|
+
- `copa_pin_command` — pin or unpin a command
|
|
595
620
|
- `copa_create_group` — create a group with commands
|
|
596
621
|
- `copa_bulk_add` — bulk add commands
|
|
622
|
+
- `copa_share_load` — load a .copa file as a shared set
|
|
623
|
+
- `copa_share_list` — list all loaded shared sets
|
|
624
|
+
- `copa_share_remove` — remove a shared set
|
|
625
|
+
- `copa_export_group` — export a group as a .copa file
|
|
597
626
|
|
|
598
627
|
## Configuration
|
|
599
628
|
|
|
@@ -641,6 +670,8 @@ continue = ["pipe", "chain", "redirect"] # default: |, &&, > re-open fzf
|
|
|
641
670
|
|
|
642
671
|
### Composition key behavior
|
|
643
672
|
|
|
673
|
+

|
|
674
|
+
|
|
644
675
|
When you press a composition key (like Ctrl-A for `&&`), Copa can either **close fzf** (placing the command + operator in your prompt) or **continue** (appending the operator and re-opening fzf so you can select the next command to chain).
|
|
645
676
|
|
|
646
677
|
By default, "connector" operators re-open fzf:
|
|
@@ -662,6 +693,10 @@ To revert to the old behavior (all keys close immediately), set:
|
|
|
662
693
|
continue = []
|
|
663
694
|
```
|
|
664
695
|
|
|
696
|
+
## End-to-End Workflow
|
|
697
|
+
|
|
698
|
+

|
|
699
|
+
|
|
665
700
|
## CLI Reference
|
|
666
701
|
|
|
667
702
|
| Command | Purpose |
|
|
@@ -45,11 +45,12 @@ eval "$(copa _fzf-config 2>/dev/null)" || {
|
|
|
45
45
|
|
|
46
46
|
# --- precmd hook: record last command ---
|
|
47
47
|
_copa_precmd() {
|
|
48
|
+
(( $+commands[copa] )) || return
|
|
48
49
|
local last_cmd
|
|
49
50
|
last_cmd="$(fc -ln -1 2>/dev/null)"
|
|
50
51
|
last_cmd="${last_cmd## }" # strip leading space
|
|
51
52
|
if [[ -n "$last_cmd" && "$last_cmd" != _copa_* ]]; then
|
|
52
|
-
copa _record "$last_cmd" &!
|
|
53
|
+
copa _record "$last_cmd" &! 2>/dev/null
|
|
53
54
|
fi
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -280,7 +281,7 @@ bindkey '^R' _copa_fzf_widget
|
|
|
280
281
|
if ! (( $+functions[compdef] )); then
|
|
281
282
|
autoload -Uz compinit && compinit -i -C
|
|
282
283
|
fi
|
|
283
|
-
eval "$(copa completion zsh)"
|
|
284
|
+
eval "$(copa completion zsh 2>/dev/null)"
|
|
284
285
|
|
|
285
286
|
# --- Supplemental tab completion from Copa database ---
|
|
286
287
|
# Mode is controlled by _COPA_COMPLETION_MODE (set via copa _fzf-config):
|
|
@@ -290,19 +291,25 @@ eval "$(copa completion zsh)"
|
|
|
290
291
|
# never — disable Copa tab completion entirely
|
|
291
292
|
if [[ "$_COPA_COMPLETION_MODE" != 'never' ]]; then
|
|
292
293
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
294
|
+
# Suggestion completer: runs FIRST so menu-complete highlights the Copa
|
|
295
|
+
# suggestion before native completions. No-op when nothing is pending.
|
|
296
|
+
# Always returns 1 so the chain continues to _complete and _copa_history_complete;
|
|
297
|
+
# compadd matches persist across completers regardless of return value.
|
|
298
|
+
_copa_suggestion_complete() {
|
|
299
|
+
[[ -z "$_COPA_SUGGEST_PENDING" ]] && return 1
|
|
300
|
+
local pending="$_COPA_SUGGEST_PENDING"
|
|
301
|
+
_COPA_SUGGEST_PENDING=""
|
|
302
|
+
local cur_word="${words[CURRENT]}"
|
|
303
|
+
local prefix_len=$(( ${#LBUFFER} - ${#cur_word} ))
|
|
304
|
+
local insert_text="${pending:$prefix_len}"
|
|
305
|
+
if [[ -n "$insert_text" ]]; then
|
|
306
|
+
compadd -U -Q -V 'copa-suggestion' -X 'SUGGESTED' -o nosort -- "$insert_text"
|
|
305
307
|
fi
|
|
308
|
+
compstate[list]='list force'
|
|
309
|
+
return 1 # don't stop chain — let _complete and _copa_history_complete also run
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_copa_history_complete() {
|
|
306
313
|
# In fallback mode, only show when native completers found nothing
|
|
307
314
|
if [[ "$_COPA_COMPLETION_MODE" == 'fallback' ]]; then
|
|
308
315
|
(( compstate[nmatches] > 0 )) && return
|
|
@@ -327,12 +334,14 @@ _copa_history_complete() {
|
|
|
327
334
|
fi
|
|
328
335
|
}
|
|
329
336
|
|
|
330
|
-
# Append to existing completers without clobbering user config
|
|
337
|
+
# Append to existing completers without clobbering user config.
|
|
338
|
+
# _copa_suggestion_complete runs first so menu-complete highlights
|
|
339
|
+
# the Copa suggestion; _copa_history_complete runs last for history matches.
|
|
331
340
|
() {
|
|
332
341
|
local -a cur
|
|
333
342
|
zstyle -g cur ':completion:*' completer 2>/dev/null
|
|
334
343
|
if (( ! ${cur[(Ie)_copa_history_complete]} )); then
|
|
335
|
-
zstyle ':completion:*' completer ${cur:-_complete} _copa_history_complete
|
|
344
|
+
zstyle ':completion:*' completer _copa_suggestion_complete ${cur:-_complete} _copa_history_complete _files
|
|
336
345
|
fi
|
|
337
346
|
# Enable group separation so Copa results appear as a distinct section
|
|
338
347
|
zstyle ':completion:*' group-name ''
|
|
@@ -341,11 +350,13 @@ _copa_history_complete() {
|
|
|
341
350
|
# Interactive menu with highlighting; Tab accepts the focused item
|
|
342
351
|
zstyle ':completion:*' menu select
|
|
343
352
|
zmodload zsh/complist 2>/dev/null
|
|
344
|
-
bindkey -M menuselect '^I'
|
|
353
|
+
bindkey -M menuselect '^I' accept-search
|
|
354
|
+
bindkey -M menuselect '^M' accept-search
|
|
355
|
+
bindkey -M menuselect '^J' accept-search
|
|
345
356
|
# Escape cancels the menu and restores the original buffer
|
|
346
357
|
bindkey -M menuselect '^[' send-break
|
|
347
358
|
# Space accepts the focused completion and closes the menu
|
|
348
|
-
bindkey -M menuselect ' '
|
|
359
|
+
bindkey -M menuselect ' ' accept-search
|
|
349
360
|
# Raise threshold before "show all N?" prompt
|
|
350
361
|
LISTMAX=200
|
|
351
362
|
# Copa completion branding: show group description headers
|
|
@@ -412,8 +423,7 @@ _copa_suggest_backward_delete_char() {
|
|
|
412
423
|
zle -N backward-delete-char _copa_suggest_backward_delete_char
|
|
413
424
|
|
|
414
425
|
# Tab: accept suggestion or open completion menu.
|
|
415
|
-
#
|
|
416
|
-
# Don't call _copa_suggest_fetch after expand-or-complete — menu-select
|
|
426
|
+
# Don't call _copa_suggest_fetch after menu-complete — menu-select
|
|
417
427
|
# runs asynchronously and fetch would overwrite the display with
|
|
418
428
|
# ghost text. The self-insert wrapper re-fetches on the next keystroke.
|
|
419
429
|
_copa_suggest_expand_or_complete() {
|
|
@@ -424,14 +434,15 @@ _copa_suggest_expand_or_complete() {
|
|
|
424
434
|
CURSOR=${#BUFFER}
|
|
425
435
|
_copa_suggest_clear
|
|
426
436
|
else
|
|
427
|
-
# tab_accept=2: open completion menu with suggestion highlighted
|
|
428
|
-
#
|
|
429
|
-
#
|
|
430
|
-
#
|
|
437
|
+
# tab_accept=2: open completion menu with suggestion highlighted.
|
|
438
|
+
# menu-complete enters menu-select mode reliably (unlike
|
|
439
|
+
# .expand-or-complete which bypasses menu-select). With
|
|
440
|
+
# group-order copa-suggestion copa-history, menu-select cycles
|
|
441
|
+
# in display order, so the Copa suggestion is highlighted first.
|
|
431
442
|
local pending="$_COPA_SUGGESTION"
|
|
432
443
|
_copa_suggest_clear
|
|
433
444
|
_COPA_SUGGEST_PENDING="$pending"
|
|
434
|
-
zle
|
|
445
|
+
zle menu-complete
|
|
435
446
|
fi
|
|
436
447
|
return
|
|
437
448
|
fi
|
|
@@ -487,6 +498,20 @@ _copa_suggest_forward_word() {
|
|
|
487
498
|
}
|
|
488
499
|
zle -N forward-word _copa_suggest_forward_word
|
|
489
500
|
|
|
501
|
+
# end-of-line (Cmd+Right / End): accept full suggestion if ghost text showing
|
|
502
|
+
_copa_suggest_end_of_line() {
|
|
503
|
+
if [[ -n "$POSTDISPLAY" && -n "$_COPA_SUGGESTION" && $CURSOR -eq ${#BUFFER} ]]; then
|
|
504
|
+
BUFFER="$_COPA_SUGGESTION"
|
|
505
|
+
CURSOR=${#BUFFER}
|
|
506
|
+
_copa_suggest_clear
|
|
507
|
+
_COPA_SUGGEST_LATCHED=0
|
|
508
|
+
_copa_suggest_fetch
|
|
509
|
+
else
|
|
510
|
+
zle .end-of-line
|
|
511
|
+
fi
|
|
512
|
+
}
|
|
513
|
+
zle -N end-of-line _copa_suggest_end_of_line
|
|
514
|
+
|
|
490
515
|
# accept-line (Enter): clear suggestion + latch, then execute
|
|
491
516
|
_copa_suggest_accept_line() {
|
|
492
517
|
_copa_suggest_clear
|
|
@@ -519,7 +544,7 @@ _copa_suggest_down_line_or_history() {
|
|
|
519
544
|
local pending="$_COPA_SUGGESTION"
|
|
520
545
|
_copa_suggest_clear
|
|
521
546
|
_COPA_SUGGEST_PENDING="$pending"
|
|
522
|
-
zle
|
|
547
|
+
zle menu-complete
|
|
523
548
|
else
|
|
524
549
|
zle .down-line-or-history
|
|
525
550
|
fi
|
|
@@ -537,7 +562,7 @@ _copa_suggest_down_line_or_search() {
|
|
|
537
562
|
local pending="$_COPA_SUGGESTION"
|
|
538
563
|
_copa_suggest_clear
|
|
539
564
|
_COPA_SUGGEST_PENDING="$pending"
|
|
540
|
-
zle
|
|
565
|
+
zle menu-complete
|
|
541
566
|
else
|
|
542
567
|
zle .down-line-or-search
|
|
543
568
|
fi
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import sqlite3
|
|
7
8
|
import time
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
from .models import Command, SharedSet
|
|
11
12
|
|
|
12
|
-
DEFAULT_DB_PATH = Path.home() / ".copa" / "copa.db"
|
|
13
|
+
DEFAULT_DB_PATH = Path(os.environ.get("COPA_DB", Path.home() / ".copa" / "copa.db"))
|
|
13
14
|
|
|
14
15
|
SCHEMA_SQL = """\
|
|
15
16
|
CREATE TABLE IF NOT EXISTS commands (
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
5
8
|
from .db import Database
|
|
6
9
|
from .scoring import rank_commands
|
|
7
10
|
|
|
@@ -105,6 +108,49 @@ def create_mcp_server():
|
|
|
105
108
|
db.update_description(command_id, description)
|
|
106
109
|
return f"Updated [{command_id}] {cmd.command}: {description}"
|
|
107
110
|
|
|
111
|
+
@mcp.tool()
|
|
112
|
+
def copa_delete_command(command_id: int) -> str:
|
|
113
|
+
"""Delete a command from Copa by its ID."""
|
|
114
|
+
cmd = db.get_command(command_id)
|
|
115
|
+
if not cmd:
|
|
116
|
+
return f"Command {command_id} not found."
|
|
117
|
+
db.remove_command(command_id)
|
|
118
|
+
return f"Deleted [{command_id}] {cmd.command}"
|
|
119
|
+
|
|
120
|
+
@mcp.tool()
|
|
121
|
+
def copa_set_group(command_id: int, group: str | None = None) -> str:
|
|
122
|
+
"""Set or change the group of a command. Pass group=None to remove from group."""
|
|
123
|
+
cmd = db.get_command(command_id)
|
|
124
|
+
if not cmd:
|
|
125
|
+
return f"Command {command_id} not found."
|
|
126
|
+
db.update_group(command_id, group)
|
|
127
|
+
if group:
|
|
128
|
+
return f"Moved [{command_id}] {cmd.command} to group '{group}'"
|
|
129
|
+
return f"Removed [{command_id}] {cmd.command} from its group"
|
|
130
|
+
|
|
131
|
+
@mcp.tool()
|
|
132
|
+
def copa_update_flags(command_id: int, flags: dict[str, str]) -> str:
|
|
133
|
+
"""Update the flags/options documentation for a command.
|
|
134
|
+
|
|
135
|
+
flags is a dict mapping flag name to description, e.g. {"-v": "Verbose output"}.
|
|
136
|
+
"""
|
|
137
|
+
cmd = db.get_command(command_id)
|
|
138
|
+
if not cmd:
|
|
139
|
+
return f"Command {command_id} not found."
|
|
140
|
+
db.update_flags(command_id, flags)
|
|
141
|
+
flag_list = ", ".join(f"{k}: {v}" for k, v in flags.items())
|
|
142
|
+
return f"Updated flags for [{command_id}] {cmd.command}: {flag_list}"
|
|
143
|
+
|
|
144
|
+
@mcp.tool()
|
|
145
|
+
def copa_pin_command(command_id: int, pinned: bool = True) -> str:
|
|
146
|
+
"""Pin or unpin a command. Pinned commands always appear at the top."""
|
|
147
|
+
cmd = db.get_command(command_id)
|
|
148
|
+
if not cmd:
|
|
149
|
+
return f"Command {command_id} not found."
|
|
150
|
+
db.pin_command(command_id, pinned)
|
|
151
|
+
action = "Pinned" if pinned else "Unpinned"
|
|
152
|
+
return f"{action} [{command_id}] {cmd.command}"
|
|
153
|
+
|
|
108
154
|
@mcp.tool()
|
|
109
155
|
def copa_create_group(name: str, commands: list[dict] | None = None) -> str:
|
|
110
156
|
"""Create a Copa group and optionally add commands to it.
|
|
@@ -146,6 +192,65 @@ def create_mcp_server():
|
|
|
146
192
|
count += 1
|
|
147
193
|
return f"Added {count} commands."
|
|
148
194
|
|
|
195
|
+
@mcp.tool()
|
|
196
|
+
def copa_share_load(file_path: str) -> str:
|
|
197
|
+
"""Load a .copa file into Copa as a shared set.
|
|
198
|
+
|
|
199
|
+
The file_path should be a path to a .copa JSON file.
|
|
200
|
+
"""
|
|
201
|
+
from .sharing import import_shared_set, load_copa_file, resolve_copa_path
|
|
202
|
+
|
|
203
|
+
resolved = resolve_copa_path(file_path)
|
|
204
|
+
if not resolved or not resolved.is_file():
|
|
205
|
+
return f"File not found: {file_path}"
|
|
206
|
+
copa_file = load_copa_file(resolved)
|
|
207
|
+
count = import_shared_set(db, copa_file, source_path=str(resolved))
|
|
208
|
+
return f"Loaded shared set '{copa_file.name}' with {count} commands from {resolved}"
|
|
209
|
+
|
|
210
|
+
@mcp.tool()
|
|
211
|
+
def copa_share_list() -> str:
|
|
212
|
+
"""List all loaded shared sets."""
|
|
213
|
+
sets = db.get_shared_sets()
|
|
214
|
+
if not sets:
|
|
215
|
+
return "No shared sets loaded."
|
|
216
|
+
lines = []
|
|
217
|
+
for ss in sets:
|
|
218
|
+
parts = [f"- {ss.name}"]
|
|
219
|
+
if ss.description:
|
|
220
|
+
parts[0] += f": {ss.description}"
|
|
221
|
+
if ss.author:
|
|
222
|
+
parts.append(f" author: {ss.author}")
|
|
223
|
+
if ss.source_path:
|
|
224
|
+
parts.append(f" source: {ss.source_path}")
|
|
225
|
+
lines.append("\n".join(parts))
|
|
226
|
+
return "\n".join(lines)
|
|
227
|
+
|
|
228
|
+
@mcp.tool()
|
|
229
|
+
def copa_share_remove(name: str) -> str:
|
|
230
|
+
"""Remove a shared set and its commands from Copa."""
|
|
231
|
+
sets = db.get_shared_sets()
|
|
232
|
+
if not any(ss.name == name for ss in sets):
|
|
233
|
+
return f"Shared set '{name}' not found."
|
|
234
|
+
db.remove_shared_set(name)
|
|
235
|
+
return f"Removed shared set '{name}'"
|
|
236
|
+
|
|
237
|
+
@mcp.tool()
|
|
238
|
+
def copa_export_group(group: str, output_path: str | None = None) -> str:
|
|
239
|
+
"""Export a Copa group as a .copa JSON file.
|
|
240
|
+
|
|
241
|
+
If output_path is not provided, returns the JSON content directly.
|
|
242
|
+
"""
|
|
243
|
+
from .sharing import export_group
|
|
244
|
+
|
|
245
|
+
copa_file = export_group(db, group)
|
|
246
|
+
if not copa_file.commands:
|
|
247
|
+
return f"Group '{group}' has no commands to export."
|
|
248
|
+
content = json.dumps(copa_file.to_dict(), indent=2)
|
|
249
|
+
if output_path:
|
|
250
|
+
Path(output_path).write_text(content)
|
|
251
|
+
return f"Exported group '{group}' ({len(copa_file.commands)} commands) to {output_path}"
|
|
252
|
+
return content
|
|
253
|
+
|
|
149
254
|
return mcp
|
|
150
255
|
|
|
151
256
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: copa-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -36,6 +36,8 @@ Dynamic: license-file
|
|
|
36
36
|
|
|
37
37
|
Copa tracks the commands you run, ranks them by frequency and recency, and gives you instant fuzzy search via fzf. Think of it as a smart, searchable, shareable upgrade to shell history.
|
|
38
38
|
|
|
39
|
+

|
|
40
|
+
|
|
39
41
|
## Features
|
|
40
42
|
|
|
41
43
|
- **Smart ranking** — commands scored by `2*log(1+freq) + 8*0.5^(age/3d)`, so frequent *and* recent commands float to the top
|
|
@@ -134,6 +136,8 @@ copa sync
|
|
|
134
136
|
|
|
135
137
|
Once shell integration is sourced, pressing **Ctrl+R** opens an fzf-powered command palette instead of the default zsh reverse search. This is Copa's primary interface.
|
|
136
138
|
|
|
139
|
+

|
|
140
|
+
|
|
137
141
|
### What you see
|
|
138
142
|
|
|
139
143
|
Copa pipes every tracked command into fzf with aligned columns:
|
|
@@ -173,6 +177,8 @@ While the fzf palette is open, these keys are available:
|
|
|
173
177
|
| **Ctrl+/** | Append `2>/dev/null` | Suppress stderr |
|
|
174
178
|
| **Ctrl+S** | Scope by group | Opens inline group list — Enter filters to that group, ESC returns to all |
|
|
175
179
|
| **Ctrl+G** | Assign group | Opens inline group list — Enter assigns the group to the highlighted command |
|
|
180
|
+
|
|
181
|
+

|
|
176
182
|
| **Ctrl+N** | Cycle group | Cycles through groups: (all) → group1 → group2 → ... → (all) |
|
|
177
183
|
| **Ctrl+D** | Describe | Generate/edit a description using LLM (with tty-aware input) |
|
|
178
184
|
| **Ctrl+F** | Edit flags | Add flag documentation to the highlighted command |
|
|
@@ -184,6 +190,8 @@ Keybindings are configurable via `~/.copa/config.toml`. See [Configuration](#con
|
|
|
184
190
|
|
|
185
191
|
### Select mode (bulk operations)
|
|
186
192
|
|
|
193
|
+

|
|
194
|
+
|
|
187
195
|
Press **Ctrl+B** from the Ctrl+R palette to enter **select mode**. This opens a new fzf view with multi-select enabled:
|
|
188
196
|
|
|
189
197
|
- **Tab** toggles selection on individual commands
|
|
@@ -220,10 +228,14 @@ Selecting a command places it directly into your shell prompt (without executing
|
|
|
220
228
|
|
|
221
229
|
## Tab Completion
|
|
222
230
|
|
|
231
|
+

|
|
232
|
+
|
|
223
233
|
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.
|
|
224
234
|
|
|
225
235
|
### Completion modes
|
|
226
236
|
|
|
237
|
+

|
|
238
|
+
|
|
227
239
|
Copa supports four completion modes, configured via `~/.copa/config.toml`:
|
|
228
240
|
|
|
229
241
|
| Mode | Behavior |
|
|
@@ -269,6 +281,10 @@ Copa's own CLI completions (`copa li<TAB>` → `list`) continue to work as befor
|
|
|
269
281
|
|
|
270
282
|
## Inline Suggestions (Ghost Text)
|
|
271
283
|
|
|
284
|
+
| Tab Mode 1 (direct accept) | Tab Mode 2 (menu select, default) |
|
|
285
|
+
|---|---|
|
|
286
|
+
|  |  |
|
|
287
|
+
|
|
272
288
|
Copa shows grey ghost text after your cursor as you type — the best matching command from your database, ranked by frequency and recency. This works like fish shell's autosuggestions or zsh-autosuggestions, with zero plugin dependencies.
|
|
273
289
|
|
|
274
290
|
### How it works
|
|
@@ -285,9 +301,10 @@ $ git pu█sh origin main ← grey ghost text
|
|
|
285
301
|
|-----|-------------------|---------------|
|
|
286
302
|
| Type chars | Insert char, re-fetch suggestion | Insert char, fetch suggestion |
|
|
287
303
|
| Backspace | Delete char, **latch** (suppress suggestions) | Delete char normally |
|
|
288
|
-
| **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default):
|
|
289
|
-
| **Down** |
|
|
304
|
+
| **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default): open completion menu with suggestion highlighted at top, native completions below | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
|
|
305
|
+
| **Down** | Open completion menu with suggestion highlighted at top | History navigation |
|
|
290
306
|
| **Right arrow** | Accept one word, re-fetch | Move cursor right |
|
|
307
|
+
| **Cmd+Right / End** | Accept full suggestion | Move to end of line |
|
|
291
308
|
| Enter | Clear suggestion, execute | Execute |
|
|
292
309
|
| Esc | Dismiss suggestion | Normal Esc |
|
|
293
310
|
| Up | Clear suggestion, navigate history | History navigation |
|
|
@@ -441,6 +458,8 @@ copa describe 42
|
|
|
441
458
|
|
|
442
459
|
Generates a description for a specific command by ID. Same accept/edit flow as `fix --auto`.
|
|
443
460
|
|
|
461
|
+

|
|
462
|
+
|
|
444
463
|
## Script Metadata Protocol
|
|
445
464
|
|
|
446
465
|
Copa recognizes `#@` headers in script files (checked in the first 50 lines):
|
|
@@ -488,6 +507,8 @@ copa scan --dir ~/bin # scan a specific directory
|
|
|
488
507
|
|
|
489
508
|
## Sharing
|
|
490
509
|
|
|
510
|
+

|
|
511
|
+
|
|
491
512
|
Export a group as a `.copa` file:
|
|
492
513
|
|
|
493
514
|
```bash
|
|
@@ -622,8 +643,16 @@ Available MCP tools:
|
|
|
622
643
|
- `copa_get_stats` — usage statistics
|
|
623
644
|
- `copa_add_command` — add a command
|
|
624
645
|
- `copa_update_description` — update a description
|
|
646
|
+
- `copa_delete_command` — delete a command by ID
|
|
647
|
+
- `copa_set_group` — set or change a command's group
|
|
648
|
+
- `copa_update_flags` — update flag documentation for a command
|
|
649
|
+
- `copa_pin_command` — pin or unpin a command
|
|
625
650
|
- `copa_create_group` — create a group with commands
|
|
626
651
|
- `copa_bulk_add` — bulk add commands
|
|
652
|
+
- `copa_share_load` — load a .copa file as a shared set
|
|
653
|
+
- `copa_share_list` — list all loaded shared sets
|
|
654
|
+
- `copa_share_remove` — remove a shared set
|
|
655
|
+
- `copa_export_group` — export a group as a .copa file
|
|
627
656
|
|
|
628
657
|
## Configuration
|
|
629
658
|
|
|
@@ -671,6 +700,8 @@ continue = ["pipe", "chain", "redirect"] # default: |, &&, > re-open fzf
|
|
|
671
700
|
|
|
672
701
|
### Composition key behavior
|
|
673
702
|
|
|
703
|
+

|
|
704
|
+
|
|
674
705
|
When you press a composition key (like Ctrl-A for `&&`), Copa can either **close fzf** (placing the command + operator in your prompt) or **continue** (appending the operator and re-opening fzf so you can select the next command to chain).
|
|
675
706
|
|
|
676
707
|
By default, "connector" operators re-open fzf:
|
|
@@ -692,6 +723,10 @@ To revert to the old behavior (all keys close immediately), set:
|
|
|
692
723
|
continue = []
|
|
693
724
|
```
|
|
694
725
|
|
|
726
|
+
## End-to-End Workflow
|
|
727
|
+
|
|
728
|
+

|
|
729
|
+
|
|
695
730
|
## CLI Reference
|
|
696
731
|
|
|
697
732
|
| Command | Purpose |
|
|
@@ -819,7 +819,7 @@ class TestSuggestBackspaceLatch:
|
|
|
819
819
|
def test_packaged_zsh_tab_unlatches(self):
|
|
820
820
|
content = self._read_zsh("copa/copa.zsh")
|
|
821
821
|
start = content.index("_copa_suggest_expand_or_complete()")
|
|
822
|
-
func_block = content[start : start +
|
|
822
|
+
func_block = content[start : start + 1000]
|
|
823
823
|
assert "_COPA_SUGGEST_LATCHED=0" in func_block
|
|
824
824
|
|
|
825
825
|
def test_packaged_zsh_backward_kill_word_latches(self):
|
|
@@ -920,9 +920,7 @@ class TestTabAcceptZsh:
|
|
|
920
920
|
# Tab widget checks tab_accept internally for mode 1 vs 2
|
|
921
921
|
assert "_COPA_SUGGEST_TAB_ACCEPT" in func_block
|
|
922
922
|
assert "_copa_suggest_clear" in func_block
|
|
923
|
-
# Mode 2 uses
|
|
924
|
-
assert "zle .expand-or-complete" in func_block
|
|
925
|
-
# Non-suggestion fallback still uses menu-complete
|
|
923
|
+
# Mode 2 uses menu-complete to reliably enter menu-select mode
|
|
926
924
|
assert "zle menu-complete" in func_block
|
|
927
925
|
# Mode 2 sets pending and opens completion menu
|
|
928
926
|
assert "_COPA_SUGGEST_PENDING" in func_block
|
|
@@ -935,7 +933,6 @@ class TestTabAcceptZsh:
|
|
|
935
933
|
func_block = content[start : start + 1000]
|
|
936
934
|
assert "_COPA_SUGGEST_TAB_ACCEPT" in func_block
|
|
937
935
|
assert "_copa_suggest_clear" in func_block
|
|
938
|
-
assert "zle .expand-or-complete" in func_block
|
|
939
936
|
assert "zle menu-complete" in func_block
|
|
940
937
|
assert "_COPA_SUGGEST_PENDING" in func_block
|
|
941
938
|
assert "bindkey '^I' _copa_suggest_expand_or_complete" in content
|
|
@@ -948,7 +945,7 @@ class TestTabAcceptZsh:
|
|
|
948
945
|
assert ".down-line-or-history" in func_block
|
|
949
946
|
# Down arrow with suggestion: hoists to completion menu
|
|
950
947
|
assert "_COPA_SUGGEST_PENDING" in func_block
|
|
951
|
-
assert "zle
|
|
948
|
+
assert "zle menu-complete" in func_block
|
|
952
949
|
|
|
953
950
|
def test_root_zsh_down_opens_menu_or_history(self):
|
|
954
951
|
content = self._read_zsh("copa.zsh")
|
|
@@ -957,7 +954,7 @@ class TestTabAcceptZsh:
|
|
|
957
954
|
assert "_copa_suggest_clear" in func_block
|
|
958
955
|
assert ".down-line-or-history" in func_block
|
|
959
956
|
assert "_COPA_SUGGEST_PENDING" in func_block
|
|
960
|
-
assert "zle
|
|
957
|
+
assert "zle menu-complete" in func_block
|
|
961
958
|
|
|
962
959
|
def test_packaged_zsh_clear_resets_pending(self):
|
|
963
960
|
content = self._read_zsh("copa/copa.zsh")
|
|
@@ -1014,33 +1011,31 @@ class TestTabAcceptZsh:
|
|
|
1014
1011
|
after_latch = func_block[latch_idx:]
|
|
1015
1012
|
assert "_copa_suggest_fetch" in after_latch
|
|
1016
1013
|
|
|
1017
|
-
def
|
|
1014
|
+
def test_packaged_zsh_suggestion_completer_exists(self):
|
|
1018
1015
|
content = self._read_zsh("copa/copa.zsh")
|
|
1019
|
-
start = content.index("
|
|
1020
|
-
func_block = content[start : start +
|
|
1016
|
+
start = content.index("_copa_suggestion_complete()")
|
|
1017
|
+
func_block = content[start : start + 500]
|
|
1021
1018
|
assert "_COPA_SUGGEST_PENDING" in func_block
|
|
1022
1019
|
assert "compadd -U -Q -V 'copa-suggestion'" in func_block
|
|
1020
|
+
assert "compstate[list]='list force'" in func_block
|
|
1023
1021
|
|
|
1024
|
-
def
|
|
1022
|
+
def test_root_zsh_suggestion_completer_exists(self):
|
|
1025
1023
|
content = self._read_zsh("copa.zsh")
|
|
1026
|
-
start = content.index("
|
|
1027
|
-
func_block = content[start : start +
|
|
1024
|
+
start = content.index("_copa_suggestion_complete()")
|
|
1025
|
+
func_block = content[start : start + 500]
|
|
1028
1026
|
assert "_COPA_SUGGEST_PENDING" in func_block
|
|
1029
1027
|
assert "compadd -U -Q -V 'copa-suggestion'" in func_block
|
|
1028
|
+
assert "compstate[list]='list force'" in func_block
|
|
1030
1029
|
|
|
1031
|
-
def
|
|
1032
|
-
"""
|
|
1030
|
+
def test_packaged_zsh_suggestion_completer_runs_first(self):
|
|
1031
|
+
"""_copa_suggestion_complete must be before _complete in completer chain."""
|
|
1033
1032
|
content = self._read_zsh("copa/copa.zsh")
|
|
1034
|
-
|
|
1035
|
-
func_block = content[start : start + 800]
|
|
1036
|
-
assert "compstate[list]='list force'" in func_block
|
|
1033
|
+
assert "_copa_suggestion_complete ${cur:-_complete} _copa_history_complete" in content
|
|
1037
1034
|
|
|
1038
|
-
def
|
|
1039
|
-
"""
|
|
1035
|
+
def test_root_zsh_suggestion_completer_runs_first(self):
|
|
1036
|
+
"""_copa_suggestion_complete must be before _complete in completer chain."""
|
|
1040
1037
|
content = self._read_zsh("copa.zsh")
|
|
1041
|
-
|
|
1042
|
-
func_block = content[start : start + 800]
|
|
1043
|
-
assert "compstate[list]='list force'" in func_block
|
|
1038
|
+
assert "_copa_suggestion_complete ${cur:-_complete} _copa_history_complete" in content
|
|
1044
1039
|
|
|
1045
1040
|
def test_packaged_zsh_has_suggestion_branding(self):
|
|
1046
1041
|
content = self._read_zsh("copa/copa.zsh")
|
|
@@ -1051,3 +1046,39 @@ class TestTabAcceptZsh:
|
|
|
1051
1046
|
content = self._read_zsh("copa.zsh")
|
|
1052
1047
|
assert "copa-suggestion" in content
|
|
1053
1048
|
assert "-X 'SUGGESTED'" in content
|
|
1049
|
+
|
|
1050
|
+
def test_packaged_zsh_menuselect_uses_accept_search(self):
|
|
1051
|
+
content = self._read_zsh("copa/copa.zsh")
|
|
1052
|
+
assert "bindkey -M menuselect '^I' accept-search" in content
|
|
1053
|
+
assert "bindkey -M menuselect '^M' accept-search" in content
|
|
1054
|
+
assert ".accept-line" not in content or "zle .accept-line" in content
|
|
1055
|
+
|
|
1056
|
+
def test_root_zsh_menuselect_uses_accept_search(self):
|
|
1057
|
+
content = self._read_zsh("copa.zsh")
|
|
1058
|
+
assert "bindkey -M menuselect '^I' accept-search" in content
|
|
1059
|
+
assert "bindkey -M menuselect '^M' accept-search" in content
|
|
1060
|
+
assert ".accept-line" not in content or "zle .accept-line" in content
|
|
1061
|
+
|
|
1062
|
+
def test_packaged_zsh_end_of_line_accepts_full(self):
|
|
1063
|
+
content = self._read_zsh("copa/copa.zsh")
|
|
1064
|
+
start = content.index("_copa_suggest_end_of_line()")
|
|
1065
|
+
func_block = content[start : start + 400]
|
|
1066
|
+
assert "_COPA_SUGGESTION" in func_block
|
|
1067
|
+
assert 'BUFFER="$_COPA_SUGGESTION"' in func_block
|
|
1068
|
+
assert "zle .end-of-line" in func_block
|
|
1069
|
+
|
|
1070
|
+
def test_root_zsh_end_of_line_accepts_full(self):
|
|
1071
|
+
content = self._read_zsh("copa.zsh")
|
|
1072
|
+
start = content.index("_copa_suggest_end_of_line()")
|
|
1073
|
+
func_block = content[start : start + 400]
|
|
1074
|
+
assert "_COPA_SUGGESTION" in func_block
|
|
1075
|
+
assert 'BUFFER="$_COPA_SUGGESTION"' in func_block
|
|
1076
|
+
assert "zle .end-of-line" in func_block
|
|
1077
|
+
|
|
1078
|
+
def test_packaged_zsh_end_of_line_registered(self):
|
|
1079
|
+
content = self._read_zsh("copa/copa.zsh")
|
|
1080
|
+
assert "zle -N end-of-line _copa_suggest_end_of_line" in content
|
|
1081
|
+
|
|
1082
|
+
def test_root_zsh_end_of_line_registered(self):
|
|
1083
|
+
content = self._read_zsh("copa.zsh")
|
|
1084
|
+
assert "zle -N end-of-line _copa_suggest_end_of_line" in content
|
|
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
|
|
File without changes
|
|
File without changes
|