- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
Some checks failed
check / check (push) Has been cancelled
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]> |
||
| .forgejo/workflows | ||
| kal | ||
| tests | ||
| .gitignore | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
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