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})} + + ); };