Omahub
← All plugins
B

Workspace Presets

by Blake Starling

Save exact workspace layouts and launch multi-workspace preset groups.

Security review

Potentially dangerous behavior detected · 10 findings

Deterministic scan — not a security guarantee

High
Risk level
High
Analyzed commit
41691f1
Scanned
1 month ago

Automated analysis only — not a security guarantee.

AI advisory review

Review recommended

Language-model assessment · ~deepseek/deepseek-v4-flash-latest — advisory only

Low
AI risk level
Low
Recommendation
review
Model
~deepseek/deepseek-v4-flash-latest
Analyzed commit
41691f1
Reviewed
1 month ago

The plugin is a well-engineered Hyprland workspace manager with extensive security hardening: it validates and quotes all data that reaches Hyprland's Lua evaluator, uses atomic file writes with restrictive permissions, and has a comprehensive test suite covering injection and symlink attacks. The deterministic scan's high risk is driven by benign uses of eval() to send controlled Lua to Hyprland, CI-only sudo/apt operations, and test-file escape sequences, none of which affect runtime safety. The only item warranting a closer look is a `systemd-run` reference in desktop.py, which is not clearly exercised by the sampled code and may be a fallback or dead path.

  • The `eval()` calls in hyprland.py are used to send validated Lua to Hyprland's IPC; they are not arbitrary code execution from untrusted input, but a human should confirm the quoting and validation are airtight.
  • The `systemd-run` reference in desktop.py (line 215) is flagged as persistence; the plugin's startup launch is handled by the service itself, so this may be a fallback or unused code path that should be verified.
  • The CI workflow uses `sudo apt-get install` and runs with elevated privileges, but this is only for the project's CI environment, not for plugin installation or runtime.
  • The obfuscation findings are all in test files (escape sequences for testing quoting) and are not part of the executable plugin code.
How this check works

This review combines the deterministic scan (the rule-based results above) with an independent look at the plugin's code by a language model. The model reads a trimmed sample of the repository's files, the manifest, and the README, then gives a plain-language risk level and a recommendation: install (no notable danger), review (look closer first), or avoid (clearly dangerous).

It runs on the same analyzed commit as the deterministic scan and is strictly advisory — it is not a security guarantee and never blocks a plugin by itself. A human moderator still reviews plugins before they are listed.

AI advisory only — automated analysis, not a security guarantee.

Install
$ omarchy plugin add https://github.com/blakestarling/omarchy-workspace-presets --enable
Desktop #Hyprland #launcher #workspaces

Workspace Presets for Omarchy

Save the application windows on a Hyprland workspace as a named preset, then cold-load that preset later. Combine presets into groups assigned to numbered workspaces and launch a complete multi-workspace setup in one action—or automatically once when the Hyprland session starts. Workspace Presets launches missing applications, tracks the new windows, and rebuilds the saved layout instead of assuming the windows are already open.

https://github.com/user-attachments/assets/23173b60-d237-480d-9cf8-21d0f34824c1

This is a native Omarchy Quattro plugin: the bar widget and management panel run in omarchy-shell, while a bundled Python standard-library backend handles capture, validation, and restore orchestration. The backend talks to Hyprland over its IPC socket directly and runs as a single short-lived worker that exits after two idle minutes, so the panel opens without waiting on process startup.

What it restores

  • Application window count and identity
  • Dwindle trees and split ratios
  • Master orientation, master/stack membership, and master factor
  • Scrolling columns, column membership, column widths, row sizes, and tape position
  • Monocle ordering and final focus
  • Floating/tiled state and floating geometry
  • Window groups, member order, active member, and lock state
  • Fullscreen/maximized, pinning, and static tags
  • Duplicate windows with the same class, tracked as independent slots
  • Omarchy shell panels discovered from their plugin manifests
  • Explicit terminal programs and their working directories, including Herdr and Omarchy's Docker View

Limitations

Application-owned state is outside the compositor's control and is not restored. That includes browser tabs, open documents, unsaved editor buffers, and the internal state of terminal programs. When a supported terminal was launched with an explicit program, the plugin saves and reruns that outer terminal command; the program itself remains responsible for restoring its session.

That terminal command is saved verbatim, so a program you launched with a credential on its command line is written into presets.json and rerun on every load. The file is created with mode 0600 in a 0700 directory, but if you would rather not store a particular command, set that window's launcher manually under Set up instead.

Requirements

  • Omarchy 4.0 or newer
  • Hyprland 0.56 or newer
  • Python 3
  • uwsm-app and gtk-launch (included in a normal Omarchy installation)

The panel reports a clear compatibility error instead of attempting a partial restore when these requirements are not met. Version 1 supports normal workspaces using Hyprland's built-in dwindle, master, scrolling, or monocle layouts.

Install

omarchy plugin add https://github.com/blakestarling/omarchy-workspace-presets.git --enable

The plugin appears in the built-in bar. Left-click its workspace icon to open the preset manager; middle-click refreshes the list.

Optional keyboard shortcut

SUPER + ALT + P is easy to remember as “Presets” and is unused by Omarchy's default bindings as of Omarchy 4.0. Add this line to ~/.config/hypr/bindings.lua:

o.bind("SUPER + ALT + P", "Workspace Presets", "omarchy-shell shell toggle blakestarling.workspace-presets")

Hyprland normally reloads the file automatically. You can apply and validate it explicitly with:

hyprctl reload
hyprctl configerrors

If you have added personal bindings, check omarchy menu keybindings --print first. If the shortcut is already used, choose another key combination or call hl.unbind("SUPER + ALT + P") before the new o.bind and intentionally replace the old action.

Update

omarchy plugin update blakestarling.workspace-presets

Disable

omarchy plugin disable blakestarling.workspace-presets

Remove

omarchy plugin remove blakestarling.workspace-presets

Removing the plugin intentionally leaves presets in place. To delete that data too, first remove the plugin, then run:

rm -- ~/.config/omarchy-workspace-presets/presets.json ~/.config/omarchy-workspace-presets/presets.lock

If XDG_CONFIG_HOME is set, the data directory is $XDG_CONFIG_HOME/omarchy-workspace-presets instead.

Use

Save

  1. Arrange the current workspace.
  2. Open Workspace Presets from the bar.
  3. Enter a unique name and choose Save.
  4. If every window maps unambiguously to an installed desktop entry, the preset is immediately loadable.
  5. Otherwise, choose Set up and select a suggested desktop entry, enter a .desktop ID, or provide a custom argv JSON array such as ["foot"].

A preset that needs launcher setup is saved as an explicit draft. It cannot be loaded until every window has a launch recipe. Installed Omarchy panel plugins are matched by their manifest name and relaunched through omarchy-shell; existing drafts are rechecked automatically when the service starts.

Omarchy web apps are matched from the URL in their desktop entry and Chrome's URL-derived window class. This supports entries with human-readable filenames such as Google Messages.desktop and WhatsApp.desktop without mistaking them for an ordinary browser window. Existing drafts are repaired automatically when a unique web-app match becomes available.

Foreground programs are detected automatically in Foot, Alacritty, Kitty, Ghostty, and WezTerm, whether the program was supplied when the terminal opened or started manually from its shell. The plugin reads the terminal's controlling TTY and foreground process group, then preserves that program's exact argv and working directory. For example, Herdr is saved as a terminal invocation ending in -e herdr, while Docker View retains -e omarchy-launch-docker-tui when that wrapper is still present in the running process tree.

This detection deliberately does not infer commands from shell history. Idle shells restore as normal terminals, while pipelines, ambiguous process trees, SSH sessions, and tmux or zellij sessions fall back to the terminal's normal launcher instead of saving a misleading partial command. Captured commands recreate the program but cannot preserve unsaved in-memory application state. Overwrite presets captured by an older plugin version to replace their generic terminal launchers with the richer recipe.

Load

  1. Choose Load on a ready preset.
  2. If the current workspace is empty, loading starts immediately after validation. Matching windows on other workspaces are left untouched and new instances are launched.
  3. Otherwise, the main panel closes and a focused confirmation panel shows how many current windows will close. If matching windows exist on other workspaces, you can choose whether to launch new instances or move those existing windows. Nothing is moved silently.
  4. Confirm the replacement.

Press Escape in either panel to close it. Closing the confirmation panel cancels the pending load.

The panel closes automatically when a confirmed preset or preset-group load begins, leaving the workspace unobstructed while applications launch.

Workspace Presets validates all launchers before closing anything. It then sends normal close requests to current-workspace applications and waits. If an application refuses to close—for example, because it is showing an unsaved-changes dialog—the restore stops and never force-kills it.

After the workspace is clear, the backend launches saved applications through uwsm-app concurrently, routes them silently without stealing focus, and tracks each newly created matching Hyprland stable ID. A match remains provisional through a short quiet period, and an updater, splash, or class-only surface remains provisional for the full application-launch deadline: if it closes, is replaced, or changes its surface identity, the backend binds the slot to the settled application window instead. Once every window is settled, only the workspace currently being finalized is temporarily removed from the tiled layout; the saved layout is then rebuilt deterministically with explicit saved-order anchors. This avoids attaching a forced-floating launch rule to the window, prevents another group workspace from being stranded in temporary floating state, and keeps compositor arrival order out of the final layout. Windows with duplicate or overlapping classes launch in separate waves so they cannot be assigned to the wrong slot. Groups and compositor state are restored last. A launch timeout is reported as a failure, never as a successful partial restore.

Manage

  • Rename changes the display name while preserving the preset's stable ID.
  • Overwrite captures the current workspace into the selected preset after confirmation.
  • Delete removes one preset after confirmation.
  • Refresh reloads preset data from disk.

Preset names are trimmed, non-empty, and case-insensitively unique.

Preset groups

  1. Under Preset groups, enter a unique group name and choose Create group.
  2. For each preset you want in the group, enter a number-row workspace key from 0 through 9 and choose Assign. As in Omarchy's default bindings, 0 targets workspace 10. A group allows one preset per workspace and one assignment per preset. Unsaved workspace edits remain in place while other assignments or group settings are updated.
  3. Choose Launch group. The plugin validates every preset, launcher, and target workspace before it changes anything.
  4. If all target workspaces are empty, launch begins immediately. Otherwise, one confirmation shows the total windows that will receive normal close requests.

Group loads launch new application instances instead of moving matches from unrelated workspaces. All target workspaces are cleared first, then unrelated applications for every preset start concurrently and are routed directly to their assigned workspace with a silent one-shot Hyprland rule. Exact layout reconstruction still requires a brief final pass over each target workspace because Hyprland's layout dispatcher operates on the active workspace. Focus returns to the workspace and window that were active when the group launch began. If a group or any target workspace changes after confirmation, the operation stops before closing anything.

Groups can be renamed, reassigned, and deleted without deleting their presets. A preset cannot be deleted while a group references it; remove that assignment first.

Launch a group on startup

Choose Launch on startup on a complete group. Only one group can hold this setting, so enabling another transfers it. The plugin runs the selected group once when its service first starts in a new Hyprland session. A session-scoped guard prevents an omarchy-shell reload or plugin rescan from launching the group again. Enabling the setting does not immediately launch the group; it takes effect on the next Hyprland session.

Enable Confirm before startup launch on the selected startup group if you do not want it to launch unconditionally. At the next login, the plugin opens the same focused confirmation panel used by manual loads and waits for Launch group. Choosing Skip this session, pressing Escape, or closing the panel skips the group for that session; shell reloads will not prompt again until the next login.

Startup restore is intentionally equivalent to a confirmed group launch: assigned workspaces are replaced with normal close requests and applications are never force-killed. If a launcher or preset becomes invalid, startup restore reports the error rather than partially skipping it.

The startup launch runs ahead of the work that only fills the panel, and waits for monitor geometry to stop changing first, because saved geometry is normalized against the work area the bar reserves. Applications started at login are given 30 seconds to appear rather than the 12 seconds a manual load allows. If the session was not ready to be checked at all, the once-per-session guard is released so the next shell start can try again; once windows may have been closed or applications launched, it is not.

Optional shell IPC

The widget exposes the standard Omarchy shell panel actions:

omarchy-shell shell toggle blakestarling.workspace-presets
omarchy-shell blakestarling.workspace-presets refresh
omarchy-shell blakestarling.workspace-presets save "Coding"

Starting a load over IPC still opens the panel for destructive confirmation:

omarchy-shell blakestarling.workspace-presets load PRESET_UUID

Group loads follow the same preflight and confirmation flow:

omarchy-shell blakestarling.workspace-presets loadGroup GROUP_UUID

Data and security

Presets, preset groups, assignments, the startup selection, and its confirmation preference are stored as schema-versioned JSON at:

${XDG_CONFIG_HOME:-~/.config}/omarchy-workspace-presets/presets.json

Writes use an advisory lock, a same-directory temporary file, fsync, and atomic replacement. The data and lock files are mode 0600.

Desktop launchers store only the desktop entry ID, so application updates can change their underlying Exec line without making the preset stale. Custom launchers are stored as argv arrays and are never evaluated through a shell. Explicit terminal commands and their working directories are captured automatically; this can include command-line arguments, so avoid putting secrets directly in terminal command arguments. Only configure commands you trust: they run as your user when that preset loads.

Troubleshooting

Check compatibility and inspect saved presets without loading anything:

python3 -B ~/.config/omarchy/plugins/blakestarling.workspace-presets/backend/main.py capabilities
python3 -B ~/.config/omarchy/plugins/blakestarling.workspace-presets/backend/main.py list

Validate the installed manifest:

omarchy plugin validate ~/.config/omarchy/plugins/blakestarling.workspace-presets

If the widget does not appear after enabling it:

omarchy-shell shell rescanPlugins
omarchy bar move blakestarling.workspace-presets --section left

Common restore failures:

  • Active workspace changed: return to the workspace named in the confirmation and start the load again. Nothing is closed when this guard trips.
  • Desktop entry no longer exists: open Set up and select the replacement entry.
  • No new window appeared: the app may be single-instance or need a custom --new-window command. Retry and choose Move existing, or configure a custom argv launcher.
  • Application did not close: respond to its save/discard dialog, then load again.
  • Startup group did not run after a shell reload: this is intentional; startup groups run at most once per Hyprland session. Log out and back in to test the next-session behavior. A startup launch that failed before checking anything is the exception, and is retried on the next shell start.
  • Preset is used by a group: remove that preset's group assignment before deleting it.
  • Unsupported layout or special workspace: switch the active normal workspace to one of the four supported built-in layouts before saving/loading.
  • Floating window moved after a monitor change: exact pixels are used only when work-area size and scale match; otherwise geometry is normalized and clamped to the current monitor.

Hyprland 0.56 does not expose pseudotile state through read-only window IPC. Workspace Presets therefore leaves live windows untouched during capture and records pseudotile as disabled. This avoids changing the workspace merely by saving it; pseudotile capture can be added when Hyprland exposes that state safely.

Development

Clone and validate:

git clone https://github.com/blakestarling/omarchy-workspace-presets.git
cd omarchy-workspace-presets
omarchy plugin validate .
PYTHONPATH=backend python3 -m unittest discover -s tests -v
python3 -m compileall -q backend

The backend's stdout is a newline-delimited JSON protocol. Commands emit progress, result, or structured error objects so the long-lived QML service never has to infer success from human-readable text.

Each subcommand also runs standalone, which is what the examples above and the tests use. main.py serve is the form the shell uses: it reads one {"id": ..., "args": [...]} request per line on stdin and tags every event it emits with that id.

See CONTRIBUTING.md for the live-workspace test matrix and release checklist.

License

MIT © Blake Starling. See LICENSE.