feat(config): add kitty pre‑shutdown session save and config updates

This commit is contained in:
Prad Nukala committed 2026-10-08 10:17:06 -04:00
1 parent cae4fb8766
commit 7e2ae53560
6 files changed
+84 -45

No files matched your search

+33 -18
View File
@@ -8,31 +8,34 @@ One file, three entry points (kitty runs it under a different __name__ each):
startup_session at it whenever no running kitty owns today's session (an
flock held for the owner's lifetime). Every launch after kitty was closed
reopens the day; a second concurrent instance starts plain instead of
duplicating today's tabs.
duplicating today's tabs. `kitty --session <past day>` (the kitty-days tv
cable) opens that day as a read-only snapshot instead.
- watcher (__kitty_watcher__): SAVE_DELAY seconds after a window opens,
closes, moves, gains focus or finishes a command (and every SAVE_EVERY
seconds, for output from long-running programs), rewrites the session this
instance started with: tabs/splits/cwds, each shell window's scrollback, and
the program running in it. On restore the shell `cat`s the scrollback, then
re-runs the program. Days opened later via the picker are left as read-only
snapshots.
- kitten (map ... kitten day_session.py): picker over every archived day,
newest first; the choice is opened with goto_session.
re-runs the program.
- kitten `day_session.py flush` (sent over remote control by kitty-day-save
before shutdown/reboot): saves right away, then holds further saves for
FREEZE seconds so windows dying during shutdown can't shrink the saved day.
"""
import fcntl
import glob
import json
import os
import re
import shlex
import sys
import tempfile
from datetime import date, timedelta
from time import monotonic
from kittens.tui.handler import result_handler
ROOT = os.path.expanduser('~/.local/share/kitty-sessions')
SAVE_DELAY = 1 # seconds; batches bursts of events, and the most change lost when kitty dies
SAVE_EVERY = 30 # seconds; catches output that arrives without any window event
FREEZE = 60 # seconds without saves after a flush; long enough for any shutdown, short if it's cancelled
SB_VAR = 'day_scrollback' # user var naming a window's scrollback dump; kitty saves/restores it via launch --var
UNSERIALIZE = 'kitty-unserialize-data=' # kitty.constants.unserialize_launch_flag
# Never replay a command that was mid-run when the session was saved if any word
@@ -149,12 +152,14 @@ def on_load(boss, data) -> None:
args[1] = UNSERIALIZE + json.dumps(data)
return shlex.join(args)
frozen_until = 0.0
def save(timer_id: int) -> None:
nonlocal last, pending
pending = False
windows = [w for w in boss.all_windows if w.created_in_session_name == name]
# Nothing left in the session means kitty is closing: keep the last good save.
if not windows:
if not windows or monotonic() < frozen_until:
return
os.makedirs(sb_dir, exist_ok=True)
keep = set()
@@ -185,7 +190,14 @@ def on_load(boss, data) -> None:
pending = True
add_timer(save, SAVE_DELAY, False)
def flush() -> None:
nonlocal frozen_until
frozen_until = 0.0
save(0)
frozen_until = monotonic() + FREEZE
_schedule_save = schedule
boss.day_session_flush = flush # the flush kitten runs in its own module copy, so it finds this via boss
add_timer(lambda timer_id: schedule(), SAVE_EVERY, True)
@@ -206,23 +218,26 @@ def main(args: list[str]) -> None:
@result_handler(no_ui=True)
def handle_result(args: list[str], answer: str, target_window_id: int, boss) -> None:
from kitty.session import goto_session
if 'flush' in args and (flush := getattr(boss, 'day_session_flush', None)):
flush()
days = sorted(glob.glob(os.path.join(ROOT, '*', '*.kitty-session')), key=os.path.basename, reverse=True)
if not days:
boss.show_error('No day sessions', f'Nothing archived under {ROOT} yet.')
return
def label(path: str) -> str:
d = date.fromisoformat(os.path.basename(path).partition('.')[0])
return f'{d} {d:%a} week of {os.path.basename(os.path.dirname(path))}'
boss.choose_entry('Open a day', ((p, label(p)) for p in days), lambda p: p and goto_session(boss, (p,)))
def session_arg() -> str | None:
"""`--session` from kitty's own command line (geninclude runs inside kitty)."""
argv = sys.argv[1:]
for i, a in enumerate(argv):
if a.startswith('--session='):
return a.partition('=')[2]
if a == '--session' and i + 1 < len(argv):
return argv[i + 1]
return None
if __name__ == '__main__':
path = today_path()
if own_day(path):
other = session_arg()
# An explicit --session for another day must not claim today's lock.
if (other is None or os.path.abspath(os.path.expanduser(other)) == path) and own_day(path):
os.makedirs(os.path.dirname(path), exist_ok=True)
try:
open(path, 'x').close() # empty session = one tab with the default shell
+7 -7
View File
@@ -35,7 +35,7 @@ action_alias launch_os launch --cwd=current --type=os-window
action_alias launch_ov launch --cwd=current --type=overlay
action_alias launch_tb launch --cwd=current --type=tab
action_alias launch_hs launch --cwd=current --location=hsplit --bias=25
action_alias launch_vs launch --cwd=current --location=vsplit --bias=30
action_alias launch_vs launch --cwd=current --location=vsplit --bias=33
action_alias launch_vs50 launch --cwd=current --location=vsplit --bias=50
# panes = kitty windows
@@ -84,12 +84,10 @@ map alt+o goto_tab 9
# reopens today's session unless another running kitty already owns it: tabs,
# splits, cwds, each shell's scrollback and the program running in it. Tabs
# made with *_with_cwd / --cwd=current join it and are saved ~1s after each
# change. ctrl+shift+d reopens any earlier day.
# change, and right before shutdown/reboot (kitty-day-save.service). `tv
# kitty-days` opens any earlier day in a new kitty.
geninclude day_session.py
watcher day_session.py
map ctrl+shift+d kitten day_session.py
# show only the active day's tabs (plus tabs outside any session)
tab_bar_filter session:~ or session:^$
# split nav (vim-aware)
map ctrl+h kitten pass_keys.py neighboring_window left ctrl+h
@@ -103,9 +101,11 @@ map super+up resize_window taller
map super+down resize_window shorter 3
tab_bar_style powerline
tab_powerline_style slanted
tab_bar_align center
tab_powerline_style angled
tab_bar_align left
tab_bar_edge top
# always shown, even with one tab: it carries the stack-layout pane indicator
tab_bar_min_tabs 1
# Titles come from ~/.config/kitty/tab_bar.py (`{custom}`): the repo the tab's
# cwd belongs to, or the file nvim is editing when nvim is the active process.