myrcp 0.9.1__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 (73) hide show
  1. myrcp-0.9.1/.gitignore +9 -0
  2. myrcp-0.9.1/ARCHITECTURE.md +531 -0
  3. myrcp-0.9.1/CMakeLists.txt +156 -0
  4. myrcp-0.9.1/LICENSE +21 -0
  5. myrcp-0.9.1/PKG-INFO +288 -0
  6. myrcp-0.9.1/README.md +266 -0
  7. myrcp-0.9.1/cmake/myrcp.pc.in +9 -0
  8. myrcp-0.9.1/cmake/myrcpConfig.cmake.in +6 -0
  9. myrcp-0.9.1/core/include/rcp/app.hpp +61 -0
  10. myrcp-0.9.1/core/include/rcp/grid.hpp +124 -0
  11. myrcp-0.9.1/core/include/rcp/keys.hpp +53 -0
  12. myrcp-0.9.1/core/include/rcp/layout.hpp +91 -0
  13. myrcp-0.9.1/core/include/rcp/msgpack.hpp +104 -0
  14. myrcp-0.9.1/core/include/rcp/utf8.hpp +22 -0
  15. myrcp-0.9.1/core/src/app.cpp +218 -0
  16. myrcp-0.9.1/core/src/console.cpp +400 -0
  17. myrcp-0.9.1/core/src/console.hpp +87 -0
  18. myrcp-0.9.1/core/src/editor_link.hpp +135 -0
  19. myrcp-0.9.1/core/src/editor_links.cpp +260 -0
  20. myrcp-0.9.1/core/src/editor_links.hpp +85 -0
  21. myrcp-0.9.1/core/src/grid.cpp +274 -0
  22. myrcp-0.9.1/core/src/history.hpp +58 -0
  23. myrcp-0.9.1/core/src/host.cpp +108 -0
  24. myrcp-0.9.1/core/src/host.hpp +59 -0
  25. myrcp-0.9.1/core/src/hub.cpp +1091 -0
  26. myrcp-0.9.1/core/src/hub.hpp +341 -0
  27. myrcp-0.9.1/core/src/hub_prefs.cpp +284 -0
  28. myrcp-0.9.1/core/src/json.cpp +280 -0
  29. myrcp-0.9.1/core/src/json.hpp +17 -0
  30. myrcp-0.9.1/core/src/keys.cpp +251 -0
  31. myrcp-0.9.1/core/src/launcher.cpp +44 -0
  32. myrcp-0.9.1/core/src/layout.cpp +920 -0
  33. myrcp-0.9.1/core/src/msgpack.cpp +336 -0
  34. myrcp-0.9.1/core/src/nvim.cpp +200 -0
  35. myrcp-0.9.1/core/src/outline.cpp +473 -0
  36. myrcp-0.9.1/core/src/outline.hpp +26 -0
  37. myrcp-0.9.1/core/src/prefs.cpp +372 -0
  38. myrcp-0.9.1/core/src/prefs.hpp +56 -0
  39. myrcp-0.9.1/core/src/process.cpp +211 -0
  40. myrcp-0.9.1/core/src/process.hpp +45 -0
  41. myrcp-0.9.1/core/src/rpc.cpp +107 -0
  42. myrcp-0.9.1/core/src/rpc.hpp +51 -0
  43. myrcp-0.9.1/core/src/search.cpp +627 -0
  44. myrcp-0.9.1/core/src/search.hpp +45 -0
  45. myrcp-0.9.1/core/src/service.hpp +63 -0
  46. myrcp-0.9.1/core/src/term.cpp +276 -0
  47. myrcp-0.9.1/core/src/term.hpp +74 -0
  48. myrcp-0.9.1/core/src/terminal.cpp +51 -0
  49. myrcp-0.9.1/core/src/terminal.hpp +40 -0
  50. myrcp-0.9.1/core/src/utf8.cpp +90 -0
  51. myrcp-0.9.1/core/src/vim.cpp +391 -0
  52. myrcp-0.9.1/core/src/workbench.cpp +1725 -0
  53. myrcp-0.9.1/core/src/workbench.hpp +203 -0
  54. myrcp-0.9.1/core/tests/service_tests.cpp +257 -0
  55. myrcp-0.9.1/core/tests/term_tests.cpp +88 -0
  56. myrcp-0.9.1/core/tests/tests.cpp +640 -0
  57. myrcp-0.9.1/gtk/app_gtk.cpp +69 -0
  58. myrcp-0.9.1/gtk/gobj.hpp +60 -0
  59. myrcp-0.9.1/gtk/render.cpp +1141 -0
  60. myrcp-0.9.1/gtk/render.hpp +89 -0
  61. myrcp-0.9.1/gtk/workbench.cpp +1548 -0
  62. myrcp-0.9.1/gtk/workbench.hpp +180 -0
  63. myrcp-0.9.1/pyproject.toml +70 -0
  64. myrcp-0.9.1/python/myrcp/__init__.py +8 -0
  65. myrcp-0.9.1/python/myrcp/_msgpack.py +237 -0
  66. myrcp-0.9.1/python/myrcp/host.py +658 -0
  67. myrcp-0.9.1/python/myrcp/launch.py +72 -0
  68. myrcp-0.9.1/python/myrcp/plugin.py +283 -0
  69. myrcp-0.9.1/python/myrcp/selection.py +81 -0
  70. myrcp-0.9.1/python/myrcp/ui.py +566 -0
  71. myrcp-0.9.1/python/pyproject.toml +11 -0
  72. myrcp-0.9.1/tools/libvterm.sh +14 -0
  73. myrcp-0.9.1/tools/wheel_smoke.py +39 -0
myrcp-0.9.1/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ build/
2
+ build-*/
3
+ .venv/
4
+ __pycache__/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ dist/
9
+ wheelhouse/
@@ -0,0 +1,531 @@
1
+ # myrcp architecture
2
+
3
+ A terminal-first rich client platform: one workbench (views, commands, keybindings,
4
+ jobs, a uniform look), tools as plugins, their views rendered in a terminal. myrcp is a
5
+ framework: applications are built on it (libmyrcp, or a product directory the `myrcp`
6
+ launcher starts) and bring their own plugins.
7
+
8
+ ## Principles
9
+
10
+ 1. **Views describe, hosts render.** A plugin view returns a *semantic* UI tree (the UI IR):
11
+ panels, lists, fields, buttons, progress bars. It never draws cells. A host lays the
12
+ tree out its own way, as SWT used native widgets per platform.
13
+ 2. **Plugins run out of process.** A plugin host process per language/venv speaks
14
+ msgpack-RPC to the core. A crashing plugin, a slow import or the GIL never freezes the
15
+ workbench, and `Ctrl+R` restarts the host while the workbench keeps its state.
16
+ 3. **Lazy activation.** What a plugin contributes (views, commands, keys) is a static
17
+ `myrcp.toml`. The host reads manifests without importing plugin code, and imports a
18
+ plugin only when one of its views opens or one of its commands runs.
19
+ 4. **Integrate, don't reimplement.** Editors are Vim/Neovim; tmux stays the multiplexer.
20
+ myrcp runs inside tmux and can open things in tmux windows and splits.
21
+ 5. **Everything stays usable without myrcp.** Plugins adapt existing tools, which keep
22
+ their CLIs; an application's plugins live with the application, not in myrcp.
23
+ 6. **What every workbench needs is built in.** Problems, Properties, Preferences,
24
+ Consoles, Search, Tasks and Outline are the hub's own (C++, no plugin host needed),
25
+ in every application.
26
+
27
+ ## Processes
28
+
29
+ ```
30
+ ┌──────────────────────────── myrcp (C++20) ─────────────────────────────┐
31
+ │ Workbench: views · command registry + palette · keybindings · jobs · │
32
+ │ status · editor handoff │
33
+ │ UI IR renderer (layout → cell grid) · grid diff → ANSI · key decoder │
34
+ └──────────────▲─────────────────────────────────────────────────────────┘
35
+ │ msgpack-RPC over pipes (stdin/stdout)
36
+ ┌──────────────┴────────────────┐
37
+ │ python -m myrcp.host │
38
+ │ manifests → contributions │
39
+ │ lazy import, View instances │
40
+ └───────────────────────────────┘
41
+ ```
42
+
43
+ Source layout:
44
+
45
+ | Path | What |
46
+ |---|---|
47
+ | `core/include/rcp/msgpack.hpp` | `mp::Value`, encoder, streaming decoder (also what Neovim `--embed` speaks) |
48
+ | `core/include/rcp/grid.hpp` | `Grid` of cells with semantic `Style`s, `Theme`, `diff()` → ANSI |
49
+ | `core/include/rcp/keys.hpp` | `KeyDecoder`: bytes → key names (`ctrl+p`, `alt+1`, `pagedown`, `paste`) |
50
+ | `core/include/rcp/layout.hpp` | UI IR → grid: layout, widget state, focus order |
51
+ | `core/include/rcp/app.hpp` | the public API: `AppInfo`, `rcp::run()`, `rcp::run_gtk()`, `load_product()` |
52
+ | `core/src/app.cpp` | `rcp::run`: options, `--list`, the terminal workbench; `launcher.cpp` the `myrcp` program |
53
+ | `core/src/workbench.cpp` | the event loop, chrome, palette, key routing, host protocol |
54
+ | `core/src/hub.hpp` | `Hub`, the workbench model; `service.hpp` its built-in features with views (`search.cpp`, `outline.cpp`), `console.hpp` the consoles |
55
+ | `core/src/editor_link.hpp` | links to Neovim (`nvim.cpp`, msgpack-RPC) and Vim (`vim.cpp`, JSON channels; `json.cpp`) |
56
+ | `core/src/editor_links.hpp` | `EditorLinks`: a front-end's linked editors (start, poll, the live one, selection, markers) |
57
+ | `gtk/` | `libmyrcp-gtk` (`-DMYRCP_GTK=ON`): the workbench as a GTK 4 application; `render.cpp` the IR as GTK widgets, `workbench.cpp` the chrome |
58
+ | `python/myrcp/` | the SDK: `ui` builders, `Plugin`, `View`, `Context`, `Job`, the host |
59
+ | `examples/demo/` | the demo application (`-DMYRCP_DEMO=ON`): a `main.cpp` on libmyrcp and its plugin (files, jobs, a table, a gallery of every widget and service) |
60
+
61
+ `rcp_core` (msgpack, JSON, grid, keys, layout) does no I/O. `rcp_hub`
62
+ (`core/src/hub.hpp`) is the workbench model: plugin hosts and their delegation,
63
+ contributions, open views and their trees, jobs, plugin commands and events, and the
64
+ built-in services. The front-end (`core/src/workbench.cpp`, the IR into a cell grid)
65
+ owns presentation only (widget state, focus, drawing) and hears the hub through
66
+ `Hub::Listener`. A second front-end, the GTK one, is built on the same hub (below, "The
67
+ GTK workbench"). These are static libraries inside the shared ones, which export only
68
+ `rcp/app.hpp` (hidden visibility for the rest).
69
+
70
+ ## Applications
71
+
72
+ An application is an `rcp::AppInfo`: an id (its state, `$XDG_STATE_HOME/<id>/`: the
73
+ perspectives as left, the host log), a name (the title bar, `--help`, `--version`), a
74
+ version and its plugin directories (searched after `$MYRCP_PLUGIN_PATH`). It starts the
75
+ workbench one of two ways:
76
+
77
+ - **A program on libmyrcp**, as `examples/demo/main.cpp` is: `find_package(myrcp)` and
78
+ `myrcp::myrcp` (or `pkg-config myrcp`), then `return rcp::run(argc, argv, app);`
79
+ (`rcp::run_gtk` with `myrcp::myrcp-gtk`).
80
+ - **A product directory** for the launcher, for an application in Python that wants no
81
+ C++ build: `myrcp --product DIR` (or `$MYRCP_PRODUCT`) reads `DIR/product.toml`:
82
+
83
+ ```toml
84
+ [product]
85
+ id = "songs"
86
+ name = "Songs"
87
+ version = "1.2.0"
88
+ plugins = ["plugins"] # relative to DIR; default: DIR itself
89
+ ```
90
+
91
+ The SDK is found next to the library that runs (`<prefix>/share/myrcp/python` beside
92
+ `<prefix>/lib/libmyrcp.so`, by `dladdr`), else in the source tree of a build tree, and
93
+ `$MYRCP_SDK` wins over both. Each plugin's manifest may name the Python it runs under
94
+ (below, "One host per interpreter"), so an application's plugins run in its own venv
95
+ while the workbench stays the same program.
96
+
97
+ An install (`cmake --install`, `./build.sh install`) has `bin/myrcp` (and `myrcp-gtk`),
98
+ `lib/libmyrcp.so` (and `libmyrcp-gtk.so`), `include/rcp/app.hpp`,
99
+ `lib/cmake/myrcp/` (the CMake package), `lib/pkgconfig/myrcp.pc` and the SDK in
100
+ `share/myrcp/python`.
101
+
102
+ ## UI IR
103
+
104
+ A node is a msgpack map with `t` (type) plus properties. Styles are semantic names
105
+ (`normal dim bold title accent error warning ok`), so every tool looks the same and each
106
+ host maps them to its own colours (terminal theme → 16 ANSI colours by default).
107
+
108
+ | `t` | Properties | Interaction |
109
+ |---|---|---|
110
+ | `text` | `text` (string, or `[[text, style], ...]` spans), `style`, `wrap` (spans wrap word by word, keeping their styles) | |
111
+ | `vbox`, `hbox` | `children`, `gap` | |
112
+ | `panel` | `title`, `child` | border highlights when focus is inside |
113
+ | `list` | `id`, `items` (`{key, text, detail, style}`), `empty`, `selected` | `select`, `activate` (value: item key) |
114
+ | `button` | `id`, `label` | `press` |
115
+ | `field` | `id`, `value`, `placeholder` | `submit` (value: text) |
116
+ | `progress` | `value` (0..1), `label` | |
117
+ | `table` | `id`, `columns` (`{title, width, grow, align, clickable, sortable}`), `rows` (`{key, cells, style, depth, expandable, expanded}`), `offset`, `total`, `cursor` (`[index, seq]`, applied when `seq` changes), `header`, `empty`, `sortable`, `sorted` (`[column, descending]`), `follow` (`[index, nonce]`), `tree` (a column index: the rows are a tree drawn in that column, indented by `depth`, with arrows) | `select`, `activate`, `window`, `cell` (clicks on `clickable` columns, Shift/Ctrl+clicks: `{key, column, shift, ctrl}`), `sort` (`[column, descending, key at the cursor]`; column -1: unsorted), `expand`/`collapse` (a `tree` table: →/←, arrow clicks) |
118
+ | `tree` | `id`, `rows` (`{key, text, detail, depth, expandable, expanded, check: all/some/none, style}`, flattened by the plugin), windowed like `table` | `select`, `activate`, `expand`, `collapse`, `toggle` |
119
+ | `checkbox` | `id`, `label`, `checked` | `toggle` (value: the new state) |
120
+ | `log` | `id` (optional: focusable, scrollable), `lines` | |
121
+ | `modal` | `id`, `title`, `child` (the base, dimmed), `dialog`, `dialog_width`, `dialog_height` | `dismiss` (Escape) |
122
+
123
+ Every node takes layout hints: `grow` (flex weight), `width`, `height`; focusable ones
124
+ take `autofocus` (the initial focus of the view). A view that shows only, say, a mode
125
+ button while it loads has that focus by default until the `autofocus` widget appears; a
126
+ focus chosen with Tab or a click stays.
127
+
128
+ **Windowed tables.** A table sends only rows `offset .. offset+len(rows)` of `total`.
129
+ When the cursor scrolls past them the workbench sends a `window` event `[first, count]`
130
+ and the SDK answers with those rows, so a plan of 100 000 items costs what a screenful
131
+ does (`ui.lazy_rows(count, make_row)` builds only the rows sent).
132
+
133
+ **Sortable tables.** `ui.table(..., sortable=True)`: a click on a column's title (or
134
+ Alt+S, the next column; Alt+R, the other way) sends a `sort` event, and the host keeps
135
+ how each table is sorted, as it keeps windows. The SDK sorts before it cuts the window,
136
+ so the front-ends never need all the rows: by the cells' text in natural order (numbers
137
+ by value, sizes with their units), stably, empty cells last. The event carries the key
138
+ at the cursor; the SDK answers with `follow`, that row's new index, which the front-end
139
+ applies once, so the cursor stays on its row. A plugin's own `cursor` (an index in its
140
+ order) is translated into the sorted one; front-ends apply it by its `seq`, so a sort
141
+ alone does not move it. A plugin with more rows than it wants to build sorts them itself
142
+ (`on_sort(column, descending)`), and its rows are shown as given. The workbench
143
+ remembers the sort it asked for until the tree catches up, so quick keys add up.
144
+
145
+ **Mouse.** The workbench reports presses and the wheel (SGR mode; `MYRCP_MOUSE=0` turns
146
+ it off; Shift+drag still selects text). Every render records where each widget was
147
+ drawn, and a click maps to the same semantic events as keys: tabs switch, buttons press,
148
+ checkboxes toggle, rows select (a click on the cursor row activates), tree arrows
149
+ expand/collapse and boxes toggle, the wheel moves the cursor or scrolls a log. Plugins
150
+ never see coordinates.
151
+
152
+ **Context menus.** A right click (or Shift+F10, at the focused row) opens a menu where
153
+ it was: on a row, the cursor goes there first and the menu lists the view's commands
154
+ (its manifest's, without their "View: " prefix, with their keys), Properties, sorting
155
+ for a sortable table, then the tab's items (move to another region, maximize, close); on
156
+ a tab or a terminal view whose program has no use for the mouse, the tab's items. The
157
+ commands run as from the palette, so they act on that row.
158
+
159
+ **Modals.** While a tree has a `modal`, only its dialog takes focus and the view's own
160
+ key bindings are off (workbench keys such as Ctrl+P still work).
161
+
162
+ **Button presses carry the form**: the press event's value is `{form: {field id: text}}`
163
+ for every field in the view, so a dialog reads what was typed without a round trip per key.
164
+ Defaults: in a `vbox`, lists (and containers holding one) grow; in an `hbox`, text and
165
+ buttons take their natural width and everything else shares the rest.
166
+
167
+ Transient widget state lives in the host, not the plugin: list cursor and scroll, field
168
+ text and caret, focus. The plugin hears only semantic events. Two props are applied only
169
+ when their value changes, so a plugin can steer without owning the state:
170
+ `field.value` (e.g. clearing a field after submit) and `list.selected` (move the cursor
171
+ to a key, e.g. the directory you came from).
172
+
173
+ ## Protocol
174
+
175
+ msgpack-RPC framing, as Neovim uses it: `[0, id, method, [params]]` request,
176
+ `[1, id, error, result]` response, `[2, method, [params]]` notification. `params` is
177
+ always one map.
178
+
179
+ Core → host (and back: the host also sends requests, e.g. `editor.open`):
180
+
181
+ | Message | Params | Result |
182
+ |---|---|---|
183
+ | request `initialize` | `protocol` | `plugins: [{id, name, views, commands, perspectives}]`, `perspectives` (the user's), `delegates`, `errors` |
184
+ | request `view.open` | `view` | `tree` |
185
+ | notify `view.event` | `view, widget, kind, value` | (host answers with `view.update`) |
186
+ | notify `view.close` | `view` | |
187
+ | notify `command.run` | `command, view` (active view), `widget, key, index` (the focused row) | |
188
+ | notify `job.cancel` | `job` | |
189
+ | notify `preferences.changed` | `values: {id: value}` (all of them before a host's views open, then what changed) | |
190
+ | notify `selection.changed` | `view, title, items` (the workbench selection; sent to every host) | |
191
+ | notify `shutdown` | | |
192
+
193
+ Host → core: `view.update {view, tree, title}`, `view.show {view}` (open/switch to,
194
+ e.g. `tool.job#photos`), `view.close {view}`, `status {text, style}`,
195
+ request `editor.open {path, line}` → `{waited}` (true: the editor ran in the foreground
196
+ and has exited), `compare.open {left, right, title}` (notify, or a request → `{waited}`),
197
+ `console.run {argv, cwd, title, name}`, `terminal.open {argv, cwd, title}`,
198
+ `job.begin {job, title}`, `job.progress {job, value, label}`, `job.end {job, ok, text}`.
199
+
200
+ **Consoles** (`core/src/console.hpp`): programs run for their output, owned by the hub
201
+ so the workbench shows them and any plugin starts one (`rcp.run_in_console(argv)`) —
202
+ the output of an Ansible run, an ffmpeg encode, a build. Each is a view `console#<n>`
203
+ (not saved in the arrangement): the command in a field (edit it, Enter runs it), the
204
+ output as rows (stdout and stderr as they come, colour codes dropped, `\r` progress
205
+ lines kept as their last state, 20 000 lines at most), the cursor following the end
206
+ while it is there. A line naming an existing `path:line` (compilers, test runners,
207
+ Python's `File "x", line n`) is a link: Enter opens it, and it is the selection.
208
+ `Ctrl+C` stops it (SIGINT, then SIGTERM, then SIGKILL, to its process group), `r` runs
209
+ it again; a run with the same `name` reuses its console. "Console: Run a command…"
210
+ opens an empty one.
211
+
212
+ **View instances.** A view id with `#arg` is an instance of that view for `arg` (one tab
213
+ per job, say; the demo's `demo.note#<colour>`). The plugin's View gets `self.arg`; key bindings scoped to the view
214
+ apply to all its instances.
215
+
216
+ **One host per interpreter.** A manifest may name the Python it needs:
217
+ `python = ["$TOOL_PYTHON", "../../.venv/bin/python"]` (first existing wins, relative to
218
+ the plugin directory). The primary host does not serve such a plugin; it
219
+ lists it under `delegates` in its `initialize` answer, and the workbench starts a host
220
+ with that interpreter and `MYRCP_HOST_PLUGINS=<ids>`. Each tool keeps its own venv.
221
+
222
+ The host coalesces re-renders (a job reporting progress 1000 times a second causes at
223
+ most one render per batch) and skips `view.update` when the tree did not change.
224
+
225
+ ## Plugins
226
+
227
+ ```toml
228
+ # examples/demo/plugins/demo/myrcp.toml
229
+ [plugin]
230
+ id = "demo"
231
+ name = "Demo"
232
+ module = "myrcp_demo.plugin" # imported on first use, must define `plugin`
233
+
234
+ [[views]]
235
+ id = "demo.files"
236
+ title = "Files"
237
+ startup = true
238
+ # region = "bottom" # main (default), side or bottom: where it opens
239
+
240
+ [[commands]]
241
+ id = "demo.files.up"
242
+ title = "Files: Parent directory"
243
+ key = "backspace"
244
+ view = "demo.files" # binding and palette entry only while this view is active
245
+
246
+ [[perspectives]] # a named arrangement: Perspective: Files in the palette
247
+ id = "files"
248
+ title = "Files"
249
+ main = ["demo.files"] # view ids per region, in tab order
250
+ bottom = ["demo.jobs", "problems"]
251
+ bottom_size = 25 # percent of the height (side_size: of the width)
252
+
253
+ [[preferences]] # a setting: demo.hidden, in the Preferences view
254
+ id = "hidden"
255
+ title = "Show dotfiles"
256
+ type = "bool" # string, bool, int, choice (choices = [...]), paths
257
+ description = "Whether Files lists names starting with a dot when it opens."
258
+ ```
259
+
260
+ Discovery: every directory in `MYRCP_PLUGIN_PATH` whose subdirectories contain a
261
+ `myrcp.toml`, and installed packages registering the entry point group `myrcp.plugins`
262
+ (value: the package holding `myrcp.toml`).
263
+
264
+ ```python
265
+ from myrcp import Plugin, View, ui
266
+
267
+ plugin = Plugin()
268
+
269
+ @plugin.view("demo.files")
270
+ class Files(View):
271
+ def setup(self): self.cwd = Path.cwd()
272
+ def render(self):
273
+ return ui.panel(str(self.cwd), ui.listbox("entries", items, on_activate=self.open))
274
+ @plugin.command("demo.files.up")
275
+ def up(self): self.cwd = self.cwd.parent
276
+ ```
277
+
278
+ All plugin code (setup, render, handlers, commands, job `on_done`) runs on the host's
279
+ main thread. `rcp.run_job(title, fn, on_done=...)` runs `fn(job)` in a thread
280
+ (`job.progress()`, `job.check()` for cancellation); `on_done` applies the result back on
281
+ the main thread.
282
+
283
+ ## Regions
284
+
285
+ The workbench has three regions, after Eclipse's editor area and its view stacks: main
286
+ (top left), side (its right) and bottom (below both). Each is a stack of tabs. Which
287
+ region a view is in belongs to the hub (`ViewData::region`, from the manifest's `region`,
288
+ changed with `Hub::set_region`); which view each region shows
289
+ and which region has the focus belong to the front-end.
290
+
291
+ - The terminal shows the side from 140 columns and the bottom from 34 rows. Below that,
292
+ or while maximized (Alt+Z), a region folds into main: its views become main's tabs, so
293
+ a small terminal works exactly as before regions. Tabs keep the hub's numbering
294
+ (Alt+N), whichever region they are in; Alt+←/→ stays within the focused region.
295
+ - Main's tabs are in the top bar, the side's there too past the separator, the bottom's
296
+ in a bar of their own. The focused region's active tab is highlighted; an unfocused
297
+ view still shows its cursor row, inactive. Each visible view asks for its table
298
+ windows and is drawn at its own size (a terminal view is resized to its region).
299
+ - F6 / Shift+F6 cycle the focus; Ctrl+\ then an arrow moves it in that direction, also
300
+ out of a terminal view; a click focuses the region it lands in, the wheel scrolls one
301
+ without focusing it.
302
+ - Sizes are the hub's (`Hub::sizes`, percent of the width and the height): `Ctrl+\`
303
+ Shift+arrows or a drag of the separator or the bottom's tab bar (SGR button-event
304
+ mouse mode, 1002; saved on release).
305
+
306
+ ## Preferences
307
+
308
+ Settings with a declared type, after Eclipse's preference store, kept where a user
309
+ expects configuration: `$XDG_CONFIG_HOME/myrcp/preferences.toml`, a table per plugin
310
+ (`[workbench]`, `[search]`, `[demo]`, ...).
311
+
312
+ - Declared by the front-end (`declare_workbench_prefs`: the theme, the editor mode and
313
+ the mouse) and by plugins (`[[preferences]]`: `id`, `title`, `type`
314
+ string/bool/int/choice/paths, `default`, `choices`, `env`, `description`; the id
315
+ becomes `<plugin>.<id>`). The hub owns the values, so they are there before any host
316
+ is: the terminal's theme and mouse apply before the first draw.
317
+ - A value is its `env` variable's while that is set (the variables myrcp read before
318
+ preferences keep working, and win), else the file's if it fits the type, else the
319
+ default. The front-end hears changes (`Listener::prefs_changed`); every host gets all
320
+ values (`preferences.changed`) right after it registers, before its views open, then
321
+ each change; `Context.pref(id)` reads them and `View.on_preferences(changed)` hears
322
+ them, after which every view renders again.
323
+ - The file is read and written by a TOML subset of the hub's own (`prefs.cpp`: tables,
324
+ strings, bools, integers, arrays, comments), which rewrites only the lines of what it
325
+ sets, so hand edits and comments survive; setting the default removes the line. A
326
+ file with a syntax error is not written to until it is fixed (the view says where).
327
+ Ctrl+R (restart the hosts) and "Preferences: Edit the file" read it again.
328
+ - The Preferences view is the hub's own, in the IR like Problems: a row per setting
329
+ with where its value comes from, its description below, a dialog to change it (a
330
+ field, or the list of choices; Default; a note when an environment variable wins).
331
+ Its rows carry selection items, so Properties shows a setting too.
332
+
333
+ ## Perspectives
334
+
335
+ A named arrangement of views for a task, after Eclipse's: which views are open, in which
336
+ region and tab order, and the regions' sizes. The hub owns them.
337
+
338
+ - Defined by plugins (`[[perspectives]]` in `myrcp.toml`: `id`, `title`, `main`/`side`/
339
+ `bottom` view ids, `side_size`/`bottom_size`) and by the user
340
+ (`$XDG_CONFIG_HOME/myrcp/perspectives.toml`, read by the main host, the same tables;
341
+ the user's win on an id). "default" is the manifests' startup views.
342
+ - An `Arrangement` is a perspective as left: views with regions in tab order, sizes,
343
+ what each region shows and the focus (the front-end's part, asked for with
344
+ `Listener::front_state`), and the definition's views at the time (`known`: a view
345
+ added to the definition since, say by a new plugin, opens next time).
346
+ - Every change of the arrangement (opening, closing, moving a view; sizes) is saved at
347
+ once, msgpack in `$XDG_STATE_HOME/myrcp/workbench.state`, written to a temporary file
348
+ and renamed; the active perspective's entry is replaced, the others kept. Terminal
349
+ views are not part of it and stay through switches.
350
+ - Switching saves the current one, closes the plugin and hub views the next one lacks,
351
+ opens the rest where it puts them (`Hub::apply`) and tells the front-end what to show
352
+ (`Listener::arranged`). Views whose host is not up yet wait in `pending_` at their
353
+ index, so tabs keep the arrangement's order whichever host answers first; the same
354
+ path opens the saved arrangement at startup, the hub's own views at once.
355
+ - A plugin view closed by a switch is disposed in its host; switching back opens it
356
+ again (its `arg` included), not its scroll position or its widgets' state.
357
+
358
+ ## The selection service
359
+
360
+ What the cursor of the focused view is on is the workbench selection, after Eclipse's
361
+ selection service and NetBeans' Lookup, made of data rather than objects since plugins
362
+ live in separate processes.
363
+
364
+ - A row says what it is: `ui.row(key, cells, selection=Item("demo.colour", "blue",
365
+ path, line, props={...}))` (also `ui.item`, `ui.tree_row`) puts `sel: [items]` on the
366
+ row in the IR. An item has a `type` ("file", "problem", "search.match",
367
+ "outline.function", ...), a `label`, an optional `path`/`line` and ordered
368
+ `props`; a tree's own `sel` stands for the whole view.
369
+ - The front-end picks it (`view_selection` in `layout.cpp`): the cursor row of the
370
+ focused rows widget, else of the first one whose rows carry any, else the tree's
371
+ `sel`. A row without one makes an empty selection; a view without any (a terminal,
372
+ Properties itself) leaves the selection as it was, so focusing Properties keeps what
373
+ it shows. The terminal sets it when what the focused view offers changed (or the
374
+ focus moved), so a linked Neovim outside the workbench keeps the selection it made
375
+ until something here moves; a Neovim tab offers its cursor.
376
+ - The hub keeps it (`Hub::set_selection`) and tells every host (`selection.changed`):
377
+ `View.on_selection(selection)` runs in every view that defines it, and
378
+ `rcp.selection` is the last one heard (`selection.of_type("demo")`). The hub's services
379
+ hear it too: Outline follows the file and line it names, so a linked editor's cursor
380
+ moves the Outline's.
381
+ - **Properties** (`properties`, a hub-owned view at the side, Alt+Enter) shows it: where
382
+ it is from, the item's properties (a table of items when several), and the source
383
+ around `path:line` with the line marked; Enter opens a line in the editor.
384
+ - Producers: Problems (each problem at its place), Search and Tasks (each match at its
385
+ line), Outline (each symbol at its line), Consoles (`path:line` lines), a linked
386
+ editor's cursor, and the demo's Files, table and gallery.
387
+
388
+ ## Built-in services: Search, Tasks, Outline
389
+
390
+ Features every application wants are the hub's own, as `Service`s
391
+ (`core/src/service.hpp`): a service declares views, commands (with keys) and preferences
392
+ the way a manifest does, builds its views' trees as UI IR itself (so both front-ends show
393
+ them with no code of their own), runs programs whose output the hub's event loop reads,
394
+ and hears the selection and preference changes. `Hub::builtin_view` covers them and the
395
+ hub's other views, which need no plugin host and open from a saved arrangement at once.
396
+
397
+ - **Search** (`search`, at the bottom; `Ctrl+F`, "Search: Find in files"): text in the
398
+ files below the working directory, as a tree of files and their matching lines, by
399
+ ripgrep when it is installed, else `git grep` in a work tree, else `grep` (each leaves
400
+ out what version control ignores as well as it can; `core/src/search.cpp`). Regex and
401
+ match case are check boxes; `F5` searches again, `*` and `/` open and close every
402
+ file; Enter on a line opens it in the editor. At most 10 000 matches are kept.
403
+ - **Tasks** (`tasks`, at the bottom; "Tasks: Show"): the comments tagged with the
404
+ preference `search.tags` (default `FIXME, XXX, TODO, HACK`, the first ones the most
405
+ urgent), most urgent first; `r` scans again.
406
+ - **Outline** (`outline`, at the side; "Outline: Show"): the structure of the file the
407
+ selection is in, following its line (the symbol around it is the cursor's): Python by
408
+ its indentation outside strings and brackets, Markdown headings, TOML/INI sections,
409
+ YAML keys, and C/C++, JavaScript/TypeScript, Rust, Go and shell by the shapes of their
410
+ lines (`core/src/outline.cpp`). `f` stops it following; Enter goes to a symbol.
411
+
412
+ ## Problem markers
413
+
414
+ A plugin reports problems as named sets (`rcp.set_markers(name, [Marker(...)])`,
415
+ `markers.set` on the wire): a message, a severity, a place in a file (path, 1-based line)
416
+ and/or a view that shows it. A set replaces the plugin's previous one of that name; a
417
+ host that exits takes its sets along.
418
+
419
+ The hub lists them in a **Problems** view (F8, or View: Open Problems) whose tree the hub
420
+ builds itself, as UI IR, so the front-end shows it with no code of their own. Errors come
421
+ first; paths under the working directory are relative. Enter opens the place: the view
422
+ that shows it, and the file at its line (`Listener::open_location`: in a linked Neovim or Vim when
423
+ there is one, else as an editor request). An editor that understands diagnostics gets the
424
+ markers too (Neovim and Vim, above).
425
+
426
+ The demo reports some for trying it out: "Files: Find TODOs" (TODO/FIXME lines at their
427
+ place) and "Demo: Problems that name views" (markers whose place is a view, one of them
428
+ a view instance).
429
+
430
+ ## Terminal views
431
+
432
+ A terminal view is a native view: the hub lists it among the tabs (`Hub::open_native`), the
433
+ terminal front-end owns its process and screen. Keys go to the program, except Alt+1…9 and
434
+ Alt+←/→ (tabs); Ctrl+\ makes the next key the workbench's (Ctrl+\ Ctrl+P: commands,
435
+ Ctrl+\ Ctrl+W: close, Ctrl+\ ↑: the region above; Ctrl+\ twice sends one). Shells open
436
+ at the bottom, editors in the main area. Mouse presses go to programs that asked for
437
+ the mouse. Cells keep the program's colours (`Style::raw`); its default colours are the
438
+ theme's, so vim sits on blue in the classic theme. The tab closes when the program exits.
439
+
440
+ `editor.open` is a request the workbench may answer later (`Rpc::on_request_later`): an
441
+ editor in a tab answers `{waited: true}` when it exits, so `on_close` runs at the right time.
442
+
443
+ ## Themes
444
+
445
+ Plugins name semantic styles only; a theme maps them to colours. Cells belong to a
446
+ layer (window or dialog) and each layer has its own palette, so dialogs can look
447
+ different from the windows under them. Modals and the command palette are dialog
448
+ layer and cast a shadow.
449
+
450
+ | Theme | Look |
451
+ |---|---|
452
+ | `terminal` (default) | the terminal's 16 colours on its own background |
453
+ | `classic` | blue windows, grey dialogs with drop shadows, grey bars with red hotkeys, green buttons, focused frames in double lines; after the 1990s text-mode IDEs |
454
+
455
+ `MYRCP_THEME=classic`, or "Theme: …" in the command palette.
456
+
457
+ ## The GTK workbench
458
+
459
+ `libmyrcp-gtk` and `myrcp-gtk` (`cmake -DMYRCP_GTK=ON`, or `./build.sh gtk`; GTK 4.12
460
+ or later, VTE for GTK 4 optional; an application calls `rcp::run_gtk`) runs the same plugin hosts, views, commands, perspectives, preferences,
461
+ markers and editor links as the terminal workbench, through the same `Hub`. Plugins
462
+ need nothing new: their UI IR becomes GTK widgets. It translates the workbench's
463
+ concepts into GTK's own ways rather than imitating the terminal's look:
464
+
465
+ | Concept | In GTK |
466
+ |---|---|
467
+ | regions | three notebooks in panes: main and side above, the bottom below; empty ones hide. Tabs drag between them (the view's region changes), the panes' handles set the sizes kept with the perspective; maximize shows the focused region alone |
468
+ | `text` | a label; styles as Pango colours chosen for a light or dark theme; a line too long is cut with an ellipsis |
469
+ | `vbox` / `hbox`, `panel` | boxes (`grow` as expand; `width`/`height` in the UI font's characters and lines), a frame |
470
+ | `list`, `tree` | a `GtkListView`: the cursor is the selection, double click or Enter activates, tree rows have expander arrows and check buttons (`some`: inconsistent) |
471
+ | `table` | a `GtkColumnView`; a sortable one's column titles send `sort` (the plugin sorts; GTK shows the arrow). Windowed tables are a list model of `total` rows: a row not sent yet shows `…` and asks for its window |
472
+ | `field`, `button`, `checkbox`, `progress` | an entry (Enter submits), a button (the press carries the form), a check button, a progress bar |
473
+ | `log` | a monospace text view that stays at the end while it is there |
474
+ | `modal` | a modal dialog window over the view, whose base is insensitive meanwhile; closing it or Escape dismisses |
475
+ | palette, Go to Anything | a search window (Ctrl+P; Ctrl+O lists views, problems and the files from `rg --files`, else `git ls-files`, else `find`) |
476
+ | context menus | popover menus on a right click or Shift+F10/Menu, the commands' keys shown as accelerators |
477
+ | perspectives, views, plugins | the header bar: Back/Forward, a perspective menu, the palette button and the main menu |
478
+ | status, jobs | a status bar; the running jobs' progress and a cancel button at its right |
479
+ | terminal views | VTE tabs: shells (Ctrl+T), and files open in `$VISUAL`/`$EDITOR` there, a Vim or Neovim linked both ways as in the terminal's `tab` mode |
480
+
481
+ The tree is reconciled into the widgets as plugins send new ones (a node keeps its
482
+ widget while its type and id stay), so focus, scrolling, cursors and typed text survive
483
+ updates, and a dialog opening over a view keeps the view's widgets as its base. Keys
484
+ are bound as in the terminal workbench, except that navigation keys stay with the
485
+ widgets, plain keys are not commands while a field has the focus, and in a terminal
486
+ view the program has the keys but for Alt+1…9, Alt+←/→, Ctrl+PgUp/PgDn and
487
+ Ctrl+Shift+<key> (the workbench's Ctrl+<key>). The theme preference is the terminal's;
488
+ GTK follows the desktop's theme. Without VTE there are no terminal views, so files have
489
+ nowhere to open but a linked editor of your own.
490
+
491
+ ## Editors
492
+
493
+ | Level | How | Status |
494
+ |---|---|---|
495
+ | 0 | Hand off to `$VISUAL`/`$EDITOR` (`+line path`). `MYRCP_EDITOR_MODE`: `suspend` (default; the TUI steps aside like git does), `tmux-window`, `tmux-split` | done |
496
+ | 1 | Terminal views: a pty + libvterm (the emulator Neovim uses) as a native view (`core/src/term.cpp`). `MYRCP_EDITOR_MODE=tab` opens files in a tab of their own, answered to the plugin when the editor exits; Ctrl+T opens a shell; plugins run programs with `rcp.open_terminal(argv)` | done |
497
+ | 2 | Neovim in a terminal view, also listening on a socket of ours (`--listen`): problem markers become `vim.diagnostic` entries (signs, virtual text) for every buffer, and opening a problem jumps in the open Neovim instead of starting another editor. Recognised by the editor command's name (`nvim…`) | done |
498
+ | 3 | The link both ways, in every editor mode (`core/src/editor_link.hpp`, `NvimLink` in `nvim.cpp`): Neovim in a tab, in a tmux window or split we open (`--listen` on our socket; the pane id from `tmux … -P` brings it forward), or the user's own (the `workbench.nvim_server` preference). Files and problems open in a linked Neovim rather than a new editor. Lua we send on connect (`nvim_get_api_info` gives our channel) notifies us where the cursor rests (debounced, only when the line or file changes) and its own diagnostics (all namespaces but ours, only when they change): the first become the workbench selection (from a tab only while it has the focus), the second a marker set `editor:<socket>` in Problems, never pushed back to it | done |
499
+ | compare | `rcp.compare(left, right)` (`compare.open`, a request when the plugin waits): `EditorLink::diff` in the linked editor (Neovim: `tabedit`, `diffthis`, `vertical diffsplit`; Vim: the same as a channel `ex` command), else `workbench.diff` (`vimdiff`) run as the editor would be: a tab, tmux, or in the foreground | done |
500
+ | 2+ | `nvim --embed` with `ext_linegrid` (Neovim's grid drawn straight into ours, without a pty) | planned |
501
+ | 2' + 3 | Vim, the same both ways (`VimLink` in `core/src/vim.cpp`). Vim has no server we can reach without `+clientserver`, so we are the server: a unix socket of ours per Vim we start (`--cmd "let g:myrcp_socket=…" -S <script>`), and a shared one (`VimListener`, `$XDG_RUNTIME_DIR/myrcp-vim.sock` or `workbench.vim_socket`) that the user's own Vims connect to with the same script installed as a plugin (`myrcp --vim-script`; it retries every two seconds). JSON channel messages, a line each (`core/src/json.cpp`): we send `["call", "MyrcpMarkers"/"MyrcpGoto", args]`; it sends `[n, {kind: hello/cursor/diagnostics}]`. Markers are signs plus virtual text (`prop_add(…, {text, text_align: 'after'})`); its own diagnostics are the quickfix list (`QuickFixCmdPost`). `EditorLink` is what the workbench sees of either kind | done |
502
+
503
+ ## Roadmap
504
+
505
+ 1. **Core + protocol + SDK**: terminal host, msgpack-RPC, the IR above, the Python host,
506
+ editor handoff, a demo plugin. Done.
507
+ 2. **Widgets from a real port**: `table`, `checkbox`, `modal`, `log`, `tree`, mouse,
508
+ view instances, per-venv hosts, tables sorted by column. Done.
509
+ 3. **Workbench services**: problem markers, terminal views, regions, the selection
510
+ service with a Properties view, perspectives, preferences, consoles, the Neovim and
511
+ Vim links both ways (editor levels 1–3), Search, Tasks and Outline built in, and the
512
+ GTK front-end. Done.
513
+ 4. **A framework**: libmyrcp with a public API, the launcher and product directories,
514
+ the demo as an application on the library. Done. Next: `nvim --embed` (editor level
515
+ 2+), Nodes/explorer views (NetBeans), a stable protocol version for plugins written
516
+ outside this repository.
517
+
518
+ ## Ideas taken from Eclipse and NetBeans
519
+
520
+ - Declarative extensions with lazy activation (Eclipse `plugin.xml`, VS Code `contributes`).
521
+ - Commands separate from handlers and keybindings (Eclipse).
522
+ - Jobs with progress and cancel (Eclipse Jobs API).
523
+ - Selection service (Eclipse) and Lookup (NetBeans), as typed data items rather than
524
+ adapters; the Properties view (Eclipse); problem markers (Eclipse).
525
+ - Perspectives (Eclipse), as data the hub saves rather than layouts per window; the
526
+ preference store (Eclipse), as a TOML file the user may edit too.
527
+ - Planned: Nodes/explorer views (NetBeans).
528
+ - Products (Eclipse RCP): an application is a name and its plugins, on the same
529
+ workbench.
530
+ - Left out: OSGi class loading. Process isolation and a versioned protocol cover the
531
+ same need.