diff --git a/cheatsheet.md b/cheatsheet.md index 16e0bae..1cdd3e5 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -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 | diff --git a/cheatsheet_admin.md b/cheatsheet_admin.md new file mode 100644 index 0000000..4fa1e36 --- /dev/null +++ b/cheatsheet_admin.md @@ -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/` | +| `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. diff --git a/cheatsheet_overlay b/cheatsheet_overlay index 38bbb7f..b45c781 100755 --- a/cheatsheet_overlay +++ b/cheatsheet_overlay @@ -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) diff --git a/cheatsheet_to_shortcuts_json b/cheatsheet_to_shortcuts_json index f4dff96..22d46cd 100755 --- a/cheatsheet_to_shortcuts_json +++ b/cheatsheet_to_shortcuts_json @@ -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": , "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": , "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 @@ -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 [] diff --git a/list_bin_commands b/list_bin_commands index 82a24d9..4a6d702 100755 --- a/list_bin_commands +++ b/list_bin_commands @@ -54,7 +54,7 @@ END_MARKER = "" # 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",