TUI for Todoist written with React+Ink.
0

Configure Feed

Select the types of activity you want to include in your feed.

separate command logic, add view context

Aly Raffauf (Feb 11, 2026, 6:30 AM EST) 61fa2d39 0fab2ccc

+112 -27
+31 -27
source/app.tsx
··· 2 2 import {Box, Text, useApp} from 'ink'; 3 3 import TextInput from 'ink-text-input'; 4 4 import {TodoistApi, type Task} from '@doist/todoist-api-typescript'; 5 + import {commands} from './commands.js'; 5 6 6 7 const token = process.env['TODOIST_API_TOKEN']; 7 8 if (!token) { ··· 15 16 const {exit} = useApp(); 16 17 const [tasks, setTasks] = useState<Task[]>([]); 17 18 const [projects, setProjects] = useState<Map<string, string>>(new Map()); 19 + 20 + const [view, setView] = useState< 21 + {type: 'filter'; query: string} | {type: 'project'; projectId: string} 22 + >({ 23 + type: 'filter', 24 + query: 'today', 25 + }); 26 + 18 27 const [loading, setLoading] = useState(true); 19 28 const [input, setInput] = useState(''); 20 29 const [message, setMessage] = useState(''); 21 30 22 - const fetchTasks = useCallback(async () => { 31 + const refresh = useCallback(async () => { 23 32 setLoading(true); 24 33 25 34 const [taskResponse, projectResponse] = await Promise.all([ 26 - api.getTasksByFilter({query: 'today'}), 35 + view.type === 'filter' 36 + ? api.getTasksByFilter({query: view.query}) 37 + : api.getTasks({projectId: view.projectId}), 27 38 api.getProjects(), 28 39 ]); 29 40 ··· 34 45 35 46 setProjects(projectMap); 36 47 setTasks(taskResponse.results); 37 - 38 48 setLoading(false); 39 - }, []); 49 + }, [view]); 40 50 41 51 useEffect(() => { 42 - fetchTasks(); 43 - }, [fetchTasks]); 52 + refresh(); 53 + }, [refresh]); 44 54 45 55 const handleSubmit = async (value: string) => { 46 56 const trimmed = value.trim(); 47 57 setInput(''); 48 58 49 - if (trimmed.startsWith('done ')) { 50 - // "done 2" → complete task #2 51 - const num = Number.parseInt(trimmed.slice(5), 10); 52 - const task = tasks[num - 1]; // arrays are 0-indexed, display is 1-indexed 53 - if (task) { 54 - await api.closeTask(task.id); 55 - setMessage(`Completed: ${task.content}`); 56 - await fetchTasks(); // refresh the list 57 - } else { 58 - setMessage(`No task #${num}`); 59 - } 60 - } else if (trimmed.startsWith('add ')) { 61 - const text = trimmed.slice(4); 62 - await api.quickAddTask({text}); 63 - setMessage(`Added: ${text}`); 64 - await fetchTasks(); 65 - } else if (trimmed === 'refresh') { 66 - await fetchTasks(); 67 - setMessage('Refreshed'); 68 - } else if (trimmed === 'quit') { 69 - exit(); 59 + const ctx = { 60 + api, 61 + tasks, 62 + projects, 63 + setMessage, 64 + refresh, 65 + setView, 66 + exit, 67 + }; 68 + 69 + const command = commands.find(c => trimmed.startsWith(c.prefix)); 70 + 71 + if (command) { 72 + const args = trimmed.slice(command.prefix.length); 73 + await command.run(args, ctx); 70 74 } else { 71 75 setMessage(`Unknown command: ${trimmed}`); 72 76 }
+81
source/commands.ts
··· 1 + import {type TodoistApi, type Task} from '@doist/todoist-api-typescript'; 2 + 3 + export type CommandContext = { 4 + api: TodoistApi; 5 + tasks: Task[]; 6 + projects: Map<string, string>; 7 + setMessage: (msg: string) => void; 8 + 9 + refresh: () => Promise<void>; 10 + setView: ( 11 + view: 12 + | {type: 'filter'; query: string} 13 + | {type: 'project'; projectId: string}, 14 + ) => void; 15 + 16 + exit: () => void; 17 + }; 18 + type Command = { 19 + prefix: string; 20 + run: (args: string, ctx: CommandContext) => Promise<void>; 21 + }; 22 + 23 + export const commands: Command[] = [ 24 + { 25 + prefix: 'done ', 26 + run: async (args, {api, tasks, setMessage, refresh}) => { 27 + const num = Number.parseInt(args, 10); 28 + const task = tasks[num - 1]; 29 + if (task) { 30 + await api.closeTask(task.id); 31 + setMessage(`Completed: ${task.content}`); 32 + await refresh(); 33 + } else { 34 + setMessage(`No task #${num}`); 35 + } 36 + }, 37 + }, 38 + { 39 + prefix: 'add ', 40 + run: async (args, {api, setMessage, refresh}) => { 41 + await api.quickAddTask({text: args}); 42 + setMessage(`Added: ${args}`); 43 + await refresh(); 44 + }, 45 + }, 46 + { 47 + prefix: 'refresh', 48 + run: async (_args, {refresh, setMessage}) => { 49 + await refresh(); 50 + setMessage('Refreshed'); 51 + }, 52 + }, 53 + { 54 + prefix: 'project ', 55 + run: async (args, {projects, setMessage, setView}) => { 56 + const projectId = [...projects.entries()].find( 57 + ([, n]) => n.toLowerCase() === args.toLowerCase(), 58 + )?.[0]; 59 + if (projectId) { 60 + setView({type: 'project', projectId}); 61 + setMessage(`Viewing project: ${args}`); 62 + } else { 63 + setMessage(`Project not found: ${args}`); 64 + } 65 + }, 66 + }, 67 + 68 + { 69 + prefix: 'today', 70 + run: async (_args, {setView, setMessage}) => { 71 + setView({type: 'filter', query: 'today'}); 72 + setMessage('Viewing today'); 73 + }, 74 + }, 75 + { 76 + prefix: 'quit', 77 + run: async (_args, {exit}) => { 78 + exit(); 79 + }, 80 + }, 81 + ];