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.
Files changed (37) hide show
  1. {copa_cli-0.7.0 → copa_cli-0.8.0}/PKG-INFO +38 -3
  2. {copa_cli-0.7.0 → copa_cli-0.8.0}/README.md +37 -2
  3. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/copa.zsh +52 -27
  4. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/db.py +2 -1
  5. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/mcp_server.py +105 -0
  6. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/PKG-INFO +38 -3
  7. {copa_cli-0.7.0 → copa_cli-0.8.0}/pyproject.toml +1 -1
  8. {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_modal.py +54 -23
  9. {copa_cli-0.7.0 → copa_cli-0.8.0}/LICENSE +0 -0
  10. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/__init__.py +0 -0
  11. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/__main__.py +0 -0
  12. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/cli.py +0 -0
  13. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/cli_common.py +0 -0
  14. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/cli_internal.py +0 -0
  15. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/cli_llm.py +0 -0
  16. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/cli_share.py +0 -0
  17. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/config.py +0 -0
  18. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/evolve.py +0 -0
  19. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/fzf.py +0 -0
  20. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/history.py +0 -0
  21. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/llm.py +0 -0
  22. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/models.py +0 -0
  23. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/scanner.py +0 -0
  24. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/scoring.py +0 -0
  25. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa/sharing.py +0 -0
  26. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/SOURCES.txt +0 -0
  27. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/dependency_links.txt +0 -0
  28. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/entry_points.txt +0 -0
  29. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/requires.txt +0 -0
  30. {copa_cli-0.7.0 → copa_cli-0.8.0}/copa_cli.egg-info/top_level.txt +0 -0
  31. {copa_cli-0.7.0 → copa_cli-0.8.0}/setup.cfg +0 -0
  32. {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_cli_and_sharing.py +0 -0
  33. {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_db.py +0 -0
  34. {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_fzf.py +0 -0
  35. {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_models.py +0 -0
  36. {copa_cli-0.7.0 → copa_cli-0.8.0}/tests/test_polish.py +0 -0
  37. {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.7.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
+ ![Copa Setup](demos/01-setup.gif)
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
+ ![Ctrl+R Palette](demos/04-ctrl-r-palette.gif)
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
+ ![Groups and Scoping](demos/05-groups-and-scoping.gif)
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
+ ![Bulk Operations](demos/06-bulk-operations.gif)
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
+ ![Tab Menu Select](demos/03-tab-menu-select.gif)
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
+ ![Completion Modes](demos/10-completion-modes.gif)
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
+ | ![Tab Mode 1](demos/02a-suggestions-tab-mode1.gif) | ![Tab Mode 2](demos/02b-suggestions-tab-mode2.gif) |
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): first Tab highlights suggestion (cyan), second Tab accepts | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
289
- | **Down** | Highlight suggestion (enter confirming state) | History navigation |
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
+ ![Flags and Describe](demos/09-flags-and-describe.gif)
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
+ ![Sharing](demos/07-sharing.gif)
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
+ ![Composition](demos/08-composition.gif)
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
+ ![Workflow](demos/11-workflow.gif)
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
+ ![Copa Setup](demos/01-setup.gif)
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
+ ![Ctrl+R Palette](demos/04-ctrl-r-palette.gif)
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
+ ![Groups and Scoping](demos/05-groups-and-scoping.gif)
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
+ ![Bulk Operations](demos/06-bulk-operations.gif)
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
+ ![Tab Menu Select](demos/03-tab-menu-select.gif)
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
+ ![Completion Modes](demos/10-completion-modes.gif)
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
+ | ![Tab Mode 1](demos/02a-suggestions-tab-mode1.gif) | ![Tab Mode 2](demos/02b-suggestions-tab-mode2.gif) |
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): first Tab highlights suggestion (cyan), second Tab accepts | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
259
- | **Down** | Highlight suggestion (enter confirming state) | History navigation |
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
+ ![Flags and Describe](demos/09-flags-and-describe.gif)
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
+ ![Sharing](demos/07-sharing.gif)
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
+ ![Composition](demos/08-composition.gif)
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
+ ![Workflow](demos/11-workflow.gif)
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
- _copa_history_complete() {
294
- # Hoist pending inline suggestion as a completion candidate
295
- if [[ -n "$_COPA_SUGGEST_PENDING" ]]; then
296
- local pending="$_COPA_SUGGEST_PENDING"
297
- _COPA_SUGGEST_PENDING=""
298
- local cur_word="${words[CURRENT]}"
299
- local prefix_len=$(( ${#LBUFFER} - ${#cur_word} ))
300
- local insert_text="${pending:$prefix_len}"
301
- if [[ -n "$insert_text" ]]; then
302
- compadd -U -Q -V 'copa-suggestion' -X 'SUGGESTED' -o nosort -- "$insert_text"
303
- fi
304
- compstate[list]='list force'
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' .accept-line
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 ' ' .accept-line
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
- # Tab: accept suggestion or open completion menu.
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
- # Use .expand-or-complete (not menu-complete) so menu-select
429
- # highlights the first item in group-order (copa-suggestion),
430
- # rather than the first match from the first completer.
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 .expand-or-complete
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 .expand-or-complete
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 .expand-or-complete
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.7.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
+ ![Copa Setup](demos/01-setup.gif)
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
+ ![Ctrl+R Palette](demos/04-ctrl-r-palette.gif)
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
+ ![Groups and Scoping](demos/05-groups-and-scoping.gif)
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
+ ![Bulk Operations](demos/06-bulk-operations.gif)
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
+ ![Tab Menu Select](demos/03-tab-menu-select.gif)
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
+ ![Completion Modes](demos/10-completion-modes.gif)
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
+ | ![Tab Mode 1](demos/02a-suggestions-tab-mode1.gif) | ![Tab Mode 2](demos/02b-suggestions-tab-mode2.gif) |
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): first Tab highlights suggestion (cyan), second Tab accepts | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
289
- | **Down** | Highlight suggestion (enter confirming state) | History navigation |
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
+ ![Flags and Describe](demos/09-flags-and-describe.gif)
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
+ ![Sharing](demos/07-sharing.gif)
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
+ ![Composition](demos/08-composition.gif)
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
+ ![Workflow](demos/11-workflow.gif)
729
+
695
730
  ## CLI Reference
696
731
 
697
732
  | Command | Purpose |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "copa-cli"
7
- version = "0.7.0"
7
+ version = "0.8.0"
8
8
  description = "Command Palette — smart command tracking, ranking, and sharing for your shell"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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 + 800]
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 expand-or-complete so menu-select highlights by group-order
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 .expand-or-complete" in func_block
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 .expand-or-complete" in func_block
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 test_packaged_zsh_history_complete_hoists_suggestion(self):
1014
+ def test_packaged_zsh_suggestion_completer_exists(self):
1018
1015
  content = self._read_zsh("copa/copa.zsh")
1019
- start = content.index("_copa_history_complete()")
1020
- func_block = content[start : start + 800]
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 test_root_zsh_history_complete_hoists_suggestion(self):
1022
+ def test_root_zsh_suggestion_completer_exists(self):
1025
1023
  content = self._read_zsh("copa.zsh")
1026
- start = content.index("_copa_history_complete()")
1027
- func_block = content[start : start + 800]
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 test_packaged_zsh_forces_completion_list(self):
1032
- """compstate[list] forces the completion list to display."""
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
- start = content.index("_copa_history_complete()")
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 test_root_zsh_forces_completion_list(self):
1039
- """compstate[list] forces the completion list to display."""
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
- start = content.index("_copa_history_complete()")
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