diff --git a/source/app.js b/source/app.js
index 707f75f..931b169 100644
--- a/source/app.js
+++ b/source/app.js
@@ -9,388 +9,389 @@ import Scrollbar from "./scrollbar.js";
const countIncompleteSubtasks = (task) => {
- if (!task.subtasks || task.subtasks.length === 0) return 0;
- return task.subtasks.reduce((count, sub) => count + (!sub.completed ? 1 : 0) + countIncompleteSubtasks(sub), 0);
+ if (!task.subtasks || task.subtasks.length === 0) return 0;
+ return task.subtasks.reduce((count, sub) => count + (!sub.completed ? 1 : 0) + countIncompleteSubtasks(sub), 0);
};
const fzfLine = content => content ? content.replace(/\n/g, "|") : ""
const findTaskPathWithContent = (allTasks, content) => {
- for (let i = 0; i < allTasks.length; i++) {
- const task = allTasks[i];
- if (fzfLine(task.content) === content) {
- return [i];
- }
- if (task.subtasks && task.subtasks.length > 0) {
- const subtaskPath = findTaskPathWithContent(task.subtasks, content);
- if (subtaskPath) {
- return [i, ...subtaskPath];
- }
- }
- }
- return null;
+ for (let i = 0; i < allTasks.length; i++) {
+ const task = allTasks[i];
+ if (fzfLine(task.content) === content) {
+ return [i];
+ }
+ if (task.subtasks && task.subtasks.length > 0) {
+ const subtaskPath = findTaskPathWithContent(task.subtasks, content);
+ if (subtaskPath) {
+ return [i, ...subtaskPath];
+ }
+ }
+ }
+ return null;
};
const taskContents = (task) => {
- if (!task.subtasks || task.subtasks.length === 0) return [task.content];
- return [task.content].concat(task.subtasks.map(taskContents).flat());
+ if (!task.subtasks || task.subtasks.length === 0) return [task.content];
+ return [task.content].concat(task.subtasks.map(taskContents).flat());
};
const randomId = () => Math.random().toString(36).substr(2, 9)
const listAt = (tasks, path) => {
- let currentSubtasks = tasks;
- for (let i of path) {
- currentSubtasks = currentSubtasks[i].subtasks;
- }
- return currentSubtasks || []
+ let currentSubtasks = tasks;
+ for (let i of path) {
+ currentSubtasks = currentSubtasks[i].subtasks;
+ }
+ return currentSubtasks || []
}
const tasksAt = listAt
const last = (list) => list[list.length - 1]
const allButLast = (l) => {
- return l.slice(0, -1)
+ return l.slice(0, -1)
}
const setLast = (list, elem) => {
- list[list.length - 1] = elem
- return list
+ list[list.length - 1] = elem
+ return list
}
const currentSubtasks = (tasks, path) => {
- return tasksAt(tasks, allButLast(path))
+ return tasksAt(tasks, allButLast(path))
}
export default function TaskApp() {
- const { exit } = useApp();
- const [tasks, setTasks] = useState(loadTasks());
- const [taskPath, setTaskPath] = useState([0]);
- const [activeTabName, setActiveTabName] = useState(null);
- const [consoleLog, setConsole] = useState([]);
- const [cutTask, setCutTask] = useState(null);
- const focusedIdx = last(taskPath)
- const [visibleSubtasks, setVisibleSubtasks] = useState(currentSubtasks(tasks, taskPath));
- useEffect(() => {
- saveTasks(tasks);
- }, [tasks]);
+ const { exit } = useApp();
+ const [tasks, setTasks] = useState(loadTasks());
+ const [taskPath, setTaskPath] = useState([0]);
+ const [activeTabName, setActiveTabName] = useState(null);
+ const [consoleLog, setConsole] = useState([]);
+ const [cutTask, setCutTask] = useState(null);
+ const focusedIdx = last(taskPath)
+ const [visibleSubtasks, setVisibleSubtasks] = useState(currentSubtasks(tasks, taskPath));
+ useEffect(() => {
+ saveTasks(tasks);
+ }, [tasks]);
- useEffect(() => {
- setVisibleSubtasks(currentSubtasks(tasks, taskPath))
- }, [tasks, taskPath])
- const log = (...elems) => {
- let x = elems.map(e => {
- if (typeof e === "object") {
- return JSON.stringify(e);
- }
- return e;
- }).join(" ");
- setConsole(prev => [...prev, x]);
- }
- const focusTask = (index) => {
- log("Focus task:", index);
- if (index >= 0 && index < visibleSubtasks.length) {
- setTaskPath(prev => {
- const n = [...prev];
- n[n.length - 1] = index;
- return n
- });
- }
- };
+ useEffect(() => {
+ setVisibleSubtasks(currentSubtasks(tasks, taskPath))
+ }, [tasks, taskPath])
+ const log = (...elems) => {
+ let x = elems.map(e => {
+ if (typeof e === "object") {
+ return JSON.stringify(e);
+ }
+ return e;
+ }).join(" ");
+ setConsole(prev => [...prev, x]);
+ }
+ const focusTask = (index) => {
+ log("Focus task:", index);
+ if (index >= 0 && index < visibleSubtasks.length) {
+ setTaskPath(prev => {
+ const n = [...prev];
+ n[n.length - 1] = index;
+ return n
+ });
+ }
+ };
- const moveFocus = (diff) => {
- if (diff > 0) {
- focusTask(Math.min(focusedIdx + diff, visibleSubtasks.length - 1));
- } else {
- focusTask(Math.max(focusedIdx + diff, 0));
- }
- }
+ const moveFocus = (diff) => {
+ if (diff > 0) {
+ focusTask(Math.min(focusedIdx + diff, visibleSubtasks.length - 1));
+ } else {
+ focusTask(Math.max(focusedIdx + diff, 0));
+ }
+ }
- const editWithNeovim = (task) => {
- const tempFile = "/tmp/task_edit.txt";
- fs.writeFileSync(tempFile, task.content || task.name);
- try {
- // Run Neovim in a tmux split and use "wait-for" to track when it closes
- execSync(`tmux split-window -h 'n ${tempFile}; tmux wait-for -S nvim_done'`);
+ const editWithNeovim = (task) => {
+ const tempFile = "/tmp/task_edit.txt";
+ fs.writeFileSync(tempFile, task.content || task.name);
+ try {
+ // Run Neovim in a tmux split and use "wait-for" to track when it closes
+ execSync(`tmux split-window -h 'n ${tempFile}; tmux wait-for -S nvim_done'`);
- // Wait for Neovim to exit (blocks execution until the user quits Neovim)
- execSync("tmux wait-for nvim_done");
- } catch (error) {
- // If tmux is not available, open Neovim normally
- spawn("n", [tempFile], { stdio: "inherit" });
- }
+ // Wait for Neovim to exit (blocks execution until the user quits Neovim)
+ execSync("tmux wait-for nvim_done");
+ } catch (error) {
+ // If tmux is not available, open Neovim normally
+ spawn("n", [tempFile], { stdio: "inherit" });
+ }
- const editedTask = fs.readFileSync(tempFile, "utf8").trim();
- log(editedTask)
- task.name = editedTask.split("\n")[0];
- task.content = editedTask;
- return task
- }
+ const editedTask = fs.readFileSync(tempFile, "utf8").trim();
+ log(editedTask)
+ task.name = editedTask.split("\n")[0];
+ task.content = editedTask;
+ return task
+ }
- useInput((input, key) => {
- if (input === "Q") exit();
- if (input === "j") moveFocus(1);
- if (input === "k") moveFocus(-1);
- if (input === "d") moveFocus(5);
- if (input === "u") moveFocus(-5);
- if (input === "g") focusTask(0);
- if (input === "G") focusTask(visibleSubtasks.length - 1);
- if (input === "h" && taskPath.length > 1) {
- setTaskPath(taskPath.slice(0, -1));
- }
- if (input === "l" && visibleSubtasks[focusedIdx]?.subtasks.length > 0) {
- setTaskPath([...taskPath, 0]);
- }
+ useInput((input, key) => {
+ if (input === "Q") exit();
+ if (input === "j") moveFocus(1);
+ if (input === "k") moveFocus(-1);
+ if (input === "d") moveFocus(5);
+ if (input === "u") moveFocus(-5);
+ if (input === "g") focusTask(0);
+ if (input === "G") focusTask(visibleSubtasks.length - 1);
+ if (input === "h" && taskPath.length > 1) {
+ setTaskPath(taskPath.slice(0, -1));
+ }
+ if (input === "l" && visibleSubtasks[focusedIdx]?.subtasks.length > 0) {
+ setTaskPath([...taskPath, 0]);
+ }
- if (input === "H" && taskPath.length > 1) {
- let parentPath = allButLast(taskPath);
- let parentTasks = tasksAt(tasks, allButLast(parentPath));
- let currentIdx = last(parentPath);
+ if (input === "H" && taskPath.length > 1) {
+ let parentPath = allButLast(taskPath);
+ let parentTasks = tasksAt(tasks, allButLast(parentPath));
+ let currentIdx = last(parentPath);
- if (currentIdx > -1) {
- let task = parentTasks[currentIdx].subtasks.splice(last(taskPath), 1)[0];
- parentTasks.push(task);
- setTaskPath([...allButLast(parentPath), parentTasks.length - 1]);
- setTasks([...tasks]);
- log(`Moved task out: ${task.name}`);
- }
- }
+ if (currentIdx > -1) {
+ let task = parentTasks[currentIdx].subtasks.splice(last(taskPath), 1)[0];
+ parentTasks.push(task);
+ setTaskPath([...allButLast(parentPath), parentTasks.length - 1]);
+ setTasks([...tasks]);
+ log(`Moved task out: ${task.name}`);
+ }
+ }
- if (input === "L" && focusedIdx > 0) {
- let task = visibleSubtasks[focusedIdx];
+ if (input === "L" && focusedIdx > 0) {
+ let task = visibleSubtasks[focusedIdx];
- let newParent = visibleSubtasks[focusedIdx - 1];
- if (newParent) {
- newParent.subtasks.push(task);
- }
- visibleSubtasks.splice(focusedIdx, 1);
- setTasks([...tasks]);
- setTaskPath([...setLast(taskPath, focusedIdx - 1), newParent.subtasks.length - 1]);
- log(`Indented task under: ${newParent.name}`);
- }
+ let newParent = visibleSubtasks[focusedIdx - 1];
+ if (newParent) {
+ newParent.subtasks.push(task);
+ }
+ visibleSubtasks.splice(focusedIdx, 1);
+ setTasks([...tasks]);
+ setTaskPath([...setLast(taskPath, focusedIdx - 1), newParent.subtasks.length - 1]);
+ log(`Indented task under: ${newParent.name}`);
+ }
- if (input === "J" && focusedIdx < visibleSubtasks.length - 1) {
- const temp = visibleSubtasks[focusedIdx];
- visibleSubtasks[focusedIdx] = visibleSubtasks[focusedIdx + 1];
- visibleSubtasks[focusedIdx + 1] = temp;
- setTaskPath([...setLast(taskPath, focusedIdx + 1)]);
- setTasks([...tasks]);
- log(`Moved task down: ${visibleSubtasks[focusedIdx].name}`);
- }
+ if (input === "J" && focusedIdx < visibleSubtasks.length - 1) {
+ const temp = visibleSubtasks[focusedIdx];
+ visibleSubtasks[focusedIdx] = visibleSubtasks[focusedIdx + 1];
+ visibleSubtasks[focusedIdx + 1] = temp;
+ setTaskPath([...setLast(taskPath, focusedIdx + 1)]);
+ setTasks([...tasks]);
+ log(`Moved task down: ${visibleSubtasks[focusedIdx].name}`);
+ }
- if (input === "K" && focusedIdx > 0) {
- const temp = visibleSubtasks[focusedIdx];
- log(`Moving task up: ${temp.name}`);
- visibleSubtasks[focusedIdx] = visibleSubtasks[focusedIdx - 1];
- visibleSubtasks[focusedIdx - 1] = temp;
- setTasks([...tasks]);
- setTaskPath([...setLast(taskPath, focusedIdx - 1)]);
- }
- if (input === "a") {
- let newTask = editWithNeovim({ id: randomId(), name: "", completed: false, subtasks: [], content: "" })
- visibleSubtasks.push(newTask);
- setTasks([...tasks]);
- setTaskPath(setLast(taskPath, visibleSubtasks.length - 1))
- }
- if (input === "A") {
- let task = visibleSubtasks[focusedIdx];
- let newTask = editWithNeovim({ id: randomId(), name: "", completed: false, subtasks: [], content: "" })
- task.subtasks.push(newTask);
- setTasks([...tasks]);
- let position = visibleSubtasks[focusedIdx].subtasks.length - 1;
- setTaskPath([...taskPath, position]);
- }
- if (input === " ") {
- let task = visibleSubtasks[focusedIdx];
- task.completed = !task.completed;
- setTasks([...tasks]);
- }
- if (input === "e") {
- let task = visibleSubtasks[focusedIdx];
- task = editWithNeovim(task)
- setTasks([...tasks]);
- }
- if (input === "D") {
- const confirmation = execSync(`echo "Yes\nNo" | fzf --prompt 'Delete task and subtasks? '`, { stdio: "pipe" }).toString().trim();
- if (confirmation === "Yes") {
- visibleSubtasks.splice(focusedIdx, 1)
- setTasks([...tasks]);
- if (visibleSubtasks.length === 0) {
- setTaskPath(taskPath.slice(0, -1))
- } else {
- setTaskPath(setLast(taskPath, focusedIdx - 1))
- }
- }
- }
+ if (input === "K" && focusedIdx > 0) {
+ const temp = visibleSubtasks[focusedIdx];
+ log(`Moving task up: ${temp.name}`);
+ visibleSubtasks[focusedIdx] = visibleSubtasks[focusedIdx - 1];
+ visibleSubtasks[focusedIdx - 1] = temp;
+ setTasks([...tasks]);
+ setTaskPath([...setLast(taskPath, focusedIdx - 1)]);
+ }
+ if (input === "a") {
+ let newTask = editWithNeovim({ id: randomId(), name: "", completed: false, subtasks: [], content: "" })
+ visibleSubtasks.push(newTask);
+ setTasks([...tasks]);
+ setTaskPath(setLast(taskPath, visibleSubtasks.length - 1))
+ }
+ if (input === "A") {
+ let task = visibleSubtasks[focusedIdx];
+ let newTask = editWithNeovim({ id: randomId(), name: "", completed: false, subtasks: [], content: "" })
+ task.subtasks.push(newTask);
+ setTasks([...tasks]);
+ let position = visibleSubtasks[focusedIdx].subtasks.length - 1;
+ setTaskPath([...taskPath, position]);
+ }
+ if (input === " ") {
+ let task = visibleSubtasks[focusedIdx];
+ task.completed = !task.completed;
+ setTasks([...tasks]);
+ }
+ if (input === "e") {
+ let task = visibleSubtasks[focusedIdx];
+ task = editWithNeovim(task)
+ setTasks([...tasks]);
+ }
+ if (input === "D") {
+ const confirmation = execSync(`echo "Yes\nNo" | fzf --prompt 'Delete task and subtasks? '`, { stdio: "pipe" }).toString().trim();
+ if (confirmation === "Yes") {
+ visibleSubtasks.splice(focusedIdx, 1)
+ setTasks([...tasks]);
+ if (visibleSubtasks.length === 0) {
+ setTaskPath(taskPath.slice(0, -1))
+ } else {
+ setTaskPath(setLast(taskPath, focusedIdx - 1))
+ }
+ }
+ }
- if (input === "/") {
- try {
- const taskNames = tasks.map(taskContents).flat().map(fzfLine).join("\n");
- const query = execSync(`echo "${taskNames}" | fzf --prompt 'Search: '`, { stdio: "pipe" }).toString().trim();
- const res = findTaskPathWithContent(tasks, query)
- log("Path:", res)
- if (res) {
- setTaskPath(res);
- setTimeout(() => {
- focusTask(res[res.length - 1]);
- }, 30)
- }
- } catch (error) {
- log("Error searching:", error);
- }
- }
+ if (input === "/") {
+ try {
+ const taskNames = tasks.map(taskContents).flat().map(fzfLine).join("\n");
+ const query = execSync(`echo "${taskNames}" | fzf --prompt 'Search: '`, { stdio: "pipe" }).toString().trim();
+ const res = findTaskPathWithContent(tasks, query)
+ log("Path:", res)
+ if (res) {
+ setTaskPath(res);
+ setTimeout(() => {
+ focusTask(res[res.length - 1]);
+ }, 30)
+ }
+ } catch (error) {
+ log("Error searching:", error);
+ }
+ }
- if (input === "x" && visibleSubtasks[focusedIdx]) {
- setCutTask({
- task: visibleSubtasks[focusedIdx],
- path: [...taskPath],
- index: focusedIdx
- });
- log(`Cut task: ${visibleSubtasks[focusedIdx].name}`);
- }
+ if (input === "x" && visibleSubtasks[focusedIdx]) {
+ setCutTask({
+ task: visibleSubtasks[focusedIdx],
+ path: [...taskPath],
+ index: focusedIdx
+ });
+ log(`Cut task: ${visibleSubtasks[focusedIdx].name}`);
+ }
- if (input === "P" && cutTask) {
- // Remove task from original location
- let originalParent = tasksAt(tasks, allButLast(cutTask.path));
- originalParent.splice(cutTask.index, 1);
+ if (input === "P" && cutTask) {
+ // Remove task from original location
+ let originalParent = tasksAt(tasks, allButLast(cutTask.path));
+ originalParent.splice(cutTask.index, 1);
- // Add task to the new location
- let targetSubtasks = tasksAt(tasks, taskPath);
- targetSubtasks.push(cutTask.task);
+ // Add task to the new location
+ let targetSubtasks = tasksAt(tasks, taskPath);
+ targetSubtasks.push(cutTask.task);
- setTasks([...tasks]);
- setCutTask(null);
- log(`Moved task: ${cutTask.task.name}`);
- }
+ setTasks([...tasks]);
+ setCutTask(null);
+ log(`Moved task: ${cutTask.task.name}`);
+ }
- if (input === "p" && cutTask) {
- // Remove task from original location
- let originalParent = tasksAt(tasks, allButLast(cutTask.path));
- originalParent.splice(cutTask.index, 1);
+ if (input === "p" && cutTask) {
+ // Remove task from original location
+ let originalParent = tasksAt(tasks, allButLast(cutTask.path));
+ originalParent.splice(cutTask.index, 1);
- // Add task to the new location
- visibleSubtasks.push(cutTask.task);
+ // Add task to the new location
+ visibleSubtasks.push(cutTask.task);
- setTasks([...tasks]);
- setCutTask(null);
- log(`Moved task: ${cutTask.task.name}`);
- }
+ setTasks([...tasks]);
+ setCutTask(null);
+ log(`Moved task: ${cutTask.task.name}`);
+ }
- if (input === "S") {
- const confirmation = execSync(`echo "push\npull" | fzf --prompt 'Sync'`, { stdio: "pipe" }).toString().trim();
- if (confirmation === "push") {
- commitAndPushTasks(log)
- }
+ if (input === "S") {
+ const confirmation = execSync(`echo "push\npull" | fzf --prompt 'Sync'`, { stdio: "pipe" }).toString().trim();
+ if (confirmation === "push") {
+ commitAndPushTasks(log)
+ }
- if (confirmation === "pull") {
- pullTasks(log)
- setTasks(loadTasks(), log)
- }
- }
- if (input === "?") {
- log("==========================");
- log("Keybindings:");
- log("j/k - Move up/down");
- log("d/u - Move down/up 5 tasks");
- log("D - remove task");
- log("g/G - Go to top/bottom");
- log("h/l - Navigate parent/subtasks");
- log("a - Add task");
- log("s - Add subtask");
- log("e - Edit task in Neovim");
- log("S - Sync (Push/Pull)");
- log("space - Toggle completion");
- log("/ - Search");
- log("? - Show help");
- log("==========================");
- setActiveTabName("console")
- }
- log("Input:", input);
- });
- const countText = (task) => {
- if (!task.subtasks || task.subtasks.length === 0) return "";
- return `(${countIncompleteSubtasks(task)})`;
- }
- const taskContent = visibleSubtasks[focusedIdx]?.content || "No content available";
- const taskRow = (task, index, onPath) => {
- let cut = task.id === cutTask?.task?.id
- let symbol = cut ? "✂" :
- task.completed ? "✔" : "☐"
- return <>
-
-
- {onPath ? ">" : '\u00A0'}{symbol}
-
-
- {task.name.trim()} {countText(task)}
-
-
- >
- }
+ if (confirmation === "pull") {
+ pullTasks(log)
+ setTasks(loadTasks(), log)
+ }
+ }
- const parentIdx = taskPath[taskPath.length - 2]
- return (
-
-
-
- Tasks
- Console
-
-
+ if (input === "?") {
+ log("==========================");
+ log("Keybindings:");
+ log("j/k - Move up/down");
+ log("d/u - Move down/up 5 tasks");
+ log("D - remove task");
+ log("g/G - Go to top/bottom");
+ log("h/l - Navigate parent/subtasks");
+ log("a - Add task");
+ log("s - Add subtask");
+ log("e - Edit task in Neovim");
+ log("S - Sync (Push/Pull)");
+ log("space - Toggle completion");
+ log("/ - Search");
+ log("? - Show help");
+ log("==========================");
+ setActiveTabName("console")
+ }
+ log("Input:", input);
+ });
+ const countText = (task) => {
+ if (!task.subtasks || task.subtasks.length === 0) return "";
+ return `(${countIncompleteSubtasks(task)})`;
+ }
+ const taskContent = visibleSubtasks[focusedIdx]?.content || "No content available";
+ const taskRow = (task, index, onPath) => {
+ let cut = task.id === cutTask?.task?.id
+ let symbol = cut ? "✂" :
+ task.completed ? "✔" : "☐"
+ return <>
+
+
+ {onPath ? ">" : '\u00A0'}{symbol}
+
+
+ {task.name.trim()} {countText(task)}
+
+
+ >
+ }
- {activeTabName === 'tasks' && (
-
-
- {/* Left Pane - Parent Task */}
-
- {taskPath.length > 1 &&
-
- {tasksAt(tasks, taskPath.slice(0, taskPath.length - 2)).map((task, index) => (
- taskRow(task, index, index === parentIdx)
- ))}
-
- }
-
+ const parentIdx = taskPath[taskPath.length - 2]
+ return (
+
+
+
+ Tasks
+ Console
+
+
- {/* Middle Pane - Visible Subtasks */}
-
-
- {visibleSubtasks.map((task, index) => (
- taskRow(task, index, index === focusedIdx)
- ))}
-
-
+ {activeTabName === 'tasks' && (
+
+
+ {/* Left Pane - Parent Task */}
+
+ {taskPath.length > 1 &&
+
+ {tasksAt(tasks, taskPath.slice(0, taskPath.length - 2)).map((task, index) => (
+ taskRow(task, index, index === parentIdx)
+ ))}
+
+ }
+
- {/* Right Pane - Subtasks of Focused Task */}
-
- {visibleSubtasks[focusedIdx]?.subtasks?.length > 0 && (
- Subtasks:
- )}
- {visibleSubtasks[focusedIdx]?.subtasks?.map((subtask, index) => (
- taskRow(subtask, index, false)
- ))}
-
+ {/* Middle Pane - Visible Subtasks */}
+
+
+ {visibleSubtasks.map((task, index) => (
+ taskRow(task, index, index === focusedIdx)
+ ))}
+
+
-
+ {/* Right Pane - Subtasks of Focused Task */}
+
+ {visibleSubtasks[focusedIdx]?.subtasks?.length > 0 && (
+ Subtasks:
+ )}
+ {visibleSubtasks[focusedIdx]?.subtasks?.map((subtask, index) => (
+ taskRow(subtask, index, false)
+ ))}
+
- {/* Bottom Drawer - Task Content Display */}
- {taskContent && (
-
- {taskContent}
-
- )}
- Path: {taskPath.join(" > ") || "Root"}
-
+
- )}
+ {/* Bottom Drawer - Task Content Display */}
+ {taskContent && (
+
+ {taskContent}
+
+ )}
+ Path: {taskPath.join(" > ") || "Root"}
+
+
+ )}
- {activeTabName === 'console' && consoleLog.map(it => {it})}
-
- );
+ {activeTabName === 'console' && consoleLog.map(it => {it})}
+
+ );
};