Guide · Panelith 0.1.0 · plain HTML edition; the interactive edition has search and live examples
Commands, hotkeys and the launcher
On this page 6 sections
The bar at the top of the shell is the one way to reach everything. Press alt+shift+. or click it, type a sigil, and search. The default sigil > lists commands, fuzzily matched on title and keywords, with the hotkey shown beside each. A list source can claim its own sigil (these docs use ? for the search). The app can change the command sigil with command_sigil, and TopBar(trigger_sigil=...) picks which sigil the bar opens with when clicked.
Everything in the bar comes from the manifest: the shell's own pane commands, the commands the app declares, and one generated opener per launcher kind. Choosing an entry either runs a client-side built-in or posts to /api/commands/<id> with the current client context, and the Python side returns actions.
Writing a command#
class Greet(Command):
id = "hello.greet"
title = "Hello: Say Hi"
keywords = "hello greet toast"
hotkey = "g"
def run(self, ctx):
focused = ctx.focused.kind if ctx.focused else "nothing"
return a.notify(f"Hi from Python. Focused: {focused}", "ok")
run(ctx) gets the same context object a pane handler gets: the focused tab, every open tab, the server's copy of any pane's state, and ctx.args for anything the caller attached. It returns actions. A command that needs to change a pane sends it an event rather than reaching into its state, so the pane's own handler stays the one place that logic lives:
class RefreshSystem(Command):
id = "sysinfo.refresh"
title = "System: Refresh"
keywords = "system refresh reload time"
scope = "kind:sysinfo"
hotkey = "u"
def run(self, ctx):
return a.send_event(ctx.cid, "refresh")
Titles follow a Noun: Verb convention so the bar groups well, but nothing enforces it.
Scope#
scope controls when a command is offered. "global" (the default) is always available. "kind:<kind>" is listed, and its hotkey is live, only while a pane of that kind is focused. System: Refresh above is scoped to the sysinfo kind, so alt+shift+u does nothing anywhere else and does not show in the bar there either. Inside a scoped command ctx.cid is the focused pane, which is why the handler can send to it directly.
Hotkeys#
Every hotkey in a Panelith app lives under one pair of modifiers, alt+shift by default, set by hotkey_modifiers on the app. A command's hotkey is just the key: "g", "\\", "left", or "ctrl+]" for the few that add control. Writing the app's own modifiers in the spec is accepted and ignored, so "alt+shift+g" means "g". The fixed namespace never collides with the browser, it is learnable, and the help overlay can list everything on one screen. The reference has every key name a hotkey= accepts.
The shell dispatches hotkeys itself. A built-in runs in the browser; an app command posts to the server. Scoped hotkeys check the focused pane's kind first.
The built-in commands#
The shell ships the pane commands from Math, You. They run client-side and need no Python, but they appear in the bar and the help overlay like any other command, and run_builtin("pane.maximize") lets a handler trigger one. The reference lists them with their default keys: split (\ and -), close (w), collapse (c), maximize (f), focus (arrows), move content (ctrl+[ / ctrl+]), cycle tabs ([ / ]), rename (r), the bar (.), the help (h), and the theme toggle (no key by default).
Two app attributes adjust the set. builtin_hotkeys is a dict of id to key that re-maps or, with None, disables a hotkey; these docs give app.theme the key t. builtin_commands replaces the list, which is how the stand-alone Boggle app keeps only the bar and the help: a one-game app has no use for splitting.
from panelith.app import BUILTIN_PANE_COMMANDS
class Boggle(App):
builtin_commands = [b for b in BUILTIN_PANE_COMMANDS if b["id"] in ("app.commandBar", "app.help")]
builtin_hotkeys = {"app.theme": "t"}
The launcher#
An empty pane shows the launcher: a grid of tiles, one per kind that has launcher = True, plus any command that sets launcher = True with its own launcher_label, launcher_glyph, launcher_color and launcher_tip, and a browse row with the command bar and every list that has a sigil. Each kind's tile is its generated <kind>.new opener, so the tile, the bar entry and open_pane(kind) are the same thing. An app that wants its own opener declares a command with the id <kind>.new and the generated one is skipped.
class Reference(Command):
id = "docs.reference"
title = "Docs: Reference"
hotkey = "r"
launcher = True
launcher_label = "Reference"
launcher_glyph = "list"
launcher_color = "#2dd4bf"
launcher_tip = "Every class, action, block and widget, generated from the source"
The canvas can also skip the launcher at start. Canvas(initial="kind", initial_kind="page") opens that kind in the first pane (what these docs do); initial="empty" leaves the pane empty; the default is the launcher. max_columns and max_rows cap the grid.
Canvas(max_columns=3, max_rows=2, initial="kind", initial_kind="note")
Canvas(initial="empty")
Canvas(bottom=True) allows one more place: a full-width pane across the bottom of every column, for a status strip, a log or a chart that should span the dashboard. It is off by default. When it is on, the pane.splitBottom built-in (give it a key with builtin_hotkeys) opens the band; it holds one pane, resizes against the columns above it with its own divider, takes dropped tabs like any pane, and goes away when its last tab is closed. Saved layouts keep it.
class CanvasBottom(App):
canvas = Canvas(max_columns=2, max_rows=2, bottom=True)
builtin_hotkeys = {"pane.splitBottom": "b"}
The top bar and the brand#
TopBar(buttons=[...], placeholder=..., trigger_sigil=...) sets the right-hand buttons ("hotkeys" for the help overlay, "commands" to open the bar, "list:<id>" to toggle a list's drawer), the text in the idle bar, and the sigil the bar opens with when clicked.
The brand in the top-left corner is a Brand. Brand.text("SUPER BOGGLE", colors=["#f5c242", "#ff7a59"]) renders a monoline wordmark with one colour per word; Brand.svg(markup), Brand.svg_file(path) and Brand.image(url) take a mark of your own. On a phone the top bar becomes a single row and the brand shows its short form: short="SB" if given, otherwise the initials of a text brand in the same colours. The hotkey hint beside the bar hides under 760px.
class Docs(App):
brand = Brand.text("PANELITH DOCS", colors=["#4f8cff", "#e8a33d"], short="PD")
topbar = TopBar(buttons=["list:contents", "commands", "hotkeys"], placeholder="Search the docs…", trigger_sigil="?")
brand = Brand.svg(markup, height=28, alt="an SVG mark", short="SV")
brand = Brand.image(src, height=28, alt="an image mark", short="IM")
command_sigil = ":" # the bar lists commands after a colon; a list claims /