From fbcf455e0f378040f55c1b1f19a1dbb04d0b1d83 Mon Sep 17 00:00:00 2001 From: Grayson Patterson Date: Thu, 23 Apr 2026 14:07:13 -0400 Subject: [PATCH] Initial commit --- .gitignore | 3 ++ LICENSE | 21 +++++++++ README.md | 71 ++++++++++++++++++++++++++++ main.js | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++ manifest.json | 10 ++++ versions.json | 3 ++ 6 files changed, 234 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 main.js create mode 100644 manifest.json create mode 100644 versions.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ba5f39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +node_modules/ +data.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8b623c3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Grayson Patterson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f67aafc --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Real Tasks + +An Obsidian plugin that deletes task lines shortly after their checkbox is ticked. Your notes keep only the tasks that still need doing — no graveyard of strikethroughs, no manual cleanup. + +## Why + +Most task workflows leave completed tasks in the file as `- [x]` lines. That's great if you want a historical log; it's noise if you want your notes to function like a todo app. Real Tasks removes that noise automatically while preserving undo. + +## How it works + +- When you check off a task in the editor, Real Tasks waits **800 ms**, then removes the whole line via the editor API. Undo (`Cmd/Ctrl-Z`) restores it. +- When a task gets completed programmatically — for example by Dataview checkbox clicks in an embedded query, or by a sync client — Real Tasks catches the `vault.modify` event, waits **400 ms**, and rewrites the file with the completed lines stripped out. +- A ribbon button and command insert a new `- [ ] ` task line at the cursor, ready to type. + +## Features + +- Auto-delete completed task lines (`- [x]`, `- [X]`, `* [x]`, `+ [x]`). +- Short delay after completion to allow for accidental clicks (`Cmd/Ctrl-Z` to recover). +- Works from both the editor and external writes (Dataview, sync, external tools). +- Ribbon icon and command **Insert new task** for quick task creation. +- Command **Sweep completed tasks in this note now** for ad-hoc cleanup. +- Desktop and mobile support. + +## Usage + +1. Install and enable the plugin. +2. Write tasks as normal: `- [ ] something to do`. +3. Check the box. ~1 second later the line vanishes. +4. Accidentally checked? Press `Cmd/Ctrl-Z` within the delay window, or immediately after, to restore the line. +5. Tap the ribbon icon (a check-square) to insert a new task at your cursor. Optionally bind `Insert new task` to a hotkey in Settings → Hotkeys. + +## Commands + +| Command | Description | +| ---------------------------------------------- | ----------------------------------------------------------- | +| `Real Tasks: Insert new task` | Insert a `- [ ] ` line at the cursor. | +| `Real Tasks: Sweep completed tasks in this note now` | Immediately remove all completed tasks from the active note. | + +## Compatibility + +- Tested on Obsidian `1.4.0` and above. +- Desktop and mobile (iOS, Android). +- Compatible with Dataview `TASK` queries — checking a task in an embedded Dataview result triggers the auto-delete. + +## Installation + +### Community plugins (once published) + +1. Open **Settings → Community plugins**. +2. Search for **Real Tasks**. +3. Install and enable. + +### Manual install + +1. Download `main.js` and `manifest.json` from the latest [release](https://github.com/cryptic0011/real-tasks/releases). +2. Create a folder `/.obsidian/plugins/real-tasks/`. +3. Drop both files inside. +4. Enable the plugin in **Settings → Community plugins**. + +## Settings + +No settings page in this release. Everything runs on sensible defaults. Future versions may expose the delete delay and checkbox filters. + +## Development + +- `main.js` is hand-written plain JavaScript — no build step, no bundler, no TypeScript. +- To modify locally, edit `main.js` then reload the plugin via **Settings → Community plugins** toggle, or restart Obsidian. + +## License + +[MIT](LICENSE) diff --git a/main.js b/main.js new file mode 100644 index 0000000..63e1144 --- /dev/null +++ b/main.js @@ -0,0 +1,126 @@ +"use strict"; + +const { Plugin, MarkdownView, Notice, TFile } = require("obsidian"); + +const TASK_PREFIX = "- [ ] "; + +const EDITOR_DELAY_MS = 800; +const VAULT_DELAY_MS = 400; +const COMPLETED_TASK_RE = /^\s*[-*+]\s+\[[xX]\]\s/; + +module.exports = class RealTasksPlugin extends Plugin { + onload() { + this._editorTimer = null; + this._vaultTimers = new Map(); // path -> timeout handle + this._suppressUntil = new Map(); // path -> timestamp we wrote the file + + // Interactive typing path: preserves undo via the editor API. + this.registerEvent( + this.app.workspace.on("editor-change", (editor) => { + if (this._editorTimer) clearTimeout(this._editorTimer); + this._editorTimer = setTimeout(() => this.sweepEditor(editor), EDITOR_DELAY_MS); + }) + ); + + // Programmatic writes (Dataview checkbox, sync, etc.) — use vault API. + this.registerEvent( + this.app.vault.on("modify", (file) => { + if (!(file instanceof TFile) || file.extension !== "md") return; + const until = this._suppressUntil.get(file.path) || 0; + if (Date.now() < until) return; // our own write bouncing back + + const existing = this._vaultTimers.get(file.path); + if (existing) clearTimeout(existing); + const handle = setTimeout(() => { + this._vaultTimers.delete(file.path); + this.sweepFile(file); + }, VAULT_DELAY_MS); + this._vaultTimers.set(file.path, handle); + }) + ); + + this.addCommand({ + id: "sweep-now", + name: "Sweep completed tasks in this note now", + editorCallback: (editor) => this.sweepEditor(editor), + }); + + this.addCommand({ + id: "insert-task", + name: "Insert new task", + editorCallback: (editor) => this.insertTask(editor), + }); + + this.addRibbonIcon("check-square", "Insert new task", () => { + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + if (!view) { + new Notice("Open a note first to insert a task."); + return; + } + this.insertTask(view.editor); + view.editor.focus(); + }); + } + + onunload() { + if (this._editorTimer) clearTimeout(this._editorTimer); + for (const h of this._vaultTimers.values()) clearTimeout(h); + this._vaultTimers.clear(); + } + + insertTask(editor) { + if (!editor) return; + const cursor = editor.getCursor(); + const currentLine = editor.getLine(cursor.line) || ""; + const atLineStart = currentLine.trim() === ""; + const prefix = atLineStart ? TASK_PREFIX : "\n" + TASK_PREFIX; + editor.replaceRange(prefix, cursor); + const newLine = atLineStart ? cursor.line : cursor.line + 1; + editor.setCursor({ line: newLine, ch: TASK_PREFIX.length }); + } + + sweepEditor(editor) { + if (!editor) return; + for (let i = editor.lineCount() - 1; i >= 0; i--) { + const line = editor.getLine(i); + if (line == null) continue; + if (!COMPLETED_TASK_RE.test(line)) continue; + + const lastLine = i === editor.lineCount() - 1; + const from = { line: i, ch: 0 }; + const to = lastLine + ? { line: i, ch: line.length } + : { line: i + 1, ch: 0 }; + editor.replaceRange("", from, to); + } + } + + async sweepFile(file) { + const leaves = this.app.workspace.getLeavesOfType("markdown"); + for (const leaf of leaves) { + const view = leaf.view; + if (view instanceof MarkdownView && view.file && view.file.path === file.path) { + this.sweepEditor(view.editor); + return; + } + } + + let content; + try { + content = await this.app.vault.read(file); + } catch { + return; + } + const lines = content.split("\n"); + const kept = lines.filter((l) => !COMPLETED_TASK_RE.test(l)); + if (kept.length === lines.length) return; + + const next = kept.join("\n"); + this._suppressUntil.set(file.path, Date.now() + 1500); + try { + await this.app.vault.modify(file, next); + } catch (e) { + console.warn("[real-tasks] vault write failed", file.path, e); + } + } +}; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..e004689 --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "real-tasks", + "name": "Real Tasks", + "version": "1.0.0", + "minAppVersion": "1.4.0", + "description": "Deletes task lines shortly after their checkbox is marked done so your notes only keep real, open work. Includes a ribbon button for quickly inserting a new task.", + "author": "Grayson Patterson", + "authorUrl": "https://github.com/YOUR-GITHUB-USERNAME", + "isDesktopOnly": false +} diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..8b24dc4 --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "1.0.0": "1.4.0" +}