TUI for Todoist written with React+Ink.
0

Configure Feed

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

make ui layout and rendering pretty

Aly Raffauf (Feb 11, 2026, 2:53 PM EST) 00aa0d03 f43e0395

+130 -88
+63 -21
source/app.tsx
··· 1 1 import React, {useState, useEffect, useCallback} from 'react'; 2 2 import {Box, Text, useApp} from 'ink'; 3 + import TextInput from 'ink-text-input'; 3 4 import {TodoistApi, type Task} from '@doist/todoist-api-typescript'; 4 5 import {type View, commands} from './commands.js'; 5 6 import {loadConfig, configPath} from './config.js'; ··· 63 64 const trimmed = value.trim(); 64 65 setInput(''); 65 66 67 + if (!trimmed || trimmed === '?') { 68 + return; 69 + } 70 + 66 71 const ctx = { 67 72 api, 68 73 tasks, ··· 91 96 const viewLabel = 92 97 view.type === 'edit' 93 98 ? `dewy ∙ edit: ${view.task.content}` 94 - : `dewy ∙ ${view.query}`; 99 + : `dewy ∙ ${view.query === homeFilter ? 'home' : view.query}`; 95 100 96 101 if (view.type === 'edit') { 97 102 return ( 98 - <Box flexDirection="column"> 103 + <Box flexDirection="column" paddingLeft={2}> 99 104 <Text bold color="cyan"> 100 105 {viewLabel} 101 106 </Text> 102 - <EditView 103 - task={view.task} 104 - api={api} 105 - onBack={() => { 106 - setView({type: 'filter', query: homeFilter}); 107 - refresh(); 108 - }} 109 - /> 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> 110 122 </Box> 111 123 ); 112 124 } 113 125 114 126 return ( 115 127 <Box flexDirection="column"> 116 - <Text bold color="cyan"> 117 - {viewLabel} 118 - </Text> 119 - <TaskListView 120 - tasks={tasks} 121 - projects={projects} 122 - message={message} 123 - input={input} 124 - setInput={setInput} 125 - onSubmit={handleSubmit} 126 - /> 128 + <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> 149 + </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> 168 + )} 127 169 </Box> 128 170 ); 129 171 }
-1
source/commands.ts
··· 8 8 projects: Map<string, string>; 9 9 homeFilter: string; 10 10 setMessage: (msg: string) => void; 11 - 12 11 refresh: () => Promise<void>; 13 12 setView: (view: View) => void; 14 13 exit: () => void;
+5 -4
source/edit-view.tsx
··· 1 1 import React, {useState} from 'react'; 2 - import {Box, Text, useInput} from 'ink'; 2 + import {Text, useInput} from 'ink'; 3 3 import TextInput from 'ink-text-input'; 4 4 import {type TodoistApi, type Task} from '@doist/todoist-api-typescript'; 5 5 import {priorityColor} from './utils.js'; ··· 98 98 99 99 if (key.return && !editing) { 100 100 setEditing(true); 101 - setEditValue(getFieldValue(task, fields[cursor]!.key)); 101 + setEditValue(fieldValues[fields[cursor]!.key] ?? ''); 102 102 } 103 103 }, 104 104 {isActive: !editing}, ··· 114 114 ); 115 115 116 116 return ( 117 - <Box flexDirection="column"> 117 + <> 118 118 {fields.map((f, i) => ( 119 119 <Text key={f.key}> 120 120 {cursor === i ? '> ' : ' '} ··· 132 132 )} 133 133 </Text> 134 134 ))} 135 + <Text>{''}</Text> 135 136 <Text dimColor> 136 137 {editing 137 138 ? ' Enter to save ∙ Escape to cancel' 138 139 : ' Enter to edit ∙ Escape to go back'} 139 140 </Text> 140 - </Box> 141 + </> 141 142 ); 142 143 }
+62 -62
source/task-list-view.tsx
··· 1 1 import React from 'react'; 2 2 import {Box, Text} from 'ink'; 3 - import TextInput from 'ink-text-input'; 4 3 import {type Task} from '@doist/todoist-api-typescript'; 5 - import {commands} from './commands.js'; 6 4 import {priorityColor} from './utils.js'; 7 5 8 6 type TaskListViewProps = { 9 7 tasks: Task[]; 10 8 projects: Map<string, string>; 11 - message: string; 12 - input: string; 13 - setInput: (value: string) => void; 14 - onSubmit: (value: string) => Promise<void>; 9 + viewLabel: string; 15 10 }; 16 11 12 + function ProjectLabel({name}: {name: string | undefined}) { 13 + if (!name) return null; 14 + return ( 15 + <Text dimColor color="blue"> 16 + {' '} 17 + #{name} 18 + </Text> 19 + ); 20 + } 21 + 22 + function Labels({labels}: {labels: string[]}) { 23 + if (labels.length === 0) return null; 24 + return ( 25 + <Text dimColor color="yellow"> 26 + {' '} 27 + {labels.map(l => `@${l}`).join(' ')} 28 + </Text> 29 + ); 30 + } 31 + 32 + function DueDate({date}: {date: string | undefined}) { 33 + if (!date) return null; 34 + return ( 35 + <Text dimColor color="magenta"> 36 + {' '} 37 + {date} 38 + </Text> 39 + ); 40 + } 41 + 42 + function Priority({priority}: {priority: number}) { 43 + const display = 5 - priority; 44 + if (display >= 4) return null; 45 + return ( 46 + <Text dimColor color={priorityColor(display)}> 47 + {' '} 48 + p{display} 49 + </Text> 50 + ); 51 + } 52 + 17 53 export default function TaskListView({ 18 54 tasks, 19 55 projects, 20 - message, 21 - input, 22 - setInput, 23 - onSubmit, 56 + viewLabel, 24 57 }: TaskListViewProps) { 25 58 return ( 26 59 <> 27 - {tasks.map((task, i) => ( 28 - <Text key={task.id}> 29 - <Text dimColor>{i + 1}.</Text> {task.content} 30 - {projects.get(task.projectId) ? ( 31 - <Text dimColor color="blue"> 32 - {' '} 33 - #{projects.get(task.projectId)} 34 - </Text> 35 - ) : ( 36 - '' 37 - )} 38 - {task.labels.length > 0 ? ( 39 - <Text dimColor color="yellow"> 40 - {' '} 41 - {task.labels.map(l => `@${l}`).join(' ')} 42 - </Text> 43 - ) : ( 44 - '' 45 - )} 46 - {task.due?.date ? ( 47 - <Text dimColor color="magenta"> 48 - {' '} 49 - {task.due.date} 50 - </Text> 51 - ) : ( 52 - '' 53 - )} 54 - {task.priority > 1 ? ( 55 - <Text dimColor color={priorityColor(5 - task.priority)}> 56 - {' '} 57 - p{5 - task.priority} 58 - </Text> 59 - ) : ( 60 - '' 61 - )} 62 - </Text> 63 - ))} 64 - {message && <Text color="yellow">{message}</Text>} 65 - <Box> 66 - <Text bold color="green"> 67 - {'> '} 68 - </Text> 69 - <TextInput value={input} onChange={setInput} onSubmit={onSubmit} /> 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 + ))} 70 79 </Box> 71 - {input && 72 - commands 73 - .filter(c => c.prefix.startsWith(input)) 74 - .map(c => ( 75 - <Text key={c.prefix} dimColor> 76 - {' '} 77 - {c.hint} 78 - </Text> 79 - ))} 80 80 </> 81 81 ); 82 82 }