diff options
27 files changed, 0 insertions, 3512 deletions
diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index aa1216e..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,90 +0,0 @@ -# AGENTS.md - -## Project Purpose - -This repository is for the creation and maintenance of Emacs 30 configuration -files. - -## Repository Layout - -- `init.el` is the hand-edited runtime entry point. -- `early-init.el` is the hand-edited early startup file. -- Runtime implementation lives in domain modules under `lisp/`. -- Keep the module set reasonably chunky and domain-based: - `ss-core.el`, `ss-ui.el`, `ss-org.el`, `ss-agenda.el`, `ss-capture.el`, - `ss-denote.el`, `ss-crm.el`, `ss-gptel.el`, and `ss-keys.el`. -- The current Org workflow lives under `~/org/` with `journal.org`, `daily/`, - `projects/`, `areas/`, `areas/people/people.org`, `resources/`, and - `archives/`. -- `~/org/` is external to this repository and must already exist. -- The configuration may open files in `~/org`, but it must not create - directories, create files, or validate note structure. -- `~/org/journal.org` is the operational journal. It must already exist. The - configuration may open it, but it must not create or manage it. -- `~/org/moc.org` is a normal note. It must already exist. The configuration - may open it, but it must not create or manage it. -- `~/org/areas/people/people.org` is the people CRM file. It must already - exist. The configuration may open it, but it must not create or manage it. -- Agenda files are discovered by explicitly including `~/org/journal.org` and - recursively scanning `.org` files under `~/org/projects/`, `~/org/areas/`, - and `~/org/resources/`. -- Agenda discovery must exclude `~/org/archives/`. -- PARA is the organising model for durable notes, and folder placement carries - meaning. -- The config includes an experimental `gptel` setup that uses GitHub Copilot - as the backend. -- Do not treat `auto-save-list/` as source content. - -## Editing Expectations - -- Prefer small, focused changes over broad rewrites. -- Edit `init.el`, `early-init.el`, and the relevant `lisp/` modules directly. -- Keep `init.el` as the central composition layer with explicit feature - inclusion. -- Keep side effects out of require time where practical; modules should expose - setup functions. -- Preserve existing Emacs Lisp style and naming where patterns already exist. -- Avoid unrelated refactors while working on a specific configuration task. -- Be explicit about GUI-versus-terminal behavior. If a change affects - `emacs -nw`, avoid moving terminal UI changes earlier in startup unless that - timing is intentional. - -## Validation Expectations - -- For Emacs Lisp changes, verify the hand-edited startup path directly. -- When Emacs is available, prefer a batch check such as - `emacs --batch -Q --load ./init.el` from the repository root. -- Keep regression checks aligned with the modular startup path documented in - `README.md`. -- For changes that affect terminal Emacs behavior, verify in an actual - `emacs -nw` session as well as batch mode; batch load alone will not catch - interactive tty regressions. - -## Git Workflow Expectations - -- Use a git-based workflow for repository changes. -- Assume changes will be committed incrementally as the work progresses. -- Keep changes small and focused so they can be reviewed and committed - independently. -- Treat verification as the gate for each commit-sized unit of work. -- After verification passes for a unit of work, summarize the result and prompt - the user before creating the commit. -- Keep commit messages concise and specific to the verified change. -- Do not bundle unrelated changes into the same commit. - -## Documentation Expectations - -- Document non-obvious conventions close to the relevant Lisp module. -- Update `README.md` whenever configuration or workflow changes alter package - usage, startup behavior, keybindings, directory layout, capture flow, or - other documented behavior. -- `README.md` must describe the current configuration truthfully. Do not leave - stale documentation behind and do not document planned behavior as current - behavior. -- Keep `README.md` and `AGENTS.md` in sync with the modular architecture. -- Use `~/org` consistently when describing the external notes directory. -- Before claiming a change is complete or asking to commit it, review whether - `README.md` needs an update; if it does not, say so explicitly in the - summary. -- Keep `AGENTS.md` concise and update it only when the repository structure or - working rules actually change. diff --git a/README.md b/README.md deleted file mode 100644 index da53922..0000000 --- a/README.md +++ /dev/null @@ -1,284 +0,0 @@ -# Emacs Configuration - -This repository contains a modular Emacs configuration built around Org mode, -Denote, a PARA-style note layout, a people CRM, and a small completion stack. -`init.el` is the hand-edited entry point, `early-init.el` handles true early -startup concerns, and the runtime implementation is hand-edited across the -domain modules under `lisp/`. - -## System Model - -This repository configures Emacs. It does not define, create, or validate the -`~/org` note system. - -- `init.el` is the source of truth for runtime composition and feature - selection. -- `early-init.el` is the source of truth for true early startup settings. -- `lisp/ss-*.el` is the source of truth for the runtime implementation by - domain. -- `~/org` is external to this repository and must already exist. -- The configuration may open files in `~/org`, but it must not create - directories, create files, or validate note structure. -- `~/org/journal.org` is the operational journal. It must already exist, and - the configuration may open it but must not create or manage it. -- `~/org/moc.org` is a normal note. It must already exist, and the - configuration may open it but must not create or manage it. -- `~/org/areas/people/people.org` is the people CRM file. It must already - exist, and the configuration may open it but must not create or manage it. -- PARA is the organising model for durable notes. Folder placement carries - meaning, and workflows must respect that placement. - -## Repository Layout - -The runtime architecture is: - -```text -early-init.el -init.el -lisp/ - ss-core.el - ss-ui.el - ss-org.el - ss-agenda.el - ss-capture.el - ss-denote.el - ss-crm.el - ss-gptel.el - ss-keys.el -``` - -The module responsibilities are: - -- `ss-core.el` bootstraps packages, defines shared paths and helpers, and - applies shared editor defaults. -- `ss-ui.el` owns theme, fonts, frame behavior, modeline, and completion UI. -- `ss-org.el` owns base Org setup, startup MOC behavior, and shared note - helpers. -- `ss-agenda.el` owns agenda discovery and agenda commands. -- `ss-capture.el` owns journal capture structure and capture templates. -- `ss-denote.el` owns Denote setup and durable-note capture helpers. -- `ss-crm.el` owns all people CRM behavior. -- `ss-gptel.el` owns the experimental GitHub Copilot-backed `gptel` setup. -- `ss-keys.el` owns global keybindings only. - -`init.el` enables high-level features centrally through `ss-enabled-features`. -Feature toggling works by including or excluding a module there. - -## Package Model - -The config bootstraps packages with built-in `package.el` and uses -`use-package` for declaration and load order. Package archives are configured -with GNU, NonGNU ELPA, and MELPA, with GNU given highest priority. - -The current setup uses: - -- `org` and `org-capture` for agenda and journal capture -- `denote` for durable notes, naming, keywords, and linking -- `git-auto-commit-mode` for optional automatic commits inside `~/org` when - enabled by directory-local settings -- `vertico` for minibuffer completion UI -- `orderless` for flexible completion matching -- `marginalia` for minibuffer annotations -- `corfu` for in-buffer completion popups in text and Org buffers -- `olivetti` for centered writing layout in Org buffers -- `gptel` with the GitHub Copilot backend as an experimental tool -- `dired` with a macOS-safe `ls` configuration -- `time` for the modeline clock -- `modus-themes`, using `modus-vivendi` - -## Org Layout - -The note system lives under `~/org/` and is organised like this: - -- `journal.org` for the operational journal -- `daily/` for older daily Org files that may still exist -- `projects/` for project notes -- `areas/` for area notes -- `areas/people/people.org` for the people CRM -- `resources/` for reference material -- `archives/` for archived notes - -Agenda discovery is rule-based: - -- include `~/org/journal.org` -- recursively scan `.org` files under `~/org/projects/`, `~/org/areas/`, and - `~/org/resources/` -- exclude `~/org/archives/` - -## People CRM - -The people workflow is a CRM rooted at `~/org/areas/people/people.org`. - -- each top-level heading represents one person -- entries are structured around heading text and flat properties -- the system rebuilds people abbrevs from the CRM file -- a CAPF provides canonical-name completion while alias matching remains - available for lookup -- Marginalia annotates people with `role | team | engagement | current focus` -- reports are available by role, team, manager, engagement, supplier, and - location -- `TEAM` captures the current working team and `MANAGER` captures the formal - organisational manager -- person cards use `ROLE`, `TEAM`, `MANAGER`, `ENGAGEMENT`, `SUPPLIER`, - `LOCATION`, and `CURRENT_FOCUS` in that order - -The CRM commands are: - -- `M-x ss-crm-open` -- `M-x ss-crm-overview` -- `M-x ss-crm-find` -- `M-x ss-crm-insert-name` -- `M-x ss-crm-insert-summary` -- `M-x ss-crm-add` -- `M-x ss-crm-report-by-role` -- `M-x ss-crm-report-by-team` -- `M-x ss-crm-report-by-manager` -- `M-x ss-crm-report-by-engagement` -- `M-x ss-crm-report-by-supplier` -- `M-x ss-crm-report-by-location` - -Persistent abbrevs live in `abbrev_defs` at the repository root. The config -loads that file on startup, enables abbrev mode only in text-like buffers, and -saves learned abbrevs back to the same file silently when buffers are saved. -People-specific abbrevs are rebuilt from `~/org/areas/people/people.org` -whenever that file changes. - -## Workflow - -`~/org/moc.org` is a normal note. It is treated as a curated navigation note, -not a generated system file. The config may open it on startup, and `C-c n M` -opens it manually. - -The capture model has two distinct paths: - -- fast operational capture goes to `~/org/journal.org` -- durable notes use Denote in the PARA directories under `~/org/` - -`~/org/journal.org` remains the operational capture surface. Journal capture -uses a Year -> Day outline in `journal.org` with explicit `Tasks`, `Notes`, -and `Meetings` headings beneath each day entry. - -Org buffers enable `olivetti-mode` automatically for a centered writing layout -in both GUI and terminal Emacs sessions. - -Invisible edits in Org are blocked with an error rather than silently changing -hidden content. - -Questions that come up during the day can be tracked as Org tasks under the -day's `Tasks` heading. The intended task workflow is `TODO`, `CLARIFY`, -`WAIT`, `DONE`, and `CANCELLED`: use `CLARIFY` for open questions or ambiguity, -and `WAIT` once the question has been asked and the answer is pending. -Completing an item prompts for a note so the answer can be recorded in the -task log, and those state logs are stored in a `LOGBOOK` drawer. - -Org refile uses the current `org-agenda-files` set as its target space and can -move entries to any heading within those files. Refile selection uses full -outline paths, including the file name, so the existing Vertico, Orderless, and -Marginalia stack can present a clearer path-based destination prompt. - -The configured capture templates cover: - -- journal tasks -- journal notes -- journal meetings -- Denote-backed captures for generic notes, projects, areas, and resources - -The people CRM remains outside `org-capture`: `M-x ss-crm-add` writes directly -to `~/org/areas/people/people.org`. - -Navigation history adds a small browser-style back and forward layer on top of -mark-based movement. It records significant note jumps from the custom journal -and MOC commands, CRM find/open commands, `org-open-at-point`, common agenda -jumps, and `denote-open-or-create`, while ignoring ordinary cursor motion and -same-location no-ops. - -## Keybindings - -The main bindings are: - -- `C-c a` for the agenda -- `C-c b` to move back through recorded note and mark navigation history -- `c` inside the agenda dispatcher to show the custom `Clarify items` view for - `CLARIFY` tasks -- `C-c c` for capture -- `C-c f` to move forward again after using navigation back -- `C-c n n` to open or create a Denote note -- `C-c n l` to insert a Denote link -- `C-c n j` to open the full `~/org/journal.org` buffer -- `C-c n M` to open the MOC -- `C-c n d` to open today's journal entry in a focused session; when today's - entry does not yet exist, the command creates it using the normal journal - datetree structure, then narrows to that entry; inside the session, - `C-c C-c` saves and dismisses, and `C-c C-k` dismisses without auto-saving -- `C-c n p` to open the people CRM -- `C-c n P` to add a new person card -- `C-c n f` to find a person card -- `C-c n i` to insert a canonical person name -- `C-c n I` to insert a compact person summary -- `C-c n o` to restore the people overview -- `C-c n O` to show people grouped by role -- `C-c n T` to show people grouped by team -- `C-c n R` to show people grouped by manager -- `C-c n E` to show people grouped by engagement -- `C-c n S` to show people grouped by supplier -- `C-c n L` to show people grouped by location -- `C-c n g` to start `gptel` -- `C-c n s` to send in `gptel` -- `C-c n r` to rewrite with `gptel` -- `C-c n a` to add context in `gptel` - -## Automatic Note Commits - -The configuration provides `git-auto-commit-mode` capability. Behaviour is -defined in `~/org/.dir-locals.el`. - -When enabled in `~/org/.dir-locals.el`, saving a file in `~/org/` makes Emacs -try to commit that change. The Emacs config supplies the package and selects -the shell command separator based on the active shell, while the note tree -defines add, push, debounce, and commit-message behavior. - -Place this file at `~/org/.dir-locals.el`: - -```emacs-lisp -((nil . - ((eval . - (progn - (setq-local gac-automatically-add-new-files-p t - gac-automatically-push-p t - gac-debounce-interval 60 - gac-default-message - (lambda (_filename) - (format-time-string "Auto-commit: %Y-%m-%d %H:%M:%S"))) - (git-auto-commit-mode 1)))))) -``` - -## Validation - -The primary verification command for the runtime path is: - -```sh -emacs --batch -Q --load ./init.el -``` - -`early-init.el` and `init.el` are hand-edited source files, not generated -artifacts. - -## Terminal and GUI Behavior - -GUI Emacs and terminal Emacs are handled slightly differently. - -- GUI frames get the preferred frame size, font setup, and UI trimming. -- In `emacs -nw`, the menu bar is disabled on `emacs-startup-hook` rather than - earlier in startup, because changing that timing too early caused - interactive terminal regressions in kitty. - -If you change terminal behavior, test it in a real `emacs -nw` session. Batch -load checks are necessary, but they are not enough for tty input and UI -behavior. - -## Maintenance Rules - -- Edit `init.el`, `early-init.el`, and `lisp/ss-*.el` directly. -- Keep this README aligned with the current configuration. -- Keep `README.md` and `AGENTS.md` in sync. -- Do not document planned behavior as if it already exists. diff --git a/abbrev_defs b/abbrev_defs deleted file mode 100644 index 64bee34..0000000 --- a/abbrev_defs +++ /dev/null @@ -1 +0,0 @@ -;;-*-coding: utf-8;-*- diff --git a/docs/plans/2026-04-09-crm-property-completion-implementation.md b/docs/plans/2026-04-09-crm-property-completion-implementation.md deleted file mode 100644 index 88b3d01..0000000 --- a/docs/plans/2026-04-09-crm-property-completion-implementation.md +++ /dev/null @@ -1,144 +0,0 @@ -# CRM Property Completion Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add data-driven CRM property completion and inline value creation to `ss-crm-add` without introducing a second source of truth. - -**Architecture:** Extend `lisp/ss-crm.el` with small lookup helpers that build on the existing parsed CRM entry cache, plus a single generic choice reader that handles blank selection, optional freeform values, confirmation, and advisory duplicate warnings. Keep seeded vocabularies in code, wire field-specific readers into `ss-crm-add`, and verify with focused ERT coverage plus batch startup loading. - -**Tech Stack:** Emacs Lisp, Org, ERT, batch Emacs verification - ---- - -### Task 1: Add failing CRM helper tests - -**Files:** -- Create: `tests/ss-crm-tests.el` -- Modify: `lisp/ss-crm.el` - -**Step 1: Write the failing test** - -```elisp -(ert-deftest ss-crm-known-property-values-sorts-and-deduplicates () - (cl-letf (((symbol-function 'ss-crm-entries) - (lambda () - (list (list :role "Engineer") - (list :role " engineer ") - (list :role "Architect") - (list :role nil))))) - (should (equal (ss-crm-known-property-values "ROLE") - '("Architect" "Engineer" " engineer "))))) -``` - -**Step 2: Run test to verify it fails** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-crm-tests.el -f ert-run-tests-batch-and-exit` -Expected: FAIL because the new CRM helper functions do not exist yet. - -**Step 3: Write minimal implementation** - -```elisp -(defun ss-crm-known-property-values (property) - ...) -``` - -**Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-crm-tests.el -f ert-run-tests-batch-and-exit` -Expected: PASS for the helper coverage. - -**Step 5: Commit** - -```bash -git add tests/ss-crm-tests.el lisp/ss-crm.el -git commit -m "Add CRM property completion helpers" -``` - -### Task 2: Add completion-reader tests and implement prompt behavior - -**Files:** -- Modify: `tests/ss-crm-tests.el` -- Modify: `lisp/ss-crm.el` - -**Step 1: Write the failing test** - -```elisp -(ert-deftest ss-crm-read-choice-warns-on-new-case-insensitive-duplicate () - (let (warning) - (cl-letf (((symbol-function 'completing-read) (lambda (&rest _) "sydney")) - ((symbol-function 'yes-or-no-p) (lambda (&rest _) t)) - ((symbol-function 'display-warning) - (lambda (_type message &rest _) (setq warning message)))) - (should (equal (ss-crm-read-choice "Location: " '("Sydney") - :allow-blank t - :allow-new t) - "sydney")) - (should (string-match-p "Sydney" warning)))) -``` - -**Step 2: Run test to verify it fails** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-crm-tests.el -f ert-run-tests-batch-and-exit` -Expected: FAIL because `ss-crm-read-choice` does not support the new behavior yet. - -**Step 3: Write minimal implementation** - -```elisp -(defun ss-crm-read-choice (prompt choices &rest plist) - ...) -``` - -**Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-crm-tests.el -f ert-run-tests-batch-and-exit` -Expected: PASS for blank selection, existing completion, and new-value confirmation coverage. - -**Step 5: Commit** - -```bash -git add tests/ss-crm-tests.el lisp/ss-crm.el -git commit -m "Add CRM completion prompts" -``` - -### Task 3: Integrate field readers into `ss-crm-add` and verify startup - -**Files:** -- Modify: `lisp/ss-crm.el` -- Review: `README.md` - -**Step 1: Write the failing test** - -```elisp -(ert-deftest ss-crm-read-manager-uses-known-person-names () - (cl-letf (((symbol-function 'ss-crm-known-person-names) - (lambda () '("Alice" "Bob"))) - ((symbol-function 'ss-crm-read-choice) - (lambda (_prompt choices &rest _plist) choices))) - (should (equal (ss-crm-read-manager) '("Alice" "Bob"))))) -``` - -**Step 2: Run test to verify it fails** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-crm-tests.el -f ert-run-tests-batch-and-exit` -Expected: FAIL until the field readers are wired up. - -**Step 3: Write minimal implementation** - -```elisp -(defun ss-crm-read-manager () - (ss-crm-read-choice "Manager: " (ss-crm-known-person-names) - :allow-blank t - :require-match t)) -``` - -**Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-crm-tests.el -f ert-run-tests-batch-and-exit` -Expected: PASS, then run `emacs --batch -Q --load ./init.el` to confirm startup remains healthy. - -**Step 5: Commit** - -```bash -git add tests/ss-crm-tests.el lisp/ss-crm.el README.md -git commit -m "Guide CRM add-person property entry" -``` diff --git a/docs/plans/2026-04-09-journal-open-narrowing-implementation.md b/docs/plans/2026-04-09-journal-open-narrowing-implementation.md deleted file mode 100644 index 59a80de..0000000 --- a/docs/plans/2026-04-09-journal-open-narrowing-implementation.md +++ /dev/null @@ -1,85 +0,0 @@ -# Journal Open Narrowing Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Make `ss-open-journal` narrow to today's subtree when today's journal entry exists, while preserving the current fallback when it does not. - -**Architecture:** Add focused ERT coverage for the journal-open helper in `tests/`, then update `lisp/ss-org.el` so it widens first, reuses the existing `ss-journal-goto-date` lookup, and narrows only on the successful path. Keep the missing-entry case unchanged by leaving point at the end of the journal buffer without creating new headings. - -**Tech Stack:** Emacs Lisp, ERT, batch Emacs verification - ---- - -### Task 1: Add failing journal-open test - -**Files:** -- Modify: `tests/ss-capture-tests.el` -- Modify: `lisp/ss-org.el` - -**Step 1: Write the failing test** - -```elisp -(ert-deftest ss-open-journal-narrows-to-today-when-entry-exists () - ...) -``` - -**Step 2: Run test to verify it fails** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-capture-tests.el -f ert-run-tests-batch-and-exit` -Expected: FAIL because `ss-open-journal` currently widens and jumps, but does not narrow. - -**Step 3: Write minimal implementation** - -```elisp -(when (ss-journal-goto-date) - (org-narrow-to-subtree)) -``` - -**Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-capture-tests.el -f ert-run-tests-batch-and-exit` -Expected: PASS, with the fallback case still leaving the buffer widened at end of file. - -**Step 5: Commit** - -```bash -git add tests/ss-capture-tests.el lisp/ss-org.el -git commit -m "Narrow journal open to today" -``` - -### Task 2: Run regression verification - -**Files:** -- Review: `README.md` -- Verify: `lisp/ss-org.el` - -**Step 1: Write the failing test** - -```elisp -(ert-deftest ss-open-journal-falls-back-to-end-when-today-missing () - ...) -``` - -**Step 2: Run test to verify it fails** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-capture-tests.el -f ert-run-tests-batch-and-exit` -Expected: FAIL until the fallback remains explicitly covered. - -**Step 3: Write minimal implementation** - -```elisp -(unless (ss-journal-goto-date) - (goto-char (point-max))) -``` - -**Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-capture-tests.el -l tests/ss-crm-tests.el -f ert-run-tests-batch-and-exit` -Expected: PASS, then run `emacs --batch -Q --load ./init.el` to confirm startup remains healthy. - -**Step 5: Commit** - -```bash -git add tests/ss-capture-tests.el lisp/ss-org.el README.md -git commit -m "Preserve journal open fallback" -``` diff --git a/docs/plans/2026-04-09-modular-emacs-architecture-design.md b/docs/plans/2026-04-09-modular-emacs-architecture-design.md deleted file mode 100644 index 1c2ce14..0000000 --- a/docs/plans/2026-04-09-modular-emacs-architecture-design.md +++ /dev/null @@ -1,81 +0,0 @@ -# Modular Emacs Architecture Design - -**Date:** 2026-04-09 - -## Goal - -Refactor the repository from a single literate `config.org` source into a -hand-edited modular Emacs configuration built around `init.el`, -`early-init.el`, and a small set of domain-based Lisp modules under `lisp/`, -while preserving existing behavior as closely as possible. - -## Scope - -This change is an architectural refactor, not a workflow redesign. Existing -startup behavior, packages, capture flows, agenda rules, CRM commands, -completion setup, and keybindings should remain materially the same unless a -small structural adjustment is required by the new module boundaries. - -## Architecture - -### Entry points - -- `early-init.el` remains a standalone file and contains only true early - startup concerns that must exist before the first GUI frame. -- `init.el` becomes the hand-edited runtime entry point. -- `init.el` adds `lisp/` to `load-path`, defines a central - `ss-enabled-features` list, requires `ss-core`, and conditionally loads and - sets up each high-level feature module. - -### Module boundaries - -- `lisp/ss-core.el` - Owns Emacs version checks, package bootstrap, shared constants and helper - functions, note-system paths, and small shared editor defaults that other - modules depend on. -- `lisp/ss-ui.el` - Owns theme, fonts, frame behavior, modeline, and terminal-versus-GUI UI - setup. -- `lisp/ss-org.el` - Owns base Org configuration, shared Org helpers, startup MOC behavior, and - note-opening helpers. -- `lisp/ss-agenda.el` - Owns agenda file discovery rules and agenda command setup. -- `lisp/ss-capture.el` - Owns journal capture helpers and `org-capture` templates. -- `lisp/ss-denote.el` - Owns Denote configuration and note creation helpers. -- `lisp/ss-crm.el` - Owns all people CRM logic, including parsing, cache management, abbrevs, - CAPF, lookup, open/find/insert/add/report commands, and prompt helpers. -- `lisp/ss-gptel.el` - Owns the experimental `gptel` integration. -- `lisp/ss-keys.el` - Owns central global keybindings only. - -### Dependency shape - -- `ss-core` is the only unconditional module. -- Other modules may depend on `ss-core` helpers and shared path constants. -- `ss-keys` binds commands provided by other modules but should not implement - workflow logic itself. -- Side effects should happen in each module's `ss-...-setup` function rather - than during `require`, except for definitions that are harmless at load time. - -## Migration decisions - -- Remove `config.org` entirely after extraction. There will be no transitional - dual system and no compatibility tangling layer. -- Keep `build` only if it remains useful for the modular setup; otherwise - remove or repurpose it truthfully. -- Keep `reset` only if it still aligns with the hand-edited architecture; - update it so it no longer treats `init.el` or `early-init.el` as generated - artifacts. - -## Validation - -- Primary validation: `emacs --batch -Q --load ./init.el` -- If the refactor preserves terminal-specific UI behavior, also verify in a - real `emacs -nw` session when practical. -- Documentation must be updated in the same change so `README.md` and - `AGENTS.md` describe the modular architecture truthfully. diff --git a/docs/plans/2026-04-09-modular-emacs-refactor.md b/docs/plans/2026-04-09-modular-emacs-refactor.md deleted file mode 100644 index ad44ca1..0000000 --- a/docs/plans/2026-04-09-modular-emacs-refactor.md +++ /dev/null @@ -1,152 +0,0 @@ -# Modular Emacs Refactor Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Replace the literate `config.org` runtime with a modular hand-edited Emacs configuration centered on `init.el`, `early-init.el`, and `lisp/ss-*.el`. - -**Architecture:** Keep `early-init.el` minimal, move reusable runtime logic into domain modules under `lisp/`, and make `init.el` the single composition layer that enables features explicitly and calls one setup function per module. Preserve current behavior unless a structural refactor requires a minimal change. - -**Tech Stack:** Emacs Lisp, built-in `package.el`, `use-package`, Org mode, Denote, gptel - ---- - -### Task 1: Create the modular file layout - -**Files:** -- Create: `early-init.el` -- Create: `init.el` -- Create: `lisp/ss-core.el` -- Create: `lisp/ss-ui.el` -- Create: `lisp/ss-org.el` -- Create: `lisp/ss-agenda.el` -- Create: `lisp/ss-capture.el` -- Create: `lisp/ss-denote.el` -- Create: `lisp/ss-crm.el` -- Create: `lisp/ss-gptel.el` -- Create: `lisp/ss-keys.el` - -**Step 1: Create `early-init.el` with only early frame settings** - -Write the standalone early startup file and keep it limited to frame defaults -that previously had to exist before the first GUI frame. - -**Step 2: Create `init.el` as the composition root** - -Add `lisp/` to `load-path`, define `ss-enabled-features`, require `ss-core`, -and conditionally require each optional module before calling its setup -function. - -**Step 3: Create module skeletons** - -Add one `ss-...-setup` function per module and provide each feature. - -### Task 2: Extract shared runtime and UI behavior - -**Files:** -- Modify: `lisp/ss-core.el` -- Modify: `lisp/ss-ui.el` - -**Step 1: Move version checks, package bootstrap, shared paths, and editing defaults into `ss-core.el`** - -Keep shared constants and helper functions in one place and avoid hidden -cross-module state. - -**Step 2: Move theme, fonts, frame behavior, and modeline setup into `ss-ui.el`** - -Preserve the current GUI-versus-terminal behavior and keep side effects in -`ss-ui-setup`. - -### Task 3: Extract Org, agenda, capture, and Denote domains - -**Files:** -- Modify: `lisp/ss-org.el` -- Modify: `lisp/ss-agenda.el` -- Modify: `lisp/ss-capture.el` -- Modify: `lisp/ss-denote.el` - -**Step 1: Move shared Org paths and note helpers into `ss-org.el`** - -Keep `~/org` invariants unchanged and preserve startup MOC behavior. - -**Step 2: Move agenda discovery and agenda command wiring into `ss-agenda.el`** - -Preserve explicit include and exclude rules. - -**Step 3: Move journal capture helpers and `org-capture` templates into `ss-capture.el`** - -Keep the existing templates and journal structure intact. - -**Step 4: Move Denote setup into `ss-denote.el`** - -Preserve prompts, keywords, and key-facing commands. - -### Task 4: Extract CRM and gptel domains - -**Files:** -- Modify: `lisp/ss-crm.el` -- Modify: `lisp/ss-gptel.el` - -**Step 1: Move all CRM logic into `ss-crm.el`** - -Keep parsing, cache invalidation, abbrevs, CAPF, reports, and commands -together in the CRM module only. - -**Step 2: Move experimental Copilot-backed gptel setup into `ss-gptel.el`** - -Preserve existing commands and defaults. - -### Task 5: Centralize keybindings - -**Files:** -- Modify: `lisp/ss-keys.el` - -**Step 1: Bind the existing workflow commands in one place** - -Move global bindings out of the feature modules so feature inclusion remains -centralized and explicit. - -### Task 6: Update scripts and documentation - -**Files:** -- Modify: `README.md` -- Modify: `AGENTS.md` -- Modify or delete: `build` -- Modify: `reset` -- Delete: `config.org` - -**Step 1: Rewrite documentation to describe the modular architecture truthfully** - -Remove stale references to tangling, generated startup files, and `config.org` -as the source of truth. - -**Step 2: Update helper scripts** - -Remove or rewrite anything that only made sense for the literate build path. - -**Step 3: Remove `config.org`** - -Delete the literate source once the extracted runtime files are in place. - -### Task 7: Validate the new startup path - -**Files:** -- Verify: `init.el` -- Verify: `early-init.el` -- Verify: `lisp/ss-*.el` - -**Step 1: Run batch load verification** - -Run: `emacs --batch -Q --load ./init.el` - -Expected: startup completes without load errors. - -**Step 2: Run a terminal startup check if practical** - -Run: `emacs -nw` - -Expected: terminal UI behavior still matches the previous configuration. - -**Step 3: Summarize validation and any intentional behavior changes** - -Note any small structural changes required by the refactor, and call out -whether `README.md` and `AGENTS.md` were updated. diff --git a/docs/plans/2026-04-10-navigation-history-implementation.md b/docs/plans/2026-04-10-navigation-history-implementation.md deleted file mode 100644 index 0d58756..0000000 --- a/docs/plans/2026-04-10-navigation-history-implementation.md +++ /dev/null @@ -1,118 +0,0 @@ -# Navigation History Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add small, browser-style back and forward navigation commands for note and mark-based movement across Org, agenda, CRM, and Denote workflows. - -**Architecture:** Keep the navigation state and restoration helpers in `lisp/ss-org.el`, using marker-based location records plus a simple back stack and forward stack. Record only significant jumps by wiring the repo's custom note commands and advising common built-in jump commands after they move, while skipping same-location noise and clearing forward history on fresh navigation. - -**Tech Stack:** Emacs Lisp, ERT, batch Emacs verification, interactive `emacs -nw` sanity check - ---- - -### Task 1: Add failing navigation tests - -**Files:** -- Modify: `tests/ss-capture-tests.el` -- Modify: `lisp/ss-org.el` - -**Step 1: Write the failing tests** - -```elisp -(ert-deftest ss-jump-back-restores-previous-location-and-enables-forward () - ...) - -(ert-deftest ss-navigation-push-current-location-clears-forward-on-fresh-jump () - ...) -``` - -**Step 2: Run test to verify it fails** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-capture-tests.el -f ert-run-tests-batch-and-exit` -Expected: FAIL because the navigation stack commands and helper functions do not exist yet. - -**Step 3: Write minimal implementation** - -```elisp -(defvar ss-navigation-back-stack nil) -(defvar ss-navigation-forward-stack nil) -... -``` - -**Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-capture-tests.el -f ert-run-tests-batch-and-exit` -Expected: PASS for the new stack behavior tests. - -**Step 5: Commit** - -```bash -git add tests/ss-capture-tests.el lisp/ss-org.el -git commit -m "Add navigation history stack" -``` - -### Task 2: Wire note and jump commands into history - -**Files:** -- Modify: `lisp/ss-org.el` -- Modify: `lisp/ss-keys.el` - -**Step 1: Write the failing test** - -```elisp -(ert-deftest ss-navigation-jump-wrapper-records-pre-jump-location () - ...) -``` - -**Step 2: Run test to verify it fails** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-capture-tests.el -f ert-run-tests-batch-and-exit` -Expected: FAIL because custom navigation commands and advised jump commands do not record history yet. - -**Step 3: Write minimal implementation** - -```elisp -(defun ss-navigation-record-before-command (&rest _) - ...) - -(advice-add 'org-open-at-point :before #'ss-navigation-record-before-command) -``` - -**Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q -L . -L lisp -l tests/ss-capture-tests.el -l tests/ss-crm-tests.el -f ert-run-tests-batch-and-exit` -Expected: PASS, with the navigation helpers loaded cleanly beside the existing CRM and capture tests. - -**Step 5: Commit** - -```bash -git add lisp/ss-org.el lisp/ss-keys.el tests/ss-capture-tests.el -git commit -m "Wire note jumps into navigation history" -``` - -### Task 3: Update docs and verify startup behavior - -**Files:** -- Modify: `README.md` -- Review: `AGENTS.md` - -**Step 1: Update docs** - -Add the new `C-c b` and `C-c f` bindings plus a short explanation of what participates in navigation history. - -**Step 2: Run verification** - -Run: `emacs --batch -Q --load ./init.el` -Expected: PASS with the updated navigation code loaded through the normal startup path. - -**Step 3: Run interactive sanity check** - -Run: `emacs -nw` -Expected: manual verification that MOC, journal, and note jumps can go back and forward, and that a fresh jump clears forward history. - -**Step 4: Commit** - -```bash -git add README.md -git commit -m "Document navigation history bindings" -``` diff --git a/docs/plans/2026-04-10-olivetti-org-design.md b/docs/plans/2026-04-10-olivetti-org-design.md deleted file mode 100644 index 863ca65..0000000 --- a/docs/plans/2026-04-10-olivetti-org-design.md +++ /dev/null @@ -1,57 +0,0 @@ -# Olivetti Org Design - -## Context - -The configuration is modular: - -- `lisp/ss-ui.el` owns visual packages and interface defaults. -- `lisp/ss-org.el` owns Org-specific setup and hooks. -- `init.el` composes modules centrally through `ss-enabled-features`. - -The requested behavior is to add `olivetti-mode` to the configuration and -enable it automatically for Org buffers in both GUI Emacs and `emacs -nw`. - -## Options Considered - -### 1. Recommended: split package ownership and activation by module - -- Declare and configure `olivetti` in `lisp/ss-ui.el`. -- Enable `olivetti-mode` from `org-mode-hook` in `lisp/ss-org.el`. - -This matches the repository boundaries: visual package ownership remains in the -UI module, while Org-specific behavior remains in the Org module. - -### 2. Put everything in `lisp/ss-org.el` - -- Add the package declaration and the hook together in the Org module. - -This is workable but muddies module boundaries by making the Org module own a -general presentation package. - -### 3. Add a new writing-focused module - -- Introduce a dedicated module for prose layout and writing helpers. - -This is clean only if more writing-mode features are expected soon. For a -single package addition, it adds unnecessary structure. - -## Chosen Design - -Use option 1. - -- Add `olivetti` in `lisp/ss-ui.el` with a modest body width that works in GUI - and terminal frames. -- Enable `olivetti-mode` automatically in Org buffers from `lisp/ss-org.el`. -- Do not enable it globally or for non-Org buffers. -- Keep startup order unchanged. - -## Verification - -- Run `emacs --batch -Q --load ./init.el` from the repository root. -- Run an actual terminal Emacs startup check with `emacs -nw` loading this - configuration, since batch mode alone will not catch tty regressions. - -## Documentation Impact - -`README.md` should be updated so the package list and Org behavior describe the -new default truthfully. diff --git a/docs/plans/2026-04-10-olivetti-org-implementation.md b/docs/plans/2026-04-10-olivetti-org-implementation.md deleted file mode 100644 index 7853be3..0000000 --- a/docs/plans/2026-04-10-olivetti-org-implementation.md +++ /dev/null @@ -1,129 +0,0 @@ -# Olivetti Org Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add `olivetti` to the Emacs config and enable it automatically for Org buffers in GUI and terminal sessions. - -**Architecture:** Keep visual package ownership in `lisp/ss-ui.el` and Org-specific activation in `lisp/ss-org.el`. Update `README.md` so the package model and Org behavior remain accurate, then verify the hand-edited startup path in batch and terminal Emacs. - -**Tech Stack:** Emacs Lisp, `use-package`, Org mode, `olivetti` - ---- - -### Task 1: Add the visual package setup - -**Files:** -- Modify: `lisp/ss-ui.el` - -**Step 1: Write the failing test** - -For this configuration-only change, there is no existing automated test harness -covering package declarations or minor-mode activation. Use startup -verification as the regression check for this unit. - -**Step 2: Run test to verify it fails** - -Not applicable for this repository layout. The useful red state is the absence -of `olivetti` configuration in the current source. - -**Step 3: Write minimal implementation** - -- Add a `use-package olivetti` declaration to `ss-ui`. -- Set a conservative width such as `olivetti-body-width 100`. - -**Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q --load ./init.el` -Expected: startup completes without Lisp errors. - -**Step 5: Commit** - -Wait for verification and user approval before creating a commit. - -### Task 2: Enable Olivetti for Org buffers - -**Files:** -- Modify: `lisp/ss-org.el` - -**Step 1: Write the failing test** - -Use the same configuration exception as Task 1. There is no existing targeted -test scaffold for `org-mode-hook` behavior here. - -**Step 2: Run test to verify it fails** - -Not applicable. The current source does not add `olivetti-mode` to -`org-mode-hook`. - -**Step 3: Write minimal implementation** - -- Extend the existing `org-mode-hook` lambda to enable `olivetti-mode`. - -**Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q --load ./init.el` -Expected: startup completes without Lisp errors. - -Run: `emacs -nw --eval '(progn (load-file \"./init.el\") (with-current-buffer (get-buffer-create \"*olivetti-check*\") (org-mode) (princ (if olivetti-mode \"olivetti-on\" \"olivetti-off\"))))'` -Expected: output includes `olivetti-on`. - -**Step 5: Commit** - -Wait for verification and user approval before creating a commit. - -### Task 3: Update documentation - -**Files:** -- Modify: `README.md` - -**Step 1: Write the failing test** - -The failing condition is documentation drift: the current README does not list -`olivetti` or mention that Org buffers enable it automatically. - -**Step 2: Run test to verify it fails** - -Review `README.md` and confirm it lacks that behavior. - -**Step 3: Write minimal implementation** - -- Add `olivetti` to the package model. -- Add a short note in the Org workflow description that Org buffers enable - `olivetti-mode` for centered writing layout. - -**Step 4: Run test to verify it passes** - -Review the updated README text for accuracy against the code. - -**Step 5: Commit** - -Wait for verification and user approval before creating a commit. - -### Task 4: Verify the complete change - -**Files:** -- Verify only - -**Step 1: Write the failing test** - -Use the repository’s expected verification path rather than adding new tests. - -**Step 2: Run test to verify it fails** - -Not applicable before implementation. - -**Step 3: Write minimal implementation** - -No code changes in this task. - -**Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q --load ./init.el` -Expected: exits successfully. - -Run: `emacs -nw --eval '(progn (load-file \"./init.el\") (with-temp-buffer (org-mode) (princ (if olivetti-mode \"olivetti-on\" \"olivetti-off\"))))'` -Expected: prints `olivetti-on`. - -**Step 5: Commit** - -Wait for verification and user approval before creating a commit. diff --git a/docs/plans/2026-04-10-org-refile-design.md b/docs/plans/2026-04-10-org-refile-design.md deleted file mode 100644 index ae7e881..0000000 --- a/docs/plans/2026-04-10-org-refile-design.md +++ /dev/null @@ -1,70 +0,0 @@ -# Org Refile Design - -## Context - -The configuration already has the core minibuffer completion stack in place: - -- `lisp/ss-ui.el` enables Vertico, Orderless, and Marginalia globally. -- `lisp/ss-org.el` owns base Org configuration. -- `lisp/ss-agenda.el` already discovers `org-agenda-files` from the journal and - PARA directories. - -The missing piece is Org refile configuration. The requested behavior is to -refile Org entries to any heading in the current `org-agenda-files`, using a -single, path-oriented prompt that works well with the existing Vertico-based -completion UI. - -## Options Considered - -### 1. Recommended: configure built-in Org refile against `org-agenda-files` - -- Use Org's standard `org-refile` command and target machinery. -- Refresh `org-agenda-files` before refile so the target set stays aligned with - the existing agenda discovery rules. -- Configure path-based completion so Vertico and Orderless present the target - list cleanly. - -This keeps the workflow conventional, reuses existing repository structure, and -avoids maintaining a parallel refile implementation. - -### 2. Add a custom wrapper command with richer target formatting - -- Build a custom candidate list for headings across agenda files. -- Pass the chosen destination back into Org's refile internals. - -This could show more custom metadata, but it duplicates behavior Org already -provides and increases maintenance cost for little practical gain. - -### 3. Add more completion packages just for refile - -- Introduce a Vertico extension or a separate package to alter refile prompts. - -This adds package surface area without first exhausting the built-in Org and -completion capabilities already present in the config. - -## Chosen Design - -Use option 1. - -- Configure `org-refile-targets` to use `org-agenda-files` with unrestricted - heading depth. -- Enable outline-path completion so identically named headings are - distinguishable by their parent path. -- Use the direct, path-based completion flow rather than an additional outline - navigation step. -- Refresh `org-agenda-files` before refile by reusing the existing agenda file - discovery helper instead of copying the directory rules. -- Keep the change inside the existing module boundaries: Org behavior in - `lisp/ss-org.el`, with a UI tweak in `lisp/ss-ui.el` only if the current - completion categories need one. - -## Verification - -- Add focused ERT coverage for the refile setup helper and variable values. -- Run `emacs --batch -Q --load ./init.el` from the repository root. - -## Documentation Impact - -`README.md` should be updated to describe that Org refile targets any heading in -`org-agenda-files` and uses the configured minibuffer completion stack for -path-based target selection.
\ No newline at end of file diff --git a/docs/plans/2026-04-10-org-refile-implementation.md b/docs/plans/2026-04-10-org-refile-implementation.md deleted file mode 100644 index 687e86d..0000000 --- a/docs/plans/2026-04-10-org-refile-implementation.md +++ /dev/null @@ -1,108 +0,0 @@ -# Org Refile Implementation Plan - -> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Configure Org refile so entries can be moved to any heading in `org-agenda-files` with a clearer Vertico-friendly path-based prompt. - -**Architecture:** Reuse the existing agenda discovery helper to keep `org-agenda-files` current before refile, then configure built-in Org refile variables in `lisp/ss-org.el`. Add narrow ERT coverage for the helper and settings, and update `README.md` so the documented workflow matches the configuration. - -**Tech Stack:** Emacs Lisp, Org mode, ERT, Vertico, Orderless, Marginalia - ---- - -## Chunk 1: Refile Configuration - -### Task 1: Add refile refresh helper and Org settings - -**Files:** -- Modify: `lisp/ss-org.el` -- Reference: `lisp/ss-agenda.el` - -- [ ] **Step 1: Write the failing test** - -Add ERT coverage that loads `ss-org`, stubs agenda refresh behavior, runs the -new setup helper, and asserts: - -- `org-refile-targets` points at `org-agenda-files` -- unlimited heading depth is enabled -- outline-path completion is enabled -- the direct path completion flow is selected - -- [ ] **Step 2: Run test to verify it fails** - -Run: `emacs --batch -Q -L lisp -l tests/ss-org-tests.el -f ert-run-tests-batch-and-exit` -Expected: FAIL because the refile helper and settings do not exist yet. - -- [ ] **Step 3: Write minimal implementation** - -- Add a small helper in `lisp/ss-org.el` that refreshes `org-agenda-files` - before refile, reusing `ss-refresh-org-agenda-files` when available. -- Configure Org refile variables during `ss-org-setup`. - -- [ ] **Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q -L lisp -l tests/ss-org-tests.el -f ert-run-tests-batch-and-exit` -Expected: PASS. - -- [ ] **Step 5: Commit** - -Wait for verification and user approval before creating a commit. - -## Chunk 2: Documentation and Startup Verification - -### Task 2: Document the refile workflow - -**Files:** -- Modify: `README.md` - -- [ ] **Step 1: Write the failing test** - -The failing condition is documentation drift: the README currently does not -describe refile behavior. - -- [ ] **Step 2: Run test to verify it fails** - -Review `README.md` and confirm it lacks refile documentation. - -- [ ] **Step 3: Write minimal implementation** - -- Add a short note describing that Org refile targets any heading in - `org-agenda-files` and uses path-based minibuffer completion. - -- [ ] **Step 4: Run test to verify it passes** - -Review the updated text against the code for accuracy. - -- [ ] **Step 5: Commit** - -Wait for verification and user approval before creating a commit. - -### Task 3: Verify the full startup path - -**Files:** -- Verify only - -- [ ] **Step 1: Write the failing test** - -Use the repository's normal startup verification path in addition to the new -targeted ERT coverage. - -- [ ] **Step 2: Run test to verify it fails** - -Not applicable before implementation. - -- [ ] **Step 3: Write minimal implementation** - -No code changes in this task. - -- [ ] **Step 4: Run test to verify it passes** - -Run: `emacs --batch -Q --load ./init.el` -Expected: exits successfully. - -Run: `emacs --batch -Q -L lisp -l tests/ss-org-tests.el -f ert-run-tests-batch-and-exit` -Expected: PASS. - -- [ ] **Step 5: Commit** - -Wait for verification and user approval before creating a commit.
\ No newline at end of file diff --git a/early-init.el b/early-init.el deleted file mode 100644 index dcf868c..0000000 --- a/early-init.el +++ /dev/null @@ -1,14 +0,0 @@ -;;; early-init.el --- Early startup settings -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Keep this file limited to settings that must exist before the first frame. - -;;; Code: - -(dolist (parameter '((width . 140) - (height . 42))) - (add-to-list 'default-frame-alist parameter) - (add-to-list 'initial-frame-alist parameter)) - -;;; early-init.el ends here diff --git a/init.el b/init.el deleted file mode 100644 index c63d477..0000000 --- a/init.el +++ /dev/null @@ -1,53 +0,0 @@ -;;; init.el --- Main Emacs entry point -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Hand-edited runtime entry point for this Emacs configuration. - -;;; Code: - -(add-to-list - 'load-path - (expand-file-name "lisp" (file-name-directory (or load-file-name user-init-file)))) - -(setq ss-enabled-features - '(ui org agenda denote capture crm gptel keys)) - -(require 'ss-core) -(ss-core-setup) - -(when (memq 'ui ss-enabled-features) - (require 'ss-ui) - (ss-ui-setup)) - -(when (memq 'org ss-enabled-features) - (require 'ss-org) - (ss-org-setup)) - -(when (memq 'agenda ss-enabled-features) - (require 'ss-agenda) - (ss-agenda-setup)) - -(when (memq 'denote ss-enabled-features) - (require 'ss-denote) - (ss-denote-setup)) - -(when (memq 'capture ss-enabled-features) - (require 'ss-capture) - (ss-capture-setup)) - -(when (memq 'crm ss-enabled-features) - (require 'ss-crm) - (ss-crm-setup)) - -(when (memq 'gptel ss-enabled-features) - (require 'ss-gptel) - (ss-gptel-setup)) - -(when (memq 'keys ss-enabled-features) - (require 'ss-keys) - (ss-keys-setup)) - -(ss-core-load-custom-file) - -;;; init.el ends here diff --git a/lisp/ss-agenda.el b/lisp/ss-agenda.el deleted file mode 100644 index a89a52e..0000000 --- a/lisp/ss-agenda.el +++ /dev/null @@ -1,40 +0,0 @@ -;;; ss-agenda.el --- Agenda configuration -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Agenda discovery and agenda commands. - -;;; Code: - -(require 'ss-core) -(require 'ss-org) - -(defun ss-refresh-org-agenda-files (&rest _) - "Refresh `org-agenda-files' from the journal and PARA directories. -Ignore any arguments passed by advice wrappers." - (require 'org-agenda) - (setq org-agenda-files - (sort - (delete-dups - (append - (list (ss-require-existing-file ss-journal-file)) - (apply #'append - (mapcar (lambda (directory) - (directory-files-recursively - (ss-require-existing-directory directory) - "\\.org\\'")) - ss-org-agenda-directories)))) - #'string<))) - -(defun ss-open-agenda () - "Refresh agenda files and invoke `org-agenda'." - (interactive) - (call-interactively #'org-agenda)) - -(defun ss-agenda-setup () - "Initialize agenda behavior." - (advice-add 'org-agenda :before #'ss-refresh-org-agenda-files)) - -(provide 'ss-agenda) - -;;; ss-agenda.el ends here diff --git a/lisp/ss-capture.el b/lisp/ss-capture.el deleted file mode 100644 index 0489888..0000000 --- a/lisp/ss-capture.el +++ /dev/null @@ -1,180 +0,0 @@ -;;; ss-capture.el --- Capture configuration -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Journal capture helpers and capture templates. - -;;; Code: - -(require 'calendar) -(require 'org) -(require 'org-capture) -(require 'ss-core) -(require 'ss-org) - -(defun ss-journal-capture-time () - "Return the effective timestamp for the current journal capture." - (or org-overriding-default-time - (org-capture-get :default-time) - (current-time))) - -(defun ss-journal-calendar-date (&optional time) - "Return TIME as a Gregorian date list for datetree helpers." - (calendar-gregorian-from-absolute - (time-to-days (or time (current-time))))) - -(defun ss-journal-year-heading (&optional time) - "Return the journal year heading text for TIME." - (format-time-string "%Y" (or time (current-time)))) - -(defun ss-journal-day-heading (&optional time) - "Return the journal day heading text for TIME." - (format-time-string "%Y-%m-%d %A" (or time (current-time)))) - -(defun ss-journal-find-or-create-heading (level heading) - "Move to HEADING at LEVEL, creating it when missing." - (goto-char (point-min)) - (if (re-search-forward - (format "^%s %s$" - (make-string level ?*) - (regexp-quote heading)) - nil t) - (goto-char (match-beginning 0)) - (goto-char (point-max)) - (unless (bolp) - (insert "\n")) - (insert (make-string level ?*) " " heading "\n") - (forward-line -1)) - (org-back-to-heading t)) - -(defun ss-journal-goto-date (&optional time create) - "Move to TIME's journal date heading. -When CREATE is non-nil, create the datetree entry when missing." - (goto-char (point-min)) - (if create - (let ((year-heading (ss-journal-year-heading time)) - (day-heading (ss-journal-day-heading time))) - (ss-journal-find-or-create-heading 1 year-heading) - (save-restriction - (org-narrow-to-subtree) - (ss-journal-find-or-create-heading 2 day-heading)) - t) - (when (re-search-forward - (format "^\\*\\* %s$" - (regexp-quote - (ss-journal-day-heading (or time (current-time))))) - nil t) - (goto-char (match-beginning 0)) - t))) - -(defun ss-journal-ensure-day-sections () - "Ensure the standard section headings exist under the current journal day." - (org-back-to-heading t) - (let ((section-level (1+ (org-outline-level)))) - (save-excursion - (save-restriction - (org-narrow-to-subtree) - (dolist (section ss-journal-section-headings) - (goto-char (point-min)) - (forward-line 1) - (unless (re-search-forward - (format "^%s %s$" - (make-string section-level ?*) - (regexp-quote section)) - nil t) - (goto-char (point-max)) - (unless (bolp) - (insert "\n")) - (insert (make-string section-level ?*) " " section "\n"))))))) - -(defun ss-journal-goto-section (section &optional time) - "Move to SECTION beneath TIME's journal date, creating structure as needed." - (unless (member section ss-journal-section-headings) - (user-error "Unknown journal section: %s" section)) - (ss-journal-goto-date time 'create) - (ss-journal-ensure-day-sections) - (let ((section-level (1+ (org-outline-level))) - position) - (save-restriction - (org-narrow-to-subtree) - (goto-char (point-min)) - (when (re-search-forward - (format "^%s %s$" - (make-string section-level ?*) - (regexp-quote section)) - nil t) - (setq position (match-beginning 0)))) - (unless position - (user-error "Journal section not found: %s" section)) - (goto-char position) - (org-back-to-heading t))) - -(defun ss-journal-capture-target (section) - "Select SECTION under today's journal datetree entry for capture." - (set-buffer (find-file-noselect (ss-require-existing-file ss-journal-file))) - (widen) - (ss-journal-goto-section section (ss-journal-capture-time))) - -(defun ss-capture--denote-templates () - "Return Denote-backed capture templates when Denote is enabled." - (when (ss-feature-enabled-p 'denote) - `(("n" "Denote") - ("nn" "Generic" plain - (file denote-last-path) - (function - (lambda () - (denote-org-capture-with-prompts :title :keywords :subdirectory))) - :no-save t - :immediate-finish nil - :kill-buffer t - :jump-to-captured t) - ("np" "Project" plain - (file denote-last-path) - (function - (lambda () - (ss-denote-capture-in-directory - ss-org-projects-directory '("project") :title :keywords :subdirectory))) - :no-save t - :immediate-finish nil - :kill-buffer t - :jump-to-captured t) - ("na" "Area" plain - (file denote-last-path) - (function - (lambda () - (ss-denote-capture-in-directory - ss-org-areas-directory nil :title :keywords :subdirectory))) - :no-save t - :immediate-finish nil - :kill-buffer t - :jump-to-captured t) - ("nr" "Resource" plain - (file denote-last-path) - (function - (lambda () - (ss-denote-capture-in-directory - ss-org-resources-directory nil :title :keywords :subdirectory))) - :no-save t - :immediate-finish nil - :kill-buffer t - :jump-to-captured t)))) - -(defun ss-capture-setup () - "Initialize capture templates." - (setq org-capture-templates - (append - '(("j" "Journal") - ("jt" "Task" entry - (function (lambda () (ss-journal-capture-target "Tasks"))) - "* TODO %?") - ("jn" "Note" entry - (function (lambda () (ss-journal-capture-target "Notes"))) - "* %?") - ("jm" "Meeting" entry - (function (lambda () (ss-journal-capture-target "Meetings"))) - "* <%<%Y-%m-%d %H:%M>> %?")) - (ss-capture--denote-templates)))) - -(provide 'ss-capture) - -;;; ss-capture.el ends here diff --git a/lisp/ss-core.el b/lisp/ss-core.el deleted file mode 100644 index 3be0711..0000000 --- a/lisp/ss-core.el +++ /dev/null @@ -1,147 +0,0 @@ -;;; ss-core.el --- Shared core setup -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Shared startup helpers, package bootstrap, paths, and editor defaults. - -;;; Code: - -(require 'subr-x) - -(defconst ss-minimum-emacs-version "27.1" - "Minimum supported Emacs version.") - -(defconst ss-warning-emacs-version "28.1" - "Version threshold for compatibility warnings.") - -(defconst ss-spell-check-support-enabled nil) -(defconst ss-is-windows (memq system-type '(windows-nt ms-dos cygwin))) -(defconst ss-is-linux (eq system-type 'gnu/linux)) -(defconst ss-is-mac (eq system-type 'darwin)) - -(defconst ss-org-directory (expand-file-name "~/org/") - "Root directory for Org files.") - -(defconst ss-journal-file (expand-file-name "journal.org" ss-org-directory) - "Single-file work journal for operational capture.") - -(defconst ss-org-projects-directory (expand-file-name "projects/" ss-org-directory) - "Directory for project notes.") - -(defconst ss-org-areas-directory (expand-file-name "areas/" ss-org-directory) - "Directory for area notes.") - -(defconst ss-org-resources-directory (expand-file-name "resources/" ss-org-directory) - "Directory for resource notes.") - -(defconst ss-org-archives-directory (expand-file-name "archives/" ss-org-directory) - "Directory for archived notes.") - -(defconst ss-moc-file (expand-file-name "moc.org" ss-org-directory) - "Central MOC note.") - -(defconst ss-crm-file (expand-file-name "areas/people/people.org" ss-org-directory) - "Single source of truth for the people CRM.") - -(defconst ss-journal-section-headings - '("Tasks" "Notes" "Meetings") - "Per-day section headings maintained under each journal datetree entry.") - -(defconst ss-org-agenda-directories - (list ss-org-projects-directory - ss-org-areas-directory - ss-org-resources-directory) - "Directories whose Org files feed the agenda.") - -(defun ss-feature-enabled-p (feature) - "Return non-nil when FEATURE is enabled in `ss-enabled-features'." - (memq feature ss-enabled-features)) - -(defun ss-require-existing-directory (directory) - "Return DIRECTORY, signaling when it does not exist." - (unless (file-directory-p directory) - (user-error "Directory does not exist: %s" directory)) - directory) - -(defun ss-require-existing-file (file) - "Return FILE, signaling when it does not exist." - (unless (file-exists-p file) - (user-error "File does not exist: %s" file)) - file) - -(defun ss-enable-prose-abbrev-mode () - "Enable abbrev mode in prose buffers. -We keep this mode-local so code buffers stay on their own completion rules." - (abbrev-mode 1)) - -(defun ss-core-setup () - "Initialize shared core behavior." - (let ((minver ss-minimum-emacs-version)) - (when (version< emacs-version minver) - (error "Your Emacs is too old -- this config requires v%s or higher" minver))) - (when (version< emacs-version ss-warning-emacs-version) - (message - (concat - "Your Emacs is old, and some functionality in this config will be " - "disabled. Please upgrade if possible."))) - - ;; Keep custom-set-variables out of the main config. - (setq custom-file (expand-file-name "custom.el" user-emacs-directory)) - - (require 'package) - (setq package-archives - (append '(("melpa" . "https://melpa.org/packages/")) - package-archives) - package-archive-priorities '(("gnu" . 10) - ("nongnu" . 8) - ("melpa" . 5)) - package-install-upgrade-built-in t - use-package-always-ensure nil) - (package-initialize) - (require 'use-package) - (require 'abbrev) - - (set-language-environment "UTF-8") - (set-default-coding-systems 'utf-8) - (prefer-coding-system 'utf-8) - - (setq abbrev-file-name (expand-file-name "abbrev_defs" user-emacs-directory) - save-abbrevs 'silently) - (when (file-exists-p abbrev-file-name) - (quietly-read-abbrev-file abbrev-file-name)) - - (dolist (hook '(text-mode-hook org-mode-hook)) - (add-hook hook #'ss-enable-prose-abbrev-mode)) - - (setq auto-save-default nil - backup-inhibited t - echo-keystrokes 0.1 - compilation-ask-about-save nil - mouse-wheel-scroll-amount '(1 ((shift) . 1)) - mouse-wheel-progressive-speed nil - mouse-wheel-follow-mouse t - scroll-step 1 - scroll-conservatively 101 - enable-recursive-minibuffers t - gc-cons-threshold (* 128 1024 1024) - read-process-output-max (* 4 1024 1024) - process-adaptive-read-buffering nil) - - (fset 'yes-or-no-p 'y-or-n-p) - (global-auto-revert-mode 1) - (delete-selection-mode 1) - - (setq-default indent-tabs-mode nil - fill-column 80 - tab-width 2 - indicate-empty-lines t - sentence-end-double-space nil)) - -(defun ss-core-load-custom-file () - "Load `custom-file' when it exists." - (when (file-exists-p custom-file) - (load custom-file nil 'nomessage))) - -(provide 'ss-core) - -;;; ss-core.el ends here diff --git a/lisp/ss-crm.el b/lisp/ss-crm.el deleted file mode 100644 index 62dc3e3..0000000 --- a/lisp/ss-crm.el +++ /dev/null @@ -1,817 +0,0 @@ -;;; ss-crm.el --- People CRM -*- lexical-binding: t; -*- - -;;; Commentary: - -;; People CRM parsing, lookup, completion, insertion, and reporting. - -;;; Code: - -(require 'abbrev) -(require 'org) -(require 'org-element) -(require 'seq) -(require 'ss-core) -(require 'subr-x) -(require 'marginalia nil t) - -(defconst ss-crm-engagement-options - '("Perm" "SOW" "SOW Fixed Outcome" "NCS India") - "Canonical engagement values for people cards.") - -(defconst ss-crm-seeded-roles - '("Account Manager" - "AD Platform Manager" - "AD, Billing & Catalogue, Engineering" - "AD, Frontend Engineering" - "AD, IT Core & MarTech" - "AD, Mobile Engineering" - "AD, Platform Engineering" - "AD, Portfolio Delivery Products and API Management" - "AD, Portfolio Mgmt. Customer Engagement" - "AD, Quality Engineering" - "AD, Tech Product Owner" - "AD, Technical Delivery" - "AD, Technical Platform Management" - "AD, Technical Platform Manager, API" - "AD, Technical Platform Manager, Catalog" - "AD, Technical Platform Manager, Order and Quote" - "AD, Technical Product Management" - "Agile Scrum Master" - "Analyst Programmer - Senior" - "Android Developer" - "Android Developer - Senior" - "Application Development Lead" - "Architect" - "Associate Engineer" - "Associate Engineer" - "Associate Engineer, Frontend" - "Associate Engineer, Platform Engineering" - "Associate Software Engineer" - "Billing Executive" - "Business Analyst" - "Business Analyst - Senior" - "Business Development Manager" - "Business Intelligence Analyst" - "Business Program Manager" - "Cloud Engineer" - "Consultant" - "CRM Developer" - "Data Analyst" - "Data Analyst - Senior" - "Delivery Lead" - "Delivery Manager" - "Demand Lead" - "Deployment Manager" - "Development Manager" - "DevOps Engineer" - "DevOps Engineer - Senior" - "Digital Delivery Manager" - "Director Customer Engagement" - "Director Sales Technology" - "Director, Billing and Charging" - "Director, Delivery Portfolio Billing and Charging" - "Director, Digital Engineering" - "Director, Portfolio Mgmt." - "Director, SF Comms Technical Platform" - "Director, SSF Core & Commerce" - "Engineer" - "Engineer - Junior" - "Engineer - Senior" - "Engineer, Platform Engineering" - "Engineer, Salesforce" - "Engineering Manager" - "Engineering Manager, Mobile" - "Engineering Manager, Platform Engineering" - "Executive Assistant" - "Integration Lead" - "Integration Solution Designer" - "Integration Technical Specialist" - "iOS Developer" - "iOS Developer - Senior" - "IT Business Partner" - "Lead Consultant" - "Lead Developer" - "Manager, Release Engineer" - "Manager, Release Train Engineer" - "Mobile API Developer" - "Optus Tech Graduate" - "Portfolio Manager" - "Process Analyst - Senior" - "Product Manager" - "Product Solution Designer" - "Program Analyst" - "Program Director" - "Program Manager" - "Project Architect" - "Project Coordinator" - "Project Manager" - "Quality Assurance Analyst" - "Quality Engineering Professional" - "Release Coordinator" - "Release Manager" - "Salesforce Administrator" - "Salesforce Developer (Heroku, Lightning, etc)" - "Salesforce Marketing Cloud Architect" - "Salesforce Operations Manager" - "Scrum Master" - "SD, Engineering Capability" - "SD, Marketing Technology" - "SD, Technical Products" - "Senior Business Analyst" - "Senior Delivery Manager" - "Senior Delivery Manger" - "Senior Digital Technical Producer" - "Senior E2E Business Analyst Lead" - "Senior Engineer" - "Senior Engineer, API Engineering" - "Senior Engineer, Billing & Catalogue" - "Senior Engineer, Platform Engineering" - "Senior Engineer, Salesforce" - "Senior Front End Developer" - "Senior Integration Solution Designer" - "Senior Quality Engineering Professional" - "Senior Salesforce Administrator" - "Senior Salesforce Consultant" - "Senior Salesforce Functional Consultant" - "Senior Salesforce Operations Lead" - "Senior Software Developer" - "Senior Software Engineer" - "Senior Software Engineer, Frontend" - "Senior Software Engineer, Mobile" - "Senior Solution Designer" - "Snr Dir, Delivery & Platform Manager" - "Snr Director, Portfolio Mgmt. & Delivery" - "Software Developer" - "Software Developer - Junior" - "Software Developer - Senior" - "Software Engineer" - "Software Engineer, Frontend" - "Software Engineer, Mobile" - "Solution Architect" - "Solution Designer" - "Solution Designer - Senior" - "Staff Engineer" - "Support Analyst" - "System Analyst" - "System Engineer" - "Technical Lead" - "Technical Platform Manager" - "Technical Platform Manager, Billing and Charging" - "Technical Product Manager" - "Technical Project Manager - Senior" - "Technical Specialist - Senior" - "Technology Delivery Analyst" - "Test Analyst" - "Test Lead" - "Test Manager" - "VP, EB IT") - "Seeded role values derived from the historic roles CSV.") - -(defconst ss-crm-supplier-options - '("Accenture Song" - "INFOSYS TECHNOLOGIES LIMITED" - "MAKK Integrations Pty Ltd" - "NCSI Technologies India Private Limited" - "TECH MAHINDRA LTD") - "Canonical supplier values for people cards.") - -(defvar ss-crm--cache nil - "Cached CRM entries loaded from `ss-crm-file'.") - -(defvar ss-crm--cache-mtime nil - "Modification time of the cached CRM entries.") - -(defun ss-crm--entry-name (entry) - "Return the canonical name in ENTRY." - (plist-get entry :name)) - -(defun ss-crm--entry-abbrev (entry) - "Return the abbrev trigger in ENTRY." - (plist-get entry :abbrev)) - -(defun ss-crm--entry-aliases (entry) - "Return alias variants in ENTRY." - (plist-get entry :aliases)) - -(defun ss-crm--entry-role (entry) - "Return the role in ENTRY." - (plist-get entry :role)) - -(defun ss-crm--entry-team (entry) - "Return the team in ENTRY." - (plist-get entry :team)) - -(defun ss-crm--entry-manager (entry) - "Return the manager in ENTRY." - (plist-get entry :manager)) - -(defun ss-crm--entry-engagement (entry) - "Return the engagement in ENTRY." - (plist-get entry :engagement)) - -(defun ss-crm--entry-supplier (entry) - "Return the supplier in ENTRY." - (plist-get entry :supplier)) - -(defun ss-crm--entry-location (entry) - "Return the location in ENTRY." - (plist-get entry :location)) - -(defun ss-crm--entry-current-focus (entry) - "Return the current focus in ENTRY." - (plist-get entry :current-focus)) - -(defun ss-crm-default-abbrev (name) - "Suggest a short abbrev trigger for NAME." - (let* ((parts (split-string (string-trim name) "[[:space:]]+" t)) - (first (downcase (substring (car parts) 0 (min 2 (length (car parts)))))) - (last (downcase (substring (car (last parts)) 0 1)))) - (if (> (length parts) 1) - (concat ";" first last) - (concat ";" first)))) - -(defun ss-crm--split-values (value) - "Split VALUE on commas and trim each item." - (when (and value (not (string-empty-p value))) - (seq-filter - (lambda (string) - (not (string-empty-p string))) - (mapcar #'string-trim (split-string value "," t))))) - -(defun ss-crm--summary (entry) - "Return the compact one-line summary for ENTRY." - (string-join - (seq-filter - (lambda (string) - (and string (not (string-empty-p string)))) - (list (ss-crm--entry-role entry) - (ss-crm--entry-team entry) - (ss-crm--entry-engagement entry) - (ss-crm--entry-current-focus entry))) - " | ")) - -(defun ss-crm--display (entry) - "Return the compact display string for ENTRY." - (let ((summary (ss-crm--summary entry))) - (if (string-empty-p summary) - (ss-crm--entry-name entry) - (format "%s %s" (ss-crm--entry-name entry) summary)))) - -(defun ss-crm--property-line (key value) - "Return an Org property line for KEY and VALUE." - (if (and value (not (string-empty-p value))) - (format ":%s: %s\n" key value) - "")) - -(defun ss-crm--require-file () - "Return `ss-crm-file', signaling when it is unavailable." - (ss-require-existing-file ss-crm-file)) - -(defun ss-crm--parse-entry-at-point (headline) - "Return the CRM entry described by HEADLINE at point." - (list :name (org-element-property :raw-value headline) - :abbrev (org-entry-get nil "ABBREV") - :aliases (ss-crm--split-values (org-entry-get nil "ALIASES")) - :role (org-entry-get nil "ROLE") - :team (org-entry-get nil "TEAM") - :manager (org-entry-get nil "MANAGER") - :engagement (org-entry-get nil "ENGAGEMENT") - :supplier (org-entry-get nil "SUPPLIER") - :location (org-entry-get nil "LOCATION") - :current-focus (org-entry-get nil "CURRENT_FOCUS"))) - -(defun ss-crm--parse-entries () - "Parse top-level CRM entries from `ss-crm-file'." - (with-temp-buffer - (insert-file-contents (ss-crm--require-file)) - ;; Parse cards without running user hooks; otherwise the CRM's own Org - ;; hooks recurse back into this parser. - (delay-mode-hooks - (org-mode)) - (let ((ast (org-element-parse-buffer)) - cards) - (org-element-map ast 'headline - (lambda (headline) - (when (= 1 (org-element-property :level headline)) - (goto-char (org-element-property :begin headline)) - (push (ss-crm--parse-entry-at-point headline) cards)))) - (sort cards - (lambda (left right) - (string< (ss-crm--entry-name left) - (ss-crm--entry-name right))))))) - -(defun ss-crm-entries () - "Return top-level people cards from `ss-crm-file'." - (let* ((file (ss-crm--require-file)) - (attributes (file-attributes file)) - (mtime (file-attribute-modification-time attributes))) - (unless (and ss-crm--cache - (equal mtime ss-crm--cache-mtime)) - (setq ss-crm--cache (ss-crm--parse-entries) - ss-crm--cache-mtime mtime)) - ss-crm--cache)) - -(defun ss-crm-reload () - "Reload the people cache and refresh prose buffers." - (interactive) - (setq ss-crm--cache nil - ss-crm--cache-mtime nil) - (ss-crm-refresh-buffers) - (message "Reloaded people CRM")) - -(defun ss-crm--entry-by-name (name) - "Return the people entry matching canonical NAME." - (seq-find - (lambda (entry) - (string= name (ss-crm--entry-name entry))) - (ss-crm-entries))) - -(defun ss-crm--search-keys (entry) - "Return canonical and alias search keys for ENTRY." - (cons (ss-crm--entry-name entry) - (ss-crm--entry-aliases entry))) - -(defun ss-crm--match-p (query entry) - "Return non-nil when QUERY matches ENTRY name or aliases." - (let* ((parts (split-string (downcase (string-trim query)) "[[:space:]]+" t)) - (keys (mapcar #'downcase (ss-crm--search-keys entry)))) - (seq-every-p - (lambda (part) - (seq-some - (lambda (key) - (string-match-p (regexp-quote part) key)) - keys)) - parts))) - -(defun ss-crm--matching-entries (query) - "Return entries whose canonical name or aliases match QUERY." - (let ((entries (ss-crm-entries))) - (if (string-empty-p (string-trim query)) - entries - (seq-filter - (lambda (entry) - (ss-crm--match-p query entry)) - entries)))) - -(defun ss-crm--completion-table (string pred action) - "Complete canonical people names while matching aliases via STRING." - (if (eq action 'metadata) - '(metadata (category . ss-person)) - (complete-with-action - action - (mapcar #'ss-crm--entry-name (ss-crm--matching-entries string)) - string - pred))) - -(defun ss-crm-marginalia-annotator (candidate) - "Return a Marginalia annotation for person CANDIDATE." - (when-let ((entry (ss-crm--entry-by-name candidate))) - (concat " " (ss-crm--summary entry)))) - -(defun ss-crm-select-entry (&optional prompt) - "Select a person entry using PROMPT." - (let ((completion-extra-properties - '(:annotation-function ss-crm-marginalia-annotator))) - (ss-crm--entry-by-name - (completing-read (or prompt "Person: ") - #'ss-crm--completion-table - nil - t)))) - -(defun ss-crm-overview () - "Open `ss-crm-file' in overview mode, widening first when needed." - (interactive) - (unless (and buffer-file-name - (string= (file-truename buffer-file-name) - (file-truename ss-crm-file))) - (find-file (ss-crm--require-file))) - (widen) - (goto-char (point-min)) - (org-overview) - (org-cycle-hide-drawers 'all)) - -(defun ss-crm-open () - "Open the people CRM by delegating to `ss-crm-overview'." - (interactive) - (ss-crm-overview)) - -(defun ss-crm--track-buffer () - "Refresh CRM caches when `ss-crm-file' is saved." - (when (and buffer-file-name - (string= (file-truename buffer-file-name) - (file-truename ss-crm-file))) - (add-hook 'after-save-hook #'ss-crm-reload nil t))) - -(defun ss-crm--source-buffer-p () - "Return non-nil when the current buffer visits `ss-crm-file'." - (and buffer-file-name - (string= (file-truename buffer-file-name) - (file-truename ss-crm-file)))) - -(defun ss-crm--open-entry (entry) - "Open the people CRM file, then narrow to ENTRY for card view." - (find-file (ss-crm--require-file)) - (widen) - (let ((position (org-find-exact-headline-in-buffer - (ss-crm--entry-name entry)))) - (unless position - (user-error "No people card for %s" (ss-crm--entry-name entry))) - (goto-char position)) - (org-narrow-to-subtree) - (org-fold-show-subtree) - (org-fold-show-entry) - (goto-char (point-min))) - -(defun ss-crm-find () - "Find a person and open that card." - (interactive) - (ss-crm--open-entry - (or (ss-crm-select-entry "Find person: ") - (user-error "No person selected")))) - -(defun ss-crm-insert-name () - "Insert a canonical person name at point." - (interactive) - (let ((entry (or (ss-crm-select-entry "Insert person name: ") - (user-error "No person selected")))) - (insert (ss-crm--entry-name entry)))) - -(defun ss-crm-insert-summary () - "Insert a compact person summary at point." - (interactive) - (let ((entry (or (ss-crm-select-entry "Insert person summary: ") - (user-error "No person selected")))) - (insert (ss-crm--display entry)))) - -(defun ss-crm--report-buffer (title group-fn) - "Render a grouped CRM report titled TITLE using GROUP-FN." - (let* ((entries (ss-crm-entries)) - (groups - (seq-group-by - (lambda (entry) - (let ((value (funcall group-fn entry))) - (if (string-empty-p (or value "")) - "(none)" - value))) - entries))) - (setq groups - (sort groups - (lambda (left right) - (string< (car left) (car right))))) - (with-current-buffer (get-buffer-create "*People Report*") - (let ((inhibit-read-only t)) - (erase-buffer) - (org-mode) - (insert "#+title: " title "\n\n") - (dolist (group groups) - (insert "* " (car group) "\n") - (dolist (entry (sort (copy-sequence (cdr group)) - (lambda (left right) - (string< (ss-crm--entry-name left) - (ss-crm--entry-name right))))) - (insert "- " (ss-crm--display entry) "\n"))) - (goto-char (point-min)) - (read-only-mode 1) - (view-mode 1)) - (pop-to-buffer (current-buffer))))) - -(defun ss-crm-report-by-team () - "Show people grouped by team." - (interactive) - (ss-crm--report-buffer "People by team" #'ss-crm--entry-team)) - -(defun ss-crm-report-by-manager () - "Show people grouped by manager." - (interactive) - (ss-crm--report-buffer "People by manager" #'ss-crm--entry-manager)) - -(defun ss-crm-report-by-engagement () - "Show people grouped by engagement." - (interactive) - (ss-crm--report-buffer "People by engagement" #'ss-crm--entry-engagement)) - -(defun ss-crm-report-by-supplier () - "Show non-empty suppliers grouped by supplier." - (interactive) - (let* ((entries - (seq-filter - (lambda (entry) - (not (string-empty-p (or (ss-crm--entry-supplier entry) "")))) - (ss-crm-entries))) - (groups (seq-group-by #'ss-crm--entry-supplier entries))) - (setq groups - (sort groups - (lambda (left right) - (string< (car left) (car right))))) - (with-current-buffer (get-buffer-create "*People Report*") - (let ((inhibit-read-only t)) - (erase-buffer) - (org-mode) - (insert "#+title: People by supplier\n\n") - (dolist (group groups) - (insert "* " (car group) "\n") - (dolist (entry (sort (copy-sequence (cdr group)) - (lambda (left right) - (string< (ss-crm--entry-name left) - (ss-crm--entry-name right))))) - (insert "- " (ss-crm--display entry) "\n"))) - (goto-char (point-min)) - (read-only-mode 1) - (view-mode 1)) - (pop-to-buffer (current-buffer))))) - -(defun ss-crm-report-by-role () - "Show people grouped by role." - (interactive) - (ss-crm--report-buffer "People by role" #'ss-crm--entry-role)) - -(defun ss-crm-report-by-location () - "Show people grouped by location." - (interactive) - (ss-crm--report-buffer "People by location" #'ss-crm--entry-location)) - -(defun ss-crm-read-string (prompt &optional default) - "Read PROMPT and trim the result." - (string-trim (read-string prompt nil nil default))) - -(defun ss-crm-read-required-string (prompt &optional default) - "Read PROMPT and require a non-empty result." - (let ((value (ss-crm-read-string prompt default))) - (if (string-empty-p value) - (user-error "%s is required" (string-remove-suffix ": " prompt)) - value))) - -(defun ss-crm-known-property-values (property) - "Return sorted unique non-empty values for PROPERTY from CRM entries." - (let ((key (intern (concat ":" (downcase property))))) - (seq-sort - #'string< - (delete-dups - (seq-filter - (lambda (value) - (and (stringp value) - (not (string-empty-p (string-trim value))))) - (mapcar - (lambda (entry) - (plist-get entry key)) - (ss-crm-entries))))))) - -(defun ss-crm-known-person-names () - "Return sorted top-level person names from CRM entries." - (seq-sort - #'string< - (delete-dups - (seq-filter - (lambda (name) - (and (stringp name) - (not (string-empty-p (string-trim name))))) - (mapcar #'ss-crm--entry-name (ss-crm-entries)))))) - -(defun ss-crm-lookup-values (property &optional seeded) - "Return sorted unique values for PROPERTY merged with SEEDED values." - (seq-sort - #'string< - (delete-dups - (seq-filter - (lambda (value) - (and (stringp value) - (not (string-empty-p (string-trim value))))) - (append seeded - (ss-crm-known-property-values property)))))) - -(defun ss-crm--read-choice-warning (prompt similar) - "Warn that PROMPT value is similar to existing SIMILAR." - (display-warning - 'ss-crm - (format "%sA similar existing value already exists: %s" - prompt - similar) - :warning)) - -(defun ss-crm-read-choice (prompt choices &rest plist) - "Read a value for PROMPT from CHOICES using options in PLIST. -Supported keywords are :allow-blank, :allow-new, and :require-match." - (let* ((allow-blank (plist-get plist :allow-blank)) - (allow-new (plist-get plist :allow-new)) - (require-match (plist-get plist :require-match)) - (blank-choice "[none]") - (collection (if allow-blank - (cons blank-choice choices) - choices)) - (completion-require-match (if allow-new nil require-match))) - (catch 'done - (while t - (let* ((value (completing-read prompt collection nil completion-require-match)) - (existing - (seq-find - (lambda (choice) - (string= value choice)) - choices))) - (cond - ((or (string-empty-p value) - (and allow-blank - (string= value blank-choice))) - (throw 'done nil)) - (existing - (throw 'done existing)) - ((not allow-new) - (user-error "Please choose an existing value")) - (t - (when-let ((similar - (seq-find - (lambda (choice) - (and (not (string= value choice)) - (string-equal (downcase value) - (downcase choice)))) - choices))) - (ss-crm--read-choice-warning prompt similar)) - (when (yes-or-no-p (format "Create new value `%s'? " value)) - (throw 'done value))))))))) - -(defun ss-crm--completion-values (extractor &optional include-empty) - "Return sorted unique values using EXTRACTOR. -When INCLUDE-EMPTY is non-nil, keep empty values." - (let ((values (delete-dups (mapcar extractor (ss-crm-entries))))) - (seq-sort - #'string< - (seq-filter - (lambda (value) - (or include-empty - (not (string-empty-p (or value ""))))) - values)))) - -(defun ss-crm-read-role () - "Read a CRM role using seeded and known role values." - (ss-crm-read-choice "Role: " - (ss-crm-lookup-values "ROLE" ss-crm-seeded-roles) - :allow-blank t - :allow-new t)) - -(defun ss-crm-read-team () - "Read a CRM team using known team values." - (ss-crm-read-choice "Team: " - (ss-crm-lookup-values "TEAM") - :allow-blank t - :allow-new t)) - -(defun ss-crm-read-manager () - "Read a CRM manager using known person names." - (ss-crm-read-choice "Manager: " - (ss-crm-known-person-names) - :allow-blank t - :require-match t)) - -(defun ss-crm-read-engagement () - "Read a CRM engagement using seeded and known engagement values." - (ss-crm-read-choice "Engagement: " - (ss-crm-lookup-values "ENGAGEMENT" - ss-crm-engagement-options) - :allow-blank t - :allow-new t)) - -(defun ss-crm-read-supplier () - "Read a CRM supplier using seeded and known supplier values." - (ss-crm-read-choice "Supplier: " - (ss-crm-lookup-values "SUPPLIER" - ss-crm-supplier-options) - :allow-blank t - :allow-new t)) - -(defun ss-crm-read-location () - "Read a CRM location using known location values." - (ss-crm-read-choice "Location: " - (ss-crm-lookup-values "LOCATION") - :allow-blank t - :allow-new t)) - -(defun ss-crm-add () - "Add a new compact person card to `ss-crm-file'." - (interactive) - (let ((name (ss-crm-read-required-string "Full name: ")) - (abbrev nil) - (aliases nil) - (role nil) - (team nil) - (manager nil) - (engagement nil) - (supplier nil) - (location nil) - (current-focus nil)) - (setq abbrev (ss-crm-read-string "Abbrev: " (ss-crm-default-abbrev name)) - aliases (ss-crm-read-string "Aliases (comma-separated, optional): ") - role (ss-crm-read-role) - team (ss-crm-read-team) - manager (ss-crm-read-manager) - engagement (ss-crm-read-engagement) - supplier (ss-crm-read-supplier) - location (ss-crm-read-location) - current-focus (ss-crm-read-required-string "Current focus: ")) - (when (ss-crm--entry-by-name name) - (user-error "A person card for %s already exists" name)) - (when (string-empty-p abbrev) - (setq abbrev (ss-crm-default-abbrev name))) - (find-file (ss-crm--require-file)) - (widen) - (goto-char (point-max)) - (unless (bolp) - (insert "\n")) - (unless (looking-back "\n\n" nil) - (insert "\n")) - (insert "* " name "\n" - ":PROPERTIES:\n" - (ss-crm--property-line "ABBREV" abbrev) - (ss-crm--property-line "ALIASES" aliases) - (ss-crm--property-line "ROLE" role) - (ss-crm--property-line "TEAM" team) - (ss-crm--property-line "MANAGER" manager) - (ss-crm--property-line "ENGAGEMENT" engagement) - (ss-crm--property-line "SUPPLIER" supplier) - (ss-crm--property-line "LOCATION" location) - (ss-crm--property-line "CURRENT_FOCUS" current-focus) - ":END:\n\n" - "** Context\n\n" - "** TODOs\n") - (save-buffer) - (ss-crm-reload) - (ss-crm--open-entry (ss-crm--entry-by-name name)))) - -(defun ss-crm--clear-installed-abbrevs () - "Remove people-specific abbrevs from the current local table." - (mapatoms - (lambda (symbol) - (when (abbrev-get symbol :ss/crm) - (define-abbrev local-abbrev-table (symbol-name symbol) nil))) - local-abbrev-table)) - -(defun ss-crm-install-abbrevs () - "Install people abbrevs into the current buffer." - (unless (ss-crm--source-buffer-p) - (setq-local local-abbrev-table (copy-abbrev-table local-abbrev-table)) - (ss-crm--clear-installed-abbrevs) - (dolist (entry (ss-crm-entries)) - (let* ((name (ss-crm--entry-name entry)) - (abbrev (ss-crm--entry-abbrev entry)) - (abbrev-name - (if (or (null abbrev) (string-empty-p abbrev)) - (ss-crm-default-abbrev name) - abbrev))) - (define-abbrev local-abbrev-table abbrev-name name) - (when-let ((abbrev-symbol - (abbrev-symbol abbrev-name local-abbrev-table))) - (abbrev-put abbrev-symbol :ss/crm t)))))) - -(defun ss-crm-refresh-buffers () - "Refresh people abbrevs in every prose buffer." - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (when (and (bound-and-true-p abbrev-mode) - (derived-mode-p 'text-mode 'org-mode)) - (ss-crm-install-abbrevs))))) - -(defun ss-crm-capf () - "Return canonical people completions at a word boundary." - (let ((end (point))) - (save-excursion - (skip-syntax-backward "w_") - (let ((beg (point))) - (when (< beg end) - (let ((annotation - (lambda (candidate) - (when-let ((entry (ss-crm--entry-by-name candidate))) - (concat " " (ss-crm--summary entry))))) - (docsig - (lambda (candidate) - (when-let ((entry (ss-crm--entry-by-name candidate))) - (ss-crm--summary entry))))) - (list beg end #'ss-crm--completion-table - :exclusive 'no - :annotation-function annotation - :company-docsig docsig))))))) - -(defun ss-enable-people-capf () - "Add `ss-crm-capf' once in prose buffers." - (unless (or (ss-crm--source-buffer-p) - (memq #'ss-crm-capf completion-at-point-functions)) - (add-hook 'completion-at-point-functions #'ss-crm-capf nil t))) - -(defun ss-crm--maybe-overview-buffer () - "Reset the people CRM buffer to overview when visiting it directly." - (when (and buffer-file-name - (string= (file-truename buffer-file-name) - (file-truename ss-crm-file))) - (widen) - (goto-char (point-min)) - (org-overview) - (org-cycle-hide-drawers 'all))) - -(defun ss-crm-setup () - "Initialize CRM hooks and helpers." - (dolist (hook '(text-mode-hook org-mode-hook)) - (add-hook hook #'ss-enable-people-capf) - (add-hook hook #'ss-crm-install-abbrevs)) - (add-hook 'find-file-hook #'ss-crm--track-buffer) - (add-hook 'find-file-hook #'ss-crm--maybe-overview-buffer)) - -(provide 'ss-crm) - -;;; ss-crm.el ends here diff --git a/lisp/ss-denote.el b/lisp/ss-denote.el deleted file mode 100644 index 65b9b53..0000000 --- a/lisp/ss-denote.el +++ /dev/null @@ -1,49 +0,0 @@ -;;; ss-denote.el --- Denote configuration -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Durable note creation and Denote integration. - -;;; Code: - -(require 'ss-core) - -(declare-function denote-keywords-prompt "denote") -(declare-function denote-org-capture "denote") -(declare-function denote-org-capture-with-prompts "denote") - -(defun ss-denote-capture-in-directory (directory &optional keywords &rest prompts) - "Start a Denote Org capture in DIRECTORY with KEYWORDS and PROMPTS. -If PROMPTS is empty, rely on `denote-prompts'." - (let* ((prompt-for-keywords (memq :keywords prompts)) - (directory (ss-require-existing-directory directory)) - (denote-directory directory) - (denote-use-directory (unless (memq :subdirectory prompts) directory)) - (denote-use-keywords - (if prompt-for-keywords - (delete-dups (append keywords (denote-keywords-prompt))) - keywords))) - (if prompts - (denote-org-capture-with-prompts - (memq :title prompts) - nil - (memq :subdirectory prompts) - (memq :date prompts) - (memq :template prompts)) - (denote-org-capture)))) - -(defun ss-denote-setup () - "Initialize Denote." - (use-package denote - :ensure t - :after org - :config - (setq denote-directory ss-org-directory - denote-known-keywords '("project") - denote-prompts '(title keywords) - denote-org-capture-specifiers "%?") - (denote-rename-buffer-mode 1))) - -(provide 'ss-denote) - -;;; ss-denote.el ends here diff --git a/lisp/ss-gptel.el b/lisp/ss-gptel.el deleted file mode 100644 index bef5ce7..0000000 --- a/lisp/ss-gptel.el +++ /dev/null @@ -1,21 +0,0 @@ -;;; ss-gptel.el --- GPTel integration -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Experimental gptel setup using the GitHub Copilot backend. - -;;; Code: - -(require 'ss-core) - -(defun ss-gptel-setup () - "Initialize gptel." - (if (require 'gptel nil t) - (setq gptel-default-mode 'org-mode - gptel-model 'gpt-4o - gptel-backend (gptel-make-gh-copilot "Copilot")) - (message "Skipping gptel setup because the package is unavailable."))) - -(provide 'ss-gptel) - -;;; ss-gptel.el ends here diff --git a/lisp/ss-keys.el b/lisp/ss-keys.el deleted file mode 100644 index 4816eea..0000000 --- a/lisp/ss-keys.el +++ /dev/null @@ -1,60 +0,0 @@ -;;; ss-keys.el --- Global keybindings -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Centralized global keybindings for enabled features. - -;;; Code: - -(require 'ss-core) - -(defun ss-keys-setup () - "Install global keybindings for enabled features." - (when (ss-feature-enabled-p 'agenda) - (global-set-key (kbd "C-c a") #'ss-open-agenda)) - - (when (ss-feature-enabled-p 'capture) - (global-set-key (kbd "C-c c") #'org-capture)) - - (when (ss-feature-enabled-p 'org) - (global-set-key (kbd "C-c b") #'ss-jump-back) - (global-set-key (kbd "C-c f") #'ss-jump-forward)) - - (when (and (ss-feature-enabled-p 'denote) - (fboundp 'denote-open-or-create) - (fboundp 'denote-link)) - (global-set-key (kbd "C-c n n") #'denote-open-or-create) - (global-set-key (kbd "C-c n l") #'denote-link)) - - (when (ss-feature-enabled-p 'org) - (global-set-key (kbd "C-c n M") #'ss-open-moc) - (global-set-key (kbd "C-c n j") #'ss-open-journal-full) - (global-set-key (kbd "C-c n d") #'ss-open-journal)) - - (when (ss-feature-enabled-p 'crm) - (global-set-key (kbd "C-c n E") #'ss-crm-report-by-engagement) - (global-set-key (kbd "C-c n f") #'ss-crm-find) - (global-set-key (kbd "C-c n i") #'ss-crm-insert-name) - (global-set-key (kbd "C-c n I") #'ss-crm-insert-summary) - (global-set-key (kbd "C-c n L") #'ss-crm-report-by-location) - (global-set-key (kbd "C-c n o") #'ss-crm-overview) - (global-set-key (kbd "C-c n O") #'ss-crm-report-by-role) - (global-set-key (kbd "C-c n p") #'ss-crm-open) - (global-set-key (kbd "C-c n P") #'ss-crm-add) - (global-set-key (kbd "C-c n R") #'ss-crm-report-by-manager) - (global-set-key (kbd "C-c n S") #'ss-crm-report-by-supplier) - (global-set-key (kbd "C-c n T") #'ss-crm-report-by-team)) - - (when (and (ss-feature-enabled-p 'gptel) - (fboundp 'gptel) - (fboundp 'gptel-send) - (fboundp 'gptel-rewrite) - (fboundp 'gptel-add)) - (global-set-key (kbd "C-c n g") #'gptel) - (global-set-key (kbd "C-c n s") #'gptel-send) - (global-set-key (kbd "C-c n r") #'gptel-rewrite) - (global-set-key (kbd "C-c n a") #'gptel-add))) - -(provide 'ss-keys) - -;;; ss-keys.el ends here diff --git a/lisp/ss-org.el b/lisp/ss-org.el deleted file mode 100644 index fc8fc9b..0000000 --- a/lisp/ss-org.el +++ /dev/null @@ -1,306 +0,0 @@ -;;; ss-org.el --- Base Org configuration -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Shared Org setup and note-opening helpers. - -;;; Code: - -(require 'org) -(require 'ss-core) - -(defvar ss-navigation-back-stack nil - "Stack of older locations for `ss-jump-back'.") - -(defvar ss-navigation-forward-stack nil - "Stack of newer locations for `ss-jump-forward'.") - -(defvar ss-navigation--inhibit-recording nil - "When non-nil, suppress navigation history recording.") - -(defun ss-navigation--current-location () - "Return the current location as a marker-backed plist." - (unless (minibufferp (current-buffer)) - (list :marker (copy-marker (point-marker)) - :window-start - (when (window-live-p (selected-window)) - (copy-marker (window-start (selected-window))))))) - -(defun ss-navigation--location-valid-p (location) - "Return non-nil when LOCATION still points to a live buffer position." - (when-let ((marker (plist-get location :marker))) - (and (markerp marker) - (marker-buffer marker) - (marker-position marker)))) - -(defun ss-navigation--same-location-p (left right) - "Return non-nil when LEFT and RIGHT identify the same buffer position." - (and (ss-navigation--location-valid-p left) - (ss-navigation--location-valid-p right) - (eq (marker-buffer (plist-get left :marker)) - (marker-buffer (plist-get right :marker))) - (= (marker-position (plist-get left :marker)) - (marker-position (plist-get right :marker))))) - -(defun ss-navigation--prune-stack (stack) - "Return STACK with dead or duplicate locations removed." - (let (pruned previous) - (dolist (location stack (nreverse pruned)) - (when (and (ss-navigation--location-valid-p location) - (not (ss-navigation--same-location-p location previous))) - (push location pruned) - (setq previous location))))) - -(defun ss-navigation--push-location (stack-symbol location &optional clear-forward) - "Push LOCATION onto STACK-SYMBOL unless it duplicates the top entry. -When CLEAR-FORWARD is non-nil, reset `ss-navigation-forward-stack'." - (when (ss-navigation--location-valid-p location) - (set stack-symbol (ss-navigation--prune-stack (symbol-value stack-symbol))) - (unless (ss-navigation--same-location-p location (car (symbol-value stack-symbol))) - (set stack-symbol (cons location (symbol-value stack-symbol)))) - (when clear-forward - (setq ss-navigation-forward-stack nil)) - t)) - -(defun ss-navigation-push-current-location () - "Push the current location onto the back stack. -This is for significant navigation points only and clears forward history." - (interactive) - (ss-navigation--push-location - 'ss-navigation-back-stack - (ss-navigation--current-location) - 'clear-forward)) - -(defun ss-navigation--restore-location (location) - "Restore LOCATION, returning non-nil when successful." - (when (ss-navigation--location-valid-p location) - (let* ((marker (plist-get location :marker)) - (buffer (marker-buffer marker)) - (window-start (plist-get location :window-start))) - (switch-to-buffer buffer) - (goto-char marker) - (when (and (markerp window-start) - (eq (marker-buffer window-start) buffer) - (window-live-p (selected-window))) - (set-window-start (selected-window) - (marker-position window-start) - t)) - t))) - -(defun ss-navigation--jump-from-stack (stack-symbol target-symbol) - "Restore the next location from STACK-SYMBOL and push current onto TARGET-SYMBOL." - (set stack-symbol (ss-navigation--prune-stack (symbol-value stack-symbol))) - (when-let ((target (car (symbol-value stack-symbol)))) - (let ((current (ss-navigation--current-location))) - (set stack-symbol (cdr (symbol-value stack-symbol))) - (when (ss-navigation--location-valid-p current) - (ss-navigation--push-location target-symbol current)) - (let ((ss-navigation--inhibit-recording t)) - (ss-navigation--restore-location target))))) - -(defun ss-navigation--jump-via-command (command) - "Use COMMAND as a mark-ring fallback jump, recording forward history." - (when (fboundp command) - (let ((before (ss-navigation--current-location)) - after) - (let ((ss-navigation--inhibit-recording t)) - (condition-case nil - (call-interactively command) - (error nil))) - (setq after (ss-navigation--current-location)) - (when (and (ss-navigation--location-valid-p before) - (ss-navigation--location-valid-p after) - (not (ss-navigation--same-location-p before after))) - (ss-navigation--push-location 'ss-navigation-forward-stack before) - t)))) - -(defun ss-jump-back () - "Move backward through navigation history." - (interactive) - (or (ss-navigation--jump-from-stack - 'ss-navigation-back-stack - 'ss-navigation-forward-stack) - (ss-navigation--jump-via-command 'pop-global-mark) - (ss-navigation--jump-via-command 'pop-to-mark-command) - (progn - (message "No back location available") - nil))) - -(defun ss-jump-forward () - "Move forward through navigation history." - (interactive) - (or (ss-navigation--jump-from-stack - 'ss-navigation-forward-stack - 'ss-navigation-back-stack) - (progn - (message "No forward location available") - nil))) - -(defun ss-navigation--record-jump (original &rest args) - "Record the pre-jump location around ORIGINAL with ARGS." - (if ss-navigation--inhibit-recording - (apply original args) - (let ((before (ss-navigation--current-location)) - result) - (setq result (apply original args)) - (let ((after (ss-navigation--current-location))) - (when (and (ss-navigation--location-valid-p before) - (ss-navigation--location-valid-p after) - (not (ss-navigation--same-location-p before after))) - (ss-navigation--push-location - 'ss-navigation-back-stack - before - 'clear-forward))) - result))) - -(defun ss-navigation--advise-command (command) - "Wrap COMMAND so significant jumps record navigation history." - (unless (advice-member-p #'ss-navigation--record-jump command) - (advice-add command :around #'ss-navigation--record-jump))) - -(defun ss-navigation-setup () - "Install navigation history advice for note-related jump commands." - (dolist (command '(ss-open-journal - ss-open-journal-today-session - ss-open-journal-full - ss-open-moc)) - (ss-navigation--advise-command command)) - (with-eval-after-load 'org - (ss-navigation--advise-command 'org-open-at-point)) - (with-eval-after-load 'org-agenda - (dolist (command '(org-agenda-goto - org-agenda-switch-to - org-agenda-open-link)) - (ss-navigation--advise-command command))) - (with-eval-after-load 'denote - (ss-navigation--advise-command 'denote-open-or-create)) - (with-eval-after-load 'ss-crm - (dolist (command '(ss-crm-find - ss-crm-open - ss-crm-overview)) - (ss-navigation--advise-command command)))) - -(defvar ss-journal-session-mode-map - (let ((map (make-sparse-keymap))) - (define-key map (kbd "C-c C-c") #'ss-journal-session-save-and-dismiss) - (define-key map (kbd "C-c C-k") #'ss-journal-session-dismiss) - map) - "Keymap for focused journal editing sessions.") - -(defconst ss-journal-session-header-line - "Journal session: C-c C-c save and dismiss, C-c C-k dismiss" - "Header line shown during focused journal editing sessions.") - -(define-minor-mode ss-journal-session-mode - "Minor mode for focused journal editing sessions." - :lighter " Journal-Session" - :keymap ss-journal-session-mode-map - (if ss-journal-session-mode - (setq-local header-line-format ss-journal-session-header-line) - (kill-local-variable 'header-line-format))) - -(defun ss-journal-session-dismiss () - "End the focused journal session without saving automatically." - (interactive) - (widen) - (ss-journal-session-mode -1) - (quit-window nil (selected-window))) - -(defun ss-journal-session-save-and-dismiss () - "Save the journal buffer, then end the focused journal session." - (interactive) - (save-buffer) - (ss-journal-session-dismiss)) - -(defun ss-open-journal () - "Open today's journal entry in a focused session, creating it when needed." - (interactive) - (ss-open-journal-today-session)) - -(defun ss-open-journal-today-session () - "Open today's journal entry in a focused, dismissable session." - (interactive) - (find-file (ss-require-existing-file ss-journal-file)) - (widen) - (unless (fboundp 'ss-journal-goto-date) - (user-error "Journal date navigation is unavailable")) - (ss-journal-goto-date nil 'create) - (when (fboundp 'ss-journal-ensure-day-sections) - (ss-journal-ensure-day-sections)) - (org-fold-show-entry) - (org-fold-show-subtree) - (org-narrow-to-subtree) - (ss-journal-session-mode 1)) - -(defun ss-open-journal-full () - "Open `ss-journal-file' with the full buffer visible." - (interactive) - (find-file (ss-require-existing-file ss-journal-file)) - (widen)) - -(defun ss-open-moc () - "Open the central MOC note." - (interactive) - (find-file (ss-require-existing-file ss-moc-file))) - -(defun ss-org-refresh-agenda-files-for-refile (&rest _args) - "Refresh `org-agenda-files' before refile when agenda setup is available." - (when (fboundp 'ss-refresh-org-agenda-files) - (ss-refresh-org-agenda-files))) - -(defun ss-org-configure-refile () - "Configure Org refile to target any heading in `org-agenda-files'." - (setq org-refile-targets '((org-agenda-files :regexp . "^\\*+ ")) - org-refile-use-outline-path 'file - org-outline-path-complete-in-steps nil) - (unless (advice-member-p #'ss-org-refresh-agenda-files-for-refile #'org-refile) - (advice-add 'org-refile :before #'ss-org-refresh-agenda-files-for-refile))) - -(defun ss-org-setup () - "Initialize base Org configuration." - (use-package org - :ensure nil - :config - (setq org-directory ss-org-directory - org-catch-invisible-edits 'error - org-hide-emphasis-markers t - org-agenda-search-headline-for-time t - org-agenda-custom-commands - '( - ("d" "Daily Agenda" - ((agenda "") - (todo "CLARIFY" - ((org-agenda-overriding-header "Open Questions")))))) - org-todo-keywords - '((sequence "TODO(t)" "CLARIFY(c)" "WAIT(w@/!)" "|" - "DONE(d)" "CANCELLED(x@)")) - org-log-done 'note - org-log-into-drawer t) - (ss-org-configure-refile) - (add-hook 'org-mode-hook - (lambda () - (setq-local org-hide-emphasis-markers t) - (when (fboundp 'olivetti-mode) - (olivetti-mode 1)) - (font-lock-flush) - (font-lock-ensure)))) - - (use-package git-auto-commit-mode - :ensure t - :pin melpa - :commands (git-auto-commit-mode) - :init - (setq gac-shell-and - (if (string-match-p "fish\\'" shell-file-name) - " ; and " - " && "))) - - (add-hook 'emacs-startup-hook - (lambda () - (find-file (ss-require-existing-file ss-moc-file)))) - - (ss-navigation-setup)) - -(provide 'ss-org) - -;;; ss-org.el ends here diff --git a/lisp/ss-ui.el b/lisp/ss-ui.el deleted file mode 100644 index 3dd728c..0000000 --- a/lisp/ss-ui.el +++ /dev/null @@ -1,144 +0,0 @@ -;;; ss-ui.el --- Interface configuration -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Theme, frame, completion, and interface setup. - -;;; Code: - -(require 'ss-core) - -(defun ss-ui--configure-frames () - "Apply GUI and terminal frame behavior." - (when (display-graphic-p) - (set-frame-size (selected-frame) 140 42) - (menu-bar-mode -1) - (tool-bar-mode -1) - (scroll-bar-mode -1) - (tooltip-mode -1) - (set-face-attribute - 'default nil - :family "JetBrains Mono" :height 140 :weight 'medium) - (set-face-attribute - 'fixed-pitch nil - :family "JetBrains Mono" :weight 'medium) - (set-face-attribute - 'fixed-pitch-serif nil - :family "JetBrains Mono" :weight 'medium)) - - (unless (display-graphic-p) - ;; Terminal menu bar removal stays on startup hook to avoid tty regressions. - (add-hook 'emacs-startup-hook (lambda () (menu-bar-mode -1))))) - -(defun ss-ui--setup-modeline () - "Configure the modeline." - (use-package time - :ensure nil - :config - (setq display-time-24hr-format t - display-time-day-and-date t - display-time-default-load-average nil - calendar-latitude -37.7667 - calendar-longitude 145.0 - calendar-location-name "Melbourne, VIC") - (display-time-mode 1)) - - ;; Keep the theme's faces, but make the right edge alignment dynamic. - (setq-default mode-line-format - (list - " " - "%e" - mode-line-front-space - mode-line-mule-info - mode-line-client - mode-line-modified - mode-line-remote - mode-line-frame-identification - mode-line-buffer-identification - " " - mode-line-position - '(vc-mode vc-mode) - " " - mode-line-modes - '(:eval (propertize - " " - 'display - `((space :align-to - (- right - ,(+ 2 (string-width - (format-mode-line mode-line-misc-info)))))))) - mode-line-misc-info - " " - mode-line-end-spaces))) - -(defun ss-ui--setup-completion () - "Configure minibuffer and in-buffer completion." - (use-package vertico - :ensure t - :pin melpa - :init - (vertico-mode 1)) - - (use-package orderless - :ensure t - :pin melpa - :custom - (completion-styles '(orderless basic)) - (completion-category-defaults nil) - (completion-category-overrides '((file (styles basic partial-completion))))) - - (use-package marginalia - :ensure t - :pin melpa - :after vertico - :init - (marginalia-mode 1)) - - (use-package corfu - :ensure t - :pin gnu - :init - (global-corfu-mode 1))) - -(defun ss-ui--setup-writing-layout () - "Configure centered writing layout helpers." - (use-package olivetti - :ensure t - :pin melpa - :custom - (olivetti-body-width 100))) - -(defun ss-ui-setup () - "Initialize interface and completion behavior." - (setq inhibit-startup-message t - inhibit-startup-screen t - ring-bell-function 'ignore) - - (ss-ui--configure-frames) - - (use-package modus-themes - :ensure nil - :no-require t - :config - (load-theme 'modus-vivendi t)) - - (use-package dired - :ensure nil - :custom - (dired-use-ls-dired nil)) - - (line-number-mode 1) - (column-number-mode 1) - (show-paren-mode 1) - - (setq-default indicate-empty-lines nil) - (setq-default indicate-buffer-boundaries nil) - (setq-default fringe-indicator-alist nil) - - (ss-ui--setup-modeline) - (ss-ui--setup-completion) - (ss-ui--setup-writing-layout)) - -(provide 'ss-ui) - -;;; ss-ui.el ends here @@ -1,6 +0,0 @@ -#!/bin/sh - -set -eu - -rm -f custom.el -rm -rf auto-save-list eln-cache elpa diff --git a/tests/ss-capture-tests.el b/tests/ss-capture-tests.el deleted file mode 100644 index 006c90d..0000000 --- a/tests/ss-capture-tests.el +++ /dev/null @@ -1,176 +0,0 @@ -;;; ss-capture-tests.el --- Tests for ss-capture -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Focused ERT coverage for journal capture structure helpers. - -;;; Code: - -(add-to-list 'load-path (expand-file-name "../lisp" (file-name-directory load-file-name))) - -(require 'cl-lib) -(require 'ert) -(require 'ss-capture) -(require 'ss-org) - -(ert-deftest ss-jump-back-restores-previous-location-and-enables-forward () - (let ((ss-navigation-back-stack nil) - (ss-navigation-forward-stack nil) - (buffer-a (generate-new-buffer " *ss-nav-a*")) - (buffer-b (generate-new-buffer " *ss-nav-b*"))) - (unwind-protect - (save-window-excursion - (with-current-buffer buffer-a - (insert "alpha") - (goto-char 3)) - (with-current-buffer buffer-b - (insert "bravo") - (goto-char 5)) - (switch-to-buffer buffer-a) - (ss-navigation-push-current-location) - (switch-to-buffer buffer-b) - (should (ss-jump-back)) - (should (eq (current-buffer) buffer-a)) - (should (= (point) 3)) - (should (ss-jump-forward)) - (should (eq (current-buffer) buffer-b)) - (should (= (point) 5))) - (kill-buffer buffer-a) - (kill-buffer buffer-b)))) - -(ert-deftest ss-navigation-push-current-location-clears-forward-after-back () - (let ((ss-navigation-back-stack nil) - (ss-navigation-forward-stack nil) - (buffer-a (generate-new-buffer " *ss-nav-a*")) - (buffer-b (generate-new-buffer " *ss-nav-b*"))) - (unwind-protect - (save-window-excursion - (with-current-buffer buffer-a - (insert "alpha") - (goto-char 2)) - (with-current-buffer buffer-b - (insert "bravo") - (goto-char 4)) - (switch-to-buffer buffer-a) - (ss-navigation-push-current-location) - (switch-to-buffer buffer-b) - (should (ss-jump-back)) - (should ss-navigation-forward-stack) - (ss-navigation-push-current-location) - (should-not ss-navigation-forward-stack)) - (kill-buffer buffer-a) - (kill-buffer buffer-b)))) - -(ert-deftest ss-navigation-record-jump-skips-noop-movements () - (let ((ss-navigation-back-stack nil) - (ss-navigation-forward-stack nil)) - (with-temp-buffer - (switch-to-buffer (current-buffer)) - (insert "alpha") - (goto-char 2) - (ss-navigation--record-jump (lambda () nil)) - (should-not ss-navigation-back-stack) - (should-not ss-navigation-forward-stack)))) - -(ert-deftest ss-journal-ensure-day-sections-adds-all-standard-sections () - (with-temp-buffer - (org-mode) - (insert "#+title: Journal\n" - "#+startup: overview\n\n" - "* 2026\n" - "** 2026-04-08 Wednesday\n" - "*** Tasks\n" - "**** TODO Existing\n\n" - "** 2026-04-09 Thursday\n") - (goto-char (point-min)) - (re-search-forward "^\\*\\* 2026-04-09 Thursday$") - (goto-char (match-beginning 0)) - (ss-journal-ensure-day-sections) - (should (string-match-p - (regexp-quote - "** 2026-04-09 Thursday\n*** Tasks\n*** Notes\n*** Meetings\n") - (buffer-string))))) - -(ert-deftest ss-open-journal-narrows-to-today-when-entry-exists () - (let* ((file (make-temp-file "ss-journal" nil ".org")) - (ss-journal-file file) - (test-time (encode-time 0 0 12 9 4 2026))) - (unwind-protect - (cl-letf (((symbol-function 'current-time) - (lambda () - test-time))) - (with-temp-file file - (insert "#+title: Journal\n" - "* 2026\n" - "** 2026-04-09 Thursday\n" - "*** Notes\n")) - (ss-open-journal) - (should (buffer-narrowed-p)) - (should (equal (org-get-outline-path t) - '("2026" "2026-04-09 Thursday"))) - (should (looking-at-p "^\\*\\* 2026-04-09 Thursday$"))) - (when-let ((buffer (get-file-buffer file))) - (kill-buffer buffer)) - (when (file-exists-p file) - (delete-file file))))) - -(ert-deftest ss-open-journal-reveals-folded-today-subtree () - (let* ((file (make-temp-file "ss-journal" nil ".org")) - (ss-journal-file file) - (test-time (encode-time 0 0 12 9 4 2026))) - (unwind-protect - (cl-letf (((symbol-function 'current-time) - (lambda () - test-time))) - (with-temp-file file - (insert "#+title: Journal\n" - "* 2026\n" - "** 2026-04-09 Thursday\n" - "*** Notes\n" - "Body\n")) - (with-current-buffer (find-file-noselect file) - (org-overview)) - (ss-open-journal) - (should (buffer-narrowed-p)) - (should-not - (save-excursion - (goto-char (point-min)) - (re-search-forward "^\\*\\*\\* Notes$" nil t) - (invisible-p (point)))) - (should-not - (save-excursion - (goto-char (point-min)) - (re-search-forward "^Body$" nil t) - (invisible-p (point))))) - (when-let ((buffer (get-file-buffer file))) - (kill-buffer buffer)) - (when (file-exists-p file) - (delete-file file))))) - -(ert-deftest ss-open-journal-creates-missing-today-entry-with-standard-sections () - (let* ((file (make-temp-file "ss-journal" nil ".org")) - (ss-journal-file file) - (test-time (encode-time 0 0 12 9 4 2026))) - (unwind-protect - (cl-letf (((symbol-function 'current-time) - (lambda () - test-time))) - (with-temp-file file - (insert "#+title: Journal\n" - "* 2026\n" - "** 2026-04-08 Wednesday\n" - "*** Notes\n")) - (ss-open-journal) - (should (buffer-narrowed-p)) - (should (equal (org-get-outline-path t) - '("2026" "2026-04-09 Thursday"))) - (should (string-match-p - (regexp-quote - "** 2026-04-09 Thursday\n*** Tasks\n*** Notes\n*** Meetings\n") - (buffer-string)))) - (when-let ((buffer (get-file-buffer file))) - (kill-buffer buffer)) - (when (file-exists-p file) - (delete-file file))))) - -;;; ss-capture-tests.el ends here diff --git a/tests/ss-crm-tests.el b/tests/ss-crm-tests.el deleted file mode 100644 index bc88ee4..0000000 --- a/tests/ss-crm-tests.el +++ /dev/null @@ -1,92 +0,0 @@ -;;; ss-crm-tests.el --- Tests for ss-crm -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Focused ERT coverage for CRM lookup and prompt helpers. - -;;; Code: - -(add-to-list 'load-path (expand-file-name "../lisp" (file-name-directory load-file-name))) - -(require 'ert) -(require 'cl-lib) -(require 'ss-crm) - -(ert-deftest ss-crm-known-property-values-sorts-and-deduplicates () - (cl-letf (((symbol-function 'ss-crm-entries) - (lambda () - (list (list :role "Engineer") - (list :role " engineer ") - (list :role "Architect") - (list :role "") - (list :role nil))))) - (should (equal (ss-crm-known-property-values "ROLE") - '(" engineer " "Architect" "Engineer"))))) - -(ert-deftest ss-crm-known-person-names-returns-sorted-top-level-names () - (cl-letf (((symbol-function 'ss-crm-entries) - (lambda () - (list (list :name "Zoe") - (list :name "Alice") - (list :name "Bob"))))) - (should (equal (ss-crm-known-person-names) - '("Alice" "Bob" "Zoe"))))) - -(ert-deftest ss-crm-lookup-values-merges-seeded-and-derived-values () - (cl-letf (((symbol-function 'ss-crm-known-property-values) - (lambda (_property) - '("Team B" "Team A")))) - (should (equal (ss-crm-lookup-values "TEAM" '("Team A" "Team C")) - '("Team A" "Team B" "Team C"))))) - -(ert-deftest ss-crm-read-choice-returns-nil-for-none-selection () - (cl-letf (((symbol-function 'completing-read) - (lambda (&rest _args) - "[none]"))) - (should-not (ss-crm-read-choice "Role: " '("Engineer") - :allow-blank t - :allow-new t)))) - -(ert-deftest ss-crm-read-choice-warns-on-new-case-insensitive-duplicate () - (let (warning) - (cl-letf (((symbol-function 'completing-read) - (lambda (&rest _args) - "sydney")) - ((symbol-function 'yes-or-no-p) - (lambda (&rest _args) - t)) - ((symbol-function 'display-warning) - (lambda (_type message &rest _args) - (setq warning message)))) - (should (equal (ss-crm-read-choice "Location: " '("Sydney") - :allow-blank t - :allow-new t) - "sydney")) - (should (string-match-p "Sydney" warning))))) - -(ert-deftest ss-crm-read-choice-does-not-warn-for-existing-selection () - (let (warning) - (cl-letf (((symbol-function 'completing-read) - (lambda (&rest _args) - "Sydney")) - ((symbol-function 'display-warning) - (lambda (_type message &rest _args) - (setq warning message)))) - (should (equal (ss-crm-read-choice "Location: " '("Sydney" "sydney") - :allow-blank t - :allow-new t - :require-match t) - "Sydney")) - (should-not warning)))) - -(ert-deftest ss-crm-read-manager-uses-known-person-names () - (cl-letf (((symbol-function 'ss-crm-known-person-names) - (lambda () - '("Alice" "Bob"))) - ((symbol-function 'ss-crm-read-choice) - (lambda (_prompt choices &rest _plist) - choices))) - (should (equal (ss-crm-read-manager) - '("Alice" "Bob"))))) - -;;; ss-crm-tests.el ends here diff --git a/tests/ss-org-tests.el b/tests/ss-org-tests.el deleted file mode 100644 index b8e1eb5..0000000 --- a/tests/ss-org-tests.el +++ /dev/null @@ -1,88 +0,0 @@ -;;; ss-org-tests.el --- Tests for ss-org -*- lexical-binding: t; -*- - -;;; Commentary: - -;; Focused ERT coverage for Org refile configuration. - -;;; Code: - -(add-to-list 'load-path (expand-file-name "../lisp" (file-name-directory load-file-name))) - -(require 'ert) -(require 'cl-lib) -(require 'org) -(require 'ss-org) - -(ert-deftest ss-org-configure-refile-sets-org-variables () - (let ((old-refile-targets (and (boundp 'org-refile-targets) - org-refile-targets)) - (old-refile-use-outline-path (and (boundp 'org-refile-use-outline-path) - org-refile-use-outline-path)) - (old-outline-path-complete-in-steps - (and (boundp 'org-outline-path-complete-in-steps) - org-outline-path-complete-in-steps))) - (unwind-protect - (progn - (advice-remove 'org-refile #'ss-org-refresh-agenda-files-for-refile) - (setq org-refile-targets nil - org-refile-use-outline-path nil - org-outline-path-complete-in-steps t) - (ss-org-configure-refile) - (should (equal org-refile-targets - '((org-agenda-files :regexp . "^\\*+ ")))) - (should (eq org-refile-use-outline-path 'file)) - (should-not org-outline-path-complete-in-steps)) - (setq org-refile-targets old-refile-targets - org-refile-use-outline-path old-refile-use-outline-path - org-outline-path-complete-in-steps - old-outline-path-complete-in-steps) - (advice-remove 'org-refile #'ss-org-refresh-agenda-files-for-refile)))) - -(ert-deftest ss-org-configure-refile-discovers-headings-in-agenda-files () - (let* ((file (make-temp-file "ss-org-refile-" nil ".org" - "* Alpha\n** Beta\n")) - (file-name (file-name-nondirectory file)) - (org-agenda-files (list file)) - (old-refile-targets (and (boundp 'org-refile-targets) - org-refile-targets)) - (old-refile-use-outline-path (and (boundp 'org-refile-use-outline-path) - org-refile-use-outline-path)) - (old-outline-path-complete-in-steps - (and (boundp 'org-outline-path-complete-in-steps) - org-outline-path-complete-in-steps)) - targets) - (unwind-protect - (progn - (advice-remove 'org-refile #'ss-org-refresh-agenda-files-for-refile) - (ss-org-configure-refile) - (with-current-buffer (find-file-noselect file) - (setq targets (org-refile-get-targets))) - (should (assoc (format "%s/Alpha" file-name) targets)) - (should (assoc (format "%s/Alpha/Beta" file-name) targets))) - (setq org-refile-targets old-refile-targets - org-refile-use-outline-path old-refile-use-outline-path - org-outline-path-complete-in-steps - old-outline-path-complete-in-steps) - (advice-remove 'org-refile #'ss-org-refresh-agenda-files-for-refile) - (when-let ((buffer (get-file-buffer file))) - (kill-buffer buffer)) - (delete-file file)))) - -(ert-deftest ss-org-configure-refile-adds-refresh-advice () - (unwind-protect - (progn - (advice-remove 'org-refile #'ss-org-refresh-agenda-files-for-refile) - (ss-org-configure-refile) - (should (advice-member-p #'ss-org-refresh-agenda-files-for-refile - 'org-refile))) - (advice-remove 'org-refile #'ss-org-refresh-agenda-files-for-refile))) - -(ert-deftest ss-org-refresh-agenda-files-for-refile-reuses-agenda-helper () - (let (called) - (cl-letf (((symbol-function 'ss-refresh-org-agenda-files) - (lambda (&rest _args) - (setq called t)))) - (ss-org-refresh-agenda-files-for-refile) - (should called)))) - -;;; ss-org-tests.el ends here
\ No newline at end of file |
