Files
operation-bin/cheatsheet_overlay
lemke_handClaude Sonnet 5 e4ec4376ee Add sub-sheet navigation and cross-sheet search to the cheat sheet overlay
cheatsheet_to_shortcuts_json now recognizes a markdown link in a table's
first column (e.g. [Admin & Account Setup](cheatsheet_admin.md) in a
Command/Shortcut/Sheet table) and emits it as a navigable entry instead
of a literal shortcut.

cheatsheet_overlay renders such entries as clickable links; clicking one
loads and displays that file in place of the current one (path resolved
relative to the linking file, arbitrary depth, cycle-safe), with a
"< Back" affordance to return. A new "Search all sheets" checkbox
instead discovers and combines every sheet reachable from the root into
one filterable view. Everything reloads fresh from disk at the moment
it's needed (no caching across a click or toggle), so editing any of
these .md files takes effect on the very next use, same as the existing
per-invocation freshness for the root sheet.

Adds cheatsheet_admin.md as the first real sub-sheet: one-time/occasional
account and host setup tasks (personal dirs, gopass onboarding, PSI
home-sync) pulled out of the main operational reference, linked from a
new "Other cheat sheets" section in cheatsheet.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 14:55:25 +02:00

548 lines
20 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Standalone floating overlay panel showing the Bernina cheat sheet,
generated fresh from cheatsheet.md every time it's shown (via
cheatsheet_to_shortcuts_json) -- no gnome-shell extension involved, so
there's no "brand-new extension not recognized until Shell restarts"
risk: this is just a normal process, like any other keybinding here.
Toggle behavior: running this while an instance is already showing closes
it instead of opening a second one (tracked via a PID file). That PID file
is verified against /proc before ever being trusted or signaled -- on a
host where its directory isn't cleared across reboots (confirmed: some
hosts' /tmp isn't, which the PID file falls back to when XDG_RUNTIME_DIR
isn't set), a stale PID number could otherwise get reused by a completely
unrelated process after reboot.
Dismiss: Escape, the close button, or pressing the trigger key again.
Sub-sheets: a shortcut entry with a "link" field (see
cheatsheet_to_shortcuts_json -- a markdown link in a table's first column,
e.g. `[Admin & Account Setup](cheatsheet_admin.md)`) renders as a clickable
link instead of a plain shortcut. Clicking it loads and displays that file
in place of the current one, with a "< Back" affordance to return; the
"Search all sheets" checkbox instead loads the whole reachable set (root
plus everything linked from it, however deep) into one combined, filterable
view. Both always re-read from disk at the moment they're needed -- nothing
is cached across a click or a checkbox toggle, matching the top-level
per-invocation freshness described in wait_for_source() below -- so editing
any of these .md files takes effect immediately, no restart of anything
required.
"""
import json
import os
import signal
import subprocess
import sys
import tempfile
import time
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
from gi.repository import Gdk, GLib, Gtk # noqa: E402
MD_PATH = "/sf/bernina/bin/cheatsheet.md"
ROOT_TITLE = "Bernina Terminal Cheat Sheet"
GENERATOR = "/sf/bernina/bin/cheatsheet_to_shortcuts_json"
PID_FILE = os.path.join(
os.environ.get("XDG_RUNTIME_DIR", tempfile.gettempdir()),
"bernina-cheatsheet-overlay.pid",
)
MAX_COLUMNS = 3
CSS = b"""
window.cheatsheet-overlay {
background-color: rgba(20, 22, 26, 0.94);
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.cs-titlebar {
margin-bottom: 10px;
}
.cs-title {
color: #ffffff;
font-size: 18px;
font-weight: 700;
}
.cs-close-btn {
color: #e8eaed;
background: transparent;
border: none;
box-shadow: none;
padding: 2px 10px;
}
.cs-close-btn:hover {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 6px;
}
.cs-back-btn {
color: #8ab4f8;
background: transparent;
border: none;
box-shadow: none;
padding: 2px 10px;
}
.cs-back-btn:hover {
color: #aecbfa;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
.cs-category {
color: #8ab4f8;
font-size: 13px;
font-weight: 700;
margin-top: 10px;
margin-bottom: 4px;
}
.cs-key {
color: #ffe082;
font-family: monospace;
font-size: 12px;
}
.cs-link-btn {
color: #8ab4f8;
background: transparent;
border: none;
box-shadow: none;
padding: 0;
}
.cs-link-btn:hover {
color: #aecbfa;
}
.cs-link-btn label {
text-decoration: underline;
font-size: 12px;
}
.cs-desc {
color: #e8eaed;
font-size: 12px;
}
.cs-hint {
color: #9aa0a6;
font-size: 11px;
margin-top: 10px;
}
.cs-search {
background-color: rgba(255, 255, 255, 0.08);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.16);
}
.cs-search-all {
color: #9aa0a6;
font-size: 12px;
}
"""
def load_categories(path):
"""Runs the generator fresh against `path` every call -- no caching --
so any edit to the source .md is picked up the moment it's needed,
whether that's a fresh Super+F1 press or a link/checkbox click on an
already-open overlay."""
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
tmp_path = tmp.name
try:
subprocess.run([GENERATOR, path, tmp_path], check=True)
with open(tmp_path, encoding="utf-8") as f:
return json.load(f)
finally:
try:
os.unlink(tmp_path)
except OSError:
pass
def discover_sheets(root_path, root_label):
"""BFS from root_path following every "link" target reachable from it
(however deep), re-reading each file fresh (see load_categories).
Returns [(path, label, categories), ...], root first; a link target
that fails to load (missing file, bad markdown) is skipped rather than
aborting the whole walk, and an already-visited path (a cycle, or two
sheets linking to the same target) is never re-queued.
Link paths are resolved relative to the *linking* file's own
directory, so a sub-sheet can itself link to siblings without needing
the full path spelled out."""
visited = set()
order = []
queue = [(os.path.normpath(root_path), root_label)]
while queue:
path, label = queue.pop(0)
if path in visited:
continue
visited.add(path)
try:
categories = load_categories(path)
except (subprocess.CalledProcessError, OSError, json.JSONDecodeError):
continue
order.append((path, label, categories))
base_dir = os.path.dirname(path)
for category in categories:
for shortcut in category["shortcuts"]:
link = shortcut.get("link")
if link:
target = os.path.normpath(os.path.join(base_dir, link))
if target not in visited:
queue.append((target, shortcut["key"]))
return order
def _is_our_process(pid):
"""True only if `pid` is actually still running *this* script -- not
just any process that happens to exist. Without this check, a PID
file surviving somewhere that isn't cleared across reboots (confirmed:
/tmp isn't, on at least one host here) could have its old PID number
reused by a completely unrelated process, which we'd otherwise
mistakenly SIGTERM and treat as "still showing"."""
try:
with open(f"/proc/{pid}/cmdline", "rb") as f:
return b"cheatsheet_overlay" in f.read()
except OSError:
return False
def already_showing():
"""If a live instance is already up, kill it and return True (caller
should then just exit, giving Super+F1 toggle-to-close behavior)."""
try:
with open(PID_FILE, encoding="utf-8") as f:
pid = int(f.read().strip())
if _is_our_process(pid):
os.kill(pid, signal.SIGTERM)
os.unlink(PID_FILE)
return True
os.unlink(PID_FILE)
return False
except (FileNotFoundError, ValueError, ProcessLookupError):
try:
os.unlink(PID_FILE)
except OSError:
pass
return False
def category_box(category, on_link):
"""Returns (box, rows) -- rows is [(key_widget, desc_label, search_text)],
handed to _apply_filter so it can show/hide individual shortcuts (and,
via the whole box, an entire category) without rebuilding anything.
key_widget is a plain label for an ordinary shortcut, or a clickable
link-styled button (calling on_link(target) when pressed) for an entry
with a "link" field."""
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
header = Gtk.Label(label=category["name"], xalign=0)
header.get_style_context().add_class("cs-category")
header.set_line_wrap(True)
box.pack_start(header, False, False, 0)
grid = Gtk.Grid(row_spacing=2, column_spacing=10)
rows = []
for row, shortcut in enumerate(category["shortcuts"]):
link = shortcut.get("link")
if link:
key_widget = Gtk.Button(label=shortcut["key"])
key_widget.get_style_context().add_class("cs-link-btn")
key_widget.set_relief(Gtk.ReliefStyle.NONE)
key_widget.connect(
"clicked",
lambda _b, target=link, label=shortcut["key"]: on_link(target, label),
)
else:
key_widget = Gtk.Label(label=shortcut["key"], xalign=0)
key_widget.get_style_context().add_class("cs-key")
key_widget.set_line_wrap(True)
desc_label = Gtk.Label(label=shortcut["description"], xalign=0)
desc_label.get_style_context().add_class("cs-desc")
desc_label.set_line_wrap(True)
desc_label.set_max_width_chars(40)
grid.attach(key_widget, 0, row, 1, 1)
grid.attach(desc_label, 1, row, 1, 1)
search_text = (shortcut["key"] + " " + shortcut["description"]).lower()
rows.append((key_widget, desc_label, search_text))
box.pack_start(grid, False, False, 0)
return box, rows
def _apply_filter(query, entries):
"""entries: [(category_box, category_name_lower, rows), ...] from
category_box() above. A category whose *name* matches shows all its
shortcuts regardless of their own text (e.g. searching "gtile" shows
the whole gTile category); otherwise each shortcut is matched
individually against its own key + description text."""
query = query.strip().lower()
for cat_box, cat_name_lower, rows in entries:
if not query:
for key_widget, desc_label, _search_text in rows:
key_widget.set_visible(True)
desc_label.set_visible(True)
cat_box.set_visible(True)
continue
cat_matches = query in cat_name_lower
any_visible = False
for key_widget, desc_label, search_text in rows:
match = cat_matches or query in search_text
key_widget.set_visible(match)
desc_label.set_visible(match)
any_visible = any_visible or match
cat_box.set_visible(any_visible)
class Overlay:
"""Holds the mutable pieces that change as the user navigates between
sheets or toggles "search all sheets", so link clicks and the checkbox
can rebuild just the content area in place rather than tearing down
and recreating the whole window."""
def __init__(self):
self.history = [] # stack of (path, label) visited via links
self.current = (MD_PATH, ROOT_TITLE)
self.search_all = False
self.entries = [] # current [(box, name_lower, rows), ...]
def build_chrome(self):
self.win = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
self.win.set_title(ROOT_TITLE)
self.win.get_style_context().add_class("cheatsheet-overlay")
self.win.set_decorated(False)
self.win.set_position(Gtk.WindowPosition.CENTER)
self.win.set_keep_above(True)
self.win.set_skip_taskbar_hint(True)
self.win.set_skip_pager_hint(True)
self.win.set_type_hint(Gdk.WindowTypeHint.DIALOG)
screen = self.win.get_screen()
visual = screen.get_rgba_visual()
if visual and screen.is_composited():
self.win.set_visual(visual)
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
outer.set_border_width(18)
outer.pack_start(self._title_bar(), False, False, 0)
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
self.back_btn = Gtk.Button(label="← Back")
self.back_btn.get_style_context().add_class("cs-back-btn")
self.back_btn.set_relief(Gtk.ReliefStyle.NONE)
self.back_btn.connect("clicked", lambda *_: self.go_back())
toolbar.pack_start(self.back_btn, False, False, 0)
self.search_entry = Gtk.SearchEntry()
self.search_entry.get_style_context().add_class("cs-search")
self.search_entry.set_placeholder_text("Search shortcuts... (Ctrl+F)")
self.search_entry.connect(
"changed", lambda e: _apply_filter(e.get_text(), self.entries)
)
toolbar.pack_start(self.search_entry, True, True, 0)
self.search_all_check = Gtk.CheckButton(label="Search all sheets")
self.search_all_check.get_style_context().add_class("cs-search-all")
self.search_all_check.connect("toggled", self._on_search_all_toggled)
toolbar.pack_start(self.search_all_check, False, False, 0)
outer.pack_start(toolbar, False, False, 0)
self.scroller = Gtk.ScrolledWindow()
self.scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
outer.pack_start(self.scroller, True, True, 0)
hint = Gtk.Label(
label="Esc or Super+F1 again to close · Ctrl+F to search",
xalign=0,
)
hint.get_style_context().add_class("cs-hint")
outer.pack_start(hint, False, False, 0)
self.win.add(outer)
geo = self.win.get_screen().get_monitor_geometry(
self.win.get_screen().get_primary_monitor()
)
self.win.set_default_size(
min(1100, int(geo.width * 0.8)), min(800, int(geo.height * 0.8))
)
self.win.connect("key-press-event", self._on_key_press)
self.win.connect("destroy", lambda *_: Gtk.main_quit())
def _title_bar(self):
"""No window decorations are used (they'd break the floating-overlay
look), so dragging and closing are reimplemented by hand here --
click-drag anywhere on the bar moves the window, the button on the
right closes it."""
bar = Gtk.EventBox()
bar.get_style_context().add_class("cs-titlebar")
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.title_label = Gtk.Label(label=ROOT_TITLE, xalign=0)
self.title_label.get_style_context().add_class("cs-title")
row.pack_start(self.title_label, True, True, 0)
close_btn = Gtk.Button(label="✕")
close_btn.get_style_context().add_class("cs-close-btn")
close_btn.set_relief(Gtk.ReliefStyle.NONE)
close_btn.connect("clicked", lambda *_: Gtk.main_quit())
row.pack_start(close_btn, False, False, 0)
bar.add(row)
bar.connect(
"button-press-event",
lambda widget, event: self.win.begin_move_drag(
event.button, int(event.x_root), int(event.y_root), event.time
)
if event.button == 1
else False,
)
return bar
def _on_key_press(self, _widget, event):
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
if ctrl and event.keyval in (Gdk.KEY_f, Gdk.KEY_F):
self.search_entry.grab_focus()
self.search_entry.select_region(0, -1)
return True
if event.keyval == Gdk.KEY_Escape:
# First Escape just clears an active search (typing Ctrl+F, then
# Escape to see everything again shouldn't also close the whole
# overlay) -- only close once there's no search text left to
# clear.
if self.search_entry.get_text():
self.search_entry.set_text("")
return True
Gtk.main_quit()
return False
def _on_search_all_toggled(self, check):
self.search_all = check.get_active()
self._render()
def navigate(self, target, label):
base_dir = os.path.dirname(self.current[0])
path = os.path.normpath(os.path.join(base_dir, target))
# A link clicked from within a combined "search all" view still
# means "go look at that one sheet" -- drop out of that mode first.
if self.search_all:
self.search_all = False
self.search_all_check.set_active(False) # fires _on_search_all_toggled itself
self.history.append(self.current)
self.current = (path, label)
self._render()
def go_back(self):
if not self.history:
return
self.current = self.history.pop()
if self.search_all:
self.search_all = False
self.search_all_check.set_active(False)
self._render()
def _render(self):
"""Rebuilds just the content area (and title/back-button state)
for whatever self.current / self.search_all currently say -- the
window chrome built in build_chrome() is untouched."""
if self.search_all:
# Always the full reachable set from the true root, regardless
# of which sheet is "current" -- "all sheets" should mean all,
# not just what's reachable from wherever you happen to be.
sheets = discover_sheets(MD_PATH, ROOT_TITLE)
categories = []
for path, label, sheet_categories in sheets:
is_root = path == os.path.normpath(MD_PATH)
for category in sheet_categories:
name = category["name"] if is_root else f"{label} — {category['name']}"
categories.append({"name": name, "shortcuts": category["shortcuts"]})
self.title_label.set_label(f"{ROOT_TITLE} — all sheets")
else:
try:
categories = load_categories(self.current[0])
except (subprocess.CalledProcessError, OSError, json.JSONDecodeError):
categories = []
if self.current[0] == os.path.normpath(MD_PATH):
self.title_label.set_label(ROOT_TITLE)
else:
self.title_label.set_label(f"{ROOT_TITLE} › {self.current[1]}")
self.back_btn.set_visible(bool(self.history) and not self.search_all)
columns = [
Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
for _ in range(MAX_COLUMNS)
]
entries = []
for i, category in enumerate(categories):
box, rows = category_box(category, self.navigate)
columns[i % MAX_COLUMNS].pack_start(box, False, False, 0)
entries.append((box, category["name"].lower(), rows))
self.entries = entries
columns_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=28)
for col in columns:
columns_row.pack_start(col, False, False, 0)
old_child = self.scroller.get_child()
if old_child:
self.scroller.remove(old_child)
self.scroller.add(columns_row)
columns_row.show_all()
_apply_filter(self.search_entry.get_text(), self.entries)
def show(self):
self.build_chrome()
self._render()
self.win.show_all()
self.back_btn.set_visible(False)
self.search_entry.grab_focus()
def wait_for_source(timeout=10):
"""Block until MD_PATH is actually readable, up to `timeout` seconds.
/sf/bernina is a real NFS mount (not autofs) so it's normally already
up by login, but this is cheap insurance against a race at boot, and
means the file always gets read fresh from the network filesystem on
every call -- no local copy to ever go stale."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if os.access(MD_PATH, os.R_OK):
return True
time.sleep(0.2)
return os.access(MD_PATH, os.R_OK)
def main():
wait_for_source()
if already_showing():
return
with open(PID_FILE, "w", encoding="utf-8") as f:
f.write(str(os.getpid()))
def cleanup(*_args):
try:
os.unlink(PID_FILE)
except OSError:
pass
Gtk.main_quit()
signal.signal(signal.SIGTERM, lambda *_: GLib.idle_add(cleanup))
provider = Gtk.CssProvider()
provider.load_from_data(CSS)
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
overlay = Overlay()
overlay.show()
Gtk.main()
try:
os.unlink(PID_FILE)
except OSError:
pass
if __name__ == "__main__":
main()