floq 1.3.3 → 1.4.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 +8 -0
- package/README.md +8 -0
- package/dist/cli.js +10 -0
- package/dist/commands/insights.d.ts +1 -0
- package/dist/commands/insights.js +283 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +6 -0
- package/dist/db/index.js +24 -0
- package/dist/db/schema.d.ts +37 -0
- package/dist/db/schema.js +2 -0
- package/dist/i18n/en.d.ts +82 -0
- package/dist/i18n/en.js +57 -5
- package/dist/i18n/ja.js +57 -5
- package/dist/ui/App.js +110 -7
- package/dist/ui/components/GtdDQ.js +117 -9
- package/dist/ui/components/GtdMario.js +113 -8
- package/dist/ui/components/HelpModal.js +8 -0
- package/dist/ui/components/InsightsModal.d.ts +6 -0
- package/dist/ui/components/InsightsModal.js +272 -0
- package/dist/ui/components/KanbanBoard.js +119 -11
- package/dist/ui/components/KanbanColumn.js +7 -2
- package/dist/ui/components/KanbanDQ.js +120 -11
- package/dist/ui/components/KanbanMario.js +125 -11
- package/dist/ui/components/TaskItem.js +7 -2
- package/dist/ui/history/commands/SetEffortCommand.d.ts +20 -0
- package/dist/ui/history/commands/SetEffortCommand.js +37 -0
- package/dist/ui/history/commands/SetFocusCommand.d.ts +20 -0
- package/dist/ui/history/commands/SetFocusCommand.js +37 -0
- package/dist/ui/history/commands/index.d.ts +2 -0
- package/dist/ui/history/commands/index.js +2 -0
- package/dist/ui/history/index.d.ts +1 -1
- package/dist/ui/history/index.js +1 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -13,6 +13,8 @@ MS-DOSスタイルのテーマを備えたターミナルベースのGTD(Getti
|
|
|
13
13
|
- **カンバンモード**: 3カラムのカンバンボード表示(TODO、Doing、Done)
|
|
14
14
|
- **プロジェクト**: タスクをプロジェクトに整理(進捗バー表示付き)
|
|
15
15
|
- **コンテキスト**: タスクにコンテキスト(@work、@homeなど)を設定してフィルタリング。タスク追加時は現在のフィルターを自動継承
|
|
16
|
+
- **集中モード**: タスクに「今日やる」マーク(★)を付け、集中タスクだけに絞り込み表示
|
|
17
|
+
- **作業量**: タスクに作業量(S/M/L)を設定し、空き時間に合ったタスクを選びやすく
|
|
16
18
|
- **タスク検索**: `/` キーで全タスクを素早く検索
|
|
17
19
|
- **コメント**: タスクにメモやコメントを追加
|
|
18
20
|
- **クラウド同期**: [Turso](https://turso.tech/)のembedded replicasによるオプションの同期機能
|
|
@@ -64,6 +66,9 @@ floq
|
|
|
64
66
|
| `P` | プロジェクトに紐付け |
|
|
65
67
|
| `c` | コンテキスト設定 |
|
|
66
68
|
| `@` | コンテキストでフィルター |
|
|
69
|
+
| `g` | 選択タスクの集中マーク(★)ON/OFF |
|
|
70
|
+
| `G` | 集中フィルター切替(集中タスクのみ表示) |
|
|
71
|
+
| `E` | 作業量設定(S/M/L) |
|
|
67
72
|
| `Enter` | タスク詳細を開く / プロジェクトを開く |
|
|
68
73
|
| `Esc/b` | 戻る |
|
|
69
74
|
| `/` | タスク検索 |
|
|
@@ -115,6 +120,9 @@ floq
|
|
|
115
120
|
| `Backspace` | タスクを左に移動(←) |
|
|
116
121
|
| `c` | コンテキスト設定 |
|
|
117
122
|
| `@` | コンテキストでフィルター |
|
|
123
|
+
| `g` | 選択タスクの集中マーク(★)ON/OFF |
|
|
124
|
+
| `G` | 集中フィルター切替(集中タスクのみ表示) |
|
|
125
|
+
| `E` | 作業量設定(S/M/L) |
|
|
118
126
|
| `Enter` | タスク詳細を開く |
|
|
119
127
|
| `/` | タスク検索 |
|
|
120
128
|
| `r` | 更新 |
|
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ A terminal-based GTD (Getting Things Done) task manager with MS-DOS style themes
|
|
|
13
13
|
- **Kanban Mode**: 3-column kanban board view (TODO, Doing, Done)
|
|
14
14
|
- **Projects**: Organize tasks into projects with progress tracking
|
|
15
15
|
- **Contexts**: Tag tasks with contexts (@work, @home, etc.) and filter by context. New tasks inherit the active context filter
|
|
16
|
+
- **Focus Mode**: Mark tasks as "today's focus" (★) and filter to show only focused tasks
|
|
17
|
+
- **Effort Size**: Tag tasks with effort size (S/M/L) to pick the right task for your available time
|
|
16
18
|
- **Task Search**: Quick search across all tasks with `/`
|
|
17
19
|
- **Comments**: Add notes and comments to tasks
|
|
18
20
|
- **Cloud Sync**: Optional sync with [Turso](https://turso.tech/) using embedded replicas
|
|
@@ -64,6 +66,9 @@ floq
|
|
|
64
66
|
| `P` | Link to project |
|
|
65
67
|
| `c` | Set context |
|
|
66
68
|
| `@` | Filter by context |
|
|
69
|
+
| `g` | Toggle focus (★) on selected task |
|
|
70
|
+
| `G` | Toggle focus filter (show only focused tasks) |
|
|
71
|
+
| `E` | Set effort size (S/M/L) |
|
|
67
72
|
| `Enter` | Open task detail / Open project |
|
|
68
73
|
| `Esc/b` | Back |
|
|
69
74
|
| `/` | Search tasks |
|
|
@@ -115,6 +120,9 @@ floq
|
|
|
115
120
|
| `Backspace` | Move task left (←) |
|
|
116
121
|
| `c` | Set context |
|
|
117
122
|
| `@` | Filter by context |
|
|
123
|
+
| `g` | Toggle focus (★) on selected task |
|
|
124
|
+
| `G` | Toggle focus filter (show only focused tasks) |
|
|
125
|
+
| `E` | Set effort size (S/M/L) |
|
|
118
126
|
| `Enter` | Open task detail |
|
|
119
127
|
| `/` | Search tasks |
|
|
120
128
|
| `r` | Refresh |
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { addProject, listProjectsCommand, showProject, completeProject, } from '
|
|
|
10
10
|
import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, enableTurso, clearTurso, syncCommand, resetDatabase, setSplashCommand, showSplash, showDateFormatCommand, setDateFormatCommand } from './commands/config.js';
|
|
11
11
|
import { addComment, listComments } from './commands/comment.js';
|
|
12
12
|
import { listContexts, addContextCommand, removeContextCommand } from './commands/context.js';
|
|
13
|
+
import { showInsights } from './commands/insights.js';
|
|
13
14
|
import { runSetupWizard } from './commands/setup.js';
|
|
14
15
|
import { addCalendar, removeCalendar, showCalendar, syncCalendar, enableCalendar, disableCalendar, configOAuthClient, loginCalendar, logoutCalendar, selectCalendar } from './commands/calendar.js';
|
|
15
16
|
import { VERSION } from './version.js';
|
|
@@ -211,6 +212,15 @@ configCmd
|
|
|
211
212
|
console.log(`Pomodoro focus mode: ${enabled ? 'on' : 'off'}`);
|
|
212
213
|
}
|
|
213
214
|
});
|
|
215
|
+
// Insights command
|
|
216
|
+
program
|
|
217
|
+
.command('insights')
|
|
218
|
+
.description('Show task completion insights and statistics')
|
|
219
|
+
.option('-w, --weeks <n>', 'Number of weeks to analyze (default: 2)', '2')
|
|
220
|
+
.action(async (options) => {
|
|
221
|
+
const weeks = Math.max(1, parseInt(options.weeks ?? '2', 10) || 2);
|
|
222
|
+
await showInsights(weeks);
|
|
223
|
+
});
|
|
214
224
|
// Sync command
|
|
215
225
|
program
|
|
216
226
|
.command('sync')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function showInsights(weeks: number): Promise<void>;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { eq, and, gte } from 'drizzle-orm';
|
|
2
|
+
import { getDb, schema } from '../db/index.js';
|
|
3
|
+
import { t, fmt } from '../i18n/index.js';
|
|
4
|
+
function getWeekStart(date) {
|
|
5
|
+
const d = new Date(date);
|
|
6
|
+
const day = d.getDay();
|
|
7
|
+
d.setDate(d.getDate() - day);
|
|
8
|
+
d.setHours(0, 0, 0, 0);
|
|
9
|
+
return d;
|
|
10
|
+
}
|
|
11
|
+
function formatDate(date) {
|
|
12
|
+
return date.toLocaleDateString();
|
|
13
|
+
}
|
|
14
|
+
function bar(count, max, width = 20) {
|
|
15
|
+
if (max === 0)
|
|
16
|
+
return '';
|
|
17
|
+
const filled = Math.round((count / max) * width);
|
|
18
|
+
return '█'.repeat(filled);
|
|
19
|
+
}
|
|
20
|
+
function stringWidth(str) {
|
|
21
|
+
let width = 0;
|
|
22
|
+
for (const char of str) {
|
|
23
|
+
const code = char.codePointAt(0) || 0;
|
|
24
|
+
// CJK characters and fullwidth forms take 2 columns
|
|
25
|
+
if ((code >= 0x1100 && code <= 0x115F) ||
|
|
26
|
+
(code >= 0x2E80 && code <= 0x303E) ||
|
|
27
|
+
(code >= 0x3040 && code <= 0x9FFF) ||
|
|
28
|
+
(code >= 0xAC00 && code <= 0xD7AF) ||
|
|
29
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
30
|
+
(code >= 0xFE30 && code <= 0xFE6F) ||
|
|
31
|
+
(code >= 0xFF01 && code <= 0xFF60) ||
|
|
32
|
+
(code >= 0xFFE0 && code <= 0xFFE6) ||
|
|
33
|
+
(code >= 0x20000 && code <= 0x2FFFF)) {
|
|
34
|
+
width += 2;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
width += 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return width;
|
|
41
|
+
}
|
|
42
|
+
function padEnd(str, targetWidth) {
|
|
43
|
+
const currentWidth = stringWidth(str);
|
|
44
|
+
const padding = Math.max(0, targetWidth - currentWidth);
|
|
45
|
+
return str + ' '.repeat(padding);
|
|
46
|
+
}
|
|
47
|
+
function groupByWeek(tasks, weeks) {
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const result = [];
|
|
50
|
+
for (let i = 0; i < weeks; i++) {
|
|
51
|
+
const weekStart = getWeekStart(now);
|
|
52
|
+
weekStart.setDate(weekStart.getDate() - i * 7);
|
|
53
|
+
const weekEnd = new Date(weekStart);
|
|
54
|
+
weekEnd.setDate(weekEnd.getDate() + 7);
|
|
55
|
+
const weekTasks = tasks.filter(task => {
|
|
56
|
+
const updated = task.updatedAt;
|
|
57
|
+
return updated >= weekStart && updated < weekEnd;
|
|
58
|
+
});
|
|
59
|
+
result.push({
|
|
60
|
+
weekStart,
|
|
61
|
+
weekLabel: formatDate(weekStart),
|
|
62
|
+
tasks: weekTasks,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
function groupByDayOfWeek(tasks) {
|
|
68
|
+
const days = new Map();
|
|
69
|
+
for (let i = 0; i < 7; i++)
|
|
70
|
+
days.set(i, 0);
|
|
71
|
+
for (const task of tasks) {
|
|
72
|
+
const day = task.updatedAt.getDay();
|
|
73
|
+
days.set(day, (days.get(day) || 0) + 1);
|
|
74
|
+
}
|
|
75
|
+
return days;
|
|
76
|
+
}
|
|
77
|
+
function calculateDistribution(tasks, getter, noValueLabel) {
|
|
78
|
+
const map = new Map();
|
|
79
|
+
for (const task of tasks) {
|
|
80
|
+
const key = getter(task) || noValueLabel;
|
|
81
|
+
map.set(key, (map.get(key) || 0) + 1);
|
|
82
|
+
}
|
|
83
|
+
return Array.from(map.entries())
|
|
84
|
+
.map(([label, count]) => ({
|
|
85
|
+
label,
|
|
86
|
+
count,
|
|
87
|
+
percentage: Math.round((count / tasks.length) * 100),
|
|
88
|
+
}))
|
|
89
|
+
.sort((a, b) => b.count - a.count);
|
|
90
|
+
}
|
|
91
|
+
function calculateAverageCompletionDays(tasks) {
|
|
92
|
+
if (tasks.length === 0)
|
|
93
|
+
return null;
|
|
94
|
+
let totalMs = 0;
|
|
95
|
+
let validCount = 0;
|
|
96
|
+
for (const task of tasks) {
|
|
97
|
+
const created = task.createdAt.getTime();
|
|
98
|
+
const updated = task.updatedAt.getTime();
|
|
99
|
+
if (updated > created) {
|
|
100
|
+
totalMs += updated - created;
|
|
101
|
+
validCount++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (validCount === 0)
|
|
105
|
+
return null;
|
|
106
|
+
return totalMs / validCount / (1000 * 60 * 60 * 24);
|
|
107
|
+
}
|
|
108
|
+
export async function showInsights(weeks) {
|
|
109
|
+
const db = getDb();
|
|
110
|
+
const i18n = t();
|
|
111
|
+
const ins = i18n.commands.insights;
|
|
112
|
+
const isJa = ins?.title === 'タスクインサイト';
|
|
113
|
+
// Labels with fallbacks
|
|
114
|
+
const l = {
|
|
115
|
+
title: ins?.title || 'Task Insights',
|
|
116
|
+
period: ins?.period || 'Period',
|
|
117
|
+
weeklyCompletion: ins?.weeklyCompletion || 'Weekly Completion',
|
|
118
|
+
weekLabel: ins?.weekLabel || 'Week of {date}',
|
|
119
|
+
tasksCompleted: ins?.tasksCompleted || '{count} tasks completed',
|
|
120
|
+
dailyBreakdown: ins?.dailyBreakdown || 'Daily Breakdown',
|
|
121
|
+
currentStatus: ins?.currentStatus || 'Current Status',
|
|
122
|
+
byContext: ins?.byContext || 'By Context',
|
|
123
|
+
byEffort: ins?.byEffort || 'By Effort',
|
|
124
|
+
noContext: ins?.noContext || 'No context',
|
|
125
|
+
noEffort: ins?.noEffort || 'No effort set',
|
|
126
|
+
projectProgress: ins?.projectProgress || 'Project Progress',
|
|
127
|
+
activeProjects: ins?.activeProjects || 'Active Projects',
|
|
128
|
+
tasksRemaining: ins?.tasksRemaining || '{count} tasks remaining',
|
|
129
|
+
averageCompletion: ins?.averageCompletion || 'Average Completion Time',
|
|
130
|
+
daysAverage: ins?.daysAverage || '{days} days',
|
|
131
|
+
noData: ins?.noData || 'No completed tasks in this period',
|
|
132
|
+
total: ins?.total || 'Total',
|
|
133
|
+
andMore: ins?.andMore || ' ... and {count} more',
|
|
134
|
+
};
|
|
135
|
+
// Calculate date range
|
|
136
|
+
const now = new Date();
|
|
137
|
+
const startDate = getWeekStart(now);
|
|
138
|
+
startDate.setDate(startDate.getDate() - (weeks - 1) * 7);
|
|
139
|
+
const endDate = new Date(now);
|
|
140
|
+
endDate.setHours(23, 59, 59, 999);
|
|
141
|
+
// Query all completed tasks in the period
|
|
142
|
+
const completedTasks = await db
|
|
143
|
+
.select()
|
|
144
|
+
.from(schema.tasks)
|
|
145
|
+
.where(and(eq(schema.tasks.status, 'done'), gte(schema.tasks.updatedAt, startDate), eq(schema.tasks.isProject, false)));
|
|
146
|
+
// Header
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(l.title);
|
|
149
|
+
console.log('═'.repeat(40));
|
|
150
|
+
console.log(`${l.period}: ${formatDate(startDate)} ~ ${formatDate(endDate)}`);
|
|
151
|
+
if (completedTasks.length === 0) {
|
|
152
|
+
console.log();
|
|
153
|
+
console.log(` ${l.noData}`);
|
|
154
|
+
console.log();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Weekly Completion
|
|
158
|
+
console.log();
|
|
159
|
+
console.log(l.weeklyCompletion);
|
|
160
|
+
console.log('─'.repeat(40));
|
|
161
|
+
const weeklyStats = groupByWeek(completedTasks, weeks);
|
|
162
|
+
for (const week of weeklyStats) {
|
|
163
|
+
const weekLabel = fmt(l.weekLabel, { date: week.weekLabel });
|
|
164
|
+
const countLabel = fmt(l.tasksCompleted, { count: week.tasks.length });
|
|
165
|
+
console.log(`${weekLabel}: ${countLabel}`);
|
|
166
|
+
const maxShow = 10;
|
|
167
|
+
for (let i = 0; i < Math.min(week.tasks.length, maxShow); i++) {
|
|
168
|
+
const task = week.tasks[i];
|
|
169
|
+
const shortId = task.id.slice(0, 8);
|
|
170
|
+
console.log(` [${shortId}] ${task.title}`);
|
|
171
|
+
}
|
|
172
|
+
if (week.tasks.length > maxShow) {
|
|
173
|
+
console.log(fmt(l.andMore, { count: week.tasks.length - maxShow }));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Daily Breakdown
|
|
177
|
+
console.log();
|
|
178
|
+
console.log(l.dailyBreakdown);
|
|
179
|
+
console.log('─'.repeat(40));
|
|
180
|
+
const localeDayNames = isJa
|
|
181
|
+
? ['日', '月', '火', '水', '木', '金', '土']
|
|
182
|
+
: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
183
|
+
const dailyStats = groupByDayOfWeek(completedTasks);
|
|
184
|
+
const maxDaily = Math.max(...dailyStats.values());
|
|
185
|
+
for (let i = 0; i < 7; i++) {
|
|
186
|
+
const count = dailyStats.get(i) || 0;
|
|
187
|
+
const dayName = padEnd(localeDayNames[i], 4);
|
|
188
|
+
const countStr = String(count).padStart(3);
|
|
189
|
+
console.log(` ${dayName}${countStr} ${bar(count, maxDaily)}`);
|
|
190
|
+
}
|
|
191
|
+
// Current Status Distribution
|
|
192
|
+
console.log();
|
|
193
|
+
console.log(l.currentStatus);
|
|
194
|
+
console.log('─'.repeat(40));
|
|
195
|
+
const allTasks = await db
|
|
196
|
+
.select()
|
|
197
|
+
.from(schema.tasks)
|
|
198
|
+
.where(eq(schema.tasks.isProject, false));
|
|
199
|
+
const statusCounts = new Map();
|
|
200
|
+
for (const task of allTasks) {
|
|
201
|
+
statusCounts.set(task.status, (statusCounts.get(task.status) || 0) + 1);
|
|
202
|
+
}
|
|
203
|
+
const statusOrder = ['inbox', 'next', 'waiting', 'someday', 'done'];
|
|
204
|
+
const maxStatus = Math.max(...statusCounts.values());
|
|
205
|
+
for (const status of statusOrder) {
|
|
206
|
+
const count = statusCounts.get(status) || 0;
|
|
207
|
+
const label = padEnd(i18n.status[status] || status, 16);
|
|
208
|
+
const countStr = String(count).padStart(3);
|
|
209
|
+
console.log(` ${label}${countStr} ${bar(count, maxStatus)}`);
|
|
210
|
+
}
|
|
211
|
+
// Context Distribution
|
|
212
|
+
console.log();
|
|
213
|
+
console.log(l.byContext);
|
|
214
|
+
console.log('─'.repeat(40));
|
|
215
|
+
const contextDist = calculateDistribution(completedTasks, t => t.context, l.noContext);
|
|
216
|
+
const maxContext = contextDist.length > 0 ? contextDist[0].count : 0;
|
|
217
|
+
for (const item of contextDist) {
|
|
218
|
+
const label = padEnd(item.label === l.noContext ? item.label : `@${item.label}`, 16);
|
|
219
|
+
const countStr = String(item.count).padStart(3);
|
|
220
|
+
console.log(` ${label}${countStr} (${String(item.percentage).padStart(2)}%) ${bar(item.count, maxContext)}`);
|
|
221
|
+
}
|
|
222
|
+
// Effort Distribution
|
|
223
|
+
console.log();
|
|
224
|
+
console.log(l.byEffort);
|
|
225
|
+
console.log('─'.repeat(40));
|
|
226
|
+
const effortLabels = {
|
|
227
|
+
small: i18n.tui.effort?.small || 'Small',
|
|
228
|
+
medium: i18n.tui.effort?.medium || 'Medium',
|
|
229
|
+
large: i18n.tui.effort?.large || 'Large',
|
|
230
|
+
};
|
|
231
|
+
const effortDist = calculateDistribution(completedTasks, t => t.effort ? (effortLabels[t.effort] || t.effort) : null, l.noEffort);
|
|
232
|
+
const maxEffort = effortDist.length > 0 ? effortDist[0].count : 0;
|
|
233
|
+
for (const item of effortDist) {
|
|
234
|
+
const label = padEnd(item.label, 16);
|
|
235
|
+
const countStr = String(item.count).padStart(3);
|
|
236
|
+
console.log(` ${label}${countStr} (${String(item.percentage).padStart(2)}%) ${bar(item.count, maxEffort)}`);
|
|
237
|
+
}
|
|
238
|
+
// Project Progress
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(l.projectProgress);
|
|
241
|
+
console.log('─'.repeat(40));
|
|
242
|
+
const activeProjects = await db
|
|
243
|
+
.select()
|
|
244
|
+
.from(schema.tasks)
|
|
245
|
+
.where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
|
|
246
|
+
if (activeProjects.length === 0) {
|
|
247
|
+
console.log(` ${l.activeProjects}: 0`);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
console.log(` ${l.activeProjects}: ${activeProjects.length}`);
|
|
251
|
+
for (const project of activeProjects) {
|
|
252
|
+
const children = await db
|
|
253
|
+
.select()
|
|
254
|
+
.from(schema.tasks)
|
|
255
|
+
.where(eq(schema.tasks.parentId, project.id));
|
|
256
|
+
const total = children.length;
|
|
257
|
+
const done = children.filter(c => c.status === 'done').length;
|
|
258
|
+
const remaining = total - done;
|
|
259
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
260
|
+
const shortId = project.id.slice(0, 8);
|
|
261
|
+
console.log(` [${shortId}] ${project.title} (${done}/${total}, ${pct}%)`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Average Completion Time
|
|
265
|
+
console.log();
|
|
266
|
+
console.log(l.averageCompletion);
|
|
267
|
+
console.log('─'.repeat(40));
|
|
268
|
+
const avgDays = calculateAverageCompletionDays(completedTasks);
|
|
269
|
+
if (avgDays !== null) {
|
|
270
|
+
if (avgDays < 1) {
|
|
271
|
+
const hours = Math.round(avgDays * 24);
|
|
272
|
+
const hoursLabel = isJa ? `${hours}時間` : `${hours}h`;
|
|
273
|
+
console.log(` ${hoursLabel}`);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
console.log(` ${fmt(l.daysAverage, { days: avgDays.toFixed(1) })}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Total
|
|
280
|
+
console.log();
|
|
281
|
+
console.log(`${l.total}: ${fmt(l.tasksCompleted, { count: completedTasks.length })}`);
|
|
282
|
+
console.log();
|
|
283
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface Config {
|
|
|
39
39
|
contexts?: string[];
|
|
40
40
|
splashDuration?: number;
|
|
41
41
|
contextFilter?: string | null;
|
|
42
|
+
focusFilter?: boolean;
|
|
42
43
|
pomodoroFocusMode?: boolean;
|
|
43
44
|
dateFormat?: DateFormat;
|
|
44
45
|
calendar?: CalendarConfig;
|
|
@@ -64,6 +65,8 @@ export declare function getSplashDuration(): number;
|
|
|
64
65
|
export declare function setSplashDuration(duration: number): void;
|
|
65
66
|
export declare function getContextFilter(): string | null;
|
|
66
67
|
export declare function setContextFilter(contextFilter: string | null): void;
|
|
68
|
+
export declare function getFocusFilter(): boolean;
|
|
69
|
+
export declare function setFocusFilter(enabled: boolean): void;
|
|
67
70
|
export declare function getPomodoroFocusMode(): boolean;
|
|
68
71
|
export declare function setPomodoroFocusMode(enabled: boolean): void;
|
|
69
72
|
export declare function getDateFormat(): DateFormat;
|
package/dist/config.js
CHANGED
|
@@ -165,6 +165,12 @@ export function getContextFilter() {
|
|
|
165
165
|
export function setContextFilter(contextFilter) {
|
|
166
166
|
saveConfig({ contextFilter });
|
|
167
167
|
}
|
|
168
|
+
export function getFocusFilter() {
|
|
169
|
+
return loadConfig().focusFilter ?? false;
|
|
170
|
+
}
|
|
171
|
+
export function setFocusFilter(enabled) {
|
|
172
|
+
saveConfig({ focusFilter: enabled });
|
|
173
|
+
}
|
|
168
174
|
export function getPomodoroFocusMode() {
|
|
169
175
|
return loadConfig().pomodoroFocusMode ?? true;
|
|
170
176
|
}
|
package/dist/db/index.js
CHANGED
|
@@ -24,6 +24,8 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
|
|
|
24
24
|
const tableInfo = tableInfoResult.rows;
|
|
25
25
|
const tableExists = tableInfo.length > 0;
|
|
26
26
|
const hasContext = tableInfo.some(col => col.name === 'context');
|
|
27
|
+
const hasIsFocused = tableInfo.some(col => col.name === 'is_focused');
|
|
28
|
+
const hasEffort = tableInfo.some(col => col.name === 'effort');
|
|
27
29
|
if (!tableExists) {
|
|
28
30
|
// Fresh install: create new schema on remote
|
|
29
31
|
await remoteClient.execute(`
|
|
@@ -36,6 +38,8 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
|
|
|
36
38
|
parent_id TEXT,
|
|
37
39
|
waiting_for TEXT,
|
|
38
40
|
context TEXT,
|
|
41
|
+
is_focused INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
effort TEXT,
|
|
39
43
|
due_date INTEGER,
|
|
40
44
|
created_at INTEGER NOT NULL,
|
|
41
45
|
updated_at INTEGER NOT NULL
|
|
@@ -51,6 +55,14 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
|
|
|
51
55
|
await remoteClient.execute("ALTER TABLE tasks ADD COLUMN context TEXT");
|
|
52
56
|
await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
|
|
53
57
|
}
|
|
58
|
+
// Migration: add is_focused column if missing
|
|
59
|
+
if (tableExists && !hasIsFocused) {
|
|
60
|
+
await remoteClient.execute("ALTER TABLE tasks ADD COLUMN is_focused INTEGER NOT NULL DEFAULT 0");
|
|
61
|
+
}
|
|
62
|
+
// Migration: add effort column if missing
|
|
63
|
+
if (tableExists && !hasEffort) {
|
|
64
|
+
await remoteClient.execute("ALTER TABLE tasks ADD COLUMN effort TEXT");
|
|
65
|
+
}
|
|
54
66
|
// Create comments table
|
|
55
67
|
await remoteClient.execute(`
|
|
56
68
|
CREATE TABLE IF NOT EXISTS comments (
|
|
@@ -88,6 +100,8 @@ async function initializeLocalSchema() {
|
|
|
88
100
|
const hasProjectId = tableInfo.some(col => col.name === 'project_id');
|
|
89
101
|
const hasIsProject = tableInfo.some(col => col.name === 'is_project');
|
|
90
102
|
const hasContext = tableInfo.some(col => col.name === 'context');
|
|
103
|
+
const hasIsFocused = tableInfo.some(col => col.name === 'is_focused');
|
|
104
|
+
const hasEffort = tableInfo.some(col => col.name === 'effort');
|
|
91
105
|
const tableExists = tableInfo.length > 0;
|
|
92
106
|
if (tableExists && hasProjectId && !hasIsProject) {
|
|
93
107
|
// Migration: old schema -> new schema
|
|
@@ -119,6 +133,8 @@ async function initializeLocalSchema() {
|
|
|
119
133
|
parent_id TEXT,
|
|
120
134
|
waiting_for TEXT,
|
|
121
135
|
context TEXT,
|
|
136
|
+
is_focused INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
effort TEXT,
|
|
122
138
|
due_date INTEGER,
|
|
123
139
|
created_at INTEGER NOT NULL,
|
|
124
140
|
updated_at INTEGER NOT NULL
|
|
@@ -134,6 +150,14 @@ async function initializeLocalSchema() {
|
|
|
134
150
|
await client.execute("ALTER TABLE tasks ADD COLUMN context TEXT");
|
|
135
151
|
await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
|
|
136
152
|
}
|
|
153
|
+
// Migration: add is_focused column if missing
|
|
154
|
+
if (tableExists && !hasIsFocused) {
|
|
155
|
+
await client.execute("ALTER TABLE tasks ADD COLUMN is_focused INTEGER NOT NULL DEFAULT 0");
|
|
156
|
+
}
|
|
157
|
+
// Migration: add effort column if missing
|
|
158
|
+
if (tableExists && !hasEffort) {
|
|
159
|
+
await client.execute("ALTER TABLE tasks ADD COLUMN effort TEXT");
|
|
160
|
+
}
|
|
137
161
|
// Create comments table
|
|
138
162
|
await client.execute(`
|
|
139
163
|
CREATE TABLE IF NOT EXISTS comments (
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -152,6 +152,42 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
|
|
|
152
152
|
}, {}, {
|
|
153
153
|
length: number | undefined;
|
|
154
154
|
}>;
|
|
155
|
+
isFocused: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
156
|
+
name: "is_focused";
|
|
157
|
+
tableName: "tasks";
|
|
158
|
+
dataType: "boolean";
|
|
159
|
+
columnType: "SQLiteBoolean";
|
|
160
|
+
data: boolean;
|
|
161
|
+
driverParam: number;
|
|
162
|
+
notNull: true;
|
|
163
|
+
hasDefault: true;
|
|
164
|
+
isPrimaryKey: false;
|
|
165
|
+
isAutoincrement: false;
|
|
166
|
+
hasRuntimeDefault: false;
|
|
167
|
+
enumValues: undefined;
|
|
168
|
+
baseColumn: never;
|
|
169
|
+
identity: undefined;
|
|
170
|
+
generated: undefined;
|
|
171
|
+
}, {}, {}>;
|
|
172
|
+
effort: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
173
|
+
name: "effort";
|
|
174
|
+
tableName: "tasks";
|
|
175
|
+
dataType: "string";
|
|
176
|
+
columnType: "SQLiteText";
|
|
177
|
+
data: string;
|
|
178
|
+
driverParam: string;
|
|
179
|
+
notNull: false;
|
|
180
|
+
hasDefault: false;
|
|
181
|
+
isPrimaryKey: false;
|
|
182
|
+
isAutoincrement: false;
|
|
183
|
+
hasRuntimeDefault: false;
|
|
184
|
+
enumValues: [string, ...string[]];
|
|
185
|
+
baseColumn: never;
|
|
186
|
+
identity: undefined;
|
|
187
|
+
generated: undefined;
|
|
188
|
+
}, {}, {
|
|
189
|
+
length: number | undefined;
|
|
190
|
+
}>;
|
|
155
191
|
dueDate: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
156
192
|
name: "due_date";
|
|
157
193
|
tableName: "tasks";
|
|
@@ -209,6 +245,7 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
|
|
|
209
245
|
export type Task = typeof tasks.$inferSelect;
|
|
210
246
|
export type NewTask = typeof tasks.$inferInsert;
|
|
211
247
|
export type TaskStatus = 'inbox' | 'next' | 'waiting' | 'someday' | 'done';
|
|
248
|
+
export type EffortSize = 'small' | 'medium' | 'large';
|
|
212
249
|
export declare const comments: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
213
250
|
name: "comments";
|
|
214
251
|
schema: undefined;
|
package/dist/db/schema.js
CHANGED
|
@@ -8,6 +8,8 @@ export const tasks = sqliteTable('tasks', {
|
|
|
8
8
|
parentId: text('parent_id'),
|
|
9
9
|
waitingFor: text('waiting_for'),
|
|
10
10
|
context: text('context'),
|
|
11
|
+
isFocused: integer('is_focused', { mode: 'boolean' }).notNull().default(false),
|
|
12
|
+
effort: text('effort'),
|
|
11
13
|
dueDate: integer('due_date', { mode: 'timestamp' }),
|
|
12
14
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
|
13
15
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|