Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI

on:
push:
branches: [master, main]
pull_request:
branches: [master, main]

jobs:
check:
name: Check & Build
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: TypeScript check
run: yarn tsc --noEmit

- name: ESLint
run: yarn lint

- name: Run tests
run: yarn test

- name: Install build dependencies
run: sudo apt-get update && sudo apt-get install -y libglib2.0-bin

- name: Build extension
run: make dist

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: todozen-extension
path: dist/
retention-days: 30
12 changes: 10 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
node_modules
dist
build

# Compiled JS (from TypeScript)
extension.js
manager.js
history.js
prefs.js
utils.js
yarn.lock
features.md

# Generated files
build-info.json
schemas/gschemas.compiled
package-lock.json
*.zip
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Changelog

## 3.1.0

### Added
- **Task Groups**: Organize tasks into color-coded groups (max 10)
- Group selector dropdown when adding tasks
- Filter tasks by group via header click (cycles: All → Inbox → Group1 → ...)
- Groups manageable in preferences (add, rename, reorder, delete, change color)
- Per-group clear button with confirmation
- **Settings button**: Gear icon in extension popup opens preferences
- **About section in prefs**: Shows version and build time
- **PERFORMANCE.md**: Documentation of performance optimizations and concerns
- GNOME 49 support in `metadata.json`
- `clearAll()` method in manager for efficient bulk deletion
- Lazy population: todo list UI is only built when menu is first opened
- **Settings UI**: Panel position can now be configured (left, center-left, center, center-right, right)
- `prefs.ts` / `prefs.ui`: New preferences dialog using GTK4/Libadwaita
- Live repositioning: Button moves immediately when position setting changes
- **History logging**: All task actions logged to `~/.config/todozen/history.jsonl`

### Changed
- **Clear All moved to prefs**: Removed from extension popup, now in preferences with confirmation dialog
- **LICENSE renamed**: From `LICENCE` to `LICENSE` (standard American spelling)

### Removed
- Deleted `src/utils.ts` - `isEmpty()` utility was unnecessary overhead
- Clear All button from extension popup (moved to prefs)

### Fixed

#### Memory Leaks
- **Confirmation dialog not destroyed**: Changed `remove_child()` to `destroy()` when dismissing confirmation dialogs. `remove_child()` only detaches from parent but keeps the widget alive in memory.
- **Manager reference held after disable**: Added `this._manager = null` in `disable()` to release GSettings reference.
- **Double-destroy attempts**: Removed redundant destroy calls for child widgets (`todosBox`, `scrollView`, `buttonText`, `input`, `clearAllBtn`). When parent (`_indicator`) is destroyed, children are automatically cleaned up. The old code tried to destroy already-destroyed widgets, causing silent errors.

#### Performance
- **JSON.stringify pretty-print removed**: Changed `JSON.stringify(todo, null, 2)` to `JSON.stringify(todo)` in `manager.ts`. The whitespace served no purpose (data is never human-read) and wasted CPU cycles and storage space on every task update.
- **O(n²) to O(1) clear-all**: Old `_clearAllTasks()` called `remove(i)` in a loop, each doing a full GSettings read-modify-write cycle. New implementation does a single `set_strv(TODOS, [])` call.
- **Eliminated double JSON parsing**: Previously `_populate()` parsed all todos, then `_refreshTodosButtonText()` called `getTotalUndone()` which parsed them ALL again. Now `_populate(true)` counts undone tasks while iterating - one pass instead of two.
- **Lazy population**: Todo list UI is deferred until first menu open. On startup, only the button text count is calculated. The full UI render happens on first user interaction, reducing startup CPU usage.
- **Removed isEmpty() function call overhead**: Replaced `isEmpty(todos)` with direct `!todos.length` check. Eliminates function call overhead for a trivial operation.
- **Task/Group caching**: `getParsed()` and `getGroups()` cache results, invalidated via GSettings signals
- **Smart repopulation**: UI only rebuilds on menu open when data has changed (via `_needsPopulate` flag)

#### Code Quality
- **Removed dead code**: Deleted unused `selectionCheckbox` creation (12 lines) that was commented out but still instantiated an object per todo item.
- **Removed unused field**: Deleted `_activeConfirmationTimeoutId` field that was declared but never assigned.
- **Fixed `var` to `const`**: Changed 3 instances of `var` to `const` for better scoping.
- **Fixed typo**: "Copty button" -> "Copy button" in comment.

### Summary of Changes by File

| File | Changes |
|------|---------|
| `metadata.json` | Added "49" to shell-version |
| `src/manager.ts` | Groups support, task/group caching, `clearAll()`, history logging, versioned data model |
| `src/history.ts` | **New** - History logger, writes to `~/.config/todozen/history.jsonl` |
| `src/extension.ts` | Groups UI, filter cycling, settings button, caching, performance fixes |
| `src/prefs.ts` | Groups management, clear all with confirmation, about section |
| `prefs.ui` | Groups section, clear all, about section |
| `schemas/*.xml` | Groups, filter-group, last-selected-group, panel-position |
| `src/utils.ts` | **Deleted** - no longer needed |
| `Makefile` | Build info generation, install/uninstall targets |
| `PERFORMANCE.md` | **New** - Performance documentation |
| `LICENSE` | Renamed from `LICENCE` |
148 changes: 148 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# TodoZen - GNOME Shell Extension

## Project Overview
TodoZen is a GNOME Shell extension for managing tasks with minimal CPU usage. It uses GSettings for data persistence (no polling, no file I/O in the main loop).

## Key Goals

### Performance (Critical)
1. **Zero idle CPU usage** - No polling, timers, or background activity when not in use
2. **Minimal memory footprint** - No caching large data structures, parse on demand
3. **Lazy UI population** - Don't build task list until menu is opened
4. **Single GSettings writes** - Batch operations (e.g., clearAll) instead of N individual writes
5. **Proper cleanup** - Disconnect all signals, remove all timeouts in disable()
6. **No memory leaks** - Destroy widgets properly, null out references, avoid circular refs

### CSS (Minimal & Performant)
1. **No unused selectors** - Remove CSS for deleted features
2. **No duplicate rules** - Each selector defined once
3. **Simple selectors** - Avoid deep nesting, prefer single class selectors
4. **Minimal specificity** - No `!important` unless absolutely necessary
5. **No expensive properties** - Avoid box-shadow, filters, animations on frequently updated elements

### Code Quality
1. TypeScript with strict mode
2. ESLint for code style
3. Unit tests for manager logic
4. Versioned data model for future migrations

## IMPORTANT: Adding New TypeScript Files

When adding a new `.ts` file to `src/`, update `JS_FILES` at the top of **`Makefile`**:

```makefile
JS_FILES = extension.js manager.js history.js prefs.js utils.js
```

This variable is used by all build targets (`make install`, `make pack`, `make dist`, `make verify-dist`).

If you forget, `make check` will fail with "ERROR: Missing files in zip: yourfile.js".

## Build & Install
```bash
make build # Compile TypeScript
make schemas # Compile GSettings schemas
make install # Install to ~/.local/share/gnome-shell/extensions/
make uninstall # Remove extension
make pack # Create distributable zip
make dist # Create dist/ directory (for CI)
make clean # Remove build artifacts
```

After install, logout/login is required (Wayland limitation - see wayland.md).

## Architecture

### Key Files
- `src/extension.ts` - Main extension UI (PanelMenu.Button, popup menu, task list)
- `src/manager.ts` - Data layer (Task/Group CRUD via GSettings)
- `src/utils.ts` - Pure functions (URL extraction, validation, task operations) - fully unit tested
- `src/history.ts` - JSONL logging to `~/.config/todozen/history.jsonl`
- `src/prefs.ts` - Settings UI (panel position, popup width, history toggle)
- `prefs.ui` - GTK4/Libadwaita preferences UI definition
- `schemas/org.gnome.shell.extensions.todozen.gschema.xml` - GSettings schema

### Data Model
Tasks and Groups have version fields for future migrations:
```typescript
interface Task {
version: number; // Current: 1
id: string; // Unique ID: task_<timestamp>_<random>
name: string;
isDone: boolean;
isFocused?: boolean;
groupId?: string; // References Group.id
}

interface Group {
version: number; // Current: 1
id: string; // Unique ID: group_<timestamp> or 'inbox'
name: string;
color: string; // Hex color like #3584e4
}
```

### History Actions
Logged to JSONL when `enable-history` is true:
- Task: added, removed, completed, uncompleted, focused, unfocused, renamed, cleared_all, moved_group
- Group: group_created, group_renamed, group_deleted

### GSettings Keys
- `todos` - string array of JSON Task objects
- `groups` - string array of JSON Group objects
- `last-selected-group` - string (group ID for new tasks)
- `filter-group` - string (filter display by group, empty = all)
- `panel-position` - enum (left, center-left, center, center-right, right)
- `popup-width` - enum (normal=500px, wide=700px, ultra=900px)
- `enable-history` - boolean
- `open-todozen` - keybinding (default Alt+Shift+Space)

## Testing & Linting
```bash
make test # Run unit tests
make lint # Check code style
make lint-fix # Auto-fix lint issues
make check # Run all checks (TypeScript + ESLint + tests)
```

## GNOME Shell Constraints
- Extensions run in the shell process - avoid blocking operations
- Use GLib.timeout_add for delays, not setTimeout
- St widgets (St.Label, St.Button, etc.) for UI
- Clutter for event handling
- Must properly disconnect signals and remove timeouts in disable()

## Migration System
On load, tasks/groups without `version` field are migrated:
- Tasks get: version=1, new ID, groupId='inbox'
- Groups get: version=1

Migrations are saved back to GSettings immediately.

## IMPORTANT: Task Identification is ID-Based

**All task operations MUST use `task.id` for identification, NEVER `task.name`.**

The manager uses array index for GSettings operations, but the index is always found by ID:
```typescript
// CORRECT: Find by ID, operate by index
const index = todos.findIndex(t => t.id === taskId);
this._manager?.update(index, updatedTask);

// WRONG: Never match by name
const index = todos.findIndex(t => t.name === taskName); // DON'T DO THIS
```

The `name` field is only used for:
- Display (showing task text in UI)
- Validation (ensuring task has a name)
- History logging (detecting renames)
- Confirmation dialogs (UX)

### Edit/Rename Behavior (v3.3.0+)
When editing a task:
1. Store `_editingTaskId` (task stays in list, just hidden from display)
2. On submit: find task by ID, update with new name
3. On cancel (close popup/lose focus): clear `_editingTaskId`, task reappears

**Never delete a task when entering edit mode** - this caused data loss in v3.2.0 if edit was cancelled.
Loading