|
| 1 | +# Implementation Considerations: Use `$EDITOR` / `$VISUAL` Instead of Hard-Coded Editor List |
| 2 | + |
| 3 | +**Reference:** [GitHub Issue #117](https://github.com/Firstp1ck/Pacsea/issues/117) — Use `$EDITOR`/`$VISUAL` instead of a hard-coded list. |
| 4 | + |
| 5 | +**Scope:** This document describes implementation considerations only. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 1. Summary of the Feature Request |
| 10 | + |
| 11 | +Many TUI tools (e.g. `git`, `yazi`, `gitui`) use environment variables to determine the user’s preferred text editor. The proposal is to pick the editor in this order: |
| 12 | + |
| 13 | +1. **`$VISUAL`** — full-screen/visual editor (preferred for interactive use) |
| 14 | +2. **`$EDITOR`** — line or visual editor |
| 15 | +3. **Current fallback list** — nvim → vim → hx/helix → emacsclient → emacs → nano (unchanged as last resort) |
| 16 | + |
| 17 | +This keeps Pacsea consistent with common Unix conventions and respects the user’s existing editor configuration. |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | +## 2. Current Behaviour (Codebase Summary) |
| 22 | + |
| 23 | +### 2.1 Where Config Files Are Opened in an Editor |
| 24 | + |
| 25 | +Config file editing (settings, theme, keybinds) is triggered from three places. In each case, a **single long shell command** is built and executed via `crate::install::spawn_shell_commands_in_terminal(&[editor_cmd])`: |
| 26 | + |
| 27 | +| Location | Function / context | Platform | |
| 28 | +|----------|--------------------|----------| |
| 29 | +| `src/events/search/normal_mode.rs` | `handle_config_menu_numeric_selection` (keyboard 1/2/3) | Non-Windows only (Windows uses `open_file`) | |
| 30 | +| `src/events/global.rs` | `handle_config_menu_selection` (menu index 0/1/2) | Non-Windows only | |
| 31 | +| `src/events/mouse/menus.rs` | `handle_config_menu_click` (mouse click on config menu row) | Non-Windows only | |
| 32 | + |
| 33 | +On Windows, the same actions use `crate::util::open_file(&target)`, which uses `Invoke-Item` / `cmd start` (default OS handler). The considerations below focus on the **non-Windows** path where the hard-coded editor chain is used. |
| 34 | + |
| 35 | +### 2.2 Current Editor Command Shape |
| 36 | + |
| 37 | +The editor command is a single shell expression: a chain of `(condition && editor 'path')` joined by `||`, ending with a fallback message and `read -rn1 -s _`. Example (conceptually): |
| 38 | + |
| 39 | +- Try: `(command -v nvim … || pacman -Qi neovim …) && nvim 'path'` |
| 40 | +- Else: same for vim, hx, helix, emacsclient, emacs, nano |
| 41 | +- Else: `echo 'No terminal editor found (nvim/vim/...).'; echo 'File: path'; read -rn1 -s _ || true` |
| 42 | + |
| 43 | +Each candidate is checked with `command -v <bin>` and/or `sudo pacman -Qi <pkg>`. The **same literal string** (with different `path_str` interpolation) appears in all three call sites; there is no shared helper that returns the editor command string. |
| 44 | + |
| 45 | +### 2.3 How the Command Is Run |
| 46 | + |
| 47 | +- `spawn_shell_commands_in_terminal` writes the command into a **temporary script** and runs it with `bash -lc <script_path>` (or via a preferred terminal emulator that runs the same). |
| 48 | +- The child process is created with Rust’s `std::process::Command`, which **inherits the environment** by default. So when the script runs, it sees the same `VISUAL`/`EDITOR` (and `PATH`) as the user’s session, **provided** the terminal that runs the script was started with that environment (e.g. from a login shell or the same desktop session). No Rust-side env stripping was found. |
| 49 | + |
| 50 | +### 2.4 Optional Deps “Editor” Category |
| 51 | + |
| 52 | +- **`src/events/mouse/menu_options.rs`** defines `build_editor_rows`, which builds the optional-deps modal rows for the **“Editor”** category. |
| 53 | +- It uses a **fixed list** of candidates: `(nvim, neovim)`, `(vim, vim)`, `(hx, helix)`, `(helix, helix)`, `(emacsclient, emacs)`, `(emacs, emacs)`, `(nano, nano)`. |
| 54 | +- Logic: if any candidate is “installed” (PATH or pacman), show that one; otherwise show all unique packages. This is **only for the UI** (suggesting an editor to install). It does **not** drive which editor is used when opening config files; that is entirely the shell chain above. |
| 55 | + |
| 56 | +### 2.5 Other Editor-Related Code |
| 57 | + |
| 58 | +- **`src/util/mod.rs` — `open_file(path)`:** Used on Windows for config files; on Unix it uses `xdg-open` / `open`. It does **not** use `EDITOR`/`VISUAL` or the terminal-editor chain. Out of scope for “open config in terminal editor” but worth a note if we ever want desktop-open to respect editor prefs. |
| 59 | +- **Tests:** `src/events/mod.rs` has optional-deps tests that expect an “Editor: nvim” row and that `nvim` is marked installed when a fake `nvim` is on `PATH`. No tests were found that assert the exact shell string used for opening config files. |
| 60 | + |
| 61 | +--- |
| 62 | + |
| 63 | +## 3. Design Considerations |
| 64 | + |
| 65 | +### 3.1 Order of Preference: VISUAL vs EDITOR |
| 66 | + |
| 67 | +- **Convention:** `VISUAL` is typically the full-screen editor, `EDITOR` the line editor. Many tools (e.g. `git`) use `VISUAL` when available, then `EDITOR`. The issue explicitly asks for that order: **VISUAL → EDITOR → fallbacks**. |
| 68 | +- **Implication:** The shell snippet (or any Rust-side logic) must try `$VISUAL` first, then `$EDITOR`, then the existing nvim/vim/… chain. |
| 69 | + |
| 70 | +### 3.2 Where to Implement the Logic |
| 71 | + |
| 72 | +**Option A — In the shell command (recommended):** |
| 73 | + |
| 74 | +- Prepend to the existing chain: e.g. “if `$VISUAL` is set and executable, run it with the path; else if `$EDITOR` is set and executable, run it; else current chain.” |
| 75 | +- **Pros:** No Rust env handling, no new dependencies; behaviour is consistent with the environment the script runs in; single source of “editor selection” in the script. |
| 76 | +- **Cons:** Shell string becomes slightly more complex; must be careful with quoting (path, and possibly `$VISUAL`/`$EDITOR` if they contain arguments). |
| 77 | + |
| 78 | +**Option B — In Rust:** |
| 79 | + |
| 80 | +- Read `std::env::var("VISUAL")` and `std::env::var("EDITOR")` at runtime, validate (e.g. first word is an executable), then build a shell command that runs that executable with the path. |
| 81 | +- **Pros:** Full control and testability in Rust; can normalize and log. |
| 82 | +- **Cons:** Must define “executable” (PATH at process start? at spawn time?); env in the Rust process might differ from the terminal’s (e.g. if Pacsea is launched from a different env); need to safely pass possibly multi-word `VISUAL`/`EDITOR` into the shell. |
| 83 | + |
| 84 | +**Recommendation:** Prefer **Option A** (shell-only) for consistency with how the rest of the editor chain is implemented and to avoid env differences between Pacsea and the terminal. If we later add a Rust helper that **generates** the shell string (see “DRY” below), that helper can still embed a shell fragment that uses `$VISUAL` and `$EDITOR` first. |
| 85 | + |
| 86 | +### 3.3 Handling `VISUAL` / `EDITOR` Content |
| 87 | + |
| 88 | +- Values may be: |
| 89 | + - A single binary name: `nvim` |
| 90 | + - A path: `/usr/bin/nvim` |
| 91 | + - A command with options: `nvim -f`, `emacsclient -t`, `code --wait` |
| 92 | +- The script must **pass the config file path** to the editor. Convention is to append the path as an argument. So the shell must run something like `$VISUAL '$path'` or `$EDITOR '$path'` in a way that: |
| 93 | + - Preserves path with spaces/special chars (single-quote the path). |
| 94 | + - Does not break if `VISUAL`/`EDITOR` contain options (e.g. run with `eval` or a wrapper that appends the path). Many tools use `eval` or a small wrapper; care is needed with quoting. |
| 95 | +- **Security:** Avoid `eval` on unvalidated user input if possible; if we use `eval`, the only user-controlled part should be the path (and we control path). `VISUAL`/`EDITOR` are already user-controlled in the same way as on other tools. |
| 96 | + |
| 97 | +### 3.4 “Executable” Check for VISUAL/EDITOR |
| 98 | + |
| 99 | +- For the **current** fallbacks we check `command -v <bin>` and/or `pacman -Qi <pkg>`. For `$VISUAL`/`$EDITOR` we only have a string; there may be no package name. |
| 100 | +- Possible approaches in shell: |
| 101 | + - Use the first word of `$VISUAL`/`$EDITOR` and check `command -v <first_word>`; if present, run the full value with the path appended. |
| 102 | + - Or: try running `$VISUAL '$path'` (and then `$EDITOR '$path'`) and fall back to the next option on failure (e.g. `(...) || (...)` chain). That matches “try and see” behaviour of many programs. |
| 103 | +- Document in the script or in comments that we expect `VISUAL`/`EDITOR` to be a runnable command that accepts a file path. |
| 104 | + |
| 105 | +### 3.5 Fallback Message and i18n |
| 106 | + |
| 107 | +- Current fallback message is a fixed English string: `'No terminal editor found (nvim/vim/emacsclient/emacs/hx/helix/nano).'`. If we add `$VISUAL`/`$EDITOR` to the chain, we might: |
| 108 | + - Keep the message as-is and optionally add “or set VISUAL/EDITOR” in the message or in docs. |
| 109 | + - Or add an i18n key (e.g. under `app.toasts` or a dedicated key) and have the Rust side pass the translated message into the script (script would receive it as a single argument or env var). That would require a small Rust change (one string) and possibly escaping for shell. |
| 110 | + |
| 111 | +### 3.6 DRY: Single Place for the Editor Command |
| 112 | + |
| 113 | +- The **same** long editor command string is duplicated in three files: `normal_mode.rs`, `global.rs`, `menus.rs`. Any change (including adding VISUAL/EDITOR) should be done in **one** place to avoid drift. |
| 114 | +- **Suggestion:** Introduce a small helper (e.g. in `crate::util` or `crate::install`) such as `fn editor_open_config_command(path: &Path) -> String` that returns the full shell command string. All three call sites then call this and pass the result to `spawn_shell_commands_in_terminal`. The helper can take an optional “fallback message” or “locale” if we add i18n later. |
| 115 | +- **Location:** `src/util/mod.rs` already has `open_file`; a sibling `editor_open_config_command` (or a dedicated small module under `util` or `install`) keeps “how we open config in an editor” in one place. Alternatively, under `install` next to `spawn_shell_commands_in_terminal` if we consider this “install/run” behaviour. |
| 116 | + |
| 117 | +### 3.7 Optional Deps Modal (“Editor” Rows) |
| 118 | + |
| 119 | +- The optional deps “Editor” list is for **suggesting** an editor to install, not for choosing the running editor. It can stay as-is (fixed list) so the modal continues to suggest common terminal editors. |
| 120 | +- **Optional enhancement:** If we want the modal to reflect the user’s choice, we could add a row like “Editor: $VISUAL” or “Editor: $EDITOR” when set, and/or show “Using: $VISUAL” in the UI. This is optional and not required by issue #117. |
| 121 | + |
| 122 | +### 3.8 Windows and `open_file` |
| 123 | + |
| 124 | +- On Windows, config open uses `open_file`, which uses the OS default handler. Issue #117 does not mention Windows; behaviour can remain unchanged unless we explicitly extend “editor” to mean “honour EDITOR/VISUAL on Windows” (e.g. via WSL or a separate setting). Left as future work. |
| 125 | + |
| 126 | +### 3.9 Dry-Run and Tests |
| 127 | + |
| 128 | +- **Dry-run:** Config editing spawns a real terminal/editor; there is no `--dry-run` branch that skips spawning. If we add a dry-run mode for “open config,” we’d only log or substitute the command. No change required for the VISUAL/EDITOR feature itself. |
| 129 | +- **Tests:** Current tests that spawn the editor are no-ops unless `PACSEA_TEST_OUT` is set. To test “editor selection” we could: |
| 130 | + - Unit-test a **Rust** helper that builds the command string (e.g. assert it contains `$VISUAL` and `$EDITOR` in the right order when we add that). |
| 131 | + - Integration test with a fake terminal and env (e.g. set `VISUAL=echo`, run the script, check log or output). This would require the script to be deterministic and the test to set env before calling the code that builds the command. |
| 132 | + |
| 133 | +### 3.10 Backward Compatibility and Documentation |
| 134 | + |
| 135 | +- **Behaviour:** If neither `VISUAL` nor `EDITOR` is set, behaviour should match current: use the nvim → … → nano chain. So existing users are unchanged. |
| 136 | +- **Docs:** Consider a short note in the repo (e.g. in a “Config” or “Environment” section) that Pacsea uses `VISUAL`, then `EDITOR`, then a built-in list when opening config files. No need to change wiki/README unless explicitly requested. |
| 137 | + |
| 138 | +--- |
| 139 | + |
| 140 | +## 4. Files and Call Sites to Touch (Checklist) |
| 141 | + |
| 142 | +When implementing, the following should be kept in mind: |
| 143 | + |
| 144 | +| Item | Location | Note | |
| 145 | +|------|----------|------| |
| 146 | +| Editor command string (add VISUAL/EDITOR + DRY) | New or existing helper used by all three below | Single source of truth for the shell command | |
| 147 | +| Config menu (keyboard 1/2/3) | `src/events/search/normal_mode.rs` — `handle_config_menu_numeric_selection` | Replace inline `editor_cmd` with call to helper | |
| 148 | +| Config menu (global menu index) | `src/events/global.rs` — `handle_config_menu_selection` | Same | |
| 149 | +| Config menu (mouse click) | `src/events/mouse/menus.rs` — `handle_config_menu_click` | Same | |
| 150 | +| Fallback message / i18n (optional) | Same helper or call sites | If we add a key for “no editor found” | |
| 151 | +| Unit tests for command builder | e.g. next to the helper or in `util`/`install` tests | Assert order: VISUAL → EDITOR → fallbacks | |
| 152 | +| Optional deps “Editor” (optional) | `src/events/mouse/menu_options.rs` — `build_editor_rows` | Only if we want to show VISUAL/EDITOR in the modal | |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## 5. Example Shell Snippet (Reference Only) |
| 157 | + |
| 158 | +The following is a **sketch** of how the start of the editor chain could look in shell. It is for discussion only; actual implementation must match project style and quoting rules. |
| 159 | + |
| 160 | +```sh |
| 161 | +# Prefer VISUAL, then EDITOR, then existing chain. |
| 162 | +( [ -n "${VISUAL}" ] && command -v "${VISUAL%% *}" >/dev/null 2>&1 && eval "${VISUAL}" '"'"'path_here'"'"' ) || \ |
| 163 | +( [ -n "${EDITOR}" ] && command -v "${EDITOR%% *}" >/dev/null 2>&1 && eval "${EDITOR}" '"'"'path_here'"'"' ) || \ |
| 164 | +( (command -v nvim ...) && nvim 'path_here' ) || ... |
| 165 | +``` |
| 166 | + |
| 167 | +Notes: |
| 168 | + |
| 169 | +- `"${VISUAL%% *}"` takes the first word (binary); `command -v` checks it. We then run the full `VISUAL`/`EDITOR` with the path appended. |
| 170 | +- Quoting of `path_here` must be done so that the path is one argument and spaces/special chars are safe. The snippet uses single-quoted path; Rust would inject the path via a variable or replace `path_here` with a properly quoted value. |
| 171 | +- `eval` is one option to support `VISUAL="nvim -f"`; alternatives are a small wrapper or documenting that we only support a single binary (simpler but less flexible). |
| 172 | + |
| 173 | +--- |
| 174 | + |
| 175 | +## 6. Summary of Recommendations |
| 176 | + |
| 177 | +1. **Order:** Use `$VISUAL` first, then `$EDITOR`, then the existing nvim → … → nano fallback list. |
| 178 | +2. **Implement in shell:** Add VISUAL/EDITOR at the beginning of the existing shell chain; avoid relying on Rust env if possible so the terminal’s environment is the source of truth. |
| 179 | +3. **DRY:** Introduce one helper (e.g. `editor_open_config_command(path)`) and use it from `normal_mode.rs`, `global.rs`, and `menus.rs`. |
| 180 | +4. **Quoting and safety:** Ensure the config file path is passed as a single, safely quoted argument; handle `VISUAL`/`EDITOR` containing options (e.g. via `eval` or documented convention). |
| 181 | +5. **Optional deps:** Leave “Editor” rows as-is unless we explicitly want to show VISUAL/EDITOR in the modal. |
| 182 | +6. **Tests:** Add unit tests for the command string (order of VISUAL/EDITOR/fallbacks); consider integration test with fake env if useful. |
| 183 | +7. **Docs:** Optionally document in the repo that Pacsea respects VISUAL and EDITOR when opening config files. |
| 184 | + |
| 185 | +This document does not prescribe exact code changes; it is intended as a reference for whoever implements the feature. |
0 commit comments