diff --git a/todos/README.md b/todos/README.md new file mode 100644 index 0000000..86df392 --- /dev/null +++ b/todos/README.md @@ -0,0 +1,12 @@ +To add a Todo: + +`todo ` + +With description / notes: +`todo -- ` + +With high prio: +`todo !` + +Low Prio: +`todo -` diff --git a/todos/add-todo.js b/todos/add-todo.js new file mode 100644 index 0000000..846bf02 --- /dev/null +++ b/todos/add-todo.js @@ -0,0 +1,128 @@ +const fs = require('fs').promises; +const path = require('path'); +const os = require('os'); + +const TODOS_FILE = path.join(os.homedir(), '.config', 'bs-plugin-todos.json'); + +const ensureTodosFile = async () => { + try { + await fs.access(TODOS_FILE); + } catch (error) { + // File doesn't exist, create it with empty array + await fs.mkdir(path.dirname(TODOS_FILE), { recursive: true }); + await fs.writeFile(TODOS_FILE, JSON.stringify([], null, 2)); + } +}; + +const loadTodos = async () => { + await ensureTodosFile(); + const data = await fs.readFile(TODOS_FILE, 'utf8'); + + // Check if file is empty + if (!data.trim()) { + await fs.writeFile(TODOS_FILE, JSON.stringify([], null, 2)); + return []; + } + + return JSON.parse(data); +}; + +const saveTodos = async (todos) => { + await fs.writeFile(TODOS_FILE, JSON.stringify(todos, null, 2)); +}; + +const parseTodoQuery = (query) => { + let title = query.trim(); + let prio = 1; // default medium priority + let notes = ''; + + // Check for priority markers + if (title.startsWith('!')) { + prio = 2; // high priority + title = title.substring(1).trim(); + } else if (title.startsWith('-')) { + prio = 0; // low priority + title = title.substring(1).trim(); + } + + // Check for notes (anything after " -- ") + const notesSeparator = title.indexOf(' -- '); + if (notesSeparator !== -1) { + notes = title.substring(notesSeparator + 4).trim(); + title = title.substring(0, notesSeparator).trim(); + } + + return { title, prio, notes }; +}; + +const run = async (query, { axios }) => { + if (!query.trim()) { + return []; + } + + try { + const { title, prio, notes } = parseTodoQuery(query); + const prioText = prio === 2 ? ' (high priority)' : prio === 0 ? ' (low priority)' : ' (medium priority)'; + const prioIcon = prio === 2 ? '△' : prio === 0 ? '▽' : '◇'; + + return [{ + data: { + id: query, + title, + notes, + prio, + originalQuery: query + }, + content: [ + { + type: 'div', + className: 'flex items-center w-full gap-2', + children: [ + { + type: 'title', + content: `Add todo: ${title}`, + }, + { + type: 'p', + content: notes ? `Notes: ${notes}` : 'No notes', + className: 'flex-1' + }, + { + type: 'badge', + content: `${prioIcon} ${prioText.trim()}`, + } + ] + } + ] + }]; + } catch (error) { + console.error('Error parsing todo:', error.message); + return []; + } +}; + +const saveTodo = async (todoData, { clipboard }) => { + try { + const todos = await loadTodos(); + + const newTodo = { + id: todoData.title, + title: todoData.title, + completed: false, + notes: todoData.notes, + prio: todoData.prio, + }; + + todos.push(newTodo); + await saveTodos(todos); + } catch (error) { + console.error('Error saving todo:', error.message); + } +}; + +module.exports = { + run, + actions: [ + { title: 'Save Todo', action: saveTodo } + ] +}; \ No newline at end of file diff --git a/todos/index.js b/todos/index.js new file mode 100644 index 0000000..e32d718 --- /dev/null +++ b/todos/index.js @@ -0,0 +1,7 @@ +module.exports = { + commands: { + 'add-todo': require('./add-todo'), + 'list-todos': require('./list-todos'), + }, + }; + \ No newline at end of file diff --git a/todos/list-todos.js b/todos/list-todos.js new file mode 100644 index 0000000..0181d37 --- /dev/null +++ b/todos/list-todos.js @@ -0,0 +1,173 @@ +const fs = require('fs').promises; +const path = require('path'); +const os = require('os'); + +const TODOS_FILE = path.join(os.homedir(), '.config', 'bs-plugin-todos.json'); + +const ensureTodosFile = async () => { + try { + await fs.access(TODOS_FILE); + } catch (error) { + // File doesn't exist, create it with empty array + await fs.mkdir(path.dirname(TODOS_FILE), { recursive: true }); + await fs.writeFile(TODOS_FILE, JSON.stringify([], null, 2)); + } +}; + +const loadTodos = async () => { + await ensureTodosFile(); + const data = await fs.readFile(TODOS_FILE, 'utf8'); + + // Check if file is empty + if (!data.trim()) { + await fs.writeFile(TODOS_FILE, JSON.stringify([], null, 2)); + return []; + } + + return JSON.parse(data); +}; + +const saveTodos = async (todos) => { + await fs.writeFile(TODOS_FILE, JSON.stringify(todos, null, 2)); +}; + +const getPriorityIcon = (prio) => { + switch (prio) { + case 2: return '△'; // high priority + case 0: return '▽'; // low priority + default: return '◇'; + } +}; + +const getPriorityText = (prio) => { + switch (prio) { + case 2: return 'High'; + case 0: return 'Low'; + default: return 'Medium'; + } +}; + +const run = async (query, { axios }) => { + try { + const todos = (await loadTodos()).filter((todo) => todo.title.includes(query) || todo.notes.includes(query)); + + if (todos.length === 0) { + return []; + } + + // Sort todos by priority (high to low) then by completion status + const sortedTodos = todos.sort((a, b) => { + if (a.completed !== b.completed) { + return a.completed ? 1 : -1; // completed todos at bottom + } + return b.prio - a.prio; // high priority first + }); + + return sortedTodos.map((todo) => ({ + data: { + id: todo.title, + title: todo.title, + completed: todo.completed, + notes: todo.notes, + prio: todo.prio + }, + content: [ + { + type: 'div', + className: 'flex items-center w-full gap-2', + props: { + style: todo.completed + ? { opacity: 0.6 } + : {} + }, + children: [ + { + type: 'span', + content: todo.completed ? '✅' : '⬜', + className: 'text-lg' + }, + { + type: 'div', + className: 'flex-1', + children: [ + { + type: 'title', + content: todo.title, + className: todo.completed ? 'line-through text-gray-500' : '' + }, + ...(todo.notes ? [{ + type: 'span', + content: todo.notes, + className: 'text-sm! text-gray-600! ml-4' + }] : []) + ] + }, + { + type: 'div', + content: getPriorityText(todo.prio) + " " + getPriorityIcon(todo.prio), + className: 'rounded-lg text-sm px-2', + props: { + style: + todo.prio === 2 + ? { backgroundColor: '#fee2e2', color: '#991b1b' } + : todo.prio === 0 + ? { backgroundColor: '#dcfce7', color: '#166534' } + : { backgroundColor: '#fef9c3', color: '#a16207' } + } + } + ] + } + ] + })); + + } catch (error) { + console.error('Error loading todos:', error.message); + return []; + } +}; + +const markAsCompleted = async (todo) => { + try { + const todos = await loadTodos(); + const todoIndex = todos.findIndex(t => t.id === todo.id); + + if (todoIndex === -1) { + console.log('Todo not found'); + return; + } + + todos[todoIndex].completed = !todos[todoIndex].completed; + await saveTodos(todos); + + const status = todos[todoIndex].completed ? 'completed' : 'reopened'; + console.log(`Todo "${todo.title}" ${status}`); + + } catch (error) { + console.error('Error updating todo:', error.message); + } +}; + +const deleteTodo = async (todo) => { + try { + const todos = await loadTodos(); + const filteredTodos = todos.filter(t => t.id !== todo.id); + + if (filteredTodos.length === todos.length) { + console.log('Todo not found'); + return; + } + + await saveTodos(filteredTodos); + console.log(`Todo "${todo.title}" deleted`); + } catch (error) { + console.error('Error deleting todo:', error.message); + } +}; + +module.exports = { + run, + actions: [ + { name: 'Mark as completed', action: markAsCompleted }, + { name: 'Delete todo', action: deleteTodo } + ] +}; \ No newline at end of file diff --git a/todos/manifest.yml b/todos/manifest.yml new file mode 100644 index 0000000..e0e670e --- /dev/null +++ b/todos/manifest.yml @@ -0,0 +1,26 @@ +name: todo +label: Todo +version: 1.0.0 +author: a3chron + +commands: + - name: add-todo + label: Add Todo + isImmediate: false + bgColor: "#cdd6f4" + color: "#181825" + icon: check + keywords: + - todo + - add + + - name: list-todos + label: List Todos + isImmediate: false + bgColor: "#cdd6f4" + color: "#181825" + icon: check + keywords: + - todo + - todos + - list