diff --git a/source/app.tsx b/source/app.tsx
index 65401ff..cf2a401 100644
--- a/source/app.tsx
+++ b/source/app.tsx
@@ -5,7 +5,7 @@ import TextInput from 'ink-text-input';
import {TodoistApi, type Task} from '@doist/todoist-api-typescript';
import {type View, commands} from './commands.js';
import {loadConfig, configPath} from './config.js';
-import EditView from './edit-view.js';
+import {Content} from './content.js';
import TaskListView from './task-list-view.js';
const config = loadConfig();
@@ -92,13 +92,8 @@ export default function App() {
const [loading, setLoading] = useState(true);
const [input, setInput] = useState('');
const [message, setMessage] = useState('');
- const [editingField, setEditingField] = useState(false);
const refresh = useCallback(async () => {
- if (view.type === 'edit') {
- return;
- }
-
setLoading(true);
const projectsStale =
@@ -155,7 +150,7 @@ export default function App() {
const args = trimmed.slice(command.prefix.length);
await command.run(args, ctx);
} else {
- setMessage(`Unknown command: ${trimmed}`);
+ setMessage(`Unknown command: ${trimmed}. Type ? for help`);
}
};
@@ -166,14 +161,9 @@ export default function App() {
);
- const viewLabel =
- view.type === 'edit'
- ? `dewy ∙ edit: ${view.task.content}`
- : `dewy ∙ ${view.query === homeFilter ? 'home' : view.query} ∙ ${
- tasks.length
- } ${tasks.length === 1 ? 'item' : 'items'}`;
-
- const isEditing = view.type === 'edit';
+ const viewLabel = `dewy ∙ ${
+ view.query === homeFilter ? 'home' : view.query
+ } ∙ ${tasks.length} ${tasks.length === 1 ? 'item' : 'items'}`;
return (
@@ -182,53 +172,17 @@ export default function App() {
{viewLabel}
{refreshIndicator}
- {isEditing ? (
- <>
-
- {
- setView({type: 'filter', query: homeFilter});
- refresh();
- }}
- onEditingChange={setEditingField}
- />
-
-
- {' '}
- {editingField
- ? 'Enter to save ∙ Escape to cancel'
- : 'Enter to edit ∙ Escape to go back'}
-
- >
- ) : (
- <>
-
-
-
- {message || ' '}
- >
- )}
+
+
+
+ {message ? : ' '}
- {!isEditing && (
-
- )}
+
);
}
diff --git a/source/commands.ts b/source/commands.ts
index e64e11b..3199432 100644
--- a/source/commands.ts
+++ b/source/commands.ts
@@ -1,6 +1,6 @@
import {type TodoistApi, type Task} from '@doist/todoist-api-typescript';
-export type View = {type: 'filter'; query: string} | {type: 'edit'; task: Task};
+export type View = {type: 'filter'; query: string};
export type CommandContext = {
api: TodoistApi;
@@ -30,21 +30,89 @@ export const commands: Command[] = [
await api.closeTask(task.id);
setTasks(tasks.filter(t => t.id !== task.id));
} else {
- setMessage(`No task #${num}`);
+ setMessage(`Not found: task ${num}`);
}
},
},
{
prefix: 'edit ',
- hint: 'edit ',
- run: async (args, {tasks, setView, setMessage}) => {
+ hint: 'edit [due|p1-p4|labels|desc|project|title]',
+ run: async (args, {api, tasks, projects, setTasks, setMessage}) => {
+ const spaceIdx = args.indexOf(' ');
+ const numStr = spaceIdx === -1 ? args : args.slice(0, spaceIdx);
+ const rest = spaceIdx === -1 ? '' : args.slice(spaceIdx + 1).trim();
+ const num = Number.parseInt(numStr, 10);
+ const task = tasks[num - 1];
+
+ if (!task) {
+ setMessage(`Not found: task ${num}`);
+ return;
+ }
+
+ if (!rest) {
+ setMessage('Missing field. Use due|p1-p4|labels|desc|project|title');
+ return;
+ }
+
+ let updated: Task;
+
+ if (rest.startsWith('due ')) {
+ updated = await api.updateTask(task.id, {
+ dueString: rest.slice(4),
+ });
+ } else if (/^p[1-4]$/.test(rest)) {
+ updated = await api.updateTask(task.id, {
+ priority: 5 - Number.parseInt(rest[1]!, 10),
+ });
+ } else if (rest.startsWith('labels ')) {
+ const labels = rest
+ .slice(7)
+ .split(/\s+/)
+ .map(l => l.replace(/^@/, ''));
+ updated = await api.updateTask(task.id, {labels});
+ } else if (rest.startsWith('desc ')) {
+ updated = await api.updateTask(task.id, {
+ description: rest.slice(5),
+ });
+ } else if (rest.startsWith('project ')) {
+ const name = rest.slice(8).replace(/^#/, '');
+ const projectId = [...projects.entries()].find(
+ ([, n]) => n.toLowerCase() === name.toLowerCase(),
+ )?.[0];
+ if (!projectId) {
+ setMessage(`Not found: project ${name}`);
+ return;
+ }
+
+ updated = await api.moveTask(task.id, {projectId});
+ } else if (rest.startsWith('title ')) {
+ updated = await api.updateTask(task.id, {
+ content: rest.slice(6),
+ });
+ } else {
+ setMessage(
+ `Unknown field: ${
+ rest.split(' ')[0]
+ }. Use due|p1-p4|labels|desc|project|title`,
+ );
+ return;
+ }
+
+ setTasks(tasks.map(t => (t.id === task.id ? updated : t)));
+ },
+ },
+
+ {
+ prefix: 'desc ',
+ hint: 'desc ',
+ run: async (args, {tasks, setMessage}) => {
const num = Number.parseInt(args, 10);
const task = tasks[num - 1];
if (task) {
- setView({type: 'edit', task});
+ setMessage(`desc ${num}: ${task.description || 'n/a'}`);
} else {
- setMessage(`No task #${num}`);
+ setMessage(`Not found: task ${num}`);
}
},
},
diff --git a/source/content.tsx b/source/content.tsx
new file mode 100644
index 0000000..7b030f1
--- /dev/null
+++ b/source/content.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import {Text} from 'ink';
+import Link from 'ink-link';
+import {parseMdLink} from './utils.js';
+
+export function Content({text, bold}: {text: string; bold?: boolean}) {
+ const segments = parseMdLink(text);
+ return (
+ <>
+ {segments.map((seg, i) =>
+ seg.type === 'link' ? (
+
+ ↗ {seg.text}
+
+ ) : (
+
+ {seg.text}
+
+ ),
+ )}
+ >
+ );
+}
diff --git a/source/edit-view.tsx b/source/edit-view.tsx
deleted file mode 100644
index a435de6..0000000
--- a/source/edit-view.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import React, {useState} from 'react';
-import {Text, useInput} from 'ink';
-import TextInput from 'ink-text-input';
-import {type TodoistApi, type Task} from '@doist/todoist-api-typescript';
-import {priorityColor} from './utils.js';
-
-type EditViewProps = {
- task: Task;
- api: TodoistApi;
- onBack: () => void;
- onEditingChange: (editing: boolean) => void;
-};
-
-const fields = [
- {key: 'content', label: 'Title', placeholder: '', color: () => undefined},
- {
- key: 'description',
- label: 'Description',
- placeholder: 'n/a',
- color: () => 'gray',
- },
- {key: 'due', label: 'Due', placeholder: 'no date', color: () => 'magenta'},
- {
- key: 'priority',
- label: 'Priority',
- placeholder: 'none',
- color: (v: string) => priorityColor(Number.parseInt(v, 10)),
- },
- {key: 'labels', label: 'Labels', placeholder: 'none', color: () => 'yellow'},
-];
-
-function getFieldValue(task: Task, key: string): string {
- switch (key) {
- case 'content':
- return task.content;
- case 'description':
- return task.description;
- case 'due':
- return task.due?.string ?? '';
- case 'priority':
- return String(5 - task.priority);
- case 'labels':
- return task.labels.join(', ');
- default:
- return '';
- }
-}
-
-function buildUpdateArgs(key: string, value: string) {
- switch (key) {
- case 'content':
- return {content: value};
- case 'description':
- return {description: value};
- case 'due':
- return value ? {dueString: value} : {dueString: 'no date'};
- case 'priority':
- return {
- priority: Math.max(
- 1,
- Math.min(4, 5 - (Number.parseInt(value, 10) || 4)),
- ),
- };
- case 'labels':
- return {
- labels: value
- .split(',')
- .map(l => l.trim())
- .filter(Boolean),
- };
- default:
- return {};
- }
-}
-
-export default function EditView({
- task,
- api,
- onBack,
- onEditingChange,
-}: EditViewProps) {
- const [cursor, setCursor] = useState(0);
- const [editing, _setEditing] = useState(false);
- const [editValue, setEditValue] = useState('');
-
- const setEditing = (value: boolean) => {
- _setEditing(value);
- onEditingChange(value);
- };
- const [fieldValues, setFieldValues] = useState(() =>
- Object.fromEntries(fields.map(f => [f.key, getFieldValue(task, f.key)])),
- );
-
- const handleSave = async (value: string) => {
- const field = fields[cursor]!;
- const args = buildUpdateArgs(fields[cursor]!.key, value);
- await api.updateTask(task.id, args);
- setFieldValues(prev => ({...prev, [field.key]: value}));
- setEditing(false);
- };
-
- useInput(
- (input, key) => {
- if (key.upArrow || input === 'k') {
- setCursor(c => Math.max(0, c - 1));
- }
-
- if (key.downArrow || input === 'j') {
- setCursor(c => Math.min(fields.length - 1, c + 1));
- }
-
- if (key.escape) {
- onBack();
- }
-
- if (key.return && !editing) {
- setEditing(true);
- setEditValue(fieldValues[fields[cursor]!.key] ?? '');
- }
- },
- {isActive: !editing},
- );
-
- useInput(
- (_input, key) => {
- if (key.escape) {
- setEditing(false);
- }
- },
- {isActive: editing},
- );
-
- return (
- <>
- {fields.map((f, i) => (
-
- {cursor === i ? '> ' : ' '}
- {f.label.padEnd(14)}
- {cursor === i && editing ? (
-
- ) : fieldValues[f.key] ? (
-
- {fieldValues[f.key]}
-
- ) : (
- {f.placeholder}
- )}
-
- ))}
- >
- );
-}
diff --git a/source/task-list-view.tsx b/source/task-list-view.tsx
index adb2545..90c95cd 100644
--- a/source/task-list-view.tsx
+++ b/source/task-list-view.tsx
@@ -1,33 +1,14 @@
import React from 'react';
import {Box, Text} from 'ink';
-import Link from 'ink-link';
import {type Task} from '@doist/todoist-api-typescript';
-import {parseMdLink, priorityColor} from './utils.js';
+import {priorityColor} from './utils.js';
+import {Content} from './content.js';
type TaskListViewProps = {
tasks: Task[];
projects: Map;
};
-function Content({text}: {text: string}) {
- const segments = parseMdLink(text);
- return (
- <>
- {segments.map((seg, i) =>
- seg.type === 'link' ? (
-
- ↗ {seg.text}
-
- ) : (
-
- {seg.text}
-
- ),
- )}
- >
- );
-}
-
function ProjectLabel({name}: {name: string | undefined}) {
if (!name) return null;
return (
@@ -80,7 +61,7 @@ export default function TaskListView({tasks, projects}: TaskListViewProps) {
{String(i + 1).padStart(numWidth)}.
-
+