floq 0.4.0 → 0.6.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.
@@ -0,0 +1,773 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useState, useEffect, useCallback } from 'react';
3
+ import { Box, Text, useInput, useApp, useStdout } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { eq, and, gte } from 'drizzle-orm';
6
+ import { v4 as uuidv4 } from 'uuid';
7
+ import { getDb, schema } from '../../db/index.js';
8
+ import { t, fmt } from '../../i18n/index.js';
9
+ import { useTheme } from '../theme/index.js';
10
+ import { isTursoEnabled, getContexts, addContext } from '../../config.js';
11
+ import { VERSION } from '../../version.js';
12
+ import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, } from '../history/index.js';
13
+ import { SearchBar } from './SearchBar.js';
14
+ import { SearchResults } from './SearchResults.js';
15
+ import { HelpModal } from './HelpModal.js';
16
+ const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
17
+ // Round border characters
18
+ const BORDER = {
19
+ topLeft: '╭',
20
+ topRight: '╮',
21
+ bottomLeft: '╰',
22
+ bottomRight: '╯',
23
+ horizontal: '─',
24
+ vertical: '│',
25
+ };
26
+ const SHADOW = '░';
27
+ function getDisplayWidth(str) {
28
+ let width = 0;
29
+ for (const char of str) {
30
+ const code = char.charCodeAt(0);
31
+ if ((code >= 0x1100 && code <= 0x115F) ||
32
+ (code >= 0x2E80 && code <= 0x9FFF) ||
33
+ (code >= 0xAC00 && code <= 0xD7AF) ||
34
+ (code >= 0xF900 && code <= 0xFAFF) ||
35
+ (code >= 0xFE10 && code <= 0xFE1F) ||
36
+ (code >= 0xFE30 && code <= 0xFE6F) ||
37
+ (code >= 0xFF00 && code <= 0xFF60) ||
38
+ (code >= 0xFFE0 && code <= 0xFFE6) ||
39
+ (code >= 0x20000 && code <= 0x2FFFF)) {
40
+ width += 2;
41
+ }
42
+ else {
43
+ width += 1;
44
+ }
45
+ }
46
+ return width;
47
+ }
48
+ // Truncate string to fit within maxWidth (display width)
49
+ function truncateString(str, maxWidth) {
50
+ if (getDisplayWidth(str) <= maxWidth)
51
+ return str;
52
+ let width = 0;
53
+ let result = '';
54
+ for (const char of str) {
55
+ const charWidth = getDisplayWidth(char);
56
+ if (width + charWidth + 2 > maxWidth) { // +2 for "…"
57
+ return result + '…';
58
+ }
59
+ result += char;
60
+ width += charWidth;
61
+ }
62
+ return result;
63
+ }
64
+ function TitledBoxInline({ title, children, width, minHeight = 1, isActive = false, showShadow = true, }) {
65
+ const theme = useTheme();
66
+ const color = isActive ? theme.colors.borderActive : theme.colors.border;
67
+ const shadowColor = theme.colors.muted;
68
+ const innerWidth = width - 2;
69
+ const titleLength = getDisplayWidth(title);
70
+ const leftDashes = 3;
71
+ const titlePadding = 2;
72
+ const rightDashes = Math.max(0, innerWidth - leftDashes - titlePadding - titleLength);
73
+ const childArray = React.Children.toArray(children);
74
+ const contentRows = childArray.length || 1;
75
+ const emptyRowsNeeded = Math.max(0, minHeight - contentRows);
76
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.topLeft }), _jsxs(Text, { color: color, children: [BORDER.horizontal.repeat(leftDashes), " "] }), _jsx(Text, { color: theme.colors.accent, bold: true, children: title }), _jsxs(Text, { color: color, children: [" ", BORDER.horizontal.repeat(rightDashes)] }), _jsx(Text, { color: color, children: BORDER.topRight }), showShadow && _jsx(Text, { children: " " })] }), childArray.length > 0 ? (childArray.map((child, i) => (_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.vertical }), _jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(Box, { flexGrow: 1, children: child }) }), _jsx(Text, { color: color, children: BORDER.vertical }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] }, i)))) : (_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.vertical }), _jsx(Box, { width: innerWidth, children: _jsx(Text, { children: " " }) }), _jsx(Text, { color: color, children: BORDER.vertical }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] })), Array.from({ length: emptyRowsNeeded }).map((_, i) => (_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.vertical }), _jsx(Box, { width: innerWidth, children: _jsx(Text, { children: " " }) }), _jsx(Text, { color: color, children: BORDER.vertical }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] }, `empty-${i}`))), _jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.bottomLeft }), _jsx(Text, { color: color, children: BORDER.horizontal.repeat(innerWidth) }), _jsx(Text, { color: color, children: BORDER.bottomRight }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] }), showShadow && (_jsx(Box, { children: _jsxs(Text, { color: shadowColor, children: [" ", SHADOW.repeat(width)] }) }))] }));
77
+ }
78
+ export function GtdDQ({ onOpenSettings }) {
79
+ const theme = useTheme();
80
+ const { exit } = useApp();
81
+ const { stdout } = useStdout();
82
+ const history = useHistory();
83
+ const i18n = t();
84
+ const [mode, setMode] = useState('normal');
85
+ const [paneFocus, setPaneFocus] = useState('tabs');
86
+ const [currentTabIndex, setCurrentTabIndex] = useState(0);
87
+ const [selectedTaskIndex, setSelectedTaskIndex] = useState(0);
88
+ const [tasks, setTasks] = useState({
89
+ inbox: [],
90
+ next: [],
91
+ waiting: [],
92
+ someday: [],
93
+ projects: [],
94
+ done: [],
95
+ });
96
+ const [message, setMessage] = useState(null);
97
+ const [inputValue, setInputValue] = useState('');
98
+ const [selectedProject, setSelectedProject] = useState(null);
99
+ const [projectTasks, setProjectTasks] = useState([]);
100
+ const [taskToLink, setTaskToLink] = useState(null);
101
+ const [projectSelectIndex, setProjectSelectIndex] = useState(0);
102
+ const [selectedTask, setSelectedTask] = useState(null);
103
+ const [taskComments, setTaskComments] = useState([]);
104
+ const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
105
+ const [taskToWaiting, setTaskToWaiting] = useState(null);
106
+ const [taskToDelete, setTaskToDelete] = useState(null);
107
+ const [projectProgress, setProjectProgress] = useState({});
108
+ const [contextFilter, setContextFilter] = useState(null);
109
+ const [contextSelectIndex, setContextSelectIndex] = useState(0);
110
+ const [availableContexts, setAvailableContexts] = useState([]);
111
+ const [searchQuery, setSearchQuery] = useState('');
112
+ const [searchResults, setSearchResults] = useState([]);
113
+ const [searchResultIndex, setSearchResultIndex] = useState(0);
114
+ const terminalWidth = stdout?.columns || 80;
115
+ const leftPaneWidth = 28;
116
+ const rightPaneWidth = terminalWidth - leftPaneWidth - 6;
117
+ const currentTab = TABS[currentTabIndex];
118
+ const currentTasks = mode === 'project-detail' ? projectTasks : tasks[currentTab];
119
+ const loadTasks = useCallback(async () => {
120
+ const db = getDb();
121
+ const newTasks = {
122
+ inbox: [],
123
+ next: [],
124
+ waiting: [],
125
+ someday: [],
126
+ projects: [],
127
+ done: [],
128
+ };
129
+ const statusList = ['inbox', 'next', 'waiting', 'someday', 'done'];
130
+ for (const status of statusList) {
131
+ const oneWeekAgo = new Date();
132
+ oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
133
+ const conditions = [
134
+ eq(schema.tasks.status, status),
135
+ eq(schema.tasks.isProject, false),
136
+ ];
137
+ if (status === 'done') {
138
+ conditions.push(gte(schema.tasks.updatedAt, oneWeekAgo));
139
+ }
140
+ let allTasks = await db
141
+ .select()
142
+ .from(schema.tasks)
143
+ .where(and(...conditions));
144
+ if (contextFilter !== null) {
145
+ if (contextFilter === '') {
146
+ allTasks = allTasks.filter(t => !t.context);
147
+ }
148
+ else {
149
+ allTasks = allTasks.filter(t => t.context === contextFilter);
150
+ }
151
+ }
152
+ newTasks[status] = allTasks;
153
+ }
154
+ newTasks.projects = await db
155
+ .select()
156
+ .from(schema.tasks)
157
+ .where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
158
+ const progress = {};
159
+ for (const project of newTasks.projects) {
160
+ const children = await db
161
+ .select()
162
+ .from(schema.tasks)
163
+ .where(eq(schema.tasks.parentId, project.id));
164
+ const total = children.length;
165
+ const completed = children.filter(t => t.status === 'done').length;
166
+ progress[project.id] = { completed, total };
167
+ }
168
+ setProjectProgress(progress);
169
+ setTasks(newTasks);
170
+ setAvailableContexts(getContexts());
171
+ }, [contextFilter]);
172
+ const loadProjectTasks = useCallback(async (projectId) => {
173
+ const db = getDb();
174
+ const children = await db
175
+ .select()
176
+ .from(schema.tasks)
177
+ .where(eq(schema.tasks.parentId, projectId));
178
+ setProjectTasks(children);
179
+ }, []);
180
+ const loadTaskComments = useCallback(async (taskId) => {
181
+ const db = getDb();
182
+ const comments = await db
183
+ .select()
184
+ .from(schema.comments)
185
+ .where(eq(schema.comments.taskId, taskId));
186
+ setTaskComments(comments);
187
+ }, []);
188
+ // Get all tasks for search (across all statuses)
189
+ const getAllTasks = useCallback(() => {
190
+ const allTasks = [];
191
+ for (const tab of TABS) {
192
+ allTasks.push(...tasks[tab]);
193
+ }
194
+ return allTasks;
195
+ }, [tasks]);
196
+ // Search tasks by query
197
+ const searchTasks = useCallback((query) => {
198
+ if (!query.trim())
199
+ return [];
200
+ const lowerQuery = query.toLowerCase();
201
+ const allTasks = getAllTasks();
202
+ return allTasks.filter(task => task.title.toLowerCase().includes(lowerQuery) ||
203
+ (task.description && task.description.toLowerCase().includes(lowerQuery)));
204
+ }, [getAllTasks]);
205
+ // Handle search query change
206
+ const handleSearchChange = useCallback((value) => {
207
+ setSearchQuery(value);
208
+ const results = searchTasks(value);
209
+ setSearchResults(results);
210
+ setSearchResultIndex(0);
211
+ }, [searchTasks]);
212
+ // Navigate to a task from search results
213
+ const navigateToTask = useCallback((task) => {
214
+ const targetTab = task.isProject ? 'projects' : task.status;
215
+ const tabIndex = TABS.indexOf(targetTab);
216
+ const tabTasks = tasks[targetTab];
217
+ const taskIndex = tabTasks.findIndex(t => t.id === task.id);
218
+ if (tabIndex >= 0 && taskIndex >= 0) {
219
+ setCurrentTabIndex(tabIndex);
220
+ setSelectedTaskIndex(taskIndex);
221
+ setPaneFocus('tasks');
222
+ setMode('normal');
223
+ }
224
+ }, [tasks]);
225
+ useEffect(() => {
226
+ loadTasks();
227
+ }, [loadTasks]);
228
+ const getTabLabel = (tab) => {
229
+ switch (tab) {
230
+ case 'inbox': return i18n.tui.tabInbox;
231
+ case 'next': return i18n.tui.tabNext;
232
+ case 'waiting': return i18n.tui.tabWaiting;
233
+ case 'someday': return i18n.tui.tabSomeday;
234
+ case 'projects': return i18n.tui.tabProjects || 'Projects';
235
+ case 'done': return i18n.tui.tabDone || 'Done';
236
+ }
237
+ };
238
+ const addTask = useCallback(async (title, parentId, context) => {
239
+ if (!title.trim())
240
+ return;
241
+ const now = new Date();
242
+ const taskId = uuidv4();
243
+ const command = new CreateTaskCommand({
244
+ task: {
245
+ id: taskId,
246
+ title: title.trim(),
247
+ status: parentId ? 'next' : 'inbox',
248
+ parentId: parentId || null,
249
+ context: context || null,
250
+ createdAt: now,
251
+ updatedAt: now,
252
+ },
253
+ description: fmt(i18n.tui.added, { title: title.trim() }),
254
+ });
255
+ await history.execute(command);
256
+ setMessage(fmt(i18n.tui.added, { title: title.trim() }));
257
+ await loadTasks();
258
+ }, [i18n.tui.added, loadTasks, history]);
259
+ const markTaskDone = useCallback(async (task) => {
260
+ const command = new MoveTaskCommand({
261
+ taskId: task.id,
262
+ fromStatus: task.status,
263
+ toStatus: 'done',
264
+ fromWaitingFor: task.waitingFor,
265
+ toWaitingFor: null,
266
+ description: fmt(i18n.tui.completed, { title: task.title }),
267
+ });
268
+ await history.execute(command);
269
+ setMessage(fmt(i18n.tui.completed, { title: task.title }));
270
+ await loadTasks();
271
+ }, [i18n.tui.completed, loadTasks, history]);
272
+ const moveTaskToStatus = useCallback(async (task, status) => {
273
+ const command = new MoveTaskCommand({
274
+ taskId: task.id,
275
+ fromStatus: task.status,
276
+ toStatus: status,
277
+ fromWaitingFor: task.waitingFor,
278
+ toWaitingFor: null,
279
+ description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }),
280
+ });
281
+ await history.execute(command);
282
+ setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }));
283
+ await loadTasks();
284
+ }, [i18n.tui.movedTo, i18n.status, loadTasks, history]);
285
+ const moveTaskToWaiting = useCallback(async (task, waitingFor) => {
286
+ const command = new MoveTaskCommand({
287
+ taskId: task.id,
288
+ fromStatus: task.status,
289
+ toStatus: 'waiting',
290
+ fromWaitingFor: task.waitingFor,
291
+ toWaitingFor: waitingFor.trim(),
292
+ description: fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }),
293
+ });
294
+ await history.execute(command);
295
+ setMessage(fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }));
296
+ await loadTasks();
297
+ }, [i18n.tui.movedToWaiting, loadTasks, history]);
298
+ const makeTaskProject = useCallback(async (task) => {
299
+ const command = new ConvertToProjectCommand({
300
+ taskId: task.id,
301
+ originalStatus: task.status,
302
+ description: fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }),
303
+ });
304
+ await history.execute(command);
305
+ setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
306
+ await loadTasks();
307
+ }, [i18n.tui.madeProject, loadTasks, history]);
308
+ const deleteTask = useCallback(async (task) => {
309
+ const command = new DeleteTaskCommand({
310
+ task,
311
+ description: fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }),
312
+ });
313
+ await history.execute(command);
314
+ setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
315
+ await loadTasks();
316
+ }, [i18n.tui.deleted, loadTasks, history]);
317
+ const linkTaskToProject = useCallback(async (task, project) => {
318
+ const command = new LinkTaskCommand({
319
+ taskId: task.id,
320
+ fromParentId: task.parentId,
321
+ toParentId: project.id,
322
+ description: fmt(i18n.tui.linkedToProject || 'Linked "{title}" to {project}', { title: task.title, project: project.title }),
323
+ });
324
+ await history.execute(command);
325
+ setMessage(fmt(i18n.tui.linkedToProject || 'Linked "{title}" to {project}', { title: task.title, project: project.title }));
326
+ await loadTasks();
327
+ }, [i18n.tui.linkedToProject, loadTasks, history]);
328
+ const handleInputSubmit = async (value) => {
329
+ // Handle search mode submit
330
+ if (mode === 'search') {
331
+ if (searchResults.length > 0) {
332
+ const task = searchResults[searchResultIndex];
333
+ navigateToTask(task);
334
+ }
335
+ else {
336
+ setMode('normal');
337
+ }
338
+ setSearchQuery('');
339
+ setSearchResults([]);
340
+ setSearchResultIndex(0);
341
+ return;
342
+ }
343
+ if (mode === 'move-to-waiting' && taskToWaiting) {
344
+ if (value.trim()) {
345
+ await moveTaskToWaiting(taskToWaiting, value);
346
+ }
347
+ setTaskToWaiting(null);
348
+ setMode('normal');
349
+ setInputValue('');
350
+ return;
351
+ }
352
+ if (mode === 'add' || mode === 'add-to-project') {
353
+ if (value.trim()) {
354
+ if (mode === 'add-to-project' && selectedProject) {
355
+ await addTask(value, selectedProject.id, contextFilter && contextFilter !== '' ? contextFilter : null);
356
+ await loadProjectTasks(selectedProject.id);
357
+ setMode('project-detail');
358
+ }
359
+ else {
360
+ await addTask(value, undefined, contextFilter && contextFilter !== '' ? contextFilter : null);
361
+ setMode('normal');
362
+ }
363
+ }
364
+ else {
365
+ setMode(mode === 'add-to-project' ? 'project-detail' : 'normal');
366
+ }
367
+ setInputValue('');
368
+ return;
369
+ }
370
+ if (mode === 'add-context') {
371
+ if (value.trim()) {
372
+ const newContext = value.trim().toLowerCase().replace(/^@/, '');
373
+ addContext(newContext);
374
+ setAvailableContexts(getContexts());
375
+ }
376
+ setInputValue('');
377
+ setContextSelectIndex(0);
378
+ setMode('normal');
379
+ return;
380
+ }
381
+ };
382
+ useInput((input, key) => {
383
+ // Handle help mode - let HelpModal handle its own input
384
+ if (mode === 'help') {
385
+ return;
386
+ }
387
+ // Handle input modes
388
+ if (mode === 'add' || mode === 'add-to-project' || mode === 'add-comment' || mode === 'move-to-waiting' || mode === 'add-context') {
389
+ if (key.escape) {
390
+ setInputValue('');
391
+ if (mode === 'add-to-project') {
392
+ setMode('project-detail');
393
+ }
394
+ else if (mode === 'add-context') {
395
+ setMode('set-context');
396
+ }
397
+ else {
398
+ setMode('normal');
399
+ }
400
+ }
401
+ return;
402
+ }
403
+ // Handle search mode
404
+ if (mode === 'search') {
405
+ if (key.escape) {
406
+ setSearchQuery('');
407
+ setSearchResults([]);
408
+ setSearchResultIndex(0);
409
+ setMode('normal');
410
+ return;
411
+ }
412
+ // Navigate search results with arrow keys, Ctrl+j/k, or Ctrl+n/p
413
+ if (key.downArrow || (key.ctrl && (input === 'j' || input === 'n'))) {
414
+ setSearchResultIndex((prev) => prev < searchResults.length - 1 ? prev + 1 : 0);
415
+ return;
416
+ }
417
+ if (key.upArrow || (key.ctrl && (input === 'k' || input === 'p'))) {
418
+ setSearchResultIndex((prev) => prev > 0 ? prev - 1 : Math.max(0, searchResults.length - 1));
419
+ return;
420
+ }
421
+ // Let TextInput handle other keys
422
+ return;
423
+ }
424
+ // Handle confirm-delete
425
+ if (mode === 'confirm-delete' && taskToDelete) {
426
+ if (input === 'y' || input === 'Y') {
427
+ deleteTask(taskToDelete).then(() => {
428
+ if (selectedTaskIndex >= currentTasks.length - 1) {
429
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
430
+ }
431
+ });
432
+ setTaskToDelete(null);
433
+ setMode('normal');
434
+ return;
435
+ }
436
+ if (input === 'n' || input === 'N' || key.escape) {
437
+ setTaskToDelete(null);
438
+ setMode('normal');
439
+ return;
440
+ }
441
+ return;
442
+ }
443
+ // Handle context-filter mode
444
+ if (mode === 'context-filter') {
445
+ if (key.escape) {
446
+ setContextSelectIndex(0);
447
+ setMode('normal');
448
+ return;
449
+ }
450
+ const contextOptions = ['all', 'none', ...availableContexts];
451
+ if (key.upArrow || input === 'k') {
452
+ setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
453
+ return;
454
+ }
455
+ if (key.downArrow || input === 'j') {
456
+ setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
457
+ return;
458
+ }
459
+ if (key.return) {
460
+ const selected = contextOptions[contextSelectIndex];
461
+ if (selected === 'all')
462
+ setContextFilter(null);
463
+ else if (selected === 'none')
464
+ setContextFilter('');
465
+ else
466
+ setContextFilter(selected);
467
+ setContextSelectIndex(0);
468
+ setMode('normal');
469
+ return;
470
+ }
471
+ return;
472
+ }
473
+ // Handle set-context mode
474
+ if (mode === 'set-context') {
475
+ if (key.escape) {
476
+ setContextSelectIndex(0);
477
+ setMode('normal');
478
+ return;
479
+ }
480
+ const contextOptions = ['clear', ...availableContexts, 'new'];
481
+ if (key.upArrow || input === 'k') {
482
+ setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
483
+ return;
484
+ }
485
+ if (key.downArrow || input === 'j') {
486
+ setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
487
+ return;
488
+ }
489
+ if (key.return && currentTasks.length > 0) {
490
+ const selected = contextOptions[contextSelectIndex];
491
+ if (selected === 'new') {
492
+ setMode('add-context');
493
+ setInputValue('');
494
+ return;
495
+ }
496
+ // TODO: implement setTaskContext
497
+ setContextSelectIndex(0);
498
+ setMode('normal');
499
+ return;
500
+ }
501
+ return;
502
+ }
503
+ // Handle select-project mode
504
+ if (mode === 'select-project') {
505
+ if (key.escape) {
506
+ setTaskToLink(null);
507
+ setProjectSelectIndex(0);
508
+ setMode('normal');
509
+ return;
510
+ }
511
+ if (key.upArrow || input === 'k') {
512
+ setProjectSelectIndex((prev) => (prev > 0 ? prev - 1 : tasks.projects.length - 1));
513
+ return;
514
+ }
515
+ if (key.downArrow || input === 'j') {
516
+ setProjectSelectIndex((prev) => (prev < tasks.projects.length - 1 ? prev + 1 : 0));
517
+ return;
518
+ }
519
+ if (key.return && taskToLink && tasks.projects.length > 0) {
520
+ const project = tasks.projects[projectSelectIndex];
521
+ linkTaskToProject(taskToLink, project);
522
+ setTaskToLink(null);
523
+ setProjectSelectIndex(0);
524
+ setMode('normal');
525
+ return;
526
+ }
527
+ return;
528
+ }
529
+ // Handle project-detail mode
530
+ if (mode === 'project-detail') {
531
+ if (key.escape || key.backspace || input === 'b' || input === 'h' || key.leftArrow) {
532
+ setMode('normal');
533
+ setSelectedProject(null);
534
+ setProjectTasks([]);
535
+ setSelectedTaskIndex(0);
536
+ setPaneFocus('tabs');
537
+ return;
538
+ }
539
+ if (input === 'a') {
540
+ setMode('add-to-project');
541
+ return;
542
+ }
543
+ if (key.upArrow || input === 'k') {
544
+ setSelectedTaskIndex((prev) => (prev > 0 ? prev - 1 : projectTasks.length - 1));
545
+ return;
546
+ }
547
+ if (key.downArrow || input === 'j') {
548
+ setSelectedTaskIndex((prev) => (prev < projectTasks.length - 1 ? prev + 1 : 0));
549
+ return;
550
+ }
551
+ if (input === 'd' && projectTasks.length > 0) {
552
+ const task = projectTasks[selectedTaskIndex];
553
+ markTaskDone(task).then(() => {
554
+ if (selectedProject)
555
+ loadProjectTasks(selectedProject.id);
556
+ });
557
+ return;
558
+ }
559
+ return;
560
+ }
561
+ if (message)
562
+ setMessage(null);
563
+ // Global keys
564
+ if (input === 'q' || (key.ctrl && input === 'c')) {
565
+ exit();
566
+ return;
567
+ }
568
+ if (input === '?') {
569
+ setMode('help');
570
+ return;
571
+ }
572
+ if (input === 'a') {
573
+ setMode('add');
574
+ return;
575
+ }
576
+ if (input === '@') {
577
+ setContextSelectIndex(0);
578
+ setMode('context-filter');
579
+ return;
580
+ }
581
+ if (input === '/') {
582
+ setMode('search');
583
+ return;
584
+ }
585
+ if (input === 'T' && onOpenSettings) {
586
+ onOpenSettings('theme-select');
587
+ return;
588
+ }
589
+ if (input === 'V' && onOpenSettings) {
590
+ onOpenSettings('mode-select');
591
+ return;
592
+ }
593
+ if (input === 'L' && onOpenSettings) {
594
+ onOpenSettings('lang-select');
595
+ return;
596
+ }
597
+ // Number keys for quick tab switch
598
+ if (input >= '1' && input <= '6') {
599
+ const idx = parseInt(input) - 1;
600
+ setCurrentTabIndex(idx);
601
+ setSelectedTaskIndex(0);
602
+ setPaneFocus('tabs');
603
+ return;
604
+ }
605
+ // Navigation
606
+ if (paneFocus === 'tabs') {
607
+ if (key.upArrow || input === 'k') {
608
+ setCurrentTabIndex((prev) => (prev > 0 ? prev - 1 : TABS.length - 1));
609
+ setSelectedTaskIndex(0);
610
+ return;
611
+ }
612
+ if (key.downArrow || input === 'j') {
613
+ setCurrentTabIndex((prev) => (prev < TABS.length - 1 ? prev + 1 : 0));
614
+ setSelectedTaskIndex(0);
615
+ return;
616
+ }
617
+ if (key.rightArrow || input === 'l' || key.return) {
618
+ if (currentTab === 'projects' && currentTasks.length > 0) {
619
+ const project = currentTasks[selectedTaskIndex];
620
+ setSelectedProject(project);
621
+ loadProjectTasks(project.id);
622
+ setMode('project-detail');
623
+ setSelectedTaskIndex(0);
624
+ }
625
+ else if (currentTasks.length > 0) {
626
+ setPaneFocus('tasks');
627
+ }
628
+ return;
629
+ }
630
+ }
631
+ if (paneFocus === 'tasks') {
632
+ if (key.escape || key.leftArrow || input === 'h') {
633
+ setPaneFocus('tabs');
634
+ return;
635
+ }
636
+ if (key.upArrow || input === 'k') {
637
+ setSelectedTaskIndex((prev) => (prev > 0 ? prev - 1 : currentTasks.length - 1));
638
+ return;
639
+ }
640
+ if (key.downArrow || input === 'j') {
641
+ setSelectedTaskIndex((prev) => (prev < currentTasks.length - 1 ? prev + 1 : 0));
642
+ return;
643
+ }
644
+ // Task actions
645
+ if (currentTasks.length > 0) {
646
+ const task = currentTasks[selectedTaskIndex];
647
+ // Mark done
648
+ if (input === 'd' && currentTab !== 'done') {
649
+ markTaskDone(task).then(() => {
650
+ if (selectedTaskIndex >= currentTasks.length - 1) {
651
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
652
+ }
653
+ });
654
+ return;
655
+ }
656
+ // Move to next
657
+ if (input === 'n' && currentTab !== 'next' && currentTab !== 'projects' && currentTab !== 'done') {
658
+ moveTaskToStatus(task, 'next').then(() => {
659
+ if (selectedTaskIndex >= currentTasks.length - 1) {
660
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
661
+ }
662
+ });
663
+ return;
664
+ }
665
+ // Move to someday
666
+ if (input === 's' && currentTab !== 'someday' && currentTab !== 'projects' && currentTab !== 'done') {
667
+ moveTaskToStatus(task, 'someday').then(() => {
668
+ if (selectedTaskIndex >= currentTasks.length - 1) {
669
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
670
+ }
671
+ });
672
+ return;
673
+ }
674
+ // Move to waiting
675
+ if (input === 'w' && currentTab !== 'waiting' && currentTab !== 'projects' && currentTab !== 'done') {
676
+ setTaskToWaiting(task);
677
+ setMode('move-to-waiting');
678
+ return;
679
+ }
680
+ // Move to inbox
681
+ if (input === 'i' && currentTab !== 'inbox' && currentTab !== 'projects' && currentTab !== 'done') {
682
+ moveTaskToStatus(task, 'inbox').then(() => {
683
+ if (selectedTaskIndex >= currentTasks.length - 1) {
684
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
685
+ }
686
+ });
687
+ return;
688
+ }
689
+ // Make project
690
+ if (input === 'p' && currentTab !== 'projects' && currentTab !== 'done') {
691
+ makeTaskProject(task).then(() => {
692
+ if (selectedTaskIndex >= currentTasks.length - 1) {
693
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
694
+ }
695
+ });
696
+ return;
697
+ }
698
+ // Link to project
699
+ if (input === 'P' && currentTab !== 'projects' && currentTab !== 'done' && tasks.projects.length > 0) {
700
+ setTaskToLink(task);
701
+ setProjectSelectIndex(0);
702
+ setMode('select-project');
703
+ return;
704
+ }
705
+ // Delete
706
+ if (input === 'D' && currentTab !== 'projects') {
707
+ setTaskToDelete(task);
708
+ setMode('confirm-delete');
709
+ return;
710
+ }
711
+ }
712
+ // Undo
713
+ if (input === 'u') {
714
+ history.undo().then((didUndo) => {
715
+ if (didUndo) {
716
+ setMessage(fmt(i18n.tui.undone, { action: history.undoDescription || '' }));
717
+ loadTasks();
718
+ }
719
+ else {
720
+ setMessage(i18n.tui.nothingToUndo);
721
+ }
722
+ });
723
+ return;
724
+ }
725
+ }
726
+ // Refresh
727
+ if (input === 'r' && !key.ctrl) {
728
+ loadTasks();
729
+ setMessage(i18n.tui.refreshed);
730
+ return;
731
+ }
732
+ });
733
+ const tursoEnabled = isTursoEnabled();
734
+ // Get parent project for display
735
+ const getParentProject = (parentId) => {
736
+ if (!parentId)
737
+ return undefined;
738
+ return tasks.projects.find(p => p.id === parentId);
739
+ };
740
+ // Help modal overlay
741
+ if (mode === 'help') {
742
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(HelpModal, { onClose: () => setMode('normal') }) }));
743
+ }
744
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: "FLOQ" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" v", VERSION] }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : ' [LOCAL]' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? 'none' : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: "?=help q=quit" })] }), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
745
+ const label = ctx === 'all' ? 'All' : ctx === 'none' ? 'No context' : `@${ctx}`;
746
+ return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
747
+ }) })] })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(TitledBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'GTD', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
748
+ const isSelected = index === currentTabIndex;
749
+ const count = tasks[tab].length;
750
+ return (_jsxs(Text, { color: isSelected && paneFocus === 'tabs' ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? '▶ ' : ' ', index + 1, ":", getTabLabel(tab), " (", count, ")"] }, tab));
751
+ })) }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TitledBoxInline, { title: mode === 'project-detail' ? (i18n.tui.projectTasks || 'Tasks') : getTabLabel(currentTab), width: rightPaneWidth, minHeight: 12, isActive: paneFocus === 'tasks' || mode === 'project-detail', children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
752
+ const isSelected = (paneFocus === 'tasks' || mode === 'project-detail') && index === selectedTaskIndex;
753
+ const parentProject = getParentProject(task.parentId);
754
+ const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
755
+ // Calculate available width for title
756
+ const prefix = isSelected ? '▶ ' : ' ';
757
+ const suffix = [
758
+ task.waitingFor ? ` (${task.waitingFor})` : '',
759
+ task.context ? ` @${task.context}` : '',
760
+ parentProject ? ` [${parentProject.title}]` : '',
761
+ progress ? ` [${progress.completed}/${progress.total}]` : '',
762
+ ].join('');
763
+ const availableWidth = rightPaneWidth - 6 - getDisplayWidth(prefix) - getDisplayWidth(suffix);
764
+ const displayTitle = truncateString(task.title, availableWidth);
765
+ return (_jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [prefix, displayTitle, task.waitingFor && _jsxs(Text, { color: theme.colors.muted, children: [" (", task.waitingFor, ")"] }), task.context && _jsxs(Text, { color: theme.colors.muted, children: [" @", task.context] }), parentProject && _jsxs(Text, { color: theme.colors.muted, children: [" [", parentProject.title, "]"] }), progress && _jsxs(Text, { color: theme.colors.muted, children: [" [", progress.completed, "/", progress.total, "]"] })] }, task.id));
766
+ })) }) })] })), (mode === 'add' || mode === 'add-to-project') && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: mode === 'add-to-project' && selectedProject
767
+ ? `${i18n.tui.newTask}[${selectedProject.title}] `
768
+ : i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder })] })), mode === 'move-to-waiting' && taskToWaiting && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.waitingFor }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "New context: " }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "Enter context name..." })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: mode === 'project-detail'
769
+ ? 'j/k=select a=add d=done Esc/b=back /=search'
770
+ : paneFocus === 'tabs'
771
+ ? 'j/k=select l/Enter=tasks 1-6=tab a=add @=filter /=search'
772
+ : 'j/k=select h/Esc=back d=done n=next s=someday w=wait i=inbox p=project P=link D=delete u=undo /=search' }) })] }));
773
+ }