TUI for Todoist written with React+Ink.
0

Configure Feed

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

improve edit view UI + extract command input and add command hints

Aly Raffauf (Feb 11, 2026, 4:49 PM EST) fd43ba82 cd961d9e

+119 -94
+94 -62
source/app.tsx
··· 20 20 21 21 const api = new TodoistApi(apiToken); 22 22 23 + function CommandHints({input}: {input: string}) { 24 + if (input === '?') { 25 + return commands.map(c => ( 26 + <Text key={c.prefix} dimColor> 27 + {' '} 28 + {c.hint} 29 + </Text> 30 + )); 31 + } 32 + 33 + if (input) { 34 + return commands 35 + .filter(c => c.prefix.startsWith(input)) 36 + .map(c => ( 37 + <Text key={c.prefix} dimColor> 38 + {' '} 39 + {c.hint} 40 + </Text> 41 + )); 42 + } 43 + 44 + return <Text dimColor>{' type ? for help'}</Text>; 45 + } 46 + 47 + function CommandInput({ 48 + input, 49 + setInput, 50 + onSubmit, 51 + }: { 52 + input: string; 53 + setInput: (value: string) => void; 54 + onSubmit: (value: string) => Promise<void>; 55 + }) { 56 + return ( 57 + <> 58 + <Box 59 + flexDirection="column" 60 + borderStyle="single" 61 + borderColor="gray" 62 + borderLeft={false} 63 + borderRight={false} 64 + > 65 + <Box> 66 + <Text bold color="white"> 67 + {'> '} 68 + </Text> 69 + <TextInput value={input} onChange={setInput} onSubmit={onSubmit} /> 70 + </Box> 71 + </Box> 72 + <CommandHints input={input} /> 73 + </> 74 + ); 75 + } 76 + 23 77 export default function App() { 24 78 const {exit} = useApp(); 25 79 const [tasks, setTasks] = useState<Task[]>([]); ··· 98 152 ? `dewy ∙ edit: ${view.task.content}` 99 153 : `dewy ∙ ${view.query === homeFilter ? 'home' : view.query}`; 100 154 101 - if (view.type === 'edit') { 102 - return ( 103 - <Box flexDirection="column" paddingLeft={2}> 104 - <Text bold color="cyan"> 105 - {viewLabel} 106 - </Text> 107 - <Box 108 - flexDirection="column" 109 - borderStyle="round" 110 - borderColor="cyan" 111 - paddingX={1} 112 - > 113 - <EditView 114 - task={view.task} 115 - api={api} 116 - onBack={() => { 117 - setView({type: 'filter', query: homeFilter}); 118 - refresh(); 119 - }} 120 - /> 121 - </Box> 122 - </Box> 123 - ); 124 - } 155 + const isEditing = view.type === 'edit'; 125 156 126 157 return ( 127 158 <Box flexDirection="column"> 128 159 <Box flexDirection="column" paddingLeft={2}> 129 - <TaskListView tasks={tasks} projects={projects} viewLabel={viewLabel} /> 130 - {message && <Text color="yellow">{message}</Text>} 131 - </Box> 132 - <Box 133 - flexDirection="column" 134 - borderStyle="single" 135 - borderColor="gray" 136 - borderLeft={false} 137 - borderRight={false} 138 - > 139 - <Box> 140 - <Text bold color="white"> 141 - {'> '} 142 - </Text> 143 - <TextInput 144 - value={input} 145 - onChange={setInput} 146 - onSubmit={handleSubmit} 147 - /> 148 - </Box> 160 + <Text bold color="cyan"> 161 + {viewLabel} 162 + </Text> 163 + {isEditing ? ( 164 + <Box 165 + flexDirection="column" 166 + borderStyle="round" 167 + borderColor="cyan" 168 + paddingX={1} 169 + width={50} 170 + > 171 + <EditView 172 + task={view.task} 173 + api={api} 174 + onBack={() => { 175 + setView({type: 'filter', query: homeFilter}); 176 + refresh(); 177 + }} 178 + /> 179 + </Box> 180 + ) : ( 181 + <> 182 + <Box 183 + flexDirection="column" 184 + borderStyle="round" 185 + borderColor="cyan" 186 + paddingX={1} 187 + > 188 + <TaskListView tasks={tasks} projects={projects} /> 189 + </Box> 190 + {message && <Text color="yellow">{message}</Text>} 191 + </> 192 + )} 149 193 </Box> 150 - {input === '?' ? ( 151 - commands.map(c => ( 152 - <Text key={c.prefix} dimColor> 153 - {' '} 154 - {c.hint} 155 - </Text> 156 - )) 157 - ) : input ? ( 158 - commands 159 - .filter(c => c.prefix.startsWith(input)) 160 - .map(c => ( 161 - <Text key={c.prefix} dimColor> 162 - {' '} 163 - {c.hint} 164 - </Text> 165 - )) 166 - ) : ( 167 - <Text dimColor>{' type ? for help'}</Text> 194 + {!isEditing && ( 195 + <CommandInput 196 + input={input} 197 + setInput={setInput} 198 + onSubmit={handleSubmit} 199 + /> 168 200 )} 169 201 </Box> 170 202 );
+13 -5
source/edit-view.tsx
··· 11 11 }; 12 12 13 13 const fields = [ 14 - {key: 'content', label: 'Title', color: () => undefined}, 15 - {key: 'description', label: 'Description', color: () => 'gray'}, 16 - {key: 'due', label: 'Due', color: () => 'magenta'}, 14 + {key: 'content', label: 'Title', placeholder: '', color: () => undefined}, 15 + { 16 + key: 'description', 17 + label: 'Description', 18 + placeholder: 'n/a', 19 + color: () => 'gray', 20 + }, 21 + {key: 'due', label: 'Due', placeholder: 'no date', color: () => 'magenta'}, 17 22 { 18 23 key: 'priority', 19 24 label: 'Priority', 25 + placeholder: 'none', 20 26 color: (v: string) => priorityColor(Number.parseInt(v, 10)), 21 27 }, 22 - {key: 'labels', label: 'Labels', color: () => 'yellow'}, 28 + {key: 'labels', label: 'Labels', placeholder: 'none', color: () => 'yellow'}, 23 29 ]; 24 30 25 31 function getFieldValue(task: Task, key: string): string { ··· 125 131 onChange={setEditValue} 126 132 onSubmit={handleSave} 127 133 /> 128 - ) : ( 134 + ) : fieldValues[f.key] ? ( 129 135 <Text color={f.color(fieldValues[f.key] ?? '')}> 130 136 {fieldValues[f.key]} 131 137 </Text> 138 + ) : ( 139 + <Text dimColor>{f.placeholder}</Text> 132 140 )} 133 141 </Text> 134 142 ))}
+12 -27
source/task-list-view.tsx
··· 1 1 import React from 'react'; 2 - import {Box, Text} from 'ink'; 2 + import {Text} from 'ink'; 3 3 import {type Task} from '@doist/todoist-api-typescript'; 4 4 import {priorityColor} from './utils.js'; 5 5 6 6 type TaskListViewProps = { 7 7 tasks: Task[]; 8 8 projects: Map<string, string>; 9 - viewLabel: string; 10 9 }; 11 10 12 11 function ProjectLabel({name}: {name: string | undefined}) { ··· 50 49 ); 51 50 } 52 51 53 - export default function TaskListView({ 54 - tasks, 55 - projects, 56 - viewLabel, 57 - }: TaskListViewProps) { 52 + export default function TaskListView({tasks, projects}: TaskListViewProps) { 58 53 return ( 59 54 <> 60 - <Text bold color="cyan"> 61 - {viewLabel} 62 - </Text> 63 - <Box 64 - flexDirection="column" 65 - borderStyle="round" 66 - borderColor="cyan" 67 - paddingX={1} 68 - > 69 - {tasks.length === 0 && <Text dimColor>No tasks</Text>} 70 - {tasks.map((task, i) => ( 71 - <Text key={task.id}> 72 - <Text dimColor>{i + 1}.</Text> {task.content} 73 - <ProjectLabel name={projects.get(task.projectId)} /> 74 - <Labels labels={task.labels} /> 75 - <DueDate date={task.due?.date} /> 76 - <Priority priority={task.priority} /> 77 - </Text> 78 - ))} 79 - </Box> 55 + {tasks.length === 0 && <Text dimColor>No tasks</Text>} 56 + {tasks.map((task, i) => ( 57 + <Text key={task.id}> 58 + <Text dimColor>{i + 1}.</Text> {task.content} 59 + <ProjectLabel name={projects.get(task.projectId)} /> 60 + <Labels labels={task.labels} /> 61 + <DueDate date={task.due?.date} /> 62 + <Priority priority={task.priority} /> 63 + </Text> 64 + ))} 80 65 </> 81 66 ); 82 67 }