tuiboard 0.7.2 → 0.8.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 +7 -0
- package/CHANGELOG.md +38 -0
- package/README.md +71 -7
- package/package.json +1 -1
- package/src/app.tsx +20 -8
- package/src/calendar/setup.ts +33 -15
- package/src/config/loader.ts +5 -0
- package/src/input/handleKey.ts +53 -0
- package/src/store/calendar.ts +193 -2
- package/src/store/index.ts +207 -1
- package/src/store/timeline.ts +19 -1
- package/src/ui/Modal.tsx +198 -11
- package/src/ui/TimelineView.tsx +75 -9
- package/src/ui/layout.ts +12 -0
- package/src/views/Dashboard.tsx +15 -12
|
@@ -68,10 +68,17 @@ archive_column: Archive
|
|
|
68
68
|
# own credentials — nothing is hosted, nothing leaves your machine. All-day
|
|
69
69
|
# events are skipped; each calendar keeps its own color. Paths support `~`.
|
|
70
70
|
#
|
|
71
|
+
# To CREATE / EDIT / DELETE events (not just read), re-auth with:
|
|
72
|
+
# calendar-setup google --write. Then press `n` / click an empty slot to create;
|
|
73
|
+
# click an existing event to select it, `e` to edit, `d` to delete.
|
|
74
|
+
# `default_calendar` (a calendar id) sets the default target for new events;
|
|
75
|
+
# override per-event in the modal.
|
|
76
|
+
#
|
|
71
77
|
# calendars:
|
|
72
78
|
# google:
|
|
73
79
|
# enabled: true
|
|
74
80
|
# token: ~/.config/tuiboard/google_token.json
|
|
81
|
+
# # default_calendar: you@example.com # default target for new events
|
|
75
82
|
# # color: "#e8a05c" # fallback when a calendar has no color
|
|
76
83
|
# microsoft:
|
|
77
84
|
# enabled: true
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ All notable changes to **tuiboard** are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.0] - 2026-06-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Create, edit & delete Google Calendar events from the Agenda** (opt-in
|
|
12
|
+
write). Re-authorize with `tuiboard calendar-setup google --write`, then:
|
|
13
|
+
- **Create** — press `n` (or click an empty Agenda slot) to open a new-event
|
|
14
|
+
modal: type a title (append `HH:MM-HH:MM` to set the time), pick the target
|
|
15
|
+
calendar, Enter to create. A `default_calendar` config sets the default
|
|
16
|
+
target (override per-event in the modal).
|
|
17
|
+
- **Edit / delete** — click an existing event on a writable calendar to select
|
|
18
|
+
it, then `e` (or Enter) to edit its title/time, `d` to delete (with confirm),
|
|
19
|
+
`Esc` to deselect. Edits stay on the same calendar. Read-only events can't be
|
|
20
|
+
selected.
|
|
21
|
+
|
|
22
|
+
Every change appears in the Agenda and on Google Calendar immediately.
|
|
23
|
+
Read-only setups are unaffected — the write UI only appears when the token
|
|
24
|
+
carries the write scope, and only Google events on owner/writer calendars are
|
|
25
|
+
selectable. Microsoft event write is not supported yet.
|
|
26
|
+
- Expanded the README with a step-by-step Google Cloud OAuth client setup (the
|
|
27
|
+
bring-your-own-credentials flow), so first-time users have a clear path.
|
|
28
|
+
|
|
29
|
+
## [0.7.3] - 2026-06-02
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Opening a modal no longer shifts the whole dashboard up by a row. The Agenda's
|
|
33
|
+
tall scrollbox was inflating the main row one line past the terminal (flex
|
|
34
|
+
basis `auto` takes the content height); a modal in its place removed that
|
|
35
|
+
overflow, which read as a jump. The main row now grows purely from the
|
|
36
|
+
available space (`flex-basis: 0`), so the layout is steady whatever's on
|
|
37
|
+
screen. Modals also render in the Agenda's exact slot, so there's no
|
|
38
|
+
horizontal reflow either.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- Reclaimed a row: the board and agenda are one row taller, and the keyboard
|
|
42
|
+
cheat-sheet sits flush on the bottom line.
|
|
43
|
+
|
|
8
44
|
## [0.7.2] - 2026-06-02
|
|
9
45
|
|
|
10
46
|
### Changed
|
|
@@ -98,6 +134,8 @@ First public release on npm. This entry captures the full feature set at launch.
|
|
|
98
134
|
|
|
99
135
|
Built with [OpenTUI](https://opentui.com) + SolidJS on Bun.
|
|
100
136
|
|
|
137
|
+
[0.8.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.0
|
|
138
|
+
[0.7.3]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.3
|
|
101
139
|
[0.7.2]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.2
|
|
102
140
|
[0.7.1]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.1
|
|
103
141
|
[0.7.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.0
|
package/README.md
CHANGED
|
@@ -193,13 +193,37 @@ tuiboard calendar-setup google # opens your browser (read-only scope)
|
|
|
193
193
|
tuiboard calendar-setup microsoft # device-code flow, no redirect
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
196
|
+
**Microsoft** needs an Azure app registration (Public client, `Calendars.Read`
|
|
197
|
+
delegated) whose client ID goes in `~/.config/tuiboard/azure_config.json` —
|
|
198
|
+
running `calendar-setup microsoft` with no config writes a template that walks
|
|
199
|
+
you through it.
|
|
200
|
+
|
|
201
|
+
#### Google: one-time OAuth client setup
|
|
202
|
+
|
|
203
|
+
There's no hosted tuiboard app — **you create your own free OAuth client** in
|
|
204
|
+
your own Google Cloud project, so nothing is shared and your data never passes
|
|
205
|
+
through anyone else's servers. It takes about five minutes, once:
|
|
206
|
+
|
|
207
|
+
1. Go to the [Google Cloud Console](https://console.cloud.google.com/) and
|
|
208
|
+
create a project (or pick an existing one) from the project dropdown.
|
|
209
|
+
2. **APIs & Services → Library** → search **"Google Calendar API"** → **Enable**.
|
|
210
|
+
3. **APIs & Services → OAuth consent screen**: if prompted, choose **External**,
|
|
211
|
+
give the app a name and your email, and save. You don't need to publish it or
|
|
212
|
+
submit for verification — as the project owner you're automatically a test
|
|
213
|
+
user of your own app, which is all tuiboard needs. (Add your Google address
|
|
214
|
+
under **Test users** if it asks.)
|
|
215
|
+
4. **APIs & Services → Credentials → Create credentials → OAuth client ID**.
|
|
216
|
+
Application type: **Desktop app**. Create.
|
|
217
|
+
5. **Download JSON** on the client you just made, and save it as
|
|
218
|
+
`~/.config/tuiboard/google_credentials.json`.
|
|
219
|
+
6. Run `tuiboard calendar-setup google` (add `--write` to also create/edit/delete
|
|
220
|
+
— see below). Your browser opens; approve the access. You'll briefly see an
|
|
221
|
+
"unverified app" notice — that's expected for your own personal client; click
|
|
222
|
+
**Advanced → go to (your app)** to continue. The token is saved and the
|
|
223
|
+
command prints the YAML block to paste into your config.
|
|
224
|
+
|
|
225
|
+
The `calendar-setup` command prints these exact steps too if it doesn't find the
|
|
226
|
+
credentials file.
|
|
203
227
|
|
|
204
228
|
After connecting, the command prints the exact YAML to paste into your config:
|
|
205
229
|
|
|
@@ -219,6 +243,46 @@ expired, or unconfigured calendar never breaks the board — it just shows no
|
|
|
219
243
|
events. Set either provider's `enabled: false` (or drop the block) to turn it
|
|
220
244
|
off; add a `color:` to override the fallback block color.
|
|
221
245
|
|
|
246
|
+
### Creating, editing & deleting events (Google, opt-in)
|
|
247
|
+
|
|
248
|
+
Reading is the default. To also **create, edit, and delete** Google Calendar
|
|
249
|
+
events from the Agenda, re-authorize with the write scope:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
tuiboard calendar-setup google --write
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Create** — in the Agenda zone, press **`n`** (or **click an empty time slot**)
|
|
256
|
+
to open the new-event modal: type a title (append `HH:MM-HH:MM` to change the
|
|
257
|
+
time), press Enter, pick the target calendar with `j`/`k`, Enter to create. Only
|
|
258
|
+
calendars you can write to (owner/writer) show in the picker.
|
|
259
|
+
|
|
260
|
+
**Edit / delete** — **click an existing event** in the Agenda to select it (only
|
|
261
|
+
events on writable calendars can be selected; read-only ones just say so). Then:
|
|
262
|
+
|
|
263
|
+
- **`e`** (or Enter) opens the edit modal, prefilled with the title and time —
|
|
264
|
+
change either (`Title HH:MM-HH:MM`) and Enter to save.
|
|
265
|
+
- **`d`** deletes it (with a confirm).
|
|
266
|
+
- **`Esc`** deselects.
|
|
267
|
+
|
|
268
|
+
Edits stay on the same calendar (moving an event between calendars isn't
|
|
269
|
+
supported). Every change appears in the Agenda right away and on Google Calendar.
|
|
270
|
+
|
|
271
|
+
Set the calendar new events default to with `default_calendar` (a calendar id;
|
|
272
|
+
unset → your primary). You can still override per-event in the modal:
|
|
273
|
+
|
|
274
|
+
```yaml
|
|
275
|
+
calendars:
|
|
276
|
+
google:
|
|
277
|
+
enabled: true
|
|
278
|
+
token: ~/.config/tuiboard/google_token.json
|
|
279
|
+
default_calendar: you@example.com # optional; default target for new events
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The write scope is opt-in: without `--write`, tuiboard stays read-only and the
|
|
283
|
+
`n` shortcut / slot-click / event-selection do nothing. Microsoft event write
|
|
284
|
+
isn't supported yet (read-only).
|
|
285
|
+
|
|
222
286
|
## Markdown board format
|
|
223
287
|
|
|
224
288
|
`tuiboard` reads and writes **plain CommonMark** with the Obsidian
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tuiboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Terminal kanban for markdown task boards, with optional Today/Tomorrow planner, 24h agenda + calendar overlay, and a live Claude Code agent view. Use only the panels you want.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/app.tsx
CHANGED
|
@@ -115,23 +115,35 @@ function App() {
|
|
|
115
115
|
width: "100%",
|
|
116
116
|
height: "100%",
|
|
117
117
|
backgroundColor: T.bg,
|
|
118
|
-
padding:
|
|
118
|
+
// No bottom padding: the keyboard cheat-sheet sits flush on the last
|
|
119
|
+
// row, which gives the board + agenda one extra row of height. Safe
|
|
120
|
+
// thanks to the flexBasis:0 main row — the content just fills the space.
|
|
121
|
+
paddingTop: 1,
|
|
122
|
+
paddingLeft: 1,
|
|
123
|
+
paddingRight: 1,
|
|
124
|
+
paddingBottom: 0,
|
|
119
125
|
}}
|
|
120
126
|
>
|
|
121
127
|
<TopBar store={store} />
|
|
122
128
|
<box style={{ height: 1 }} />
|
|
123
129
|
{/*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
The default Dashboard renders its modal inside the Agenda's slot, so the
|
|
131
|
+
layout stays put when a modal opens. The standalone `--view=` modes have
|
|
132
|
+
no Agenda slot, so they get the modal here as a right-hand panel.
|
|
133
|
+
(`view` is fixed at launch, never reactive.)
|
|
134
|
+
|
|
135
|
+
`flexBasis: 0` + `minHeight: 0` are load-bearing: without them the row's
|
|
136
|
+
flex-basis is `auto`, so the Agenda's tall scrollbox inflates the row to
|
|
137
|
+
its content height (1 row past the terminal). That overflow vanishes
|
|
138
|
+
when a modal (no scrollbox) takes the slot — which is exactly the 1-row
|
|
139
|
+
"shift" the whole bottom strip used to do. Basis 0 makes the row grow
|
|
140
|
+
purely from the available space, ignoring content, so it's stable.
|
|
129
141
|
*/}
|
|
130
|
-
<box style={{ flexDirection: "row", flexGrow: 1 }}>
|
|
142
|
+
<box style={{ flexDirection: "row", flexGrow: 1, flexBasis: 0, minHeight: 0 }}>
|
|
131
143
|
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
132
144
|
{rootViewFor(view, store)}
|
|
133
145
|
</box>
|
|
134
|
-
<ModalLayer store={store} />
|
|
146
|
+
{view ? <ModalLayer store={store} /> : null}
|
|
135
147
|
</box>
|
|
136
148
|
<BottomBar store={store} />
|
|
137
149
|
</box>
|
package/src/calendar/setup.ts
CHANGED
|
@@ -21,7 +21,9 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
21
21
|
import { loadConfig } from "~/config/loader";
|
|
22
22
|
|
|
23
23
|
const TUIBOARD_DIR = join(homedir(), ".config", "tuiboard");
|
|
24
|
-
const
|
|
24
|
+
const GOOGLE_SCOPE_RO = "https://www.googleapis.com/auth/calendar.readonly";
|
|
25
|
+
/** Added with `--write`: lets tuiboard CREATE events (and still read them). */
|
|
26
|
+
const GOOGLE_SCOPE_EVENTS = "https://www.googleapis.com/auth/calendar.events";
|
|
25
27
|
const MS_SCOPE = "Calendars.Read offline_access openid profile";
|
|
26
28
|
|
|
27
29
|
function expandPath(p: string, root: string): string {
|
|
@@ -32,8 +34,10 @@ function expandPath(p: string, root: string): string {
|
|
|
32
34
|
function openBrowser(url: string): void {
|
|
33
35
|
try {
|
|
34
36
|
if (process.platform === "win32") {
|
|
35
|
-
// `
|
|
36
|
-
|
|
37
|
+
// NOT `cmd /c start`: cmd treats the `&` in an OAuth URL as a command
|
|
38
|
+
// separator and truncates it (→ Google "invalid_request"). rundll32 gets
|
|
39
|
+
// the URL as a single literal argv, no shell parsing.
|
|
40
|
+
spawn("rundll32", ["url.dll,FileProtocolHandler", url], { detached: true, stdio: "ignore" }).unref();
|
|
37
41
|
} else if (process.platform === "darwin") {
|
|
38
42
|
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
39
43
|
} else {
|
|
@@ -58,8 +62,10 @@ interface GoogleClientSecrets {
|
|
|
58
62
|
token_uri?: string;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
async function setupGoogle(credsPath: string, tokenPath: string): Promise<number> {
|
|
65
|
+
async function setupGoogle(credsPath: string, tokenPath: string, write: boolean): Promise<number> {
|
|
66
|
+
const scopes = write ? [GOOGLE_SCOPE_RO, GOOGLE_SCOPE_EVENTS] : [GOOGLE_SCOPE_RO];
|
|
62
67
|
console.log("\n── Google Calendar setup ──────────────────────────────────");
|
|
68
|
+
console.log(write ? "Mode: read + create events" : "Mode: read-only (add --write to create events)");
|
|
63
69
|
if (!existsSync(credsPath)) {
|
|
64
70
|
console.log(`OAuth client file not found:\n ${credsPath}\n
|
|
65
71
|
Create it:
|
|
@@ -88,7 +94,10 @@ Create it:
|
|
|
88
94
|
console.log("client_id / client_secret missing from the OAuth client file.");
|
|
89
95
|
return 1;
|
|
90
96
|
}
|
|
91
|
-
|
|
97
|
+
// Force the current v2 authorization endpoint. Desktop client_secret files
|
|
98
|
+
// still ship the legacy `/o/oauth2/auth` in `auth_uri`, which returns
|
|
99
|
+
// "Error 400: invalid_request (GeneralOAuthFlow)" for loopback + multi-scope.
|
|
100
|
+
const authUri = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
92
101
|
const tokenUri = secrets.token_uri || "https://oauth2.googleapis.com/token";
|
|
93
102
|
|
|
94
103
|
// Loopback server captures the ?code= redirect.
|
|
@@ -96,15 +105,22 @@ Create it:
|
|
|
96
105
|
const result = new Promise<{ code?: string; error?: string }>((r) => (resolveResult = r));
|
|
97
106
|
const server = Bun.serve({
|
|
98
107
|
port: 0,
|
|
108
|
+
// Bind the IPv4 loopback explicitly. On Windows `localhost` resolves to
|
|
109
|
+
// IPv6 `::1` first; if the server is IPv4-only the redirect is refused.
|
|
110
|
+
// We also use 127.0.0.1 in the redirect URI so the two always match.
|
|
111
|
+
hostname: "127.0.0.1",
|
|
99
112
|
fetch(req) {
|
|
100
113
|
const u = new URL(req.url);
|
|
101
114
|
const code = u.searchParams.get("code") ?? undefined;
|
|
102
115
|
const error = u.searchParams.get("error") ?? undefined;
|
|
103
116
|
if (code || error) {
|
|
104
|
-
resolveResult({ code, error });
|
|
105
117
|
const msg = error
|
|
106
118
|
? `Authorization failed: ${error}`
|
|
107
119
|
: "tuiboard is now connected to Google Calendar. You can close this tab.";
|
|
120
|
+
// Resolve AFTER the response is handed back so the success page has a
|
|
121
|
+
// moment to flush; resolving synchronously lets the caller stop the
|
|
122
|
+
// server before the bytes reach the browser (→ ERR_CONNECTION_REFUSED).
|
|
123
|
+
setTimeout(() => resolveResult({ code, error }), 300);
|
|
108
124
|
return new Response(`<!doctype html><meta charset=utf-8><body style="font:16px system-ui;padding:3rem">${msg}</body>`, {
|
|
109
125
|
headers: { "content-type": "text/html" },
|
|
110
126
|
});
|
|
@@ -112,24 +128,24 @@ Create it:
|
|
|
112
128
|
return new Response("waiting for Google…", { headers: { "content-type": "text/plain" } });
|
|
113
129
|
},
|
|
114
130
|
});
|
|
115
|
-
const redirectUri = `http://
|
|
131
|
+
const redirectUri = `http://127.0.0.1:${server.port}`;
|
|
116
132
|
const authUrl =
|
|
117
133
|
`${authUri}?` +
|
|
118
134
|
new URLSearchParams({
|
|
119
135
|
client_id: clientId,
|
|
120
136
|
redirect_uri: redirectUri,
|
|
121
137
|
response_type: "code",
|
|
122
|
-
scope:
|
|
138
|
+
scope: scopes.join(" "),
|
|
123
139
|
access_type: "offline",
|
|
124
140
|
prompt: "consent",
|
|
125
141
|
}).toString();
|
|
126
142
|
|
|
127
|
-
console.log(
|
|
143
|
+
console.log(`Opening your browser to authorize Google Calendar (${write ? "read + write" : "read-only"})…`);
|
|
128
144
|
console.log(`If it doesn't open, visit:\n ${authUrl}\n`);
|
|
129
145
|
openBrowser(authUrl);
|
|
130
146
|
|
|
131
147
|
const { code, error } = await result;
|
|
132
|
-
server.stop(
|
|
148
|
+
server.stop(); // graceful: lets the success page finish sending
|
|
133
149
|
if (error || !code) {
|
|
134
150
|
console.log(`\nSetup cancelled${error ? `: ${error}` : ""}.`);
|
|
135
151
|
return 1;
|
|
@@ -165,7 +181,7 @@ Create it:
|
|
|
165
181
|
token_uri: tokenUri,
|
|
166
182
|
client_id: clientId,
|
|
167
183
|
client_secret: clientSecret,
|
|
168
|
-
scopes
|
|
184
|
+
scopes,
|
|
169
185
|
expiry: data.expires_in ? new Date(Date.now() + data.expires_in * 1000).toISOString() : undefined,
|
|
170
186
|
});
|
|
171
187
|
console.log(`\n✓ Google Calendar connected. Token saved:\n ${tokenPath}\n`);
|
|
@@ -279,8 +295,9 @@ function usage(): void {
|
|
|
279
295
|
console.log(`tuiboard calendar-setup — connect a calendar to the Agenda overlay
|
|
280
296
|
|
|
281
297
|
Usage:
|
|
282
|
-
tuiboard calendar-setup google
|
|
283
|
-
tuiboard calendar-setup
|
|
298
|
+
tuiboard calendar-setup google Browser OAuth, read-only
|
|
299
|
+
tuiboard calendar-setup google --write Browser OAuth, read + create events
|
|
300
|
+
tuiboard calendar-setup microsoft Device-code flow for Microsoft 365
|
|
284
301
|
|
|
285
302
|
Tokens are written to the paths in your tuiboard config (calendars.google.token
|
|
286
303
|
/ calendars.microsoft.tokenCache), or ~/.config/tuiboard/ if no calendars block
|
|
@@ -289,7 +306,8 @@ prints the exact YAML.`);
|
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
export async function runCalendarSetup(argv: string[]): Promise<number> {
|
|
292
|
-
const
|
|
309
|
+
const write = argv.includes("--write");
|
|
310
|
+
const provider = (argv.find((a) => !a.startsWith("--")) ?? "").toLowerCase();
|
|
293
311
|
const cfg = loadConfig();
|
|
294
312
|
const root = cfg.root;
|
|
295
313
|
|
|
@@ -299,7 +317,7 @@ export async function runCalendarSetup(argv: string[]): Promise<number> {
|
|
|
299
317
|
? expandPath(g.credentials, root)
|
|
300
318
|
: join(TUIBOARD_DIR, "google_credentials.json");
|
|
301
319
|
const tokenPath = g?.token ? expandPath(g.token, root) : join(TUIBOARD_DIR, "google_token.json");
|
|
302
|
-
const rc = await setupGoogle(credsPath, tokenPath);
|
|
320
|
+
const rc = await setupGoogle(credsPath, tokenPath, write);
|
|
303
321
|
if (rc === 0 && !g) {
|
|
304
322
|
console.log(`Add this to your tuiboard config (${cfg.loaded ? "config found" : "create ~/.config/tuiboard/config.yaml"}):
|
|
305
323
|
|
package/src/config/loader.ts
CHANGED
|
@@ -83,6 +83,9 @@ export interface GoogleCalendarConfig {
|
|
|
83
83
|
credentials?: string;
|
|
84
84
|
/** Fallback color when a calendar has none of its own. */
|
|
85
85
|
color?: string;
|
|
86
|
+
/** Calendar id new events are created on by default (override in the modal).
|
|
87
|
+
* Unset → the account's primary calendar. Requires `--write` setup. */
|
|
88
|
+
defaultCalendar?: string;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
export interface MicrosoftCalendarConfig {
|
|
@@ -152,6 +155,7 @@ interface RawConfig {
|
|
|
152
155
|
token?: string;
|
|
153
156
|
credentials?: string;
|
|
154
157
|
color?: string;
|
|
158
|
+
default_calendar?: string;
|
|
155
159
|
};
|
|
156
160
|
microsoft?: {
|
|
157
161
|
enabled?: boolean;
|
|
@@ -191,6 +195,7 @@ function normalizeCalendars(
|
|
|
191
195
|
token: expandPath(g.token, root),
|
|
192
196
|
credentials: g.credentials ? expandPath(g.credentials, root) : undefined,
|
|
193
197
|
color: g.color,
|
|
198
|
+
defaultCalendar: g.default_calendar,
|
|
194
199
|
};
|
|
195
200
|
}
|
|
196
201
|
const m = raw.microsoft;
|
package/src/input/handleKey.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { isHiddenColumn } from "~/config/loader";
|
|
17
|
+
import { googleTokenCanWrite } from "~/store/calendar";
|
|
17
18
|
import { isTask } from "~/parser/markdown";
|
|
18
19
|
import {
|
|
19
20
|
isoToday,
|
|
@@ -59,6 +60,14 @@ export function handleKey(
|
|
|
59
60
|
}
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
63
|
+
if (ui.modal.kind === "confirm-delete-event") {
|
|
64
|
+
if (key.name === "y" || key.name === "enter" || key.name === "return") {
|
|
65
|
+
void store.confirmDeleteEvent();
|
|
66
|
+
} else if (key.name === "n") {
|
|
67
|
+
store.closeModal();
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
62
71
|
if ((ui.modal.kind === "help" ||
|
|
63
72
|
ui.modal.kind === "detail" ||
|
|
64
73
|
ui.modal.kind === "agent-detail") &&
|
|
@@ -66,6 +75,17 @@ export function handleKey(
|
|
|
66
75
|
store.closeModal();
|
|
67
76
|
return;
|
|
68
77
|
}
|
|
78
|
+
// New-event modal: step 1 typing goes to the <input>; step 2 (no input
|
|
79
|
+
// focused) is the calendar picker, driven here.
|
|
80
|
+
if (ui.modal.kind === "event") {
|
|
81
|
+
const p = ui.eventPicker;
|
|
82
|
+
if (p && p.step === 2) {
|
|
83
|
+
if (key.name === "j" || key.name === "down") { store.setEventSel(p.sel + 1); return; }
|
|
84
|
+
if (key.name === "k" || key.name === "up") { store.setEventSel(p.sel - 1); return; }
|
|
85
|
+
if (key.name === "enter" || key.name === "return") { void store.confirmEventPicker(); return; }
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
69
89
|
return;
|
|
70
90
|
}
|
|
71
91
|
|
|
@@ -85,6 +105,11 @@ export function handleKey(
|
|
|
85
105
|
store.flashBanner("info", wasMode ? "Arm mode off" : "Disarmed");
|
|
86
106
|
return;
|
|
87
107
|
}
|
|
108
|
+
if (ui.selectedCalEvent) {
|
|
109
|
+
store.clearCalSelection();
|
|
110
|
+
store.flashBanner("info", "Event deselected");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
88
113
|
if (ui.grabbing) {
|
|
89
114
|
store.exitGrab();
|
|
90
115
|
store.flashBanner("info", "Grab released");
|
|
@@ -294,6 +319,34 @@ function handleTimelineZone(
|
|
|
294
319
|
);
|
|
295
320
|
const target = entries[ui.row];
|
|
296
321
|
|
|
322
|
+
// A selected (clicked) calendar event takes over e/d/Enter for edit/delete.
|
|
323
|
+
// Any other key drops the selection and is then handled normally below.
|
|
324
|
+
const selCal = ui.selectedCalEvent;
|
|
325
|
+
if (selCal) {
|
|
326
|
+
if (key.name === "e" || key.name === "enter" || key.name === "return") {
|
|
327
|
+
store.openEventEditModal();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (key.name === "d") {
|
|
331
|
+
openLater({ kind: "confirm-delete-event" });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
store.clearCalSelection();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// `n` (Agenda zone) = create a Google Calendar event at the now-rounded slot.
|
|
338
|
+
// Mirrors `n` = new task in the board. Gated on Google write being connected.
|
|
339
|
+
if ((key.name === "n" || key.name === "N") && !key.ctrl) {
|
|
340
|
+
const g = store.config.calendars?.google;
|
|
341
|
+
if (g && googleTokenCanWrite(g.token)) {
|
|
342
|
+
const { startMin, endMin } = nextNowBlock();
|
|
343
|
+
store.openEventModal(store.agendaDate(), startMin, endMin);
|
|
344
|
+
} else {
|
|
345
|
+
store.flashBanner("warn", "Connect Google write first: tuiboard calendar-setup google --write");
|
|
346
|
+
}
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
297
350
|
// Armed-block adjustments take priority over navigation. While a block
|
|
298
351
|
// is armed, j/k nudge its start time and +/- nudge its end.
|
|
299
352
|
const armedRef = ui.armedTimelineRef;
|