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