2026-03-18-edit-config-design.md (5675B)
1 # Edit Config from TUI 2 3 ## Summary 4 5 Add an `e` key to the TUI dashboard that opens `.esync.toml` in `$EDITOR`. On save, the config is reloaded and the watcher/syncer are rebuilt with the new values. If no config file exists, a template is created; if the user exits without saving, nothing is persisted. 6 7 Also renames the project-level config from `esync.toml` to `.esync.toml` (dotfile). 8 9 ## Config File Rename 10 11 - `FindConfigFile()` in `internal/config/config.go` searches `./.esync.toml` instead of `./esync.toml` 12 - System-level paths (`~/.config/esync/config.toml`, `/etc/esync/config.toml`) remain unchanged 13 - `cmd/init.go` default output path changes from `./esync.toml` to `./.esync.toml` 14 - `esync.toml.example` renamed to `.esync.toml.example` 15 - `README.md` references updated from `esync.toml` to `.esync.toml` 16 - The `e` key always targets `./.esync.toml` in cwd 17 18 ## Key Flow 19 20 ### 1. Keypress 21 22 Dashboard handles `e` keypress in `updateNormal()` and sends `EditConfigMsg{}` up to `AppModel`. Only active in dashboard view (not logs). Already gated by `updateNormal` vs `updateFiltering` dispatch, so typing `e` during filter input is safe. 23 24 ### 2. AppModel receives EditConfigMsg 25 26 - Target path: `./.esync.toml` (cwd) 27 - **File exists:** record SHA-256 checksum, open in editor via `tea.ExecProcess` 28 - **File does not exist:** write template to a temp file (with `.toml` suffix for syntax highlighting, e.g. `os.CreateTemp("", "esync-*.toml")`), record its checksum, open temp file in editor 29 - **Editor resolution:** check `$VISUAL`, then `$EDITOR`, fall back to `vi` 30 31 ### 3. Editor exits (editorConfigFinishedMsg) 32 33 - **New file flow (was temp):** compare checksums. If unchanged, delete temp file, done. If changed, copy temp contents to `./.esync.toml`, delete temp file. 34 - **Existing file flow:** compare checksums. If unchanged, done. 35 - **Config changed:** parse with `config.Load()`. 36 - **Parse fails:** push error to TUI status line (e.g., "config error: missing sync.remote"), keep old config running. 37 - **Parse succeeds:** send new `*config.Config` on `configReloadCh` channel. 38 39 Note: `tea.ExecProcess` blocks the TUI program, so the user cannot press `e` again while the editor is open. This makes the capacity-1 channel safe without needing non-blocking sends. 40 41 ### 4. cmd/sync.go handles reload 42 43 Listens on `app.ConfigReloadChan()`: 44 45 1. Stop existing watcher (blocks until `<-w.done`, ensuring no in-flight handler) 46 2. Wait for any in-flight sync to complete before proceeding 47 3. Rebuild syncer with new config 48 4. Create new watcher with new config values (local path, debounce, ignore patterns, includes) 49 5. Create new sync handler closure capturing the new syncer 50 6. Push status event: "config reloaded" 51 52 ### 5. Flag-only mode (--local/--remote without config file) 53 54 When esync was started with `--local`/`--remote` flags and no config file, pressing `e` still opens `./.esync.toml`. If the file doesn't exist, the template is shown. After save, the reloaded config replaces the flag-derived config entirely (CLI flags are not re-applied on top). This lets users transition from quick flag-based usage to a persistent config file. 55 56 ## Template Content 57 58 A new `EditTemplateTOML()` function in `internal/config/config.go`, separate from the existing `DefaultTOML()` used by `esync init`. The edit template is minimal with most options commented out, while `DefaultTOML()` remains unchanged for `esync init`'s string-replacement logic. 59 60 ```toml 61 # esync configuration 62 # Docs: https://github.com/LouLouLibs/esync 63 64 [sync] 65 local = "." 66 remote = "user@host:/path/to/dest" 67 # interval = 1 # seconds between syncs 68 69 # [sync.ssh] 70 # key = "~/.ssh/id_ed25519" 71 # port = 22 72 73 [settings] 74 # watcher_debounce = 500 # ms 75 # initial_sync = false 76 # include = ["src/", "cmd/"] 77 # ignore = [".git", "*.tmp"] 78 79 # [settings.rsync] 80 # archive = true 81 # compress = true 82 # delete = false 83 # copy_links = false 84 # extra_args = ["--exclude=.DS_Store"] 85 86 # [settings.log] 87 # file = "esync.log" 88 # format = "text" 89 ``` 90 91 ## Help Bar 92 93 Updated dashboard help line: 94 95 ``` 96 q quit p pause r resync ↑↓ navigate enter expand v view e config l logs / filter 97 ``` 98 99 `e config` inserted between `v view` and `l logs`, using existing `helpKey()`/`helpDesc()` styling. 100 101 ## New Types and Channels 102 103 | Item | Location | Purpose | 104 |------|----------|---------| 105 | `EditConfigMsg` | `internal/tui/app.go` | Dashboard → AppModel signal | 106 | `editorConfigFinishedMsg` | `internal/tui/app.go` | Editor exit result (distinct from existing `editorFinishedMsg`) | 107 | `configReloadCh` | `AppModel` field | `chan *config.Config`, capacity 1 | 108 | `ConfigReloadChan()` | `AppModel` method | Exposes channel to `cmd/sync.go` | 109 110 ## Error Handling 111 112 - Bad TOML or missing required fields: status line error, old config retained 113 - Editor not set: check `$VISUAL` → `$EDITOR` → `vi` 114 - Editor returns non-zero exit: treat as "no change", discard 115 - Watcher detects `.esync.toml` write: harmless (rsync transfers the small file). Not added to default ignore since users may intentionally sync config files. 116 117 ## Files Modified 118 119 - `internal/config/config.go` — rename `esync.toml` → `.esync.toml` in `FindConfigFile()`, add `EditTemplateTOML()` 120 - `internal/tui/app.go` — new message types, `configReloadCh`, editor launch/return handling 121 - `internal/tui/dashboard.go` — `e` key binding, help line update 122 - `cmd/sync.go` — listen on `configReloadCh`, tear down and rebuild watcher/syncer 123 - `cmd/init.go` — update default output path to `./.esync.toml`, update `defaultIgnorePatterns` 124 - `esync.toml.example` — rename to `.esync.toml.example` 125 - `README.md` — update references from `esync.toml` to `.esync.toml`