From 191d072be7b407a58acbc747ddf00a771e452823 Mon Sep 17 00:00:00 2001 From: a3chron Date: Mon, 4 Aug 2025 13:14:45 +0200 Subject: [PATCH 1/3] feat: add absic structure for todo plugin --- todos/add-todo.js | 6 ++++ todos/index.js | 7 ++++ todos/list-todos.js | 87 +++++++++++++++++++++++++++++++++++++++++++++ todos/manifest.yml | 26 ++++++++++++++ 4 files changed, 126 insertions(+) create mode 100644 todos/add-todo.js create mode 100644 todos/index.js create mode 100644 todos/list-todos.js create mode 100644 todos/manifest.yml diff --git a/todos/add-todo.js b/todos/add-todo.js new file mode 100644 index 0000000..28dc9c0 --- /dev/null +++ b/todos/add-todo.js @@ -0,0 +1,6 @@ +const run = async (_, { exec }) => { + await exec("echo 'TODO: add logic to create todo json in .config/bs-plugin-todo'"); + }; + + module.exports = { run, actions: [] }; + \ 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..16cf89a --- /dev/null +++ b/todos/list-todos.js @@ -0,0 +1,87 @@ +const run = async (query, { axios }) => { + try { + const response = await axios.get( + `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=10` + ); + + if (response.data && response.data.objects && response.data.objects.length > 0) { + return response.data.objects.map((obj) => ({ + data: { + id: obj.package.name, + name: obj.package.name, + version: obj.package.version, + link: `https://www.npmjs.com/package/${obj.package.name}`, + }, + content: [ + { + type: 'div', + className: 'flex items-center w-full gap-2', + children: [ + { + type: 'title', + content: obj.package.name, + className: '' + }, + { + type: 'p', + content: obj.package.description, + className: 'flex-1' + }, + { + type: 'badge', + content: `v${obj.package.version}`, + className: '' + } + ] + } + ] + })); + } else { + return []; + } + } catch (error) { + console.log(error); + } + }; + + const openNpmPackage = async (package, { shell }) => { + shell.openExternal(`https://www.npmjs.com/package/${package.name}`); + }; + + const openBundlephobia = async (package, { shell }) => { + shell.openExternal(`https://bundlephobia.com/result?p=${package.name}`); + }; + + const openESM = async (package, { shell }) => { + shell.openExternal(`https://esm.sh/${package.name}`); + }; + + const openSnykVulnCheck = async (package, { shell }) => { + shell.openExternal(`https://snyk.io/test/npm/${package.name}`); + }; + + const copyNpmInstallCommand = async (package, { clipboard }) => { + clipboard.writeText(`npm install ${package.name}`); + }; + + const copyYarnInstallCommand = async (package, { clipboard }) => { + clipboard.writeText(`yarn add ${package.name}`); + }; + + const copyPackageName = async (package, { clipboard }) => { + clipboard.writeText(package.name); + }; + + module.exports = { + run, + actions: [ + { name: 'Open NPM Package', action: openNpmPackage }, + { name: 'Open Bundlephobia', action: openBundlephobia }, + { name: 'Open ESM', action: openESM }, + { name: 'Open Snyk Vulnerability Check', action: openSnykVulnCheck }, + { name: 'Copy npm install command', action: copyNpmInstallCommand }, + { name: 'Copy yarn add command', action: copyYarnInstallCommand }, + { name: 'Copy package name', action: copyPackageName }, + ], + }; + \ No newline at end of file diff --git a/todos/manifest.yml b/todos/manifest.yml new file mode 100644 index 0000000..7c8da4c --- /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: true + 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 + - list + - ls From 9671b53c94777d69e44958f2a4e77150cf8042da Mon Sep 17 00:00:00 2001 From: a3chron Date: Thu, 7 Aug 2025 21:25:37 +0200 Subject: [PATCH 2/3] feat: finish preliminary todos --- todos/README.md | 12 +++ todos/add-todo.js | 134 +++++++++++++++++++++++-- todos/list-todos.js | 236 ++++++++++++++++++++++++++++++-------------- todos/manifest.yml | 4 +- 4 files changed, 303 insertions(+), 83 deletions(-) create mode 100644 todos/README.md diff --git a/todos/README.md b/todos/README.md new file mode 100644 index 0000000..5252e90 --- /dev/null +++ b/todos/README.md @@ -0,0 +1,12 @@ +To add a Todo: + +`` + +With description / notes: +` -- ` + +With high prio: +`!` + +Low Prio: +`-` diff --git a/todos/add-todo.js b/todos/add-todo.js index 28dc9c0..846bf02 100644 --- a/todos/add-todo.js +++ b/todos/add-todo.js @@ -1,6 +1,128 @@ -const run = async (_, { exec }) => { - await exec("echo 'TODO: add logic to create todo json in .config/bs-plugin-todo'"); - }; - - module.exports = { run, actions: [] }; - \ No newline at end of file +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/list-todos.js b/todos/list-todos.js index 16cf89a..f87281c 100644 --- a/todos/list-todos.js +++ b/todos/list-todos.js @@ -1,87 +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 response = await axios.get( - `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=10` - ); - - if (response.data && response.data.objects && response.data.objects.length > 0) { - return response.data.objects.map((obj) => ({ - data: { - id: obj.package.name, - name: obj.package.name, - version: obj.package.version, - link: `https://www.npmjs.com/package/${obj.package.name}`, + 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 } + : {} }, - content: [ + children: [ + { + type: 'span', + content: todo.completed ? '✅' : '⬜', + className: 'text-lg' + }, { type: 'div', - className: 'flex items-center w-full gap-2', + className: 'flex-1', children: [ { type: 'title', - content: obj.package.name, - className: '' - }, - { - type: 'p', - content: obj.package.description, - className: 'flex-1' + content: todo.title + todo.id, + className: todo.completed ? 'line-through text-gray-500' : '' }, - { - type: 'badge', - content: `v${obj.package.version}`, - className: '' - } + ...(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' } + } } ] - })); - } else { - return []; - } - } catch (error) { - console.log(error); + } + ] + })); + + } 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; } - }; - - const openNpmPackage = async (package, { shell }) => { - shell.openExternal(`https://www.npmjs.com/package/${package.name}`); - }; - - const openBundlephobia = async (package, { shell }) => { - shell.openExternal(`https://bundlephobia.com/result?p=${package.name}`); - }; - - const openESM = async (package, { shell }) => { - shell.openExternal(`https://esm.sh/${package.name}`); - }; - - const openSnykVulnCheck = async (package, { shell }) => { - shell.openExternal(`https://snyk.io/test/npm/${package.name}`); - }; - - const copyNpmInstallCommand = async (package, { clipboard }) => { - clipboard.writeText(`npm install ${package.name}`); - }; - - const copyYarnInstallCommand = async (package, { clipboard }) => { - clipboard.writeText(`yarn add ${package.name}`); - }; - - const copyPackageName = async (package, { clipboard }) => { - clipboard.writeText(package.name); - }; - - module.exports = { - run, - actions: [ - { name: 'Open NPM Package', action: openNpmPackage }, - { name: 'Open Bundlephobia', action: openBundlephobia }, - { name: 'Open ESM', action: openESM }, - { name: 'Open Snyk Vulnerability Check', action: openSnykVulnCheck }, - { name: 'Copy npm install command', action: copyNpmInstallCommand }, - { name: 'Copy yarn add command', action: copyYarnInstallCommand }, - { name: 'Copy package name', action: copyPackageName }, - ], - }; - \ No newline at end of file + + 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 index 7c8da4c..e0e670e 100644 --- a/todos/manifest.yml +++ b/todos/manifest.yml @@ -6,7 +6,7 @@ author: a3chron commands: - name: add-todo label: Add Todo - isImmediate: true + isImmediate: false bgColor: "#cdd6f4" color: "#181825" icon: check @@ -22,5 +22,5 @@ commands: icon: check keywords: - todo + - todos - list - - ls From c8496b9b199144f2e773158ee00097e90e6f48cb Mon Sep 17 00:00:00 2001 From: a3chron Date: Thu, 7 Aug 2025 21:31:26 +0200 Subject: [PATCH 3/3] fix: remove loggin or double title --- todos/README.md | 8 ++++---- todos/list-todos.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/todos/README.md b/todos/README.md index 5252e90..86df392 100644 --- a/todos/README.md +++ b/todos/README.md @@ -1,12 +1,12 @@ To add a Todo: -`` +`todo ` With description / notes: -` -- ` +`todo -- ` With high prio: -`!` +`todo !` Low Prio: -`-` +`todo -` diff --git a/todos/list-todos.js b/todos/list-todos.js index f87281c..0181d37 100644 --- a/todos/list-todos.js +++ b/todos/list-todos.js @@ -92,7 +92,7 @@ const run = async (query, { axios }) => { children: [ { type: 'title', - content: todo.title + todo.id, + content: todo.title, className: todo.completed ? 'line-through text-gray-500' : '' }, ...(todo.notes ? [{