Textual calendar TUI over khal
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Joerg Ziefle a66224ce9e
Some checks failed
check / check (push) Has been cancelled
feat: kal today — daily briefing (agenda + upcoming birthdays)
New 'kal today [--days N]' (aliasable to k) prints today's events + upcoming
birthdays. Pure format_briefing (tested); briefing() splits birthday entries by
read-only calendar (khal type=birthdays = calendar_names - writable_names), so a
manual '...birthday' calendar event or a place-event isn't miscounted. 7 tests.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 23:51:18 +02:00
.forgejo/workflows refactor(theme): use shared ktui.theme; drop local copy 2026-06-24 18:49:11 +02:00
kal feat: kal today — daily briefing (agenda + upcoming birthdays) 2026-06-24 23:51:18 +02:00
tests feat: kal today — daily briefing (agenda + upcoming birthdays) 2026-06-24 23:51:18 +02:00
.gitignore chore: gitignore build/ and dist/ (wheel-build artifacts) 2026-06-21 00:52:13 +02:00
pyproject.toml refactor: use ktui keymap-core, config-base, and sync 2026-06-24 19:34:01 +02:00
README.md feat: background deep-search index for older events 2026-06-22 11:05:06 +02:00
uv.lock refactor: use ktui keymap-core, config-base, and sync 2026-06-24 19:34:01 +02:00

kal

A Textual calendar TUI built on khal's engine. Adds a persistent per-calendar toggle sidebar, Tokyo Night theming, Space-leader/which-key keys, agenda/month/week views, and modal event create/edit/delete — while reusing khal for recurrence, timezones, and .ics writeback. Sync stays with vdirsyncer.

How it works

kal/engine.py is the only module that imports khal. It wraps khal's CalendarCollection behind a plain Event dataclass and a small query/CRUD API; every other module (views, widgets, screens, app) depends only on that DTO. This means kal inherits khal's recurrence/timezone/writeback correctness without reimplementing any of it, and the UI is fully decoupled from khal internals.

Install

pipx install ~/src/kal      # or: pipx install git+ssh://forgejo/jmz/kal.git
kal --version
kal                         # launch the TUI

kal reads your existing khal config for calendars, colors, and locale. Per-calendar colors come from the vdir color files (hex). Its own UI state (hidden calendars, default view) lives in ~/.config/kal/kal.toml. If khal isn't configured, kal exits with a message pointing you at https://lostpackets.de/khal/ rather than a traceback.

Keys

Press Space for the which-key menu:

Leader chords (Space then):

Chord Action
Space c focus the calendar sidebar
Space v cycle view (agenda / month / week)
Space n new event (modal form)
Space a quick-add (natural language)
Space g goto date (jump the agenda)
Space / fuzzy search by title/participant/location (typo-tolerant); jumps to the match (nearest future occurrence for recurring)
Space s sync now (runs vdirsyncer sync, then refreshes)

Direct keys:

Key Action
j / k (↓/↑) move the cursor (events in the agenda, calendars in the sidebar)
gg / G jump the agenda cursor to the first / last event
ctrl+d / ctrl+u agenda: half-page down / up
ctrl+f / ctrl+b agenda: full-page down / up
{ / } agenda: jump to the previous / next day
H / M / L agenda: cursor to top / middle / bottom of the viewport
zz / zt / zb agenda: scroll so the cursor is centered / at top / at bottom
5j, 2}, 3G count prefix repeats a motion (vim-style)
/ search (from any view)
n / N jump to the next / previous match of the last search
enter show details of the selected event (read-only)
e edit the selected event (recurring → this occurrence / whole series; can move calendars)
d delete the selected event (recurring → choose this occurrence / whole series)
[ / ] previous / next period (week in week view, month in month view)
. jump to today
? show the help screen (all keybindings)
q quit

In the focused sidebar, space/enter toggles the highlighted calendar (bold = shown, dim = hidden) and escape returns to the agenda; the choice persists across launches (~/.config/kal/kal.toml).

Quick-add syntax

Space a opens a one-line natural-language add, e.g.:

lunch tomorrow 1pm @personal
dentist fri 9-10am
gym every mon 5pm            # recurring
review in 3 days for 90m @work
pay rent 2026-07-01          # all-day

Understands: today/tomorrow/weekday names/next <weekday>/next week/ in N days|weeks|months/ISO dates; times (1pm, 13:00, ranges 9-10am, and a bare-number heuristic where 1–6 = pm); durations (for 90m, 2h); recurrence (every mon, daily/weekly/monthly/yearly); and @calendar. No time → an all-day event.

Configuration

~/.config/kal/kal.toml (created on first calendar-toggle) also accepts:

[keymap]            # remap command keys (action = key)
new_event = "a"     # leader chord
delete    = "x"     # direct key

[theme]             # override Tokyo Night palette tokens (#rrggbb)
accent = "#bb9af7"

Remappable actions: focus_sidebar, cycle_view, new_event, goto_date, open_search, sync_now (leader chords); edit_event, delete_event, show_help, quit (direct keys). Theme tokens: background, surface, panel, foreground, primary, accent, secondary, warning, error, success, dim. Invalid entries are ignored with a warning toast and fall back to defaults; navigation keys (j/k) are fixed.

Notifications (macOS)

An opt-in launchd daemon fires macOS notifications for upcoming events, even when kal is closed. Enable it in ~/.config/kal/kal.toml:

[notify]
enabled = true
default_lead = "10m"     # fallback when an event has no alarm
poll_seconds = 60

Then install the background agent:

kal notify install       # load the launchd agent (org.kal.notify)
kal notify --once        # run a single scan now (for testing)
kal notify uninstall     # remove the agent

Events with their own alarm (VALARM) notify at that time; others use default_lead before the start. All-day events do not notify. Fired notifications are de-duplicated via ~/.local/state/kal/notified.json.

Troubleshooting

  • "No khal configuration found" — kal needs a khal config at ~/.config/khal/config. See https://lostpackets.de/khal/.
  • "khal's configuration has no calendars" — add a [calendars] section to the khal config.
  • "The calendar database is locked" — another kal/khal process is using the database; retry in a moment.

Continuous integration

.forgejo/workflows/check.yml runs the pytest suite on the self-hosted Forgejo runner for every push and pull request (job check).

Status

Working: agenda/month/week views (the week view is an hours×days time-grid with an all-day band and a +N marker for busy cells; multi-day events appear on every day they span — the agenda marks them HH:MM → / → all-day → / → HH:MM, and the month grid highlights all spanned days), per-calendar colors, persistent toggle sidebar, event create/edit/delete (recurring delete and edit both offer occurrence-vs-series; edit can move an event to another calendar), fuzzy search (rapidfuzz, typo-tolerant, matches title/participant/location, with a result preview, and a background deep index so older events are searchable too) and goto-date, and an optional vdirsyncer sync action. Write conflicts (the event changed on disk) prompt a reload instead of overwriting; other write failures surface as toasts.

Known gaps / next steps:

  • Editing a single occurrence of a recurring series uses a detach model: the occurrence is removed from the series and re-created as a standalone event. This is intentionally NOT a CalDAV RECURRENCE-ID exception — other clients see a separate event, and it won't follow if the series is later moved.
  • No undo: if a write conflicts (the event changed on disk), kal offers to reload and the in-flight edit is dropped — re-apply it against the fresh data.

Development

python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest          # 214 tests