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

+1
View File
@@ -46,6 +46,7 @@ television
.config/mimeapps.list .config/mimeapps.list
.config/systemd .config/systemd
.config/systemd/** .config/systemd/**
.local/bin/kitty-day-save
.local/bin/niri-launch .local/bin/niri-launch
.config/fish/auto-Niri.fish .config/fish/auto-Niri.fish
.local/bin/cp-appid .local/bin/cp-appid
@@ -9,6 +9,13 @@ if [ -f "$HOME/.config/systemd/user/nirinit.service" ] && command -v systemctl >
systemctl --user daemon-reload || true systemctl --user daemon-reload || true
systemctl --user enable nirinit.service || true systemctl --user enable nirinit.service || true
fi fi
# Pre-shutdown save of kitty's daily session (dot_config/systemd/user, only on
# linux desktop/laptop roles; see .chezmoiignore.tmpl).
if [ -f "$HOME/.config/systemd/user/kitty-day-save.service" ] && command -v systemctl >/dev/null 2>&1; then
systemctl --user daemon-reload || true
systemctl --user enable --now kitty-day-save.service || true
fi
{{ else -}} {{ else -}}
#!/bin/sh #!/bin/sh
exit 0 exit 0
+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 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 flock held for the owner's lifetime). Every launch after kitty was closed
reopens the day; a second concurrent instance starts plain instead of 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, - watcher (__kitty_watcher__): SAVE_DELAY seconds after a window opens,
closes, moves, gains focus or finishes a command (and every SAVE_EVERY closes, moves, gains focus or finishes a command (and every SAVE_EVERY
seconds, for output from long-running programs), rewrites the session this seconds, for output from long-running programs), rewrites the session this
instance started with: tabs/splits/cwds, each shell window's scrollback, and 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 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 re-runs the program.
snapshots. - kitten `day_session.py flush` (sent over remote control by kitty-day-save
- kitten (map ... kitten day_session.py): picker over every archived day, before shutdown/reboot): saves right away, then holds further saves for
newest first; the choice is opened with goto_session. FREEZE seconds so windows dying during shutdown can't shrink the saved day.
""" """
import fcntl import fcntl
import glob
import json import json
import os import os
import re import re
import shlex import shlex
import sys
import tempfile import tempfile
from datetime import date, timedelta from datetime import date, timedelta
from time import monotonic
from kittens.tui.handler import result_handler from kittens.tui.handler import result_handler
ROOT = os.path.expanduser('~/.local/share/kitty-sessions') 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_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 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 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 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 # 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) args[1] = UNSERIALIZE + json.dumps(data)
return shlex.join(args) return shlex.join(args)
frozen_until = 0.0
def save(timer_id: int) -> None: def save(timer_id: int) -> None:
nonlocal last, pending nonlocal last, pending
pending = False pending = False
windows = [w for w in boss.all_windows if w.created_in_session_name == name] 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. # 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 return
os.makedirs(sb_dir, exist_ok=True) os.makedirs(sb_dir, exist_ok=True)
keep = set() keep = set()
@@ -185,7 +190,14 @@ def on_load(boss, data) -> None:
pending = True pending = True
add_timer(save, SAVE_DELAY, False) 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 _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) add_timer(lambda timer_id: schedule(), SAVE_EVERY, True)
@@ -206,23 +218,26 @@ def main(args: list[str]) -> None:
@result_handler(no_ui=True) @result_handler(no_ui=True)
def handle_result(args: list[str], answer: str, target_window_id: int, boss) -> None: 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: def session_arg() -> str | None:
d = date.fromisoformat(os.path.basename(path).partition('.')[0]) """`--session` from kitty's own command line (geninclude runs inside kitty)."""
return f'{d} {d:%a} week of {os.path.basename(os.path.dirname(path))}' argv = sys.argv[1:]
for i, a in enumerate(argv):
boss.choose_entry('Open a day', ((p, label(p)) for p in days), lambda p: p and goto_session(boss, (p,))) 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__': if __name__ == '__main__':
path = today_path() 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) os.makedirs(os.path.dirname(path), exist_ok=True)
try: try:
open(path, 'x').close() # empty session = one tab with the default shell 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_ov launch --cwd=current --type=overlay
action_alias launch_tb launch --cwd=current --type=tab action_alias launch_tb launch --cwd=current --type=tab
action_alias launch_hs launch --cwd=current --location=hsplit --bias=25 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 action_alias launch_vs50 launch --cwd=current --location=vsplit --bias=50
# panes = kitty windows # 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, # 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 # 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 # 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 geninclude day_session.py
watcher 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) # split nav (vim-aware)
map ctrl+h kitten pass_keys.py neighboring_window left ctrl+h 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 map super+down resize_window shorter 3
tab_bar_style powerline tab_bar_style powerline
tab_powerline_style slanted tab_powerline_style angled
tab_bar_align center tab_bar_align left
tab_bar_edge top 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 # 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. # cwd belongs to, or the file nvim is editing when nvim is the active process.
+23 -20
View File
@@ -1,6 +1,7 @@
"$schema" = 'https://starship.rs/config-schema.json' "$schema" = 'https://starship.rs/config-schema.json'
palette = "cyberdream" palette = "cyberdream"
format = """ format = """
$username\ $username\
$hostname\ $hostname\
@@ -12,6 +13,8 @@ $cmd_duration\
$python\ $python\
$character""" $character"""
add_newline = true
right_format = """$time""" right_format = """$time"""
[directory] [directory]
@@ -59,30 +62,30 @@ style = "bright-black"
[palettes.cyberdream] [palettes.cyberdream]
# https://github.com/scottmckendry/cyberdream.nvim # https://github.com/scottmckendry/cyberdream.nvim
black = "#1e2124" black = "#1e2124"
red = "#ff6e5e" red = "#ff6e5e"
green = "#5eff6c" green = "#5eff6c"
yellow = "#f1ff5e" yellow = "#f1ff5e"
blue = "#5ea1ff" blue = "#5ea1ff"
purple = "#bd5eff" purple = "#bd5eff"
cyan = "#5ef1ff" cyan = "#5ef1ff"
white = "#ffffff" white = "#ffffff"
bright-black = "#4a4d4f" bright-black = "#4a4d4f"
bright-red = "#ff8b7e" bright-red = "#ff8b7e"
bright-green = "#7eff89" bright-green = "#7eff89"
bright-yellow = "#f3ff7e" bright-yellow = "#f3ff7e"
bright-blue = "#7eb3ff" bright-blue = "#7eb3ff"
bright-purple = "#ff7ef3" bright-purple = "#ff7ef3"
bright-cyan = "#7ef3ff" bright-cyan = "#7ef3ff"
bright-white = "#ffffff" bright-white = "#ffffff"
# Dim variants (usable as named colors in styles) # Dim variants (usable as named colors in styles)
dim-black = "#1c1f22" dim-black = "#1c1f22"
dim-red = "#d05c50" dim-red = "#d05c50"
dim-green = "#4fd05b" dim-green = "#4fd05b"
dim-yellow = "#c5d050" dim-yellow = "#c5d050"
dim-blue = "#4f85d1" dim-blue = "#4f85d1"
dim-purple = "#d050c6" dim-purple = "#d050c6"
dim-cyan = "#4fc5d1" dim-cyan = "#4fc5d1"
dim-white = "#d0d0d1" dim-white = "#d0d0d1"
@@ -0,0 +1,13 @@
[Unit]
Description=Save kitty daily sessions before shutdown/reboot
PartOf=graphical-session.target
After=graphical-session.target
[Service]
ExecStart=%h/.local/bin/kitty-day-save
# Re-arm after a flush, in case the shutdown is cancelled.
Restart=always
RestartSec=10
[Install]
WantedBy=graphical-session.target