@mindtnv/todoist-cli 0.3.1 → 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/marketplace.json +16 -0
- package/package.json +7 -6
- package/src/api/activity.ts +8 -0
- package/src/api/client.ts +214 -0
- package/src/api/comments.ts +18 -0
- package/src/api/completed.ts +15 -0
- package/src/api/labels.ts +18 -0
- package/src/api/projects.ts +22 -0
- package/src/api/sections.ts +20 -0
- package/src/api/stats.ts +38 -0
- package/src/api/tasks.ts +34 -0
- package/src/api/types.ts +202 -0
- package/src/cli/auth.ts +40 -0
- package/src/cli/commands/task/add.ts +328 -0
- package/src/cli/commands/task/complete.ts +62 -0
- package/src/cli/commands/task/delete.ts +62 -0
- package/src/cli/commands/task/helpers.ts +289 -0
- package/src/cli/commands/task/index.ts +27 -0
- package/src/cli/commands/task/list.ts +151 -0
- package/src/cli/commands/task/move.ts +49 -0
- package/src/cli/commands/task/reopen.ts +43 -0
- package/src/cli/commands/task/show.ts +115 -0
- package/src/cli/commands/task/update.ts +122 -0
- package/src/cli/comment.ts +83 -0
- package/src/cli/completed.ts +87 -0
- package/src/cli/completion.ts +360 -0
- package/src/cli/filter.ts +115 -0
- package/src/cli/index.ts +638 -0
- package/src/cli/label.ts +120 -0
- package/src/cli/log.ts +57 -0
- package/src/cli/matrix.ts +100 -0
- package/src/cli/plugin-loader.ts +38 -0
- package/src/cli/plugin.ts +289 -0
- package/src/cli/project.ts +172 -0
- package/src/cli/review.ts +116 -0
- package/src/cli/section.ts +98 -0
- package/src/cli/stats.ts +62 -0
- package/src/cli/template.ts +89 -0
- package/src/config/index.ts +229 -0
- package/src/plugins/api-proxy.ts +70 -0
- package/src/plugins/extension-registry.ts +53 -0
- package/src/plugins/hook-registry.ts +36 -0
- package/src/plugins/loader.ts +200 -0
- package/src/plugins/marketplace-types.ts +55 -0
- package/src/plugins/marketplace.ts +576 -0
- package/src/plugins/palette-registry.ts +21 -0
- package/src/plugins/storage.ts +101 -0
- package/src/plugins/types.ts +226 -0
- package/src/plugins/view-registry.ts +19 -0
- package/src/ui/App.tsx +234 -0
- package/src/ui/components/Breadcrumb.tsx +18 -0
- package/src/ui/components/CommandPalette.tsx +237 -0
- package/src/ui/components/ConfirmDialog.tsx +28 -0
- package/src/ui/components/EditTaskModal.tsx +484 -0
- package/src/ui/components/HelpOverlay.tsx +195 -0
- package/src/ui/components/InputPrompt.tsx +109 -0
- package/src/ui/components/LabelPicker.tsx +110 -0
- package/src/ui/components/ModalManager.tsx +275 -0
- package/src/ui/components/ProjectPicker.tsx +95 -0
- package/src/ui/components/Sidebar.tsx +282 -0
- package/src/ui/components/SortMenu.tsx +77 -0
- package/src/ui/components/StatusBar.tsx +67 -0
- package/src/ui/components/TaskList.tsx +258 -0
- package/src/ui/components/TaskRow.tsx +105 -0
- package/src/ui/hooks/useKeyboardHandler.ts +291 -0
- package/src/ui/hooks/useStatusMessage.ts +32 -0
- package/src/ui/hooks/useTaskOperations.ts +558 -0
- package/src/ui/hooks/useUndoSystem.ts +218 -0
- package/src/ui/views/ActivityView.tsx +213 -0
- package/src/ui/views/CompletedView.tsx +337 -0
- package/src/ui/views/StatsView.tsx +178 -0
- package/src/ui/views/TaskDetailView.tsx +438 -0
- package/src/ui/views/TasksView.tsx +851 -0
- package/src/utils/colors.ts +27 -0
- package/src/utils/date-format.ts +54 -0
- package/src/utils/errors.ts +159 -0
- package/src/utils/exit.ts +11 -0
- package/src/utils/format.ts +46 -0
- package/src/utils/open-url.ts +9 -0
- package/src/utils/output.ts +29 -0
- package/src/utils/quick-add.ts +202 -0
- package/src/utils/resolve.ts +359 -0
- package/src/utils/sorting.ts +27 -0
- package/src/utils/validation.ts +88 -0
- package/dist/index.js +0 -10989
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { Command as CliCommand } from "commander";
|
|
2
|
+
import type {
|
|
3
|
+
Task, Project, Label, Section, Comment,
|
|
4
|
+
CreateTaskParams, UpdateTaskParams, TaskFilter,
|
|
5
|
+
} from "../api/types.ts";
|
|
6
|
+
|
|
7
|
+
// ── Plugin Storage ──
|
|
8
|
+
|
|
9
|
+
export interface PluginStorage {
|
|
10
|
+
get<T>(key: string): Promise<T | null>;
|
|
11
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
12
|
+
delete(key: string): Promise<void>;
|
|
13
|
+
list(prefix?: string): Promise<string[]>;
|
|
14
|
+
getTaskData<T>(taskId: string, key: string): Promise<T | null>;
|
|
15
|
+
setTaskData<T>(taskId: string, key: string, value: T): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── Plugin Logger ──
|
|
19
|
+
|
|
20
|
+
export interface PluginLogger {
|
|
21
|
+
info(message: string): void;
|
|
22
|
+
warn(message: string): void;
|
|
23
|
+
error(message: string): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Plugin Context ──
|
|
27
|
+
|
|
28
|
+
export interface PluginContext {
|
|
29
|
+
api: PluginApi;
|
|
30
|
+
storage: PluginStorage;
|
|
31
|
+
config: Record<string, unknown>;
|
|
32
|
+
pluginDir: string;
|
|
33
|
+
log: PluginLogger;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PluginApi {
|
|
37
|
+
getTasks: (filter?: TaskFilter) => Promise<Task[]>;
|
|
38
|
+
getTask: (id: string) => Promise<Task>;
|
|
39
|
+
createTask: (params: CreateTaskParams) => Promise<Task>;
|
|
40
|
+
updateTask: (id: string, params: UpdateTaskParams) => Promise<Task>;
|
|
41
|
+
closeTask: (id: string) => Promise<void>;
|
|
42
|
+
reopenTask: (id: string) => Promise<void>;
|
|
43
|
+
deleteTask: (id: string) => Promise<void>;
|
|
44
|
+
getProjects: () => Promise<Project[]>;
|
|
45
|
+
getProject: (id: string) => Promise<Project>;
|
|
46
|
+
getLabels: () => Promise<Label[]>;
|
|
47
|
+
getLabel: (id: string) => Promise<Label>;
|
|
48
|
+
getSections: (projectId?: string) => Promise<Section[]>;
|
|
49
|
+
getComments: (taskId: string) => Promise<Comment[]>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Hook Registry ──
|
|
53
|
+
|
|
54
|
+
export type HookEvent =
|
|
55
|
+
| "task.creating"
|
|
56
|
+
| "task.created"
|
|
57
|
+
| "task.completing"
|
|
58
|
+
| "task.completed"
|
|
59
|
+
| "task.updating"
|
|
60
|
+
| "task.updated"
|
|
61
|
+
| "task.deleting"
|
|
62
|
+
| "task.deleted";
|
|
63
|
+
|
|
64
|
+
// ── Per-event hook context types ──
|
|
65
|
+
|
|
66
|
+
export interface TaskCreatingContext { params: CreateTaskParams }
|
|
67
|
+
export interface TaskCreatedContext { task: Task }
|
|
68
|
+
export interface TaskCompletingContext { task: Task }
|
|
69
|
+
export interface TaskCompletedContext { task: Task }
|
|
70
|
+
export interface TaskUpdatingContext { task: Task; changes: UpdateTaskParams }
|
|
71
|
+
export interface TaskUpdatedContext { task: Task; changes: UpdateTaskParams }
|
|
72
|
+
export interface TaskDeletingContext { task: Task }
|
|
73
|
+
export interface TaskDeletedContext { task: Task }
|
|
74
|
+
|
|
75
|
+
export type HookContextMap = {
|
|
76
|
+
"task.creating": TaskCreatingContext;
|
|
77
|
+
"task.created": TaskCreatedContext;
|
|
78
|
+
"task.completing": TaskCompletingContext;
|
|
79
|
+
"task.completed": TaskCompletedContext;
|
|
80
|
+
"task.updating": TaskUpdatingContext;
|
|
81
|
+
"task.updated": TaskUpdatedContext;
|
|
82
|
+
"task.deleting": TaskDeletingContext;
|
|
83
|
+
"task.deleted": TaskDeletedContext;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Union of all hook context types — kept for backward compatibility */
|
|
87
|
+
export type HookContext = HookContextMap[HookEvent];
|
|
88
|
+
|
|
89
|
+
export type HookHandler<E extends HookEvent = HookEvent> = (ctx: HookContextMap[E]) => Promise<{ message?: string } | void>;
|
|
90
|
+
|
|
91
|
+
export interface HookRegistry {
|
|
92
|
+
on<E extends HookEvent>(event: E, handler: HookHandler<E>): void;
|
|
93
|
+
off<E extends HookEvent>(event: E, handler: HookHandler<E>): void;
|
|
94
|
+
emit<E extends HookEvent>(event: E, ctx: HookContextMap[E]): Promise<string[]>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── View Registry ──
|
|
98
|
+
|
|
99
|
+
export interface PluginViewDefinition {
|
|
100
|
+
name: string;
|
|
101
|
+
label: string;
|
|
102
|
+
component: React.ComponentType<PluginViewProps>;
|
|
103
|
+
sidebar?: { icon: string; section: string };
|
|
104
|
+
shortcut?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface PluginViewProps {
|
|
108
|
+
onBack: () => void;
|
|
109
|
+
onNavigate: (view: string) => void;
|
|
110
|
+
ctx: PluginContext;
|
|
111
|
+
tasks: Task[];
|
|
112
|
+
projects: Project[];
|
|
113
|
+
labels: Label[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ViewRegistry {
|
|
117
|
+
addView(view: PluginViewDefinition): void;
|
|
118
|
+
getViews(): PluginViewDefinition[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Extension Registry ──
|
|
122
|
+
|
|
123
|
+
export interface TaskColumnDefinition {
|
|
124
|
+
id: string;
|
|
125
|
+
label: string;
|
|
126
|
+
width: number;
|
|
127
|
+
position: "after-priority" | "after-due" | "before-content";
|
|
128
|
+
render: (task: Task, ctx: PluginContext) => string;
|
|
129
|
+
color?: (task: Task) => string;
|
|
130
|
+
refreshInterval?: number; // ms — triggers re-render at this interval (e.g. 1000 for live timer)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface StatusBarItemDefinition {
|
|
134
|
+
id: string;
|
|
135
|
+
render: (ctx: PluginContext) => string;
|
|
136
|
+
color?: (ctx: PluginContext) => string;
|
|
137
|
+
refreshInterval?: number; // ms
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface DetailSectionDefinition {
|
|
141
|
+
id: string;
|
|
142
|
+
label: string;
|
|
143
|
+
position: "after-comments" | "after-subtasks" | "after-labels";
|
|
144
|
+
component: React.ComponentType<{ task: Task; ctx: PluginContext }>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface KeybindingDefinition {
|
|
148
|
+
key: string;
|
|
149
|
+
description: string;
|
|
150
|
+
helpSection: string;
|
|
151
|
+
action: (ctx: PluginContext, currentTask: Task | null) => Promise<{ statusMessage?: string } | void>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ExtensionRegistry {
|
|
155
|
+
addTaskColumn(column: TaskColumnDefinition): void;
|
|
156
|
+
addDetailSection(section: DetailSectionDefinition): void;
|
|
157
|
+
addKeybinding(binding: KeybindingDefinition): void;
|
|
158
|
+
addStatusBarItem(item: StatusBarItemDefinition): void;
|
|
159
|
+
getTaskColumns(): TaskColumnDefinition[];
|
|
160
|
+
getDetailSections(): DetailSectionDefinition[];
|
|
161
|
+
getKeybindings(): KeybindingDefinition[];
|
|
162
|
+
getStatusBarItems(): StatusBarItemDefinition[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Palette Registry ──
|
|
166
|
+
|
|
167
|
+
export interface PaletteInputPrompt {
|
|
168
|
+
label: string;
|
|
169
|
+
placeholder?: string;
|
|
170
|
+
formatPreview?: (value: string) => string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface PaletteCommandDefinition {
|
|
174
|
+
label: string;
|
|
175
|
+
category: string;
|
|
176
|
+
shortcut?: string;
|
|
177
|
+
inputPrompt?: PaletteInputPrompt;
|
|
178
|
+
action: (
|
|
179
|
+
ctx: PluginContext,
|
|
180
|
+
currentTask: Task | null,
|
|
181
|
+
navigate: (view: string) => void,
|
|
182
|
+
input?: string,
|
|
183
|
+
) => Promise<void> | void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface PaletteRegistry {
|
|
187
|
+
addCommands(commands: PaletteCommandDefinition[]): void;
|
|
188
|
+
getCommands(): PaletteCommandDefinition[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Plugin Interface ──
|
|
192
|
+
|
|
193
|
+
export interface TodoistPlugin {
|
|
194
|
+
name: string;
|
|
195
|
+
version: string;
|
|
196
|
+
description?: string;
|
|
197
|
+
registerCommands?(program: CliCommand, ctx: PluginContext): void;
|
|
198
|
+
registerViews?(registry: ViewRegistry): void;
|
|
199
|
+
registerHooks?(hooks: HookRegistry): void;
|
|
200
|
+
registerExtensions?(extensions: ExtensionRegistry): void;
|
|
201
|
+
registerPaletteCommands?(palette: PaletteRegistry): void;
|
|
202
|
+
onLoad?(ctx: PluginContext): Promise<void>;
|
|
203
|
+
onUnload?(): Promise<void>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Plugin Manifest (plugin.json) ──
|
|
207
|
+
|
|
208
|
+
export interface PluginManifest {
|
|
209
|
+
name: string;
|
|
210
|
+
version: string;
|
|
211
|
+
description?: string;
|
|
212
|
+
main: string;
|
|
213
|
+
author?: string;
|
|
214
|
+
source?: string;
|
|
215
|
+
engines?: { "todoist-cli"?: string };
|
|
216
|
+
permissions?: string[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Plugin Config (in config.toml) ──
|
|
220
|
+
|
|
221
|
+
export interface PluginConfigEntry {
|
|
222
|
+
source: string;
|
|
223
|
+
enabled?: boolean;
|
|
224
|
+
after?: string;
|
|
225
|
+
[key: string]: unknown;
|
|
226
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PluginViewDefinition, ViewRegistry } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export function createViewRegistry(): ViewRegistry {
|
|
4
|
+
const views: PluginViewDefinition[] = [];
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
addView(view: PluginViewDefinition) {
|
|
8
|
+
if (views.some(v => v.name === view.name)) {
|
|
9
|
+
console.warn(`[plugin] View "${view.name}" already registered, skipping`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
views.push(view);
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
getViews() {
|
|
16
|
+
return [...views];
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
package/src/ui/App.tsx
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { render, Box, Text, useApp } from "ink";
|
|
3
|
+
import type { Task, Project, Label, Section } from "../api/types.ts";
|
|
4
|
+
import { getTasks } from "../api/tasks.ts";
|
|
5
|
+
import { getProjects } from "../api/projects.ts";
|
|
6
|
+
import { getLabels } from "../api/labels.ts";
|
|
7
|
+
import { getSections } from "../api/sections.ts";
|
|
8
|
+
import { TasksView } from "./views/TasksView.tsx";
|
|
9
|
+
import type { ListViewState } from "./views/TasksView.tsx";
|
|
10
|
+
import { TaskDetailView } from "./views/TaskDetailView.tsx";
|
|
11
|
+
import { StatsView } from "./views/StatsView.tsx";
|
|
12
|
+
import { CompletedView } from "./views/CompletedView.tsx";
|
|
13
|
+
import { ActivityView } from "./views/ActivityView.tsx";
|
|
14
|
+
import { createHookRegistry } from "../plugins/hook-registry.ts";
|
|
15
|
+
import { createViewRegistry } from "../plugins/view-registry.ts";
|
|
16
|
+
import { createExtensionRegistry } from "../plugins/extension-registry.ts";
|
|
17
|
+
import { createPaletteRegistry } from "../plugins/palette-registry.ts";
|
|
18
|
+
import { loadPlugins } from "../plugins/loader.ts";
|
|
19
|
+
import type { LoadedPlugins } from "../plugins/loader.ts";
|
|
20
|
+
|
|
21
|
+
type View =
|
|
22
|
+
| { type: "list" }
|
|
23
|
+
| { type: "detail"; task: Task }
|
|
24
|
+
| { type: "stats" }
|
|
25
|
+
| { type: "completed" }
|
|
26
|
+
| { type: "activity" }
|
|
27
|
+
| { type: "plugin"; name: string };
|
|
28
|
+
|
|
29
|
+
function App() {
|
|
30
|
+
const { exit } = useApp();
|
|
31
|
+
const [tasks, setTasks] = useState<Task[]>([]);
|
|
32
|
+
const [projects, setProjects] = useState<Project[]>([]);
|
|
33
|
+
const [labels, setLabels] = useState<Label[]>([]);
|
|
34
|
+
const [sections, setSections] = useState<Section[]>([]);
|
|
35
|
+
const [loading, setLoading] = useState(true);
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
37
|
+
const [view, setView] = useState<View>({ type: "list" });
|
|
38
|
+
const [statusMessage, setStatusMessage] = useState("");
|
|
39
|
+
const [loadedPlugins, setLoadedPlugins] = useState<LoadedPlugins | null>(null);
|
|
40
|
+
const listStateRef = useRef<ListViewState | null>(null);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
|
|
45
|
+
async function init() {
|
|
46
|
+
try {
|
|
47
|
+
const [t, p, l, s] = await Promise.all([
|
|
48
|
+
getTasks(),
|
|
49
|
+
getProjects(),
|
|
50
|
+
getLabels(),
|
|
51
|
+
getSections(),
|
|
52
|
+
]);
|
|
53
|
+
if (!cancelled) {
|
|
54
|
+
setTasks(t);
|
|
55
|
+
setProjects(p);
|
|
56
|
+
setLabels(l);
|
|
57
|
+
setSections(s);
|
|
58
|
+
setLoading(false);
|
|
59
|
+
|
|
60
|
+
// Load plugins
|
|
61
|
+
try {
|
|
62
|
+
const hooks = createHookRegistry();
|
|
63
|
+
const viewReg = createViewRegistry();
|
|
64
|
+
const extReg = createExtensionRegistry();
|
|
65
|
+
const palReg = createPaletteRegistry();
|
|
66
|
+
const lp = await loadPlugins(hooks, viewReg, extReg, palReg);
|
|
67
|
+
if (!cancelled) setLoadedPlugins(lp);
|
|
68
|
+
} catch {
|
|
69
|
+
// Plugin loading failure is non-fatal
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (!cancelled) {
|
|
74
|
+
setError(err instanceof Error ? err.message : "Failed to load data");
|
|
75
|
+
setLoading(false);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
init();
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true;
|
|
83
|
+
};
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const handleOpenTask = useCallback((task: Task) => {
|
|
87
|
+
setView({ type: "detail", task });
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const handleBackToList = useCallback(() => {
|
|
91
|
+
setView({ type: "list" });
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const handleNavigate = useCallback((viewName: string) => {
|
|
95
|
+
switch (viewName) {
|
|
96
|
+
case "stats":
|
|
97
|
+
setView({ type: "stats" });
|
|
98
|
+
break;
|
|
99
|
+
case "completed":
|
|
100
|
+
setView({ type: "completed" });
|
|
101
|
+
break;
|
|
102
|
+
case "activity":
|
|
103
|
+
setView({ type: "activity" });
|
|
104
|
+
break;
|
|
105
|
+
default: {
|
|
106
|
+
const pluginViews = loadedPlugins?.views.getViews() ?? [];
|
|
107
|
+
if (pluginViews.some(v => v.name === viewName)) {
|
|
108
|
+
setView({ type: "plugin", name: viewName });
|
|
109
|
+
} else {
|
|
110
|
+
setView({ type: "list" });
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}, [loadedPlugins]);
|
|
116
|
+
|
|
117
|
+
const handleTaskChanged = useCallback(async (message?: string) => {
|
|
118
|
+
if (message) setStatusMessage(message);
|
|
119
|
+
try {
|
|
120
|
+
const newTasks = await getTasks();
|
|
121
|
+
setTasks(newTasks);
|
|
122
|
+
} catch {
|
|
123
|
+
setStatusMessage("Failed to refresh tasks after change");
|
|
124
|
+
}
|
|
125
|
+
setView({ type: "list" });
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
if (loading) {
|
|
129
|
+
return (
|
|
130
|
+
<Box flexDirection="column" justifyContent="center" alignItems="center" width="100%" height="100%">
|
|
131
|
+
<Text bold color="cyan">Todoist CLI</Text>
|
|
132
|
+
<Box marginTop={1}>
|
|
133
|
+
<Text color="gray">Loading your tasks...</Text>
|
|
134
|
+
</Box>
|
|
135
|
+
</Box>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (error) {
|
|
140
|
+
return (
|
|
141
|
+
<Box flexDirection="column" justifyContent="center" alignItems="center" width="100%" height="100%">
|
|
142
|
+
<Text color="red">Error: {error}</Text>
|
|
143
|
+
<Text color="gray">Make sure you have configured your API token with: todoist auth login</Text>
|
|
144
|
+
</Box>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (view.type === "detail") {
|
|
149
|
+
return (
|
|
150
|
+
<TaskDetailView
|
|
151
|
+
task={view.task}
|
|
152
|
+
allTasks={tasks}
|
|
153
|
+
projects={projects}
|
|
154
|
+
labels={labels}
|
|
155
|
+
onBack={handleBackToList}
|
|
156
|
+
onTaskChanged={handleTaskChanged}
|
|
157
|
+
pluginSections={loadedPlugins?.extensions.getDetailSections()}
|
|
158
|
+
pluginSectionContextMap={loadedPlugins?.detailSectionContextMap}
|
|
159
|
+
pluginHooks={loadedPlugins?.hooks ?? null}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (view.type === "stats") {
|
|
165
|
+
return <StatsView onBack={handleBackToList} />;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (view.type === "completed") {
|
|
169
|
+
return <CompletedView onBack={handleBackToList} />;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (view.type === "activity") {
|
|
173
|
+
return <ActivityView onBack={handleBackToList} />;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (view.type === "plugin" && loadedPlugins) {
|
|
177
|
+
const pluginView = loadedPlugins.views.getViews().find(v => v.name === view.name);
|
|
178
|
+
if (pluginView) {
|
|
179
|
+
const ctx = loadedPlugins.viewContextMap.get(view.name);
|
|
180
|
+
if (ctx) {
|
|
181
|
+
const Component = pluginView.component;
|
|
182
|
+
return (
|
|
183
|
+
<Component
|
|
184
|
+
onBack={handleBackToList}
|
|
185
|
+
onNavigate={handleNavigate}
|
|
186
|
+
ctx={ctx}
|
|
187
|
+
tasks={tasks}
|
|
188
|
+
projects={projects}
|
|
189
|
+
labels={labels}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<TasksView
|
|
198
|
+
tasks={tasks}
|
|
199
|
+
projects={projects}
|
|
200
|
+
labels={labels}
|
|
201
|
+
onTasksChange={setTasks}
|
|
202
|
+
onProjectsChange={setProjects}
|
|
203
|
+
onLabelsChange={setLabels}
|
|
204
|
+
onQuit={exit}
|
|
205
|
+
onOpenTask={handleOpenTask}
|
|
206
|
+
sections={sections}
|
|
207
|
+
onNavigate={handleNavigate}
|
|
208
|
+
initialStatus={statusMessage}
|
|
209
|
+
onStatusClear={() => setStatusMessage("")}
|
|
210
|
+
savedStateRef={listStateRef}
|
|
211
|
+
pluginExtensions={loadedPlugins?.extensions ?? null}
|
|
212
|
+
pluginPalette={loadedPlugins?.palette ?? null}
|
|
213
|
+
pluginViews={loadedPlugins?.views ?? null}
|
|
214
|
+
pluginKeybindingContextMap={loadedPlugins?.keybindingContextMap}
|
|
215
|
+
pluginColumnContextMap={loadedPlugins?.columnContextMap}
|
|
216
|
+
pluginPaletteContextMap={loadedPlugins?.paletteContextMap}
|
|
217
|
+
pluginStatusBarContextMap={loadedPlugins?.statusBarContextMap}
|
|
218
|
+
pluginHooks={loadedPlugins?.hooks ?? null}
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function launchUI(): Promise<void> {
|
|
224
|
+
// Enter alternate screen buffer (like vim/htop) — clean TUI, restores terminal on exit
|
|
225
|
+
process.stdout.write("\x1b[?1049h");
|
|
226
|
+
process.stdout.write("\x1b[H\x1b[2J");
|
|
227
|
+
|
|
228
|
+
const instance = render(<App />, { exitOnCtrlC: true });
|
|
229
|
+
|
|
230
|
+
await instance.waitUntilExit();
|
|
231
|
+
|
|
232
|
+
// Leave alternate screen buffer — restore previous terminal content
|
|
233
|
+
process.stdout.write("\x1b[?1049l");
|
|
234
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
|
|
3
|
+
interface BreadcrumbProps {
|
|
4
|
+
segments: Array<{ label: string; color?: string }>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Breadcrumb({ segments }: BreadcrumbProps) {
|
|
8
|
+
return (
|
|
9
|
+
<Box>
|
|
10
|
+
{segments.map((seg, i) => (
|
|
11
|
+
<Text key={seg.label}>
|
|
12
|
+
{i > 0 && <Text color="gray"> / </Text>}
|
|
13
|
+
<Text color={seg.color ?? "white"} bold={i === segments.length - 1}>{seg.label}</Text>
|
|
14
|
+
</Text>
|
|
15
|
+
))}
|
|
16
|
+
</Box>
|
|
17
|
+
);
|
|
18
|
+
}
|