TUI for Todoist written with React+Ink.
0

Configure Feed

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

add support for rendering markdown links

Aly Raffauf (Feb 14, 2026, 12:51 PM EST) 0e9467a4 6ddcc2e8

+48 -3
-1
source/config.ts
··· 1 1 import {mkdirSync, readFileSync, writeFileSync, existsSync} from 'node:fs'; 2 2 import {join} from 'node:path'; 3 - // @ts-expect-error no type declarations 4 3 import xdg from '@folder/xdg'; 5 4 6 5 export type Config = {
+20 -2
source/task-list-view.tsx
··· 1 1 import React from 'react'; 2 2 import {Text} from 'ink'; 3 + import Link from 'ink-link'; 3 4 import {type Task} from '@doist/todoist-api-typescript'; 4 - import {priorityColor} from './utils.js'; 5 + import {parseMdLink, priorityColor} from './utils.js'; 5 6 6 7 type TaskListViewProps = { 7 8 tasks: Task[]; 8 9 projects: Map<string, string>; 9 10 }; 11 + 12 + function Content({text}: {text: string}) { 13 + const segments = parseMdLink(text); 14 + return ( 15 + <> 16 + {segments.map((seg, i) => 17 + seg.type === 'link' ? ( 18 + <Link key={i} url={seg.url}> 19 + {seg.text} 20 + </Link> 21 + ) : ( 22 + <Text key={i}>{seg.text}</Text> 23 + ), 24 + )} 25 + </> 26 + ); 27 + } 10 28 11 29 function ProjectLabel({name}: {name: string | undefined}) { 12 30 if (!name) return null; ··· 55 73 {tasks.length === 0 && <Text dimColor>No tasks</Text>} 56 74 {tasks.map((task, i) => ( 57 75 <Text key={task.id}> 58 - <Text dimColor>{i + 1}.</Text> {task.content} 76 + <Text dimColor>{i + 1}.</Text> <Content text={task.content} /> 59 77 <ProjectLabel name={projects.get(task.projectId)} /> 60 78 <Labels labels={task.labels} /> 61 79 <DueDate date={task.due?.date} />
+28
source/utils.ts
··· 10 10 return undefined; 11 11 } 12 12 } 13 + 14 + export type ContentSegment = 15 + | {type: 'text'; text: string} 16 + | {type: 'link'; text: string; url: string}; 17 + 18 + export function parseMdLink(content: string): ContentSegment[] { 19 + const segments: ContentSegment[] = []; 20 + const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g; 21 + 22 + let lastIndex = 0; 23 + let match; 24 + 25 + while ((match = linkPattern.exec(content)) !== null) { 26 + if (match.index > lastIndex) { 27 + segments.push({type: 'text', text: content.slice(lastIndex, match.index)}); 28 + } 29 + 30 + segments.push({type: 'link', text: match[1]!, url: match[2]!}); 31 + lastIndex = match.index + match[0].length; 32 + } 33 + 34 + if (lastIndex < content.length) { 35 + segments.push({type: 'text', text: content.slice(lastIndex)}); 36 + } 37 + 38 + return segments; 39 + } 40 +