commit c7636812788fef0c9fec550ed20deca3119f4de7 Author: Chris McCormick Date: Tue Dec 30 17:14:27 2025 +0800 Initial specification, first pass implementation. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..7ff9982 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,303 @@ +# KBTD - Kanban TODO + +A minimal Kanban board web app that uses a Markdown file as its data store. + +## Overview + +KBTD presents a Kanban board interface for managing tasks stored in a standard Markdown TODO file. The Markdown file is the canonical data store—the app provides a visual interface for viewing and manipulating it. + +## Core Concepts + +| Term | Description | +|------|-------------| +| **Project** | A working directory containing a TODO.md file (identified by absolute file path) | +| **Section** | An H1 (`# Name`) in the TODO file — rendered as a selectable Kanban board | +| **Column** | An H2 (`## Name`) under a section — a column within that board | +| **Card** | A `- [ ]` or `- [x]` item — draggable between columns | + +## Markdown Format + +```markdown +# Project Alpha + +## Backlog + +- [ ] Research competitors +- [ ] Write RFC + +## In Progress + +- [x] Design mockups +- [ ] Build prototype + +## Done + +- [x] Initial brainstorm + +# Project Beta + +## TODO + +- [ ] Something else +``` + +### Parsing Rules + +- H1 headers (`#`) define sections (boards) +- H2 headers (`##`) define columns within the preceding section +- `- [ ]` is an incomplete task +- `- [x]` is a complete task +- Task text is everything after the `] ` until end of line +- Content before the first H1 is ignored +- Content between H1 and first H2 is ignored +- Nested content under tasks (indented lines) is preserved but not displayed + +## Architecture + +### Components + +1. **Shell script (`kbtd`)** - Cross-platform launcher +2. **Single HTML file (`index.html`)** - The entire web application + +### Data Flow + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ TODO.md │◄───►│ Web App │◄───►│ localStorage│ +│ (file) │ │ (browser) │ │ (UI state) │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ + │ File System │ IndexedDB + │ Access API │ (FileHandle) + │ │ + └────────────────────┘ +``` + +### State Management + +- **File state**: The TODO.md file is the source of truth for task data +- **UI state**: localStorage stores current section selection, keyed by file path +- **File handle**: IndexedDB stores the FileSystemFileHandle for persistence across sessions + +#### localStorage Schema + +Key: `kbtd:${filePath}` + +```json +{ + "currentSection": "Project Alpha" +} +``` + +#### IndexedDB Schema + +Database: `kbtd` +Object Store: `fileHandles` +Key: file path from URL query string +Value: FileSystemFileHandle + +## User Interface + +### States + +1. **No file associated**: Shows file picker button and instructions +2. **Permission needed**: Shows button to re-grant permission (after page reload) +3. **File not found**: Shows error message if TODO.md doesn't exist +4. **Section picker**: Shows list of sections (H1s) to choose from +5. **Kanban board**: Shows columns and cards for selected section + +### Kanban Board Features + +- Columns displayed horizontally +- Cards displayed vertically within columns +- Drag and drop cards between columns +- Click card to toggle complete/incomplete +- Visual distinction for completed tasks (strikethrough or muted) + +### Navigation + +- Back button to return to section picker +- Current section name displayed in header + +## File Synchronization + +### Polling + +- Poll file every 250ms using `file.lastModified` +- On change detected: reload file, rebuild state, re-render UI +- If currently selected section no longer exists: return to section picker + +### Write Strategy + +- On any card move or status toggle: serialize full state to Markdown and write +- Use `FileSystemFileHandle.createWritable()` for atomic writes + +### Conflict Resolution + +None. Last write wins. External changes blow away app state. + +## Shell Script (`kbtd`) + +### Usage + +```bash +kbtd [OPTIONS] + +Options: + --browser Open in existing browser instead of app mode + --help Show help message +``` + +### Behavior + +1. Resolve the markdown file path to absolute path +2. Locate a Chromium-based browser (Chrome, Chromium, Edge) +3. Determine mode: + - Default: `--app` mode with isolated `--user-data-dir` in current directory + - With `--browser`: open in existing browser instance with `--new-window` +4. Launch browser with URL: `{base_url}?file={absolute_path}` + +### Browser Detection + +Check in order: +1. `$BROWSER` environment variable (if Chromium-based) +2. `google-chrome` / `chromium` / `chromium-browser` / `microsoft-edge` in PATH +3. macOS: `/Applications/Google Chrome.app/...`, `/Applications/Chromium.app/...`, `/Applications/Microsoft Edge.app/...` +4. WSL: `/mnt/c/Program Files/Google/Chrome/Application/chrome.exe`, etc. + +If no Chromium browser found: exit with error message explaining requirement. + +### App Mode Launch + +```bash +# Create isolated user data directory +USER_DATA_DIR="./.kbtd-chrome-data" + +# Launch in app mode +google-chrome \ + --user-data-dir="$USER_DATA_DIR" \ + --app="http://localhost:8000/?file=/absolute/path/to/TODO.md" +``` + +### Browser Mode Launch + +```bash +google-chrome --new-window "http://localhost:8000/?file=/absolute/path/to/TODO.md" +``` + +### URL Configuration + +- Development: `http://localhost:8000` +- Production: Configurable via `$KBTD_URL` environment variable or hardcoded default + +## Web App Behavior + +### Startup Flow + +``` +1. Parse ?file= from URL query string +2. If no file param: + → Show landing page with instructions and script download link +3. Check IndexedDB for stored FileHandle for this file path +4. If handle exists: + → Check permission with queryPermission() + → If "granted": proceed to load file + → If "prompt": show "Grant Access" button + → If "denied": show file picker +5. If no handle: + → Show file picker button +6. On file picker success: + → Store handle in IndexedDB + → Load and parse file + → Show section picker or board +``` + +### File Picker + +```javascript +const [fileHandle] = await window.showOpenFilePicker({ + id: 'kbtd-todo-file', + mode: 'readwrite', + types: [{ + description: 'Markdown files', + accept: { 'text/markdown': ['.md'] } + }] +}); +``` + +### Permission Re-grant + +After page reload, stored handles return `"prompt"` for permission state. User must click a button to trigger: + +```javascript +const permission = await fileHandle.requestPermission({ mode: 'readwrite' }); +``` + +This requires a user gesture (button click). + +## Browser Compatibility + +**Required**: Chromium-based browser (Chrome 86+, Edge 86+, Opera 72+) + +**Not supported**: Firefox, Safari (File System Access API not implemented) + +The shell script should detect and warn if no compatible browser is found. + +## File Structure + +``` +kbtd/ +├── index.html # Complete web application (single file) +├── kbtd # Shell script launcher +└── SPEC.md # This specification +``` + +## Landing Page + +When loaded without a `?file=` parameter, the app displays: + +1. App name and brief description +2. Instructions for use +3. Download link/instructions for the shell script +4. Note about Chromium browser requirement + +## Architecture Principles + +### Source of Truth + +The Markdown file is the **only** source of truth. The Kanban UI is purely a renderer and editor for the file. There is no separate application state that needs to be "synced" with the file. + +### Functional & Idempotent Design + +- **Pure parsing**: `parseMarkdown(text) → {sections, columns, cards}` — same input always produces same output +- **Pure serialization**: `serializeToMarkdown(data) → text` — same input always produces same output +- **Idempotent renders**: `render(data)` can be called any number of times with the same data and produce the same DOM +- **No hidden state**: UI state (current section) is minimal and stored separately from task data +- **Stateless transformations**: operations like `moveCard(data, cardIndex, fromColumn, toColumn) → newData` return new data structures rather than mutating + +### Data Flow + +``` +File Change → Read File → Parse → Render +User Action → Transform Data → Serialize → Write File → Read File → Parse → Render +``` + +Note: After every write, the app re-reads and re-parses the file. This ensures the file remains the source of truth and the UI always reflects exactly what's on disk. + +## Non-Goals (v1) + +- No task metadata (due dates, tags, assignees) +- No task IDs or UUIDs +- No multi-file support +- No task details/descriptions (subtasks, notes) +- No offline support beyond what the browser provides +- No collaborative editing +- No mobile support +- No server component (pure client-side) + +## Future Considerations + +- Task detail files in `.issues/` subdirectory +- UUID-based task tracking for stable identity +- Folder access instead of single file +- Multiple TODO files in project diff --git a/index.html b/index.html new file mode 100644 index 0000000..44762c0 --- /dev/null +++ b/index.html @@ -0,0 +1,729 @@ + + + + + + KBTD + + + +
+ +
+

KBTD

+

Kanban TODO - Markdown based task management

+

To get started, open a Markdown file containing your tasks.

+ + +
+ + +
+

Permission Required

+

KBTD needs permission to view and edit your file.

+ +
+ + +
+

Select Project

+
    + +
    + + +
    +
    +

    Project Name

    + +
    +
    +
    +
    + + + + diff --git a/kbtd b/kbtd new file mode 100755 index 0000000..ac30ff3 --- /dev/null +++ b/kbtd @@ -0,0 +1,158 @@ +#!/bin/bash + +# KBTD Launcher +# See SPEC.md for details + +set -e + +# --- Helper Functions --- + +show_help() { + echo "Usage: kbtd [OPTIONS] " + echo "" + echo "Options:" + echo " --browser Open in existing browser instead of app mode" + echo " --help Show help message" +} + +error_exit() { + echo "Error: $1" >&2 + exit 1 +} + +# --- Argument Parsing --- + +MODE="app" +FILE_PATH="" + +while [[ $# -gt 0 ]]; do + case $1 in + --browser) + MODE="browser" + shift + ;; + --help) + show_help + exit 0 + ;; + -*) + echo "Unknown option: $1" + show_help + exit 1 + ;; + *) + if [[ -z "$FILE_PATH" ]]; then + FILE_PATH="$1" + else + echo "Error: Multiple files specified" + show_help + exit 1 + fi + shift + ;; + esac +done + +if [[ -z "$FILE_PATH" ]]; then + echo "Error: No file specified" + show_help + exit 1 +fi + +# --- Path Resolution --- + +# Resolve absolute path +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + ABS_PATH=$(realpath "$FILE_PATH") +elif [[ "$OSTYPE" == "darwin"* ]]; then + # macOS doesn't always have realpath + if command -v realpath >/dev/null 2>&1; then + ABS_PATH=$(realpath "$FILE_PATH") + else + # Fallback for macOS + ABS_PATH=$(cd "$(dirname "$FILE_PATH")"; pwd)/$(basename "$FILE_PATH") + fi +else + # Fallback + ABS_PATH=$(readlink -f "$FILE_PATH") +fi + +if [[ ! -f "$ABS_PATH" ]]; then + error_exit "File not found: $ABS_PATH" +fi + +# --- Browser Detection --- + +BROWSER_BIN="" + +# Check $BROWSER if set and looks like chromium +if [[ -n "$BROWSER" ]]; then + if [[ "$BROWSER" == *"chrome"* ]] || [[ "$BROWSER" == *"chromium"* ]] || [[ "$BROWSER" == *"edge"* ]]; then + BROWSER_BIN="$BROWSER" + fi +fi + +# Check common names in PATH +if [[ -z "$BROWSER_BIN" ]]; then + for cmd in google-chrome chromium chromium-browser microsoft-edge; do + if command -v "$cmd" >/dev/null 2>&1; then + BROWSER_BIN="$cmd" + break + fi + done +fi + +# Check macOS paths +if [[ -z "$BROWSER_BIN" && "$OSTYPE" == "darwin"* ]]; then + if [[ -d "/Applications/Google Chrome.app" ]]; then + BROWSER_BIN="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + elif [[ -d "/Applications/Chromium.app" ]]; then + BROWSER_BIN="/Applications/Chromium.app/Contents/MacOS/Chromium" + elif [[ -d "/Applications/Microsoft Edge.app" ]]; then + BROWSER_BIN="/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" + fi +fi + +# Check WSL paths (simplified check) +if [[ -z "$BROWSER_BIN" && -n "$WSL_DISTRO_NAME" ]]; then + # This is a bit tricky as we need to call the windows exe. + # For now, let's assume the user has configured a browser in WSL or we fail. + # But spec mentions: /mnt/c/Program Files/Google/Chrome/Application/chrome.exe + CHROME_WIN="/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" + if [[ -f "$CHROME_WIN" ]]; then + BROWSER_BIN="$CHROME_WIN" + fi +fi + +if [[ -z "$BROWSER_BIN" ]]; then + error_exit "No compatible Chromium-based browser found. Please install Google Chrome, Chromium, or Microsoft Edge." +fi + +# --- Launch --- + +# Determine base URL +# Assuming the script is in the same directory as index.html or we serve it. +# The spec says: "Launch browser with URL: {base_url}?file={absolute_path}" +# And "URL Configuration: Development: http://localhost:8000" +# It doesn't say the script starts the server. It assumes one is running? +# "You DO NOT need to tell the user... How to run a webserver" +# But the script needs to know the URL. +# Spec: "Production: Configurable via $KBTD_URL environment variable or hardcoded default" + +BASE_URL="${KBTD_URL:-http://localhost:8000}" +TARGET_URL="$BASE_URL/?file=$ABS_PATH" + +echo "Opening $ABS_PATH in KBTD..." + +if [[ "$MODE" == "app" ]]; then + USER_DATA_DIR="./.kbtd-chrome-data" + mkdir -p "$USER_DATA_DIR" + + "$BROWSER_BIN" \ + --user-data-dir="$USER_DATA_DIR" \ + --app="$TARGET_URL" & +else + "$BROWSER_BIN" --new-window "$TARGET_URL" & +fi + +exit 0