floq 0.4.0 → 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/README.ja.md
CHANGED
|
@@ -7,10 +7,10 @@ MS-DOSスタイルのテーマを備えたターミナルベースのGTD(Getti
|
|
|
7
7
|
## 特徴
|
|
8
8
|
|
|
9
9
|
- **TUIインターフェース**: Ink(CLI用React)で構築されたインタラクティブなターミナルUI
|
|
10
|
-
- **GTDワークフロー**: Inbox、Next Actions、Waiting For、Someday/Maybe、Done
|
|
10
|
+
- **GTDワークフロー**: Inbox、Next Actions、Waiting For、Someday/Maybe、Done(過去7日間表示)
|
|
11
11
|
- **カンバンモード**: 3カラムのカンバンボード表示(TODO、Doing、Done)
|
|
12
12
|
- **プロジェクト**: タスクをプロジェクトに整理(進捗バー表示付き)
|
|
13
|
-
- **コンテキスト**: タスクにコンテキスト(@work、@home
|
|
13
|
+
- **コンテキスト**: タスクにコンテキスト(@work、@homeなど)を設定してフィルタリング。タスク追加時は現在のフィルターを自動継承
|
|
14
14
|
- **タスク検索**: `/` キーで全タスクを素早く検索
|
|
15
15
|
- **コメント**: タスクにメモやコメントを追加
|
|
16
16
|
- **クラウド同期**: [Turso](https://turso.tech/)のembedded replicasによるオプションの同期機能
|
package/README.md
CHANGED
|
@@ -7,10 +7,10 @@ A terminal-based GTD (Getting Things Done) task manager with MS-DOS style themes
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **TUI Interface**: Interactive terminal UI built with Ink (React for CLI)
|
|
10
|
-
- **GTD Workflow**: Inbox, Next Actions, Waiting For, Someday/Maybe, Done
|
|
10
|
+
- **GTD Workflow**: Inbox, Next Actions, Waiting For, Someday/Maybe, Done (shows last 7 days)
|
|
11
11
|
- **Kanban Mode**: 3-column kanban board view (TODO, Doing, Done)
|
|
12
12
|
- **Projects**: Organize tasks into projects with progress tracking
|
|
13
|
-
- **Contexts**: Tag tasks with contexts (@work, @home, etc.) and filter by context
|
|
13
|
+
- **Contexts**: Tag tasks with contexts (@work, @home, etc.) and filter by context. New tasks inherit the active context filter
|
|
14
14
|
- **Task Search**: Quick search across all tasks with `/`
|
|
15
15
|
- **Comments**: Add notes and comments to tasks
|
|
16
16
|
- **Cloud Sync**: Optional sync with [Turso](https://turso.tech/) using embedded replicas
|
package/dist/commands/list.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq } from 'drizzle-orm';
|
|
1
|
+
import { eq, and, gte } from 'drizzle-orm';
|
|
2
2
|
import { getDb, schema } from '../db/index.js';
|
|
3
3
|
import { t, fmt } from '../i18n/index.js';
|
|
4
4
|
export async function listTasks(status) {
|
|
@@ -11,10 +11,22 @@ export async function listTasks(status) {
|
|
|
11
11
|
console.error(fmt(i18n.commands.list.validStatuses, { statuses: validStatuses.join(', ') }));
|
|
12
12
|
process.exit(1);
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
// For done status, only show tasks from the last week by default
|
|
15
|
+
let tasks;
|
|
16
|
+
if (status === 'done') {
|
|
17
|
+
const oneWeekAgo = new Date();
|
|
18
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
19
|
+
tasks = await db
|
|
20
|
+
.select()
|
|
21
|
+
.from(schema.tasks)
|
|
22
|
+
.where(and(eq(schema.tasks.status, 'done'), gte(schema.tasks.updatedAt, oneWeekAgo)));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
tasks = await db
|
|
26
|
+
.select()
|
|
27
|
+
.from(schema.tasks)
|
|
28
|
+
.where(eq(schema.tasks.status, status));
|
|
29
|
+
}
|
|
18
30
|
console.log(`\n${i18n.status[status]} (${tasks.length})`);
|
|
19
31
|
console.log('─'.repeat(40));
|
|
20
32
|
if (tasks.length === 0) {
|
package/dist/ui/App.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useState, useEffect, useCallback } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useApp } from 'ink';
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
|
-
import { eq, and } from 'drizzle-orm';
|
|
5
|
+
import { eq, and, gte } from 'drizzle-orm';
|
|
6
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
7
7
|
import { TaskItem } from './components/TaskItem.js';
|
|
8
8
|
import { HelpModal } from './components/HelpModal.js';
|
|
@@ -105,10 +105,21 @@ function AppContent({ onOpenSettings }) {
|
|
|
105
105
|
// Load all tasks (including project children) by status
|
|
106
106
|
const statusList = ['inbox', 'next', 'waiting', 'someday', 'done'];
|
|
107
107
|
for (const status of statusList) {
|
|
108
|
+
// For done tasks, only show those completed in the last week
|
|
109
|
+
const oneWeekAgo = new Date();
|
|
110
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
111
|
+
const conditions = [
|
|
112
|
+
eq(schema.tasks.status, status),
|
|
113
|
+
eq(schema.tasks.isProject, false),
|
|
114
|
+
];
|
|
115
|
+
// Filter done tasks to last week only
|
|
116
|
+
if (status === 'done') {
|
|
117
|
+
conditions.push(gte(schema.tasks.updatedAt, oneWeekAgo));
|
|
118
|
+
}
|
|
108
119
|
let allTasks = await db
|
|
109
120
|
.select()
|
|
110
121
|
.from(schema.tasks)
|
|
111
|
-
.where(and(
|
|
122
|
+
.where(and(...conditions));
|
|
112
123
|
// Apply context filter
|
|
113
124
|
if (contextFilter !== null) {
|
|
114
125
|
if (contextFilter === '') {
|
|
@@ -205,7 +216,7 @@ function AppContent({ onOpenSettings }) {
|
|
|
205
216
|
setMode('normal');
|
|
206
217
|
}
|
|
207
218
|
}, [tasks]);
|
|
208
|
-
const addTask = useCallback(async (title, parentId) => {
|
|
219
|
+
const addTask = useCallback(async (title, parentId, context) => {
|
|
209
220
|
if (!title.trim())
|
|
210
221
|
return;
|
|
211
222
|
const now = new Date();
|
|
@@ -216,6 +227,7 @@ function AppContent({ onOpenSettings }) {
|
|
|
216
227
|
title: title.trim(),
|
|
217
228
|
status: parentId ? 'next' : 'inbox',
|
|
218
229
|
parentId: parentId || null,
|
|
230
|
+
context: context || null,
|
|
219
231
|
createdAt: now,
|
|
220
232
|
updatedAt: now,
|
|
221
233
|
},
|
|
@@ -298,12 +310,13 @@ function AppContent({ onOpenSettings }) {
|
|
|
298
310
|
setMode('task-detail');
|
|
299
311
|
}
|
|
300
312
|
else if (mode === 'add-to-project' && selectedProject) {
|
|
301
|
-
await addTask(value, selectedProject.id);
|
|
313
|
+
await addTask(value, selectedProject.id, contextFilter && contextFilter !== '' ? contextFilter : null);
|
|
302
314
|
await loadProjectTasks(selectedProject.id);
|
|
303
315
|
setMode('project-detail');
|
|
304
316
|
}
|
|
305
317
|
else {
|
|
306
|
-
|
|
318
|
+
// Pass contextFilter when adding a task, so it inherits the current filter context
|
|
319
|
+
await addTask(value, undefined, contextFilter && contextFilter !== '' ? contextFilter : null);
|
|
307
320
|
setMode('normal');
|
|
308
321
|
}
|
|
309
322
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useState, useEffect, useCallback } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useApp } from 'ink';
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
|
-
import { eq, and, inArray } from 'drizzle-orm';
|
|
5
|
+
import { eq, and, inArray, gte } from 'drizzle-orm';
|
|
6
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
7
7
|
import { KanbanColumn } from './KanbanColumn.js';
|
|
8
8
|
import { HelpModal } from './HelpModal.js';
|
|
@@ -72,11 +72,13 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
72
72
|
.select()
|
|
73
73
|
.from(schema.tasks)
|
|
74
74
|
.where(and(inArray(schema.tasks.status, ['next', 'waiting']), eq(schema.tasks.isProject, false)));
|
|
75
|
-
// Done: done (non-project tasks)
|
|
75
|
+
// Done: done (non-project tasks) - only show last week
|
|
76
|
+
const oneWeekAgo = new Date();
|
|
77
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
76
78
|
let doneTasks = await db
|
|
77
79
|
.select()
|
|
78
80
|
.from(schema.tasks)
|
|
79
|
-
.where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)));
|
|
81
|
+
.where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false), gte(schema.tasks.updatedAt, oneWeekAgo)));
|
|
80
82
|
// Load projects for linking
|
|
81
83
|
const projectTasks = await db
|
|
82
84
|
.select()
|
|
@@ -189,7 +191,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
189
191
|
setMode('normal');
|
|
190
192
|
}
|
|
191
193
|
}, [tasks]);
|
|
192
|
-
const addTask = useCallback(async (title) => {
|
|
194
|
+
const addTask = useCallback(async (title, context) => {
|
|
193
195
|
if (!title.trim())
|
|
194
196
|
return;
|
|
195
197
|
const now = new Date();
|
|
@@ -199,6 +201,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
199
201
|
id: taskId,
|
|
200
202
|
title: title.trim(),
|
|
201
203
|
status: 'inbox', // New tasks go to inbox (which maps to TODO)
|
|
204
|
+
context: context || null,
|
|
202
205
|
createdAt: now,
|
|
203
206
|
updatedAt: now,
|
|
204
207
|
},
|
|
@@ -249,7 +252,8 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
249
252
|
return;
|
|
250
253
|
}
|
|
251
254
|
if (value.trim()) {
|
|
252
|
-
|
|
255
|
+
// Pass contextFilter when adding a task, so it inherits the current filter context
|
|
256
|
+
await addTask(value, contextFilter && contextFilter !== '' ? contextFilter : null);
|
|
253
257
|
}
|
|
254
258
|
setInputValue('');
|
|
255
259
|
setMode('normal');
|
|
@@ -711,7 +715,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
711
715
|
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), (mode === 'task-detail' || mode === 'add-comment' || mode === 'select-project') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), selectedTask.dueDate && (_jsxs(Text, { color: theme.colors.textMuted, children: ["Due: ", selectedTask.dueDate.toLocaleDateString()] }))] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
712
716
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
713
717
|
return (_jsxs(Box, { flexDirection: "row", marginBottom: 1, children: [_jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.textMuted, children: isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.textMuted, children: ["[", comment.createdAt.toLocaleString(), "]"] }), _jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: comment.content })] })] }, comment.id));
|
|
714
|
-
})) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] }))] })) : mode === 'search' ? (_jsx(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })) })) : mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.filter || 'Filter by context' }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
|
|
718
|
+
})) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] }))] })) : mode === 'search' ? (_jsx(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery, viewMode: "kanban" })) })) : mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.filter || 'Filter by context' }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
|
|
715
719
|
const label = ctx === 'all'
|
|
716
720
|
? (i18n.tui.context?.all || 'All')
|
|
717
721
|
: ctx === 'none'
|
|
@@ -4,6 +4,7 @@ interface SearchResultsProps {
|
|
|
4
4
|
results: Task[];
|
|
5
5
|
selectedIndex: number;
|
|
6
6
|
query: string;
|
|
7
|
+
viewMode?: 'gtd' | 'kanban';
|
|
7
8
|
}
|
|
8
|
-
export declare function SearchResults({ results, selectedIndex, query }: SearchResultsProps): React.ReactElement;
|
|
9
|
+
export declare function SearchResults({ results, selectedIndex, query, viewMode }: SearchResultsProps): React.ReactElement;
|
|
9
10
|
export {};
|
|
@@ -2,7 +2,17 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { t } from '../../i18n/index.js';
|
|
4
4
|
import { useTheme } from '../theme/index.js';
|
|
5
|
-
|
|
5
|
+
// Map GTD status to Kanban column
|
|
6
|
+
function getKanbanColumn(status) {
|
|
7
|
+
if (status === 'inbox' || status === 'someday') {
|
|
8
|
+
return 'todo';
|
|
9
|
+
}
|
|
10
|
+
if (status === 'next' || status === 'waiting') {
|
|
11
|
+
return 'doing';
|
|
12
|
+
}
|
|
13
|
+
return 'done';
|
|
14
|
+
}
|
|
15
|
+
export function SearchResults({ results, selectedIndex, query, viewMode = 'gtd' }) {
|
|
6
16
|
const i18n = t();
|
|
7
17
|
const theme = useTheme();
|
|
8
18
|
const search = i18n.tui.search;
|
|
@@ -12,7 +22,17 @@ export function SearchResults({ results, selectedIndex, query }) {
|
|
|
12
22
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, paddingY: 1, minHeight: 5, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: ["[", search.resultsTitle, "] (", results.length, ")"] }) }), results.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: search.noResults })) : (results.slice(0, 10).map((task, index) => {
|
|
13
23
|
const isSelected = index === selectedIndex;
|
|
14
24
|
const shortId = task.id.slice(0, 8);
|
|
15
|
-
|
|
25
|
+
let displayLabel;
|
|
26
|
+
if (task.isProject) {
|
|
27
|
+
displayLabel = i18n.tui.keyBar.project;
|
|
28
|
+
}
|
|
29
|
+
else if (viewMode === 'kanban') {
|
|
30
|
+
const kanbanColumn = getKanbanColumn(task.status);
|
|
31
|
+
displayLabel = i18n.kanban[kanbanColumn];
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
displayLabel = i18n.status[task.status];
|
|
35
|
+
}
|
|
16
36
|
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, "[", shortId, "] ", task.title, _jsxs(Text, { color: theme.colors.textMuted, children: [" (", displayLabel, ")"] }), task.waitingFor && (_jsxs(Text, { color: theme.colors.statusWaiting, children: [" - ", task.waitingFor] }))] }) }, task.id));
|
|
17
37
|
})), results.length > 10 && (_jsxs(Text, { color: theme.colors.textMuted, italic: true, children: ["... and ", results.length - 10, " more"] }))] }));
|
|
18
38
|
}
|