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>
This commit is contained in:
1 parent
9c4b29ee76
commit
e4ec4376ee
5 files changed
+368
-128
No files matched your search
@@ -14,6 +14,7 @@ to quit.
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Other cheat sheets](#other-cheat-sheets)
|
||||
- [Application & window shortcuts](#application--window-shortcuts)
|
||||
- [Primary controls terminal only](#primary-controls-terminal-only)
|
||||
- [Window tiling (gTile)](#window-tiling-gtile)
|
||||
@@ -27,6 +28,18 @@ supports Markdown anchors, e.g. VS Code or GitHub/GitLab — the
|
||||
terminal viewer doesn't follow them; use `/` search instead, see
|
||||
above.)
|
||||
|
||||
## Other cheat sheets
|
||||
|
||||
In the `Super+F1` overlay, the entry below is clickable: it loads and
|
||||
shows that file in place of this one, with a "← Back" link to return.
|
||||
The "Search all sheets" checkbox next to the search box instead
|
||||
searches this sheet and everything linked from it (however deep) all
|
||||
at once.
|
||||
|
||||
| Sheet | Description |
|
||||
|-------|--------------|
|
||||
| [Admin & Account Setup](cheatsheet_admin.md) | One-time/occasional account and host setup tasks — gopass onboarding, personal dirs, PSI home-sync |
|
||||
|
||||
## Application & window shortcuts
|
||||
|
||||
| Shortcut | Action | Runs |
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Admin & Account Setup
|
||||
|
||||
One-time or occasional account/host setup tasks — not day-to-day
|
||||
operation shortcuts, which live in the
|
||||
[main cheat sheet](cheatsheet.md).
|
||||
|
||||
## Personal directories & sync
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `create_personal_dir.sh` | Create/verify a personal working directory under `/sf/bernina/config/personal/<username>` |
|
||||
| `create-links-in-home` | Force-apply PSI's Partially Shared Home symlinks now (normally applied automatically at login) |
|
||||
| `migrate_to_network_drive.sh` | Move a local dotfile onto PSI's Partially Shared Home and register it for sync across hosts |
|
||||
| `sync_personal_config.sh` | Seed `.ipython`/`.condarc` onto PSI's Partially Shared Home for cross-host sync |
|
||||
|
||||
## gopass secrets
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `gopass_bernina_setup.sh` | One-shot onboarding for a human PSI account joining the gopass store |
|
||||
| `gopass_bernina_service_setup.sh` | One-shot setup: give a service account (e.g. `gac-bernina`) access to the gopass store |
|
||||
|
||||
This is a hand-picked subset of the full script index (the "Scripts in
|
||||
this directory" section of the main sheet still lists every one of
|
||||
these too, alongside everything else) — pulled out here specifically
|
||||
because they're setup/onboarding tasks a supporter reaches for
|
||||
occasionally, not something to hunt for in the full operational
|
||||
reference.
|
||||
+299
-118
@@ -14,6 +14,19 @@ 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
|
||||
@@ -30,6 +43,7 @@ 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()),
|
||||
@@ -63,6 +77,18 @@ window.cheatsheet-overlay {
|
||||
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;
|
||||
@@ -75,6 +101,20 @@ window.cheatsheet-overlay {
|
||||
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;
|
||||
@@ -88,16 +128,23 @@ window.cheatsheet-overlay {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.cs-search-all {
|
||||
color: #9aa0a6;
|
||||
font-size: 12px;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def load_categories():
|
||||
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, MD_PATH, tmp_path], check=True)
|
||||
subprocess.run([GENERATOR, path, tmp_path], check=True)
|
||||
with open(tmp_path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
finally:
|
||||
@@ -107,6 +154,40 @@ def load_categories():
|
||||
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
|
||||
@@ -141,11 +222,13 @@ def already_showing():
|
||||
return False
|
||||
|
||||
|
||||
def category_box(category):
|
||||
"""Returns (box, rows) -- rows is [(key_label, desc_label, search_text)],
|
||||
handed to _apply_filter below so it can show/hide individual shortcuts
|
||||
(and, via the whole box, an entire category) without rebuilding
|
||||
anything."""
|
||||
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")
|
||||
@@ -155,17 +238,27 @@ def category_box(category):
|
||||
grid = Gtk.Grid(row_spacing=2, column_spacing=10)
|
||||
rows = []
|
||||
for row, shortcut in enumerate(category["shortcuts"]):
|
||||
key_label = Gtk.Label(label=shortcut["key"], xalign=0)
|
||||
key_label.get_style_context().add_class("cs-key")
|
||||
key_label.set_line_wrap(True)
|
||||
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_label, 0, row, 1, 1)
|
||||
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_label, desc_label, search_text))
|
||||
rows.append((key_widget, desc_label, search_text))
|
||||
box.pack_start(grid, False, False, 0)
|
||||
return box, rows
|
||||
|
||||
@@ -179,135 +272,226 @@ def _apply_filter(query, entries):
|
||||
query = query.strip().lower()
|
||||
for cat_box, cat_name_lower, rows in entries:
|
||||
if not query:
|
||||
for key_label, desc_label, _search_text in rows:
|
||||
key_label.set_visible(True)
|
||||
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_label, desc_label, search_text in rows:
|
||||
for key_widget, desc_label, search_text in rows:
|
||||
match = cat_matches or query in search_text
|
||||
key_label.set_visible(match)
|
||||
key_widget.set_visible(match)
|
||||
desc_label.set_visible(match)
|
||||
any_visible = any_visible or match
|
||||
cat_box.set_visible(any_visible)
|
||||
|
||||
|
||||
def search_bar(entries):
|
||||
entry = Gtk.SearchEntry()
|
||||
entry.get_style_context().add_class("cs-search")
|
||||
entry.set_placeholder_text("Search shortcuts... (Ctrl+F)")
|
||||
entry.connect("changed", lambda e: _apply_filter(e.get_text(), entries))
|
||||
return entry
|
||||
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 title_bar(win):
|
||||
"""Custom title bar: 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")
|
||||
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)
|
||||
|
||||
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
title = Gtk.Label(label="Bernina Terminal Cheat Sheet", xalign=0)
|
||||
title.get_style_context().add_class("cs-title")
|
||||
row.pack_start(title, True, True, 0)
|
||||
screen = self.win.get_screen()
|
||||
visual = screen.get_rgba_visual()
|
||||
if visual and screen.is_composited():
|
||||
self.win.set_visual(visual)
|
||||
|
||||
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)
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
outer.set_border_width(18)
|
||||
outer.pack_start(self._title_bar(), False, False, 0)
|
||||
|
||||
bar.add(row)
|
||||
bar.connect(
|
||||
"button-press-event",
|
||||
lambda widget, event: win.begin_move_drag(
|
||||
event.button, int(event.x_root), int(event.y_root), event.time
|
||||
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)
|
||||
)
|
||||
if event.button == 1
|
||||
else False,
|
||||
)
|
||||
return bar
|
||||
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)
|
||||
|
||||
def build_window(categories):
|
||||
win = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
|
||||
win.set_title("Bernina Cheat Sheet")
|
||||
win.get_style_context().add_class("cheatsheet-overlay")
|
||||
win.set_decorated(False)
|
||||
win.set_position(Gtk.WindowPosition.CENTER)
|
||||
win.set_keep_above(True)
|
||||
win.set_skip_taskbar_hint(True)
|
||||
win.set_skip_pager_hint(True)
|
||||
win.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
self.scroller = Gtk.ScrolledWindow()
|
||||
self.scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
outer.pack_start(self.scroller, True, True, 0)
|
||||
|
||||
screen = win.get_screen()
|
||||
visual = screen.get_rgba_visual()
|
||||
if visual and screen.is_composited():
|
||||
win.set_visual(visual)
|
||||
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)
|
||||
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
outer.set_border_width(18)
|
||||
outer.pack_start(title_bar(win), False, False, 0)
|
||||
self.win.add(outer)
|
||||
|
||||
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)
|
||||
columns[i % MAX_COLUMNS].pack_start(box, False, False, 0)
|
||||
entries.append((box, category["name"].lower(), rows))
|
||||
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))
|
||||
)
|
||||
|
||||
entry = search_bar(entries)
|
||||
outer.pack_start(entry, False, False, 0)
|
||||
self.win.connect("key-press-event", self._on_key_press)
|
||||
self.win.connect("destroy", lambda *_: Gtk.main_quit())
|
||||
|
||||
columns_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=28)
|
||||
for col in columns:
|
||||
columns_row.pack_start(col, False, False, 0)
|
||||
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")
|
||||
|
||||
scroller = Gtk.ScrolledWindow()
|
||||
scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
scroller.add(columns_row)
|
||||
outer.pack_start(scroller, True, True, 0)
|
||||
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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
win.add(outer)
|
||||
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
|
||||
|
||||
geo = win.get_screen().get_monitor_geometry(
|
||||
win.get_screen().get_primary_monitor()
|
||||
)
|
||||
win.set_default_size(min(1100, int(geo.width * 0.8)), min(800, int(geo.height * 0.8)))
|
||||
|
||||
win.connect("key-press-event", lambda w, e: _on_key_press(w, e, entry))
|
||||
win.connect("destroy", lambda *_: Gtk.main_quit())
|
||||
return win, entry
|
||||
|
||||
|
||||
def _on_key_press(widget, event, search_entry):
|
||||
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
|
||||
if ctrl and event.keyval in (Gdk.KEY_f, Gdk.KEY_F):
|
||||
search_entry.grab_focus()
|
||||
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 search_entry.get_text():
|
||||
search_entry.set_text("")
|
||||
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
|
||||
Gtk.main_quit()
|
||||
return False
|
||||
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):
|
||||
@@ -342,8 +526,6 @@ def main():
|
||||
|
||||
signal.signal(signal.SIGTERM, lambda *_: GLib.idle_add(cleanup))
|
||||
|
||||
categories = load_categories()
|
||||
|
||||
provider = Gtk.CssProvider()
|
||||
provider.load_from_data(CSS)
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
@@ -352,9 +534,8 @@ def main():
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
|
||||
win, search_entry = build_window(categories)
|
||||
win.show_all()
|
||||
search_entry.grab_focus()
|
||||
overlay = Overlay()
|
||||
overlay.show()
|
||||
Gtk.main()
|
||||
try:
|
||||
os.unlink(PID_FILE)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert cheatsheet.md into the JSON format the "Shortcuts" gnome-shell
|
||||
extension (Shortcuts@kyle.aims.ac.za) expects for its `shortcuts-file`
|
||||
setting: a list of {"name": <category>, "shortcuts": [{"key", "description"}, ...]}.
|
||||
"""Convert a cheatsheet .md file (cheatsheet.md, or a linked sub-sheet
|
||||
alongside it) into the JSON format cheatsheet_overlay renders: a list of
|
||||
{"name": <category>, "shortcuts": [{"key", "description"[, "link"]}, ...]}.
|
||||
(Originally written to feed the "Shortcuts" gnome-shell extension's
|
||||
shortcuts-file setting -- that extension is no longer installed, see
|
||||
startup_bernina_terminal's "Defensive cleanup" comment -- cheatsheet_overlay
|
||||
is now this JSON's only consumer, so its shape is free to extend.)
|
||||
|
||||
Usage: cheatsheet_to_shortcuts_json <input.md> <output.json>
|
||||
|
||||
@@ -12,12 +16,20 @@ since they aren't keyboard shortcuts. The category name is always the
|
||||
*nearest* preceding heading, so a "##" section containing "###" subsections
|
||||
each with their own table (Compose key) yields one category per "###", not
|
||||
one lumped-together "##" category.
|
||||
|
||||
A first-column cell written as a markdown link, e.g.
|
||||
`[Admin & Account Setup](cheatsheet_admin.md)` in a Command/Shortcut/Sheet
|
||||
table, becomes a navigable entry instead of a plain shortcut: cheatsheet_overlay
|
||||
shows it as a clickable link and, on click, loads and displays *that* file
|
||||
in place of the current one (path resolved relative to the linking file's
|
||||
own directory), rather than treating ".md" as a literal key combo.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
CELL_STRIP = re.compile(r"`|\*\*")
|
||||
MD_LINK = re.compile(r"^\[([^\]]+)\]\(([^)]+\.md)\)$")
|
||||
|
||||
|
||||
def clean(text):
|
||||
@@ -97,12 +109,18 @@ def build_shortcuts(header, rows):
|
||||
|
||||
# Shortcut | Action [| Runs] (app shortcuts, gTile, primary-controls)
|
||||
# Command | Description (the bin/ script index)
|
||||
if ncols >= 2 and header[0].lower().startswith(("shortcut", "command")):
|
||||
return [
|
||||
{"key": row[0], "description": row[1]}
|
||||
for row in rows
|
||||
if row[0]
|
||||
]
|
||||
# Sheet | Description (links to other cheat sheets)
|
||||
if ncols >= 2 and header[0].lower().startswith(("shortcut", "command", "sheet")):
|
||||
out = []
|
||||
for row in rows:
|
||||
if not row[0]:
|
||||
continue
|
||||
m = MD_LINK.match(row[0])
|
||||
if m:
|
||||
out.append({"key": m.group(1), "description": row[1], "link": m.group(2)})
|
||||
else:
|
||||
out.append({"key": row[0], "description": row[1]})
|
||||
return out
|
||||
|
||||
return []
|
||||
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ END_MARKER = "<!-- END SCRIPT INDEX -->"
|
||||
# this generator's own sibling infra (already covered in its own section
|
||||
# above in cheatsheet.md).
|
||||
EXCLUDE = {
|
||||
"cheatsheet.md", "anaconda_env.bak", "bpy-select_bak",
|
||||
"cheatsheet.md", "cheatsheet_admin.md", "anaconda_env.bak", "bpy-select_bak",
|
||||
"jungfrau_console_nox~", "me", "mosaic01.vlm",
|
||||
"bs_pvs_SLAAR21-CVME-LAS6991", "create_personal_dir.md", "gopass.md",
|
||||
"migrate_to_network_drive.md", "list_bin_commands",
|
||||
|
||||
Reference in new issue
Block a user