tuiboard 0.5.0
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.
- package/.tuiboard/config.example.yaml +32 -0
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/bin/tuiboard.ts +28 -0
- package/package.json +62 -0
- package/src/app.tsx +129 -0
- package/src/cli/args.test.ts +40 -0
- package/src/cli/args.ts +41 -0
- package/src/config/loader.ts +169 -0
- package/src/input/handleKey.ts +733 -0
- package/src/io/watcher.ts +85 -0
- package/src/io/writer.ts +92 -0
- package/src/parser/markdown.ts +351 -0
- package/src/parser/serialize.ts +97 -0
- package/src/scripts/agents-check.ts +24 -0
- package/src/scripts/parse-check.ts +124 -0
- package/src/scripts/roundtrip-check.ts +79 -0
- package/src/store/agents.test.ts +181 -0
- package/src/store/agents.ts +435 -0
- package/src/store/index.test.ts +110 -0
- package/src/store/index.ts +972 -0
- package/src/store/parsers.ts +243 -0
- package/src/store/timeline.test.ts +279 -0
- package/src/store/timeline.ts +279 -0
- package/src/store/virtual-panel.ts +0 -0
- package/src/types.ts +116 -0
- package/src/ui/AgentRow.tsx +79 -0
- package/src/ui/AgentsBar.tsx +102 -0
- package/src/ui/BoardView.tsx +333 -0
- package/src/ui/Chrome.tsx +122 -0
- package/src/ui/Modal.tsx +613 -0
- package/src/ui/TaskRow.tsx +240 -0
- package/src/ui/TimelineView.tsx +643 -0
- package/src/ui/VirtualPanel.tsx +237 -0
- package/src/ui/board-scroll.test.ts +63 -0
- package/src/ui/board-scroll.ts +49 -0
- package/src/ui/glyphs.ts +129 -0
- package/src/views/AgentsOnly.tsx +103 -0
- package/src/views/BoardOnly.tsx +35 -0
- package/src/views/Dashboard.tsx +106 -0
- package/src/views/TimelineOnly.tsx +12 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# tuiboard configuration example — copy this file to `config.yaml` (same
|
|
2
|
+
# directory) and edit to point at your real boards.
|
|
3
|
+
#
|
|
4
|
+
# tuiboard walks up from the cwd looking for `.tuiboard/config.yaml`, so
|
|
5
|
+
# placing the file at the root of a vault makes every subdirectory of that
|
|
6
|
+
# vault auto-configured.
|
|
7
|
+
#
|
|
8
|
+
# Without a config file, tuiboard falls back to scanning the cwd for any
|
|
9
|
+
# .md file containing `- [ ]` tasks.
|
|
10
|
+
|
|
11
|
+
boards:
|
|
12
|
+
# Each entry is a markdown file in the Obsidian Kanban Plugin format
|
|
13
|
+
# (frontmatter `kanban-plugin: board`, `## Column` headings, `- [ ] task`
|
|
14
|
+
# rows). Paths can be absolute or relative to this config file.
|
|
15
|
+
- path: ./Work.md
|
|
16
|
+
name: Work
|
|
17
|
+
- path: ./Personal.md
|
|
18
|
+
name: Personal
|
|
19
|
+
|
|
20
|
+
# Assignees available in the assign modal (`a` shortcut). Plain list of
|
|
21
|
+
# strings — tuiboard prefixes each with `@` when displayed.
|
|
22
|
+
assignees:
|
|
23
|
+
- Alice
|
|
24
|
+
- Bob
|
|
25
|
+
|
|
26
|
+
# Name of the column that counts as "done" for stats and status colors.
|
|
27
|
+
# Defaults to "Done".
|
|
28
|
+
done_column: Done
|
|
29
|
+
|
|
30
|
+
# Name of the column used by the archive action (Shift-X). If the column
|
|
31
|
+
# doesn't exist in a board, tuiboard creates it on the fly.
|
|
32
|
+
archive_column: Archive
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nazzareno Giannelli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# tuiboard
|
|
2
|
+
|
|
3
|
+
A terminal dashboard that unifies **kanban**, a **Today/Tomorrow virtual
|
|
4
|
+
panel**, a **24-hour timeline**, and a **live agent view** for Claude Code
|
|
5
|
+
sessions — all on top of plain markdown task files.
|
|
6
|
+
|
|
7
|
+
Built with [OpenTUI](https://opentui.com) + SolidJS on Bun. Cross-platform
|
|
8
|
+
(Linux, macOS, Windows). No vendor lock-in: boards are CommonMark with
|
|
9
|
+
the Obsidian Tasks-plugin emoji vocabulary, so they open and edit fine in
|
|
10
|
+
any markdown editor.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
┌─tuiboard──[1 Work · 2 Personal]──────────open · done · cols───────────────┐
|
|
14
|
+
│ ┌Today/Tom──┐ ┌Board──────────────────────────┐ ┌─Timeline──┐ │
|
|
15
|
+
│ │● Today │ │ Inbox 3 In Progress 5 Done │ │ 07 ────── │ │
|
|
16
|
+
│ │ ⏰ Agenda │ │ ▶ Task 1 │ │ 08 ────── │ │
|
|
17
|
+
│ │ ⌚09:00…│ │ Task 2 │ │ 09 ⌚ deep │ │
|
|
18
|
+
│ │ 🔺 Prio │ │ Task 3 │ │ 10 ────── │ │
|
|
19
|
+
│ └───────────┘ └────────────────────────────────┘ │ 11 ────── │ │
|
|
20
|
+
│ ┌Agents (live)──────────────────────────────────┐│ 12 ────── │ │
|
|
21
|
+
│ │● tuiboard Shadow 💬 active 📂 ... ││ 13 ⌚ call │ │
|
|
22
|
+
│ │ pulse Laptop 💤 3m ago 📂 ... ││ ... │ │
|
|
23
|
+
│ └────────────────────────────────────────────────┘└────────────┘ │
|
|
24
|
+
│ hjkl move · Tab board · S-Tab zone · F1/F2/F3 toggle · z zoom · ? help │
|
|
25
|
+
└────────────────────────────────────────────────────────────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
Requires [Bun](https://bun.sh) ≥ 1.2 — tuiboard runs on the Bun runtime (it's
|
|
31
|
+
not a Node CLI). OpenTUI ships its own native renderer binaries; Bun picks the
|
|
32
|
+
right one for your platform automatically. Pick whichever install fits:
|
|
33
|
+
|
|
34
|
+
**Global, straight from GitHub** (no npm needed) — run `tuiboard` from anywhere:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun install -g github:NazzarenoGiannelli/tuiboard
|
|
38
|
+
tuiboard
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Global, from npm** (once published):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bun install -g tuiboard # or run once, no install: bunx tuiboard
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**From source** (for hacking on it):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/NazzarenoGiannelli/tuiboard.git
|
|
51
|
+
cd tuiboard
|
|
52
|
+
bun install
|
|
53
|
+
bun run dev # or: bun link → then `tuiboard` globally, live-linked
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configure
|
|
57
|
+
|
|
58
|
+
Copy `.tuiboard/config.example.yaml` to a config location and edit the
|
|
59
|
+
`boards:` list to point at your markdown files. tuiboard resolves the config
|
|
60
|
+
in this order (first hit wins):
|
|
61
|
+
|
|
62
|
+
1. **`$TUIBOARD_CONFIG`** — explicit path to a config file.
|
|
63
|
+
2. **Project-local** — `.tuiboard/config.yaml`, walking up from the cwd. Drop
|
|
64
|
+
a `.tuiboard/` folder at a project/vault root and it's used whenever you
|
|
65
|
+
launch from inside that tree.
|
|
66
|
+
3. **Global** — `~/.config/tuiboard/config.yaml` (or `~/.tuiboard/config.yaml`).
|
|
67
|
+
Use **absolute** board paths here and `tuiboard` shows your boards from
|
|
68
|
+
*any* directory — the usual setup for a single-vault user.
|
|
69
|
+
4. **Fallback** — scan the cwd for any `.md` file containing `- [ ]` tasks.
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
boards:
|
|
73
|
+
- path: ./Work.md
|
|
74
|
+
name: Work
|
|
75
|
+
- path: ./Personal.md
|
|
76
|
+
name: Personal
|
|
77
|
+
|
|
78
|
+
assignees: [Alice, Bob]
|
|
79
|
+
done_column: Done
|
|
80
|
+
archive_column: Archive
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Markdown board format
|
|
84
|
+
|
|
85
|
+
`tuiboard` reads and writes **plain CommonMark** with the Obsidian
|
|
86
|
+
Tasks-plugin emoji vocabulary. Any markdown editor renders these files
|
|
87
|
+
sensibly; the Obsidian Kanban plugin renders them as a kanban; we render
|
|
88
|
+
them as a TUI.
|
|
89
|
+
|
|
90
|
+
### Minimal example
|
|
91
|
+
|
|
92
|
+
```markdown
|
|
93
|
+
---
|
|
94
|
+
kanban-plugin: board
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Today
|
|
98
|
+
|
|
99
|
+
- [ ] Fix auth flow @nazza ⏳ 2026-05-27 ⌚ 09:00-10:30 #pr-followup
|
|
100
|
+
- [x] Review PR #412 ✅ 2026-05-26
|
|
101
|
+
|
|
102
|
+
## In Progress
|
|
103
|
+
|
|
104
|
+
- [ ] Migrate timeline to OpenTUI @nazza
|
|
105
|
+
|
|
106
|
+
## Done
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Metadata vocabulary
|
|
110
|
+
|
|
111
|
+
| Symbol | Meaning | Notes |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `## Heading` | Column name | One column per H2 heading |
|
|
114
|
+
| `- [ ]` / `- [x]` | Task (open / done) | Standard markdown task list |
|
|
115
|
+
| `@name` | Assignee | Configurable list in config.yaml |
|
|
116
|
+
| `#tag` | Tag | Any hashtag; passed through verbatim |
|
|
117
|
+
| `⏳ YYYY-MM-DD` | Scheduled date | Tasks-plugin convention |
|
|
118
|
+
| `📅 YYYY-MM-DD` | Due date | Tasks-plugin convention |
|
|
119
|
+
| `🛫 YYYY-MM-DD` | Start date | Tasks-plugin convention |
|
|
120
|
+
| `✅ YYYY-MM-DD` | Done date | Tasks-plugin convention |
|
|
121
|
+
| `⌚ HH:MM-HH:MM` | Time block | tuiboard-specific (Tasks plugin has no time-of-day) |
|
|
122
|
+
| `🔺 ⏫ 🔼 🔽 ⏬` | Priority | Tasks-plugin convention |
|
|
123
|
+
|
|
124
|
+
Anything else stays in the task text untouched on write-back. Roundtrip is
|
|
125
|
+
byte-for-byte preserving when a task hasn't been edited; structured fields
|
|
126
|
+
are rebuilt only after an in-app mutation.
|
|
127
|
+
|
|
128
|
+
## Layouts
|
|
129
|
+
|
|
130
|
+
Launch `tuiboard` with no flag for the default 4-zone dashboard.
|
|
131
|
+
|
|
132
|
+
| Flag | View | Use case |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| (none) | **Dashboard** — all 4 zones | Default; everything in one terminal |
|
|
135
|
+
| `--view=board` | Kanban + virtual panel only | Focus mode, or a single WezTerm pane |
|
|
136
|
+
| `--view=timeline` | Timeline fullscreen | Wall-mounted "what's now" |
|
|
137
|
+
| `--view=agents` | Agent view fullscreen | Cross-machine session monitor |
|
|
138
|
+
|
|
139
|
+
The dashboard auto-collapses optional zones on narrow terminals:
|
|
140
|
+
|
|
141
|
+
| Terminal width | Default zones visible |
|
|
142
|
+
|---|---|
|
|
143
|
+
| ≥ 150 cols | virtual + board + timeline + agents |
|
|
144
|
+
| 120–149 | virtual + board + agents |
|
|
145
|
+
| 100–119 | virtual + board |
|
|
146
|
+
| < 100 | board only |
|
|
147
|
+
|
|
148
|
+
`F1` / `F2` / `F3` toggles override the auto-collapse for the current
|
|
149
|
+
session (until the next terminal resize).
|
|
150
|
+
|
|
151
|
+
## Keyboard
|
|
152
|
+
|
|
153
|
+
### Navigation
|
|
154
|
+
|
|
155
|
+
| Key | Action |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `h j k l` / arrows | Move cursor inside the active zone |
|
|
158
|
+
| `Tab` | Cycle to next board |
|
|
159
|
+
| `1`..`9` | Jump to board N |
|
|
160
|
+
| `v` | Toggle Today/Tomorrow virtual panel focus |
|
|
161
|
+
| `Shift-Tab` | Cycle active zone (virtual → board → timeline → agents) |
|
|
162
|
+
| `F1` / `F2` / `F3` | Toggle visibility of Virtual / Timeline / Agents zones |
|
|
163
|
+
| `z` | Zoom active zone to full screen |
|
|
164
|
+
|
|
165
|
+
### Task actions (work in board, virtual, AND timeline zones)
|
|
166
|
+
|
|
167
|
+
| Key | Action |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `Enter` | Toggle done |
|
|
170
|
+
| `o` | Open detail view |
|
|
171
|
+
| `e` | Edit task text |
|
|
172
|
+
| `s` | Schedule date modal |
|
|
173
|
+
| `t` | Set scheduled = today |
|
|
174
|
+
| `m` | Set scheduled = tomorrow |
|
|
175
|
+
| `.` | Schedule **now** — time block at the next 15-min slot |
|
|
176
|
+
| `b` | Set time block modal |
|
|
177
|
+
| `p` | Cycle priority (none → 🔺 → ⏫ → 🔼 → 🔽 → ⏬ → none) |
|
|
178
|
+
| `a` | Set assignee |
|
|
179
|
+
| `d` | Delete task (with confirm) |
|
|
180
|
+
| `Shift-X` | Archive task → moves to Archive column |
|
|
181
|
+
|
|
182
|
+
### Multi-select
|
|
183
|
+
|
|
184
|
+
| Key | Action |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `Space` | Mark / unmark task — task actions then apply to ALL marked |
|
|
187
|
+
| `Esc` | Clear marks (when no modal is open) |
|
|
188
|
+
|
|
189
|
+
### Board-only / bulk / global
|
|
190
|
+
|
|
191
|
+
| Key | Action |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `n` | New task in current column (quick-add syntax) |
|
|
194
|
+
| `Shift-T` | Reset ALL overdue tasks (any board) to today |
|
|
195
|
+
| `Ctrl-Z` | Undo last mutation |
|
|
196
|
+
| `?` | Help modal with the full reference |
|
|
197
|
+
| `q` · `Ctrl-C` | Quit |
|
|
198
|
+
|
|
199
|
+
## Status
|
|
200
|
+
|
|
201
|
+
- **v0.5** — daily-driver ready. Kanban + virtual + timeline + agents
|
|
202
|
+
all functional, multi-select, undo, atomic file roundtrip, mouse click,
|
|
203
|
+
responsive layout. Tested on Windows with WezTerm; Linux/macOS should
|
|
204
|
+
work via the same OpenTUI binaries (untested).
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT — see [LICENSE](LICENSE).
|
package/bin/tuiboard.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Global entry point for `tuiboard` (after `bun install -g tuiboard`,
|
|
4
|
+
* `bunx tuiboard`, or `bun link`).
|
|
5
|
+
*
|
|
6
|
+
* OpenTUI's Solid JSX runtime must be registered via `bun --preload` BEFORE
|
|
7
|
+
* the module graph is parsed — otherwise app.tsx's JSX is transformed against
|
|
8
|
+
* the wrong runtime and bun throws `Export named 'Fragment' not found`.
|
|
9
|
+
* The `--preload` flag can't travel through a shebang cross-platform (Windows
|
|
10
|
+
* global bins are .cmd shims), so we re-exec bun with the flag here, forward
|
|
11
|
+
* any CLI args, and inherit stdio so the TUI keeps the real terminal.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const appPath = join(here, "..", "src", "app.tsx");
|
|
20
|
+
const preload = fileURLToPath(import.meta.resolve("@opentui/solid/preload"));
|
|
21
|
+
|
|
22
|
+
const result = spawnSync(
|
|
23
|
+
process.execPath, // the bun binary running this script
|
|
24
|
+
["--preload", preload, appPath, ...process.argv.slice(2)],
|
|
25
|
+
{ stdio: "inherit" },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
process.exit(result.status ?? 1);
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tuiboard",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Terminal dashboard for markdown task boards. Kanban + Today/Tomorrow + 24h timeline + Claude Code agent view, all in one TUI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Nazzareno Giannelli <nazzareno.giannelli@gmail.com>",
|
|
8
|
+
"homepage": "https://github.com/NazzarenoGiannelli/tuiboard#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/NazzarenoGiannelli/tuiboard.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/NazzarenoGiannelli/tuiboard/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"tui",
|
|
18
|
+
"kanban",
|
|
19
|
+
"terminal",
|
|
20
|
+
"task-board",
|
|
21
|
+
"markdown",
|
|
22
|
+
"obsidian",
|
|
23
|
+
"productivity",
|
|
24
|
+
"opentui",
|
|
25
|
+
"bun"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"bun": ">=1.2.0"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"tuiboard": "./bin/tuiboard.ts"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src/",
|
|
35
|
+
"bin/",
|
|
36
|
+
".tuiboard/config.example.yaml",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"dev": "bun --preload @opentui/solid/preload src/app.tsx",
|
|
42
|
+
"start": "bun --preload @opentui/solid/preload src/app.tsx",
|
|
43
|
+
"parse:check": "bun run src/scripts/parse-check.ts",
|
|
44
|
+
"roundtrip:check": "bun run src/scripts/roundtrip-check.ts",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "bun test",
|
|
47
|
+
"agents:check": "bun run src/scripts/agents-check.ts",
|
|
48
|
+
"prepublishOnly": "bun run typecheck && bun test"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@opentui/core": "^0.2.15",
|
|
52
|
+
"@opentui/solid": "^0.2.15",
|
|
53
|
+
"chokidar": "^4.0.3",
|
|
54
|
+
"js-yaml": "^4.1.0",
|
|
55
|
+
"solid-js": "^1.9.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/bun": "latest",
|
|
59
|
+
"@types/js-yaml": "^4.0.9",
|
|
60
|
+
"typescript": "^5.7.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/app.tsx
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tuiboard — bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* Loads config, builds the reactive store, parses argv, and dispatches
|
|
5
|
+
* to one of four root views:
|
|
6
|
+
* - undefined → Dashboard (all 4 zones)
|
|
7
|
+
* - "board" → BoardOnly (kanban + virtual fullscreen)
|
|
8
|
+
* - "timeline"→ TimelineOnly
|
|
9
|
+
* - "agents" → AgentsOnly
|
|
10
|
+
*
|
|
11
|
+
* The store and keyboard handler are shared across all four; only the
|
|
12
|
+
* root layout component changes.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createMemo } from "solid-js";
|
|
16
|
+
import { render, useKeyboard } from "@opentui/solid";
|
|
17
|
+
|
|
18
|
+
import { parseArgs, type ViewKind } from "~/cli/args";
|
|
19
|
+
import { loadConfig } from "~/config/loader";
|
|
20
|
+
import { handleKey } from "~/input/handleKey";
|
|
21
|
+
import {
|
|
22
|
+
createTuiStore,
|
|
23
|
+
type TuiStore,
|
|
24
|
+
} from "~/store/index";
|
|
25
|
+
import { buildVirtualItems } from "~/store/virtual-panel";
|
|
26
|
+
import { T } from "~/ui/glyphs";
|
|
27
|
+
import { TopBar, BottomBar } from "~/ui/Chrome";
|
|
28
|
+
import { ModalLayer } from "~/ui/Modal";
|
|
29
|
+
import { BoardOnly } from "~/views/BoardOnly";
|
|
30
|
+
import { Dashboard } from "~/views/Dashboard";
|
|
31
|
+
import { TimelineOnly } from "~/views/TimelineOnly";
|
|
32
|
+
import { AgentsOnly } from "~/views/AgentsOnly";
|
|
33
|
+
|
|
34
|
+
// ─── Bootstrap ──────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const config = loadConfig();
|
|
37
|
+
if (config.boards.length === 0) {
|
|
38
|
+
console.error(
|
|
39
|
+
"No boards found. Create `.tuiboard/config.yaml` with a `boards:` list," +
|
|
40
|
+
" or run from a directory containing markdown files with `- [ ]` tasks.",
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const store = createTuiStore({ config });
|
|
46
|
+
|
|
47
|
+
if (store.state.boards.length === 0) {
|
|
48
|
+
console.error("All boards failed to load. Check paths in .tuiboard/config.yaml.");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
process.on("SIGINT", () => {
|
|
53
|
+
store.dispose().finally(() => process.exit(0));
|
|
54
|
+
});
|
|
55
|
+
process.on("SIGTERM", () => {
|
|
56
|
+
store.dispose().finally(() => process.exit(0));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ─── Responsive layout ──────────────────────────────────────────────────────
|
|
60
|
+
// Auto-hide optional zones when the terminal isn't wide enough to host them
|
|
61
|
+
// comfortably. Breakpoints from the design spec (§4.2):
|
|
62
|
+
// ≥ 150 col → all four zones
|
|
63
|
+
// 120–149 → hide timeline
|
|
64
|
+
// 100–119 → hide agents too
|
|
65
|
+
// < 100 → hide virtual too (board is non-hideable)
|
|
66
|
+
//
|
|
67
|
+
// User F1/F2/F3 toggles still work — they last until the next resize, at
|
|
68
|
+
// which point auto re-evaluates. Acceptable trade-off: resize events are
|
|
69
|
+
// rare, predictable layout > sticky overrides.
|
|
70
|
+
function applyResponsiveLayout(): void {
|
|
71
|
+
const width = process.stdout.columns ?? 200;
|
|
72
|
+
store.setZoneVisible("timeline", width >= 150);
|
|
73
|
+
store.setZoneVisible("agents", width >= 120);
|
|
74
|
+
store.setZoneVisible("virtual", width >= 100);
|
|
75
|
+
}
|
|
76
|
+
applyResponsiveLayout();
|
|
77
|
+
process.stdout.on("resize", applyResponsiveLayout);
|
|
78
|
+
|
|
79
|
+
const { view } = parseArgs(process.argv.slice(2));
|
|
80
|
+
|
|
81
|
+
// ─── App shell ──────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
function rootViewFor(v: ViewKind | undefined, s: TuiStore) {
|
|
84
|
+
switch (v) {
|
|
85
|
+
case "board": return <BoardOnly store={s} />;
|
|
86
|
+
case "timeline": return <TimelineOnly store={s} />;
|
|
87
|
+
case "agents": return <AgentsOnly store={s} />;
|
|
88
|
+
default: return <Dashboard store={s} />;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function App() {
|
|
93
|
+
const virtualItems = createMemo(() =>
|
|
94
|
+
buildVirtualItems(store.state.boards.map((b) => b.board)),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
useKeyboard((key) => handleKey(store, key, virtualItems().length));
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<box
|
|
101
|
+
style={{
|
|
102
|
+
flexDirection: "column",
|
|
103
|
+
width: "100%",
|
|
104
|
+
height: "100%",
|
|
105
|
+
backgroundColor: T.bg,
|
|
106
|
+
padding: 1,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<TopBar store={store} />
|
|
110
|
+
<box style={{ height: 1 }} />
|
|
111
|
+
{/*
|
|
112
|
+
rootView and ModalLayer are siblings inside a flex-row so the
|
|
113
|
+
modal can sit beside the view as a fixed-width, full-height
|
|
114
|
+
side panel. ModalLayer renders only when ui.modal is set,
|
|
115
|
+
otherwise its <Show> resolves to nothing and the rootView gets
|
|
116
|
+
the whole row.
|
|
117
|
+
*/}
|
|
118
|
+
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
119
|
+
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
120
|
+
{rootViewFor(view, store)}
|
|
121
|
+
</box>
|
|
122
|
+
<ModalLayer store={store} />
|
|
123
|
+
</box>
|
|
124
|
+
<BottomBar store={store} />
|
|
125
|
+
</box>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await render(() => <App />, { useMouse: true });
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { parseArgs, type ViewKind } from "./args";
|
|
3
|
+
|
|
4
|
+
describe("parseArgs", () => {
|
|
5
|
+
it("returns view=undefined when no flag is present", () => {
|
|
6
|
+
expect(parseArgs([])).toEqual({ view: undefined });
|
|
7
|
+
expect(parseArgs(["bun", "src/app.tsx"])).toEqual({ view: undefined });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("parses --view=board", () => {
|
|
11
|
+
expect(parseArgs(["--view=board"])).toEqual({ view: "board" });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("parses --view=timeline", () => {
|
|
15
|
+
expect(parseArgs(["--view=timeline"])).toEqual({ view: "timeline" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("parses --view=agents", () => {
|
|
19
|
+
expect(parseArgs(["--view=agents"])).toEqual({ view: "agents" });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("parses --view <value> with space separator", () => {
|
|
23
|
+
expect(parseArgs(["--view", "board"])).toEqual({ view: "board" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns view=undefined for unknown view value (with warning to stderr)", () => {
|
|
27
|
+
const result = parseArgs(["--view=garbage"]);
|
|
28
|
+
expect(result.view).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("ignores other flags", () => {
|
|
32
|
+
expect(parseArgs(["--debug", "--view=board", "--something-else"])).toEqual({
|
|
33
|
+
view: "board",
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Type-level assertion: ensure ViewKind covers exactly the four expected values.
|
|
39
|
+
const _viewKinds: ViewKind[] = ["board", "timeline", "agents"];
|
|
40
|
+
void _viewKinds;
|
package/src/cli/args.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal argv parser for tuiboard. Only handles `--view=X` and `--view X`
|
|
3
|
+
* because that's all this app uses. Anything fancier (subcommands, multi-
|
|
4
|
+
* value flags) would warrant a real CLI library — YAGNI here.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type ViewKind = "board" | "timeline" | "agents";
|
|
8
|
+
|
|
9
|
+
const VALID_VIEWS: readonly ViewKind[] = ["board", "timeline", "agents"];
|
|
10
|
+
|
|
11
|
+
export interface ParsedArgs {
|
|
12
|
+
/** Undefined means: render the default Dashboard (all 4 zones). */
|
|
13
|
+
view?: ViewKind;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
17
|
+
let view: ViewKind | undefined;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const arg = argv[i]!;
|
|
21
|
+
let candidate: string | undefined;
|
|
22
|
+
|
|
23
|
+
if (arg.startsWith("--view=")) {
|
|
24
|
+
candidate = arg.slice("--view=".length);
|
|
25
|
+
} else if (arg === "--view") {
|
|
26
|
+
candidate = argv[i + 1];
|
|
27
|
+
i++; // consume the value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (candidate === undefined) continue;
|
|
31
|
+
if ((VALID_VIEWS as readonly string[]).includes(candidate)) {
|
|
32
|
+
view = candidate as ViewKind;
|
|
33
|
+
} else {
|
|
34
|
+
console.error(
|
|
35
|
+
`tuiboard: unknown --view value "${candidate}" — must be one of ${VALID_VIEWS.join(", ")}. Falling back to dashboard.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { view };
|
|
41
|
+
}
|