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.
Files changed (36) hide show
  1. {copa_cli-0.3.0/copa_cli.egg-info → copa_cli-0.4.0}/PKG-INFO +59 -7
  2. {copa_cli-0.3.0 → copa_cli-0.4.0}/README.md +58 -6
  3. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/cli_internal.py +6 -6
  4. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/config.py +8 -1
  5. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/copa.zsh +31 -13
  6. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/fzf.py +5 -6
  7. {copa_cli-0.3.0 → copa_cli-0.4.0/copa_cli.egg-info}/PKG-INFO +59 -7
  8. {copa_cli-0.3.0 → copa_cli-0.4.0}/pyproject.toml +1 -1
  9. {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_modal.py +43 -5
  10. {copa_cli-0.3.0 → copa_cli-0.4.0}/LICENSE +0 -0
  11. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/__init__.py +0 -0
  12. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/__main__.py +0 -0
  13. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/cli.py +0 -0
  14. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/cli_common.py +0 -0
  15. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/cli_llm.py +0 -0
  16. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/cli_share.py +0 -0
  17. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/db.py +0 -0
  18. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/evolve.py +0 -0
  19. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/history.py +0 -0
  20. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/llm.py +0 -0
  21. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/mcp_server.py +0 -0
  22. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/models.py +0 -0
  23. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/scanner.py +0 -0
  24. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/scoring.py +0 -0
  25. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa/sharing.py +0 -0
  26. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa_cli.egg-info/SOURCES.txt +0 -0
  27. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa_cli.egg-info/dependency_links.txt +0 -0
  28. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa_cli.egg-info/entry_points.txt +0 -0
  29. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa_cli.egg-info/requires.txt +0 -0
  30. {copa_cli-0.3.0 → copa_cli-0.4.0}/copa_cli.egg-info/top_level.txt +0 -0
  31. {copa_cli-0.3.0 → copa_cli-0.4.0}/setup.cfg +0 -0
  32. {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_cli_and_sharing.py +0 -0
  33. {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_db.py +0 -0
  34. {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_fzf.py +0 -0
  35. {copa_cli-0.3.0 → copa_cli-0.4.0}/tests/test_models.py +0 -0
  36. {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.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 fallback completer so that any command gets completion candidates from your Copa database (see [Tab Completion](#tab-completion) below).
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+G** | Set group | Assign or change the group for the highlighted command |
147
- | **Ctrl+D** | Describe | Generate/edit a description for the highlighted command |
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 the `[keys]` section.
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 fallback completer in zsh's completion system.
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, zsh runs its normal completers first. If Copa's fallback also fires, it queries the Copa database for commands matching what you've typed so far and suggests the next word(s):
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 fallback completer so that any command gets completion candidates from your Copa database (see [Tab Completion](#tab-completion) below).
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+G** | Set group | Assign or change the group for the highlighted command |
117
- | **Ctrl+D** | Describe | Generate/edit a description for the highlighted command |
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 the `[keys]` section.
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 fallback completer in zsh's completion system.
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, zsh runs its normal completers first. If Copa's fallback also fires, it queries the Copa database for commands matching what you've typed so far and suggests the next word(s):
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 branding
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
- # Registers as a fallback completer so that any command (e.g. adb <TAB>)
155
- # gets completion candidates from Copa's command history.
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
- # Only provide Copa completions when primary completers found nothing
158
- (( compstate[nmatches] > 0 )) && return
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} )) && compadd -V 'copa-history' -o nosort -- "${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 = min(max(len(c.command) for c in commands), 60)
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, padded for column alignment
52
- cmd_text = cmd.command
53
- if len(cmd_text) > 60:
54
- cmd_text = cmd_text[:57] + "..."
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.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 fallback completer so that any command gets completion candidates from your Copa database (see [Tab Completion](#tab-completion) below).
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+G** | Set group | Assign or change the group for the highlighted command |
147
- | **Ctrl+D** | Describe | Generate/edit a description for the highlighted command |
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 the `[keys]` section.
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 fallback completer in zsh's completion system.
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, zsh runs its normal completers first. If Copa's fallback also fires, it queries the Copa database for commands matching what you've typed so far and suggests the next word(s):
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 |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "copa-cli"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Command Palette — smart command tracking, ranking, and sharing for your shell"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- assert lines[0] == "(none)"
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 lines[0] == "(none)"
41
- assert "alpha" in lines
42
- assert "beta" in lines
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
- assert result.output.strip() == "(none)"
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