Workbench#

A two-tier application scaffold: a VS Code-style icon rail of workspaces down the far left, each revealing its own navigation sidebar and content area, plus an optional toolbar stack and status band. Use it when an app has several distinct sections — Mail, Calendar, Contacts — each with its own navigation. For a single sidebar with no rail, use AppShell instead.

Workbench demo — light theme Workbench demo — dark theme

Usage#

Think of a Workbench as several AppShell sidebars behind one rail: you add each section as a workspace with its own navigation and content, and the rail switches between them.

Workspaces#

Add a workspace with add_workspace(key, text=, icon=). Each returns a Workspace — a sidebar host authored with the exact same front doors as a single-tier AppShell: page_nav() / list_nav() / tree_nav() / custom_nav(). The rail appears once there is more than one workspace, and each workspace can use a different provider.

with bs.Workbench(title="Console", size=(960, 600)) as shell:
    # An authored page nav.
    with shell.add_workspace("acquire", text="Acquire", icon="cpu") as ws:
        with ws.page_nav() as nav:
            with nav.add_page("sensors", text="Sensors", icon="thermometer-half", padding=20):
                bs.Label("Sensors", font="heading-lg")
            nav.add_header("Hardware")
            with nav.add_page("ports", text="Ports", icon="usb-symbol", padding=20):
                bs.Label("Ports", font="heading-lg")

    # A data-bound master–detail list.
    with shell.add_workspace("devices", text="Devices", icon="hdd-stack") as ws:
        ws.list_nav(devices)

        @ws.detail
        def show(record):
            bs.Label(record["title"], font="heading-lg")

    shell.navigate("acquire", "sensors")   # workspace first, then page
shell.run()

Rail labels#

By default the rail shows icons only. Pass rail_labels=True to caption each icon (this widens the rail); rail_width sets the width explicitly.

bs.Workbench(rail_labels=True)

Toolbars, status bar, and configuration#

A Workbench shares the rest of its surface with AppShelladd_toolbar(), shell.statusbar, the flat configuration kwargs and live shell.* properties, from_store(), and the window options all work identically. See the AppShell guide for those.

Styling#

Kwarg

Default

Region

rail_surface

'chrome'

The workspace rail.

sidebar_surface

'raised'

The per-workspace navigation sidebar.

statusbar_surface

'chrome'

The bottom status band.

nav_accent colors the rail indicator bar and the sidebar selection wash (None keeps it neutral). Under-rail sidebars always use the subtle wash — the filled solid selection is reserved for the standalone AppShell page nav.

Events#

In addition to the shell events (on_page_change / on_sidebar_toggle / on_sidebar_mode_change), a Workbench fires on_workspace_change when the rail switches workspace.

Shorthand

Handler receives

on_page_change

PageChangeEvent

on_workspace_change

WorkspaceChangeEvent

on_sidebar_toggle

PaneToggleEvent

on_sidebar_mode_change

DisplayModeEvent

shell.on_workspace_change(lambda e: print("workspace:", e.workspace))

See also#

AppShell — the single-tier shell: one navigation sidebar, no rail.

PageStack — page navigation without a built-in sidebar.

API#

The complete reference for Workbench lives on the Application API page. At a glance:

Workbench

Two-tier application window: a workspace rail plus per-workspace sidebars.

Full Example#

 1from bootstack.data import MemoryDataSource
 2
 3inbox = MemoryDataSource().load([
 4    {"id": 1, "title": "Dana Reyes", "text": "Q3 roadmap review", "icon": "envelope", "body": "Can we move it to Thursday?"},
 5    {"id": 2, "title": "Billing", "text": "Your receipt for June", "icon": "envelope-open", "body": "Thanks for your payment."},
 6])
 7contacts = MemoryDataSource().load([
 8    {"id": 1, "title": "Dana Reyes", "text": "dana@acme.io", "icon": "person-circle", "phone": "555-0142"},
 9    {"id": 2, "title": "Sam Okonkwo", "text": "sam@acme.io", "icon": "person-circle", "phone": "555-0177"},
10])
11
12with bs.Workbench(title="Workspace", size=(980, 620)) as shell:
13    with shell.add_toolbar() as bar:
14        with bar.add_menu("File") as file:
15            file.add_action("New", shortcut="Mod+N", on_click=lambda: None)
16            file.add_action("Open", shortcut="Mod+O", on_click=lambda: None)
17            file.add_divider()
18            file.add_action("Quit", shortcut="Mod+Q", on_click=shell.close)
19        with bar.add_menu("View") as view:
20            view.add_action("Refresh", shortcut="Mod+R", on_click=lambda: None)
21        bar.add_spacer()
22        bar.add_button(icon="search", on_click=lambda: None)
23        bar.add_theme_toggle()
24
25    # Mail — a master–detail list.
26    with shell.add_workspace("mail", text="Mail", icon="envelope") as ws:
27        ws.list_nav(inbox, chevron=True)
28
29        @ws.detail
30        def read(message):
31            with bs.Column(horizontal_items="left", gap=12, padding=(16, 10)):
32                bs.Label(message["text"], font="heading-lg")
33                bs.Label(f"From {message['title']}", font="caption")
34                bs.Divider()
35                bs.Label(message["body"])
36
37    # Calendar — a page nav (authored pages).
38    with shell.add_workspace("calendar", text="Calendar", icon="calendar3") as ws:
39        with ws.page_nav() as nav:
40            with nav.add_page("today", text="Today", icon="calendar-day", padding=20, gap=8):
41                bs.Label("Today", font="heading-lg")
42                bs.Label("No events.")
43            with nav.add_page("week", text="Week", icon="calendar-week", padding=20, gap=8):
44                bs.Label("This week", font="heading-lg")
45
46    # Contacts — another list.
47    with shell.add_workspace("contacts", text="Contacts", icon="people") as ws:
48        ws.list_nav(contacts, chevron=True)
49
50        @ws.detail
51        def card(person):
52            with bs.Column(grow=True, horizontal="stretch", gap=12, padding=20):
53                bs.Label(person["title"], font="heading-lg")
54                bs.Label(person["text"], font="caption")
55                bs.Label(f"Phone: {person['phone']}")
56
57    with shell.add_workspace("settings", text="Settings", icon="gear", pin_to_footer=True) as ws:
58        with ws.page_nav() as nav:
59            with nav.add_page("general", text="General", icon="sliders", padding=20, gap=8):
60                bs.Label("Settings", font="heading-lg")
61
62    # Mail is the first workspace, so it opens active with its first message
63    # selected — no explicit navigate needed.
64
65shell.run()