Maruzzella
Maruzzella is an original GTK4 desktop shell prototype built in Rust.
It focuses on the shell itself rather than on any specific product domain: custom tabbed workbench areas, side panels, bottom panels, split layouts, lightweight command handling, and persisted workspace state.
Current Status
Today the project is in an intermediate but coherent state:
- the GTK shell itself is working
- layout persistence is working
- the public crate API is usable by downstream apps
- the first plugin ABI/runtime slice is working for loading plugins, resolving dependencies, merging commands and menus, and dispatching plugin commands
- a built-in
maruzzella.baseplugin now provides coreAboutandPluginsshell commands/menu entries - plugin-backed GTK views can now be mounted into tabs
- the default app now boots into a coherent base-plugin-backed shell slice instead of placeholder-first
ProductSpectext tabs - plugin configuration persistence and plugin settings-summary surfaces are wired through the host
- plugin runtime services, host events, and discovery conventions are now available to downstreams
- the built-in base plugin now ships a real editor tab with untitled buffers, file-backed documents, native open/save-as flows, and draft restore for dirty buffers
- the host can now start in a dedicated launcher mode and switch between launcher and workspace shells at runtime
That means the plugin architecture is proven and the shell now exercises it in its default startup experience, but many shared shell contracts are still not formalized.
What It Does
- renders a multi-pane desktop shell with left, right, bottom, and central workbench regions
- supports a compact launcher shell distinct from the normal workspace shell
- uses custom tab groups instead of
GtkNotebook - supports tab activation, reordering, and workbench split previews
- persists tab arrangement and pane sizes to a local JSON layout file
- exposes shell commands for theme reload, command palette style actions, and editor buffer workflows
- exposes semantic appearance roles for panels, buttons, typography, inputs, and tab strips
- ships with a built-in workspace slice that can be replaced or extended by downstream products
Editor Tabs
The built-in maruzzella.base plugin now includes an editor workbench view.
New Bufferopens an untitled in-memory bufferOpen File In Editoraccepts an explicit path payload or uses the native file picker when invoked without oneSave Buffersaves file-backed documents in place and falls back toSave Asfor untitled buffersSave Buffer Asuses the native save dialog and converts the current tab into a file-backed document- dirty editor drafts are stored through the host-backed plugin config record and restored on restart
This is still a text editor slice, not a full document system: there is no syntax highlighting, multi-document search, or conflict resolution yet.
Project Structure
src/app.rs: application bootstrap, shell construction, pane persistence wiringsrc/product.rs: default branding, menus, toolbar actions, and starter layoutsrc/shell/: shell UI components including top bar, tabbed panels, and custom workbench tabssrc/layout.rs: load/save persisted shell statesrc/spec.rs: serializable shell, tab, menu, and workbench layout modelsrc/theme.rs: runtime theme loading and token renderingresources/style.css: default theme template
Run
Requirements:
- Rust toolchain
- GTK4 development libraries available on the system
Start the app with:
Run the example app with:
Run the semantic appearance demo with:
Run the plugin view demo with:
Public API
The intended integration surface is the crate root:
MaruzzellaConfig: application id, persistence namespace, and product definitionMaruzzellaConfig::with_startup_mode(...): choose launcher or workspace as the initial shell modeMaruzzellaConfig::with_launcher(...): provide a dedicated launcher shell specMaruzzellaConfig::with_launcher_window_policy(...): set launcher-specific default size/maximize behaviorMaruzzellaConfig::with_workspace_window_policy(...): override workspace window startup policybuild_application_with_handle(...): build the GTK application and keep a runtime handle for mode switchingMaruzzellaHandle::switch_to_workspace(...): replace launcher mode with a real workspace shell and optional project handleMaruzzellaHandle::switch_to_launcher(): return from a workspace to the configured launcher without quittingThemeSpec: configurable palette, typography, density, semantic appearance registry, and optional external stylesheet templateMaruzzellaConfig::with_plugin_path(...): register a dynamic plugin library to load at startupMaruzzellaConfig::with_plugin_dir(...): add a directory to plugin discoveryMaruzzellaConfig::without_default_plugin_discovery(): opt out of the built-in discovery conventionMaruzzellaConfig::with_theme(...): swap semantic appearances, typography, palette, sizing, or the whole stylesheetdefault_plugin_discovery_dirs(...): inspect the built-in plugin discovery conventiondiscover_plugin_paths_in_dir(...): enumerate loadable plugin artifacts in a directoryrun(config): launch a configured shellbuild_application(config): build a GTK application without running it yetProductSpecand related spec types: define branding, menus, toolbar actions, panels, and workbench layout
Minimal example:
use ;
Dynamic plugins can also be attached through config:
use MaruzzellaConfig;
let config = new
.with_plugin_path;
Launcher-mode startup:
use ApplicationExtManual;
use ;
let launcher = new;
let config = new
.with_startup_mode
.with_launcher;
let = build_application_with_handle;
// Later, when a project is opened:
// handle.switch_to_workspace(WorkspaceSession::new(product.shell_spec()))?;
app.run;
Discovery can also be directory-based. By default Maruzzella now scans:
$XDG_CONFIG_HOME/<persistence_id>/plugins./plugins
You can add more directories or opt out of the built-in convention:
use MaruzzellaConfig;
let config = new
.with_plugin_dir
.without_default_plugin_discovery;
Styling And Theming
The preferred downstream styling path is now semantic, not selector-driven.
ThemeSpec::default()gives the bundled Maruzzella lookThemeSpecexposes typed palette, typography, density, and semantic appearance registries- downstream apps can define or override named appearances such as
primary,secondary,workbench,console,toolbar,title, ordanger ShellSpec,TabGroupSpec,TabSpec, andToolbarItemSpecreference those appearances by stable idsThemeDensityalso controls window defaults and panel minimum sizes- typography is standardized through named text roles such as
title,body,meta,section-label,tab-label, andcode - buttons are standardized through named button roles such as
primary,secondary,ghost,toolbar,icon, anddanger ThemeSpec::with_stylesheet_path(...)points at an external GTK CSS template for full theme swappingThemeSpec::with_override(key, value)injects extra{{token}}values for custom templates- the stylesheet/template path remains an escape hatch for product-specific edge cases, not the normal downstream customization path
Minimal semantic appearance override:
use ;
let theme = default
.with_surface_appearance
.with_button_appearance;
let config = new.with_theme;
Minimal shell spec usage:
use ;
let mut product = default_product_spec;
product.layout.left_panel = product
.layout
.left_panel
.clone
.with_panel_appearance
.with_panel_header_appearance
.with_tab_strip_appearance;
for item in &mut product.toolbar_items
let config = new.with_product;
Minimal theme override:
use ;
let mut theme = default;
theme.typography.font_family = "\"IBM Plex Sans\", \"Cantarell\", sans-serif".to_string;
theme.typography.mono_font_family = "\"IBM Plex Mono\", monospace".to_string;
theme.palette.accent = "#d06b2f".to_string;
theme.density.radius_medium = 14;
theme.density.toolbar_height = 44;
theme.density.window_default_width = 1680;
theme.density.min_side_panel_width = 260;
let config = new.with_theme;
See docs/appearance-api.md for the semantic styling model and built-in appearance ids.
Full stylesheet swap:
use ;
let theme = default
.with_stylesheet_path
.with_override;
let config = new.with_theme;
The bundled Reload Theme command now reloads the active theme spec, including the external stylesheet file if one is configured.
Persistence
By default, Maruzzella stores its workspace layout at:
$XDG_CONFIG_HOME/maruzzella/layout.json
If XDG_CONFIG_HOME is not set, it falls back to:
$HOME/.config/maruzzella/layout.json
If you set MaruzzellaConfig::with_persistence_id(...), the directory name changes accordingly.
Launcher and workspace modes persist independent shell layouts so switching modes does not overwrite the other mode's layout.
Deleting the layout file resets the shell to the default layout supplied in the config's ProductSpec.
Extending It
The easiest way to evolve the shell is to change the default ProductSpec in src/product.rs:
- rename branding and window text
- add commands, menus, and toolbar items
- assign semantic appearances to panels, tab strips, buttons, and tab content
- replace placeholder tabs with real views
- reshape the central workbench tree with horizontal or vertical splits
The shell model in src/spec.rs is serializable, so layouts can be generated or persisted without coupling them to widget code.
Plugin Author Workflow
The repeatable plugin author path is now:
- Create a
cdylibcrate that depends onmaruzzella_sdk. - Export the plugin with
export_plugin!(YourPlugin). - Build the library for the current platform.
- Either point the host at the exact library with
with_plugin_path(...)or place it in one of the discovery directories.
The sample plugin in plugins/example_plugin demonstrates:
- command, menu, toolbar, settings, and view contributions
- host-owned config persistence
- service registration
- host event subscription
See docs/plugin-author-workflow.md for a concrete workflow.
Packaging Notes
Maruzzella currently loads raw platform-native plugin libraries.
- Linux plugins are expected as
.so - macOS plugins are expected as
.dylib - Windows plugins are expected as
.dll
The current recommended packaging convention is to install plugin libraries into a product-managed plugin directory and point Maruzzella at that directory with discovery, rather than relying on ad hoc working-directory copies in production.
Versioning Policy
maruzzella_api now follows a simple policy:
- additive changes that preserve
MZ_ABI_VERSION_V1keep the ABI version stable - any breaking C-ABI layout or semantic incompatibility requires a new ABI constant and corresponding host/plugin upgrade
maruzzella_sdkis expected to track the API crate closely and should be upgraded together in downstream plugin work
Design Notes
The first plugin ABI draft lives in docs/plugin-abi-rfc.md.
The workspace now also contains:
maruzzella_api: ABI-safe plugin boundary typesmaruzzella_sdk: ergonomic Rust helpers and export macro for plugin authorsmaruzzella.base: built-in plugin providing core shell commands and menu contributionsplugins/example_plugin: samplecdylibplugin using the SDK
On the host side, maruzzella now exposes the first loading and activation primitives:
load_plugin(path): open a dynamic library and decode its descriptorresolve_load_order(&plugins): dependency-aware orderingPluginRuntime::activate(plugins): invoke pluginregisterandstartupagainst a host API and collect contributions
Plugin commands are now executable, not just declarative metadata: a plugin can register a command together with an ABI-safe handler function, and Maruzzella will dispatch GTK menu actions back into that plugin.
What is still rough:
- some contribution surfaces are still intentionally small
- packaging beyond raw platform libraries is not standardized
- the SDK can still be tightened further as more third-party plugins appear