tuiboard 0.7.3 → 0.8.1
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 +10 -2
- package/CHANGELOG.md +41 -0
- package/README.md +93 -13
- package/package.json +1 -1
- 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 +243 -4
- package/src/store/index.ts +214 -1
- package/src/store/timeline.ts +19 -1
- package/src/ui/Modal.tsx +231 -0
- package/src/ui/TimelineView.tsx +107 -14
|
@@ -65,13 +65,21 @@ archive_column: Archive
|
|
|
65
65
|
# Optional: overlay read-only calendar events on the Agenda (the 24h timeline).
|
|
66
66
|
# Connect a provider with `tuiboard calendar-setup google` / `... microsoft`,
|
|
67
67
|
# which opens the auth flow and prints the exact block to paste here. Bring your
|
|
68
|
-
# own credentials — nothing is hosted, nothing leaves your machine.
|
|
69
|
-
#
|
|
68
|
+
# own credentials — nothing is hosted, nothing leaves your machine. Timed events
|
|
69
|
+
# render on the 24h grid; all-day events show as chips at the top. Each calendar
|
|
70
|
+
# keeps its own color. Paths support `~`.
|
|
71
|
+
#
|
|
72
|
+
# To CREATE / EDIT / DELETE events (not just read), re-auth with:
|
|
73
|
+
# calendar-setup google --write. Then press `n` / click an empty slot to create;
|
|
74
|
+
# click an existing event to select it, `e` to edit, `d` to delete.
|
|
75
|
+
# `default_calendar` (a calendar id) sets the default target for new events;
|
|
76
|
+
# override per-event in the modal.
|
|
70
77
|
#
|
|
71
78
|
# calendars:
|
|
72
79
|
# google:
|
|
73
80
|
# enabled: true
|
|
74
81
|
# token: ~/.config/tuiboard/google_token.json
|
|
82
|
+
# # default_calendar: you@example.com # default target for new events
|
|
75
83
|
# # color: "#e8a05c" # fallback when a calendar has no color
|
|
76
84
|
# microsoft:
|
|
77
85
|
# enabled: true
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,45 @@ 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.1] - 2026-06-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Set an event's date from the Agenda modal.** When creating or editing a
|
|
12
|
+
Google Calendar event you can now append a date token to the title — `t` /
|
|
13
|
+
`tm` / `+3` / `lun` / `2026-06-10`, the same shortcuts as task scheduling — so
|
|
14
|
+
you're no longer limited to the day the Agenda is showing. Natural order is
|
|
15
|
+
`Title [date] HH:MM-HH:MM` (e.g. `Lunch tomorrow 12-13`); without a date token
|
|
16
|
+
it still defaults to the viewed day. On edit, a date token moves the event to
|
|
17
|
+
another day.
|
|
18
|
+
- **All-day events now show in the Agenda.** Previously skipped, all-day events
|
|
19
|
+
(Google and Microsoft) render as a chip strip at the top of the Agenda — like
|
|
20
|
+
Google Calendar's all-day band — instead of being dropped.
|
|
21
|
+
- **Create all-day events from the Agenda.** Add `allday` (or `all-day`) anywhere
|
|
22
|
+
in the new-event title — e.g. `Holiday 2026-12-25 allday` — to create a
|
|
23
|
+
date-only Google event instead of a timed one. It appears in the top chip
|
|
24
|
+
strip. (Editing all-day events isn't supported; create only.)
|
|
25
|
+
|
|
26
|
+
## [0.8.0] - 2026-06-03
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **Create, edit & delete Google Calendar events from the Agenda** (opt-in
|
|
30
|
+
write). Re-authorize with `tuiboard calendar-setup google --write`, then:
|
|
31
|
+
- **Create** — press `n` (or click an empty Agenda slot) to open a new-event
|
|
32
|
+
modal: type a title (append `HH:MM-HH:MM` to set the time), pick the target
|
|
33
|
+
calendar, Enter to create. A `default_calendar` config sets the default
|
|
34
|
+
target (override per-event in the modal).
|
|
35
|
+
- **Edit / delete** — click an existing event on a writable calendar to select
|
|
36
|
+
it, then `e` (or Enter) to edit its title/time, `d` to delete (with confirm),
|
|
37
|
+
`Esc` to deselect. Edits stay on the same calendar. Read-only events can't be
|
|
38
|
+
selected.
|
|
39
|
+
|
|
40
|
+
Every change appears in the Agenda and on Google Calendar immediately.
|
|
41
|
+
Read-only setups are unaffected — the write UI only appears when the token
|
|
42
|
+
carries the write scope, and only Google events on owner/writer calendars are
|
|
43
|
+
selectable. Microsoft event write is not supported yet.
|
|
44
|
+
- Expanded the README with a step-by-step Google Cloud OAuth client setup (the
|
|
45
|
+
bring-your-own-credentials flow), so first-time users have a clear path.
|
|
46
|
+
|
|
8
47
|
## [0.7.3] - 2026-06-02
|
|
9
48
|
|
|
10
49
|
### Fixed
|
|
@@ -113,6 +152,8 @@ First public release on npm. This entry captures the full feature set at launch.
|
|
|
113
152
|
|
|
114
153
|
Built with [OpenTUI](https://opentui.com) + SolidJS on Bun.
|
|
115
154
|
|
|
155
|
+
[0.8.1]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.1
|
|
156
|
+
[0.8.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.0
|
|
116
157
|
[0.7.3]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.3
|
|
117
158
|
[0.7.2]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.2
|
|
118
159
|
[0.7.1]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.1
|
package/README.md
CHANGED
|
@@ -179,12 +179,14 @@ at all — handy if you don't use Claude Code and don't want tuiboard reading
|
|
|
179
179
|
|
|
180
180
|
## Calendars (Agenda overlay)
|
|
181
181
|
|
|
182
|
-
The **Agenda** zone (the 24h timeline) can overlay
|
|
183
|
-
|
|
184
|
-
colored `📅` blocks
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
credentials** — there's nothing to sign
|
|
182
|
+
The **Agenda** zone (the 24h timeline) can overlay events from Google Calendar
|
|
183
|
+
and Microsoft 365 alongside your time-blocked tasks — timed events render as
|
|
184
|
+
colored `📅` blocks on the grid, and all-day events ride in a chip strip at the
|
|
185
|
+
top (like Google Calendar's all-day band). The day's real shape is visible at a
|
|
186
|
+
glance. Each calendar keeps its own color. Events are cached 30 min on disk and
|
|
187
|
+
refreshed every 5 min. **Bring your own credentials** — there's nothing to sign
|
|
188
|
+
up for and nothing leaves your machine. (Reading is the default; opt into
|
|
189
|
+
creating/editing events below.)
|
|
188
190
|
|
|
189
191
|
Connect a calendar with the built-in setup command:
|
|
190
192
|
|
|
@@ -193,13 +195,37 @@ tuiboard calendar-setup google # opens your browser (read-only scope)
|
|
|
193
195
|
tuiboard calendar-setup microsoft # device-code flow, no redirect
|
|
194
196
|
```
|
|
195
197
|
|
|
196
|
-
**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
**Microsoft** needs an Azure app registration (Public client, `Calendars.Read`
|
|
199
|
+
delegated) whose client ID goes in `~/.config/tuiboard/azure_config.json` —
|
|
200
|
+
running `calendar-setup microsoft` with no config writes a template that walks
|
|
201
|
+
you through it.
|
|
202
|
+
|
|
203
|
+
#### Google: one-time OAuth client setup
|
|
204
|
+
|
|
205
|
+
There's no hosted tuiboard app — **you create your own free OAuth client** in
|
|
206
|
+
your own Google Cloud project, so nothing is shared and your data never passes
|
|
207
|
+
through anyone else's servers. It takes about five minutes, once:
|
|
208
|
+
|
|
209
|
+
1. Go to the [Google Cloud Console](https://console.cloud.google.com/) and
|
|
210
|
+
create a project (or pick an existing one) from the project dropdown.
|
|
211
|
+
2. **APIs & Services → Library** → search **"Google Calendar API"** → **Enable**.
|
|
212
|
+
3. **APIs & Services → OAuth consent screen**: if prompted, choose **External**,
|
|
213
|
+
give the app a name and your email, and save. You don't need to publish it or
|
|
214
|
+
submit for verification — as the project owner you're automatically a test
|
|
215
|
+
user of your own app, which is all tuiboard needs. (Add your Google address
|
|
216
|
+
under **Test users** if it asks.)
|
|
217
|
+
4. **APIs & Services → Credentials → Create credentials → OAuth client ID**.
|
|
218
|
+
Application type: **Desktop app**. Create.
|
|
219
|
+
5. **Download JSON** on the client you just made, and save it as
|
|
220
|
+
`~/.config/tuiboard/google_credentials.json`.
|
|
221
|
+
6. Run `tuiboard calendar-setup google` (add `--write` to also create/edit/delete
|
|
222
|
+
— see below). Your browser opens; approve the access. You'll briefly see an
|
|
223
|
+
"unverified app" notice — that's expected for your own personal client; click
|
|
224
|
+
**Advanced → go to (your app)** to continue. The token is saved and the
|
|
225
|
+
command prints the YAML block to paste into your config.
|
|
226
|
+
|
|
227
|
+
The `calendar-setup` command prints these exact steps too if it doesn't find the
|
|
228
|
+
credentials file.
|
|
203
229
|
|
|
204
230
|
After connecting, the command prints the exact YAML to paste into your config:
|
|
205
231
|
|
|
@@ -219,6 +245,60 @@ expired, or unconfigured calendar never breaks the board — it just shows no
|
|
|
219
245
|
events. Set either provider's `enabled: false` (or drop the block) to turn it
|
|
220
246
|
off; add a `color:` to override the fallback block color.
|
|
221
247
|
|
|
248
|
+
### Creating, editing & deleting events (Google, opt-in)
|
|
249
|
+
|
|
250
|
+
Reading is the default. To also **create, edit, and delete** Google Calendar
|
|
251
|
+
events from the Agenda, re-authorize with the write scope:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
tuiboard calendar-setup google --write
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Create** — in the Agenda zone, press **`n`** (or **click an empty time slot**)
|
|
258
|
+
to open the new-event modal: type a title, press Enter, pick the target calendar
|
|
259
|
+
with `j`/`k`, Enter to create. Only calendars you can write to (owner/writer)
|
|
260
|
+
show in the picker. Append tokens to the title to set the **time** and **date**:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
Standup 9:00-9:30 # today (or the viewed day), 09:00–09:30
|
|
264
|
+
Lunch tomorrow 12-13 # tomorrow, 12:00–13:00
|
|
265
|
+
Review 2026-06-10 15-16 # that date, 15:00–16:00
|
|
266
|
+
Call +3 16:00-16:30 # in 3 days · lun = next Monday also works
|
|
267
|
+
Holiday 2026-12-25 allday # an all-day event (no time)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The date defaults to whichever day the Agenda is showing; an explicit date token
|
|
271
|
+
(`t` / `tm` / `+N` / weekday / `YYYY-MM-DD`) overrides it. The time is taken from
|
|
272
|
+
the clicked slot, or `HH:MM-HH:MM`. Add **`allday`** (or `all-day`) anywhere in
|
|
273
|
+
the title to create an all-day event instead — it lands in the top chip strip.
|
|
274
|
+
|
|
275
|
+
**Edit / delete** — **click an existing event** in the Agenda to select it (only
|
|
276
|
+
events on writable calendars can be selected; read-only ones just say so). Then:
|
|
277
|
+
|
|
278
|
+
- **`e`** (or Enter) opens the edit modal, prefilled with the title and time —
|
|
279
|
+
change the title, time, and/or date (same `Title [date] HH:MM-HH:MM` syntax)
|
|
280
|
+
and Enter to save. A date token moves the event to another day.
|
|
281
|
+
- **`d`** deletes it (with a confirm).
|
|
282
|
+
- **`Esc`** deselects.
|
|
283
|
+
|
|
284
|
+
Edits stay on the same calendar (moving an event between calendars isn't
|
|
285
|
+
supported). Every change appears in the Agenda right away and on Google Calendar.
|
|
286
|
+
|
|
287
|
+
Set the calendar new events default to with `default_calendar` (a calendar id;
|
|
288
|
+
unset → your primary). You can still override per-event in the modal:
|
|
289
|
+
|
|
290
|
+
```yaml
|
|
291
|
+
calendars:
|
|
292
|
+
google:
|
|
293
|
+
enabled: true
|
|
294
|
+
token: ~/.config/tuiboard/google_token.json
|
|
295
|
+
default_calendar: you@example.com # optional; default target for new events
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The write scope is opt-in: without `--write`, tuiboard stays read-only and the
|
|
299
|
+
`n` shortcut / slot-click / event-selection do nothing. Microsoft event write
|
|
300
|
+
isn't supported yet (read-only).
|
|
301
|
+
|
|
222
302
|
## Markdown board format
|
|
223
303
|
|
|
224
304
|
`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.1",
|
|
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/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;
|