@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,237 @@
|
|
|
1
|
+
import React, { useState, useMemo, useEffect, useRef } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
|
|
4
|
+
export interface Command {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
action: () => void;
|
|
8
|
+
category?: string;
|
|
9
|
+
shortcut?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CommandPaletteProps {
|
|
13
|
+
commands: Command[];
|
|
14
|
+
onCancel: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CATEGORY_ORDER = ["task", "navigation", "view", "project", "template", "bulk", "general"];
|
|
18
|
+
const CATEGORY_LABELS: Record<string, string> = {
|
|
19
|
+
task: "Task Actions",
|
|
20
|
+
navigation: "Navigation",
|
|
21
|
+
view: "Views",
|
|
22
|
+
project: "Projects",
|
|
23
|
+
template: "Templates",
|
|
24
|
+
bulk: "Bulk Actions",
|
|
25
|
+
general: "General",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function CommandRow({ cmd, isActive }: { cmd: Command; isActive: boolean }) {
|
|
29
|
+
return (
|
|
30
|
+
<Box justifyContent="space-between">
|
|
31
|
+
<Text
|
|
32
|
+
backgroundColor={isActive ? "cyan" : undefined}
|
|
33
|
+
color={isActive ? "black" : "white"}
|
|
34
|
+
bold={isActive}
|
|
35
|
+
>
|
|
36
|
+
{isActive ? "\u25B6 " : " "}
|
|
37
|
+
<Text bold={isActive}>{cmd.name}</Text>
|
|
38
|
+
<Text color={isActive ? "black" : "gray"}>{` ${cmd.description}`}</Text>
|
|
39
|
+
</Text>
|
|
40
|
+
{cmd.shortcut ? (
|
|
41
|
+
<Text color={isActive ? "black" : "gray"} backgroundColor={isActive ? "cyan" : undefined} dimColor={!isActive}>
|
|
42
|
+
{` ${cmd.shortcut}`}
|
|
43
|
+
</Text>
|
|
44
|
+
) : null}
|
|
45
|
+
</Box>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function CommandPalette({ commands, onCancel }: CommandPaletteProps) {
|
|
50
|
+
const [query, setQuery] = useState("");
|
|
51
|
+
const [cursor, setCursor] = useState(0);
|
|
52
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
53
|
+
const scrollOffsetRef = useRef(0);
|
|
54
|
+
|
|
55
|
+
// Filter commands by query
|
|
56
|
+
const filtered = useMemo(() => {
|
|
57
|
+
if (!query) return commands;
|
|
58
|
+
const q = query.toLowerCase();
|
|
59
|
+
return commands.filter(
|
|
60
|
+
(cmd) =>
|
|
61
|
+
cmd.name.toLowerCase().includes(q) ||
|
|
62
|
+
cmd.description.toLowerCase().includes(q) ||
|
|
63
|
+
(cmd.category ?? "").toLowerCase().includes(q),
|
|
64
|
+
);
|
|
65
|
+
}, [commands, query]);
|
|
66
|
+
|
|
67
|
+
// Reorder by category for grouped display; flat order when searching
|
|
68
|
+
const ordered = useMemo(() => {
|
|
69
|
+
if (query) return filtered;
|
|
70
|
+
const groups = new Map<string, Command[]>();
|
|
71
|
+
for (const cmd of filtered) {
|
|
72
|
+
const cat = cmd.category ?? "general";
|
|
73
|
+
const existing = groups.get(cat) ?? [];
|
|
74
|
+
existing.push(cmd);
|
|
75
|
+
groups.set(cat, existing);
|
|
76
|
+
}
|
|
77
|
+
const result: Command[] = [];
|
|
78
|
+
for (const cat of CATEGORY_ORDER) {
|
|
79
|
+
const items = groups.get(cat);
|
|
80
|
+
if (items) result.push(...items);
|
|
81
|
+
}
|
|
82
|
+
for (const [cat, items] of groups) {
|
|
83
|
+
if (CATEGORY_ORDER.includes(cat)) continue;
|
|
84
|
+
result.push(...items);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}, [filtered, query]);
|
|
88
|
+
|
|
89
|
+
// Clamp selectedIndex when list shrinks
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (ordered.length > 0 && selectedIndex >= ordered.length) {
|
|
92
|
+
setSelectedIndex(ordered.length - 1);
|
|
93
|
+
}
|
|
94
|
+
}, [ordered.length, selectedIndex]);
|
|
95
|
+
|
|
96
|
+
// Reset scroll when query changes
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
scrollOffsetRef.current = 0;
|
|
99
|
+
}, [query]);
|
|
100
|
+
|
|
101
|
+
const maxVisible = 18;
|
|
102
|
+
|
|
103
|
+
// Adjust scroll to keep selected item visible
|
|
104
|
+
if (selectedIndex < scrollOffsetRef.current) {
|
|
105
|
+
scrollOffsetRef.current = selectedIndex;
|
|
106
|
+
} else if (selectedIndex >= scrollOffsetRef.current + maxVisible) {
|
|
107
|
+
scrollOffsetRef.current = selectedIndex - maxVisible + 1;
|
|
108
|
+
}
|
|
109
|
+
scrollOffsetRef.current = Math.max(0, Math.min(
|
|
110
|
+
scrollOffsetRef.current,
|
|
111
|
+
Math.max(0, ordered.length - maxVisible),
|
|
112
|
+
));
|
|
113
|
+
const scrollOffset = scrollOffsetRef.current;
|
|
114
|
+
|
|
115
|
+
useInput((input, key) => {
|
|
116
|
+
if (key.escape) {
|
|
117
|
+
onCancel();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (key.return) {
|
|
121
|
+
const cmd = ordered[selectedIndex];
|
|
122
|
+
if (cmd) cmd.action();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (key.upArrow) {
|
|
126
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (key.downArrow) {
|
|
130
|
+
setSelectedIndex((i) => Math.min(ordered.length - 1, i + 1));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (key.backspace || key.delete) {
|
|
134
|
+
if (cursor > 0) {
|
|
135
|
+
setQuery((v) => v.slice(0, cursor - 1) + v.slice(cursor));
|
|
136
|
+
setCursor((c) => c - 1);
|
|
137
|
+
setSelectedIndex(0);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (key.leftArrow) {
|
|
142
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (key.rightArrow) {
|
|
146
|
+
setCursor((c) => Math.min(query.length, c + 1));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (input && !key.ctrl && !key.meta) {
|
|
150
|
+
setQuery((v) => v.slice(0, cursor) + input + v.slice(cursor));
|
|
151
|
+
setCursor((c) => c + input.length);
|
|
152
|
+
setSelectedIndex(0);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const before = query.slice(0, cursor);
|
|
157
|
+
const cursorChar = query[cursor] ?? " ";
|
|
158
|
+
const after = query.slice(cursor + 1);
|
|
159
|
+
|
|
160
|
+
// Visible slice
|
|
161
|
+
const visibleCommands = ordered.slice(scrollOffset, scrollOffset + maxVisible);
|
|
162
|
+
|
|
163
|
+
// Render items with category headers when not searching
|
|
164
|
+
const renderItems = () => {
|
|
165
|
+
const elements: React.ReactNode[] = [];
|
|
166
|
+
let lastCategory = "";
|
|
167
|
+
|
|
168
|
+
for (let vi = 0; vi < visibleCommands.length; vi++) {
|
|
169
|
+
const cmd = visibleCommands[vi]!;
|
|
170
|
+
const globalIdx = scrollOffset + vi;
|
|
171
|
+
const cat = cmd.category ?? "general";
|
|
172
|
+
|
|
173
|
+
if (!query && cat !== lastCategory) {
|
|
174
|
+
if (lastCategory) elements.push(<Box key={`s-${cat}`} height={1} />);
|
|
175
|
+
elements.push(
|
|
176
|
+
<Text key={`h-${cat}`} color="yellow" bold>
|
|
177
|
+
{"\u2500\u2500 "}{CATEGORY_LABELS[cat] ?? cat}{" \u2500\u2500"}
|
|
178
|
+
</Text>,
|
|
179
|
+
);
|
|
180
|
+
lastCategory = cat;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
elements.push(
|
|
184
|
+
<CommandRow key={cmd.name} cmd={cmd} isActive={globalIdx === selectedIndex} />,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return elements;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const termWidth = process.stdout.columns ?? 80;
|
|
191
|
+
const paletteWidth = Math.min(60, Math.max(40, Math.floor(termWidth * 0.5)));
|
|
192
|
+
|
|
193
|
+
const hasMore = ordered.length > scrollOffset + maxVisible;
|
|
194
|
+
const hasAbove = scrollOffset > 0;
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<Box
|
|
198
|
+
flexDirection="column"
|
|
199
|
+
borderStyle="round"
|
|
200
|
+
borderColor="cyan"
|
|
201
|
+
paddingX={1}
|
|
202
|
+
width={paletteWidth}
|
|
203
|
+
>
|
|
204
|
+
<Box>
|
|
205
|
+
<Text>
|
|
206
|
+
<Text color="cyan" bold>{"\u276F "}</Text>
|
|
207
|
+
<Text>{before}</Text>
|
|
208
|
+
<Text backgroundColor="white" color="black">{cursorChar}</Text>
|
|
209
|
+
<Text>{after}</Text>
|
|
210
|
+
</Text>
|
|
211
|
+
</Box>
|
|
212
|
+
{ordered.length > 0 ? (
|
|
213
|
+
<Box flexDirection="column" marginTop={1}>
|
|
214
|
+
{hasAbove && (
|
|
215
|
+
<Text color="gray" dimColor>{` \u2191 ${scrollOffset} more above`}</Text>
|
|
216
|
+
)}
|
|
217
|
+
{renderItems()}
|
|
218
|
+
{hasMore && (
|
|
219
|
+
<Text color="gray" dimColor>{` \u2193 ${ordered.length - scrollOffset - maxVisible} more below`}</Text>
|
|
220
|
+
)}
|
|
221
|
+
</Box>
|
|
222
|
+
) : (
|
|
223
|
+
<Box marginTop={1}>
|
|
224
|
+
<Text color="gray">No matching commands</Text>
|
|
225
|
+
</Box>
|
|
226
|
+
)}
|
|
227
|
+
<Box marginTop={1} justifyContent="space-between">
|
|
228
|
+
<Text color="gray" dimColor>
|
|
229
|
+
{ordered.length} command{ordered.length !== 1 ? "s" : ""}
|
|
230
|
+
</Text>
|
|
231
|
+
<Text color="gray" dimColor>
|
|
232
|
+
{"\u2191\u2193"} navigate {"\u23CE"} execute Esc cancel
|
|
233
|
+
</Text>
|
|
234
|
+
</Box>
|
|
235
|
+
</Box>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Box, Text, useInput } from "ink";
|
|
2
|
+
|
|
3
|
+
interface ConfirmDialogProps {
|
|
4
|
+
message: string;
|
|
5
|
+
onConfirm: () => void;
|
|
6
|
+
onCancel: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ConfirmDialog({ message, onConfirm, onCancel }: ConfirmDialogProps) {
|
|
10
|
+
useInput((input, key) => {
|
|
11
|
+
if (input === "y" || input === "Y") {
|
|
12
|
+
onConfirm();
|
|
13
|
+
} else if (input === "n" || input === "N" || key.escape || key.return) {
|
|
14
|
+
onCancel();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Box borderStyle="single" borderColor="red" paddingX={1}>
|
|
20
|
+
<Text>
|
|
21
|
+
<Text color="red" bold>⚠ {message}</Text>
|
|
22
|
+
<Text dimColor> [y/</Text>
|
|
23
|
+
<Text bold>N</Text>
|
|
24
|
+
<Text dimColor>]</Text>
|
|
25
|
+
</Text>
|
|
26
|
+
</Box>
|
|
27
|
+
);
|
|
28
|
+
}
|