Skip to content
hyperupcall-projectsPublic

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

sidepanel

A small, low-latency side-panel launcher for KDE Plasma on Wayland.

Press a global shortcut (default Meta+F) and a frame of four edge panels and four corner boxes appears around the screen. The desktop behind them dims slightly; the center and gaps let pointer clicks reach the desktop. Press it again to dismiss. The first website is highlighted on a fresh session; reopening preserves your selection and panel state. Use Alt+J to move down, Alt+K to move up, and Enter to open the highlighted website. Navigation stops at the first and last visible entries; holding Alt+J or Alt+K repeats the movement.

The panel starts with a Websites section in the left rectangle. Each of the four rectangles can contain sections of website links, with a favicon on each entry. Clicking a row opens the site in a separate browser window that is pre-warmed at startup — created hidden on about:blank, so the window appears immediately on the first click while loading nothing until then.

Inside that window, links to the current hostname stay in sidepanel. Links to other hostnames open through xdg-open in your default desktop browser (for example Brave or Chrome), leaving the sidepanel page open. This also applies to links requesting a new tab or window. Subdomains count as different hostnames; automatic redirects and sign-in flows continue normally.

Click the gear in the bottom-left corner box to open settings just above and to the right of that box. Click the box again or the settings close button to dismiss it. Choose Contents, then Left, Right, Top, or Bottom to customize that rectangle. Add or remove sections, edit a section name and click Rename, and add or remove websites in each section. Each website has separate Title and URL fields; click Save website (or Add website for a new entry) to apply them. A blank title uses the URL's host. URLs must start with http:// or https://. Changes appear immediately and are saved to your config. Corner-box contents stay fixed.

The editor supports Tab/Shift+Tab, arrow keys, Home/End, Ctrl+A, and Enter to save the active form. Ctrl+V pastes text when wl-paste from the optional wl-clipboard package is installed. Section and website lists have page controls, and rectangle rails show arrow buttons when their content overflows. Top and bottom rectangles lay out links horizontally; side rectangles lay them out vertically.

Settings has independent Left width, Right width, Top height, and Bottom height controls. Each corner box takes its width from the adjacent left/right panel and its height from the adjacent top/bottom panel. For example, a left width of 64 and top height of 48 give a 64×48 top-left box. Matching widths and heights make square boxes. Edge panels stop before the corner boxes, so none of the eight pieces overlap.

One Margin value controls the gap between every neighboring piece and the inset from the screen edges. Defaults are 64 px for all four edge thicknesses (giving square corner boxes) and a shared 12 px margin. Slim left panels show centered website favicons; wider panels also show site labels. Left/right widths range from 48–800 px and top/bottom heights from 40–400 px. When the screen is too small, dimensions are fitted proportionally; saved preferences remain unchanged. The settings card also scales down to stay on screen.

The existing Light, Dark, and System appearance choices, corner radius, opacity, browser window size, and shortcut recorder are retained. All changes apply immediately and persist to the config file. Margin (0–80 px), corner radius (0–48 px), and opacity (20–100%) apply to every frame piece and the settings card. At 100% opacity the card interiors are fully opaque. Desktop dimming adjusts the black backdrop independently, from 0% (off) to 100% (black) in 5% steps, defaulting to 20%. It updates and saves immediately, covers the center, gaps, and rounded corners, and disappears with the frame. Opaque panels retain their normal appearance, and the dimmed gaps remain clickable. The launcher’s browser windows stay above the dimming, including after moves, resizes, and reopening. Panels and settings stay above the browser. System mode follows live desktop changes through the XDG Settings portal, using light mode when no preference is available.

Panel opening in Settings offers Desktop Default (the default, leaving animation to KWin), Fade In, and Instant. Fade In changes opacity at the final size, with a Fade duration control from 0–5000 ms in 25 ms steps (default 150 ms; 0 is immediate). The config accepts any whole millisecond value in that range. Changes are saved immediately and apply the next time the frame or settings card opens. The backdrop shares the frame's fade. The separate browser window retains the desktop's normal animation.

Pressing the configured shortcut (for example Meta+H) hides the frame, settings, and browser together. Press it again to restore what was open. The same live browser page stays loaded, including navigation, scroll position, unsaved form input, and JavaScript state. Closing the browser with its close button leaves it closed when the panels reopen. Section expansion, panel scrolling, keyboard selection, the open Settings tab, and editor drafts are remembered too.

Settings → Appearance → Persist Changes to Panels controls when remembered session data is cleared. Its default is 0 (Always persist). The choices are 1m, 5m, 10m, 15m, 30m, 1hr, 2hr, 4hr, User login/logout, and Computer restart. Timed choices expire after the panels have stayed hidden for that duration; reopening before the deadline starts a new hidden interval on the next dismissal. Login/logout uses the real logind graphical-session identity: screen locking, unlocking, and suspend do not reset it. Computer restart uses the kernel boot ID. Expired data is discarded before restoration, and timed sessions are also cleared in the background while the app runs. Bookmarks, panel sizes, appearance, and other preferences remain saved.

Session snapshots and the private browser profile live under $XDG_STATE_HOME/sidepanel/ (normally ~/.local/state/sidepanel/), separately from config.toml. App restarts restore the current URL, WebKit history, window size/position, and panel state on the next shortcut press. Live page memory is preserved across hide/show; after a process or computer restart the page must load again, and recovery of page-managed drafts depends on the site. The browser profile retains cookies and site storage until the chosen reset boundary. On expiration the old browser process is stopped and its profile removed.

Window placement is recorded and restored through a temporary, browser-PID-scoped KWin script, since Wayland clients cannot choose their own global positions. It is unloaded with the helper and does not install window rules. If KWin scripting is unavailable, page/panel persistence still works, with placement left to the compositor. Windows whose monitor has disappeared are brought back onto a current output.

Why this design

On Wayland, clients cannot grab global keys — the compositor owns the keyboard. So the app uses KDE's own broker, KGlobalAccel:

Meta+F ──► KWin (owns the keyboard; hosts the KGlobalAccel daemon)
              │
              └── D-Bus signal `globalShortcutPressed` ──► this app
                                                              │
                                                   create layer surface
                                                              │
                                                   zwlr_layer_shell_v1

The frame uses a transparent, output-sized zwlr_layer_shell_v1 overlay, anchored to all four edges with exclusive_zone = 0. Its input region contains only the eight visible pieces; the empty center and margins pass pointer input through. Dimming uses a separate, non-interactive top-layer surface below it. The browser imports that surface through xdg-foreign as its transient parent, so KWin keeps the browser above the dimming. Settings uses another overlay on the same output. If xdg-foreign is unavailable, dimming is disabled.

Requirements

  • KDE Plasma on Wayland (KWin provides zwlr_layer_shell_v1).
  • The KGlobalAccel D-Bus service org.kde.kglobalaccel. On Plasma 6 Wayland this is hosted inside KWin (KWin links libKGlobalAccelD and calls KGlobalAccelD::init()); the standalone kglobalacceld daemon is only used by other sessions.
  • Rust toolchain to build.

No X11 support, by design.

Build

cargo build --release
# binary at target/release/sidepanel

Shortcut capture uses SCTK's xkbcommon feature and requires the system libxkbcommon development package (see "Changing the shortcut" below).

The embedded browser uses WebKitGTK via wry, which needs build-time system development packages:

# Debian/Ubuntu
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libxkbcommon-dev pkg-config
# Fedora
sudo dnf install gtk3-devel webkit2gtk4.1-devel libxkbcommon-devel pkgconf-pkg-config
# Arch
sudo pacman -S gtk3 webkit2gtk-4.1 pkgconf

Without them cargo build fails in the webkit2gtk-sys/gtk-sys build scripts (pkg-config cannot find webkit2gtk-4.1/gtk+-3.0). At run time, if only the WebKitGTK runtime (libwebkit2gtk-4.1-0) is missing, the panel degrades gracefully: it logs browser disabled: WebKitGTK runtime not found and everything else keeps working.

Run

./target/release/sidepanel

It runs as a daemon (it does not exit). Press Meta+F to toggle the panel.

Install / autostart

Run the installer, which builds the binary, installs it to ~/.local/bin, and sets up a systemd user service bound to the graphical session:

./contrib/install.sh

That is equivalent to:

  • copy target/release/sidepanel to ~/.local/bin/sidepanel,
  • copy contrib/sidepanel.service to ~/.config/systemd/user/,
  • systemctl --user enable --now sidepanel.service.

The service starts with graphical-session.target and stops with it. It is deliberately not lingered: the app needs WAYLAND_DISPLAY, which only exists inside a graphical session.

Useful flags: --prefix DIR (install elsewhere), --no-start (enable but do not start now), --uninstall.

The installer uses systemd rather than the XDG autostart protocol. On Plasma 6 the autostart path is backed by systemd anyway, but its generated unit hardcodes Restart=no, so a crashed daemon would not come back until the next login. (contrib/sidepanel.desktop is retained for reference but is not used by the installer.)

Configuration

Copy contrib/config.toml to ~/.config/sidepanel/config.toml:

left_width = 64    # logical pixels; 48-800
right_width = 64   # independent of the left width
top_height = 64    # logical pixels; 40-400
bottom_height = 64 # independent of the top height
margin = 12        # 0-80 px shared gap between pieces and screen edges
border_radius = 16 # 0-48 px rounded corners
opacity = 100      # 20-100%; 100 means no transparency
desktop_dim = 20   # 0-100%; 0 disables desktop dimming
panel_animation = "desktop_default" # "desktop_default", "fade_in", or "instant"
fade_duration_ms = 150 # 0-5000 ms; used by "fade_in"
persist_changes_to_panels = "0" # always; also 1m, 5m, 10m, 15m, 30m, 1hr, 2hr, 4hr, login_logout, restart
shortcut = "Meta+F"
theme = "system"   # "light", "dark", or "system"

# Browser window size (640-3840 x 400-2160); adjustable in the panel's
# Settings UI, which rewrites this file (comments are not preserved).
browser_width = 1280
browser_height = 800

[[sections]]
rectangle = "left" # "left", "right", "top", or "bottom"
title = "Websites"

[[sections.websites]]
url = "http://localhost:6005/"
title = "Dev 6005" # optional; defaults to the host, e.g. "localhost:6005"

[[sections.websites]]
url = "http://localhost:6006/"

The legacy width key is accepted as left_width; the obsolete side key is ignored because all four edges are now shown. Saving settings writes the new keys. Legacy [[websites]] entries are read into a left-side Websites section and saved in the new format. When both sections and legacy websites are omitted, the two localhost entries above are used. Set sections = [] before any tables for an empty frame. Favicons are fetched in the background — the page's <link rel="icon"> first, then /favicon.ico; PNG/ICO/JPEG/GIF/WebP and SVG are all supported — and each row shows a globe placeholder until its icon arrives. See contrib/config.toml for the annotated example.

shortcut is only the default. Once registered, the action also appears in System Settings → Shortcuts (component "Side Panel"), where the user can rebind it. A user-set binding is respected on later launches.

Latency notes

The single biggest win is architectural: this is a long-running daemon, so there is no process spawn on each keypress. Beyond that:

  • The panel is created on demand and destroyed on hide. That costs one configure round-trip (~1 frame). This is deliberate: KWin has had bugs around re-mapping the same layer surface (no fresh configure), so creating a fresh surface each time is the reliable path.
  • The release profile uses LTO + strip so the binary loads fast.
  • Next optimization (not in the MVP): keep the surface mapped but hidden (transparent buffer + empty input region) and "show" it with a single commit, avoiding the configure round-trip entirely.

Safety

A full-height surface that grabs the keyboard sounds risky, so it is worth being precise: the panel uses keyboard_interactivity = Exclusive so it is focused automatically, but global shortcuts still work. KWin's GlobalShortcutFilter::keyboardKey processes shortcuts unless isKeyboardShortcutsInhibited() is set, which a layer surface does not set. The configured shortcut dismisses the panel except while recording or applying a replacement. Recording can be cancelled with Escape or its button.

Changing the shortcut

Open Settings → Open panels shortcut, click the current binding, then press a new combination using Ctrl, Alt, or Meta with a letter, digit, F1–F12, Space, or navigation key. Shift can be added. Press Escape or click the shortcut button again to cancel. Repeat key events are ignored.

The new shortcut applies immediately and is written to config.toml after KDE accepts it. Conflicting shortcuts are rejected without taking them from other apps; failed assignments restore the previous binding. No restart is needed. During recording, the existing global shortcut does not dismiss the panel. Compositor-reserved shortcuts may be intercepted before reaching the recorder; cancel and choose another combination in that case.

Registration uses the KGlobalAccel D-Bus API. Startup continues to respect existing KDE shortcut overrides and displays the binding KDE returns. The default remains Meta+F. It can also be changed in System Settings → Shortcuts → Side Panel.

Keyboard capture needs these development packages:

# Debian/Ubuntu
sudo apt install libxkbcommon-dev
# Fedora
sudo dnf install libxkbcommon-devel
# Arch
sudo pacman -S libxkbcommon

Escape cancels shortcut recording; outside recording, use your configured global shortcut to dismiss the panel.

Known limitations / next steps

  • No Escape-to-dismiss (see above).
  • No multi-monitor output selection (the compositor picks the active output).
  • No fractional-scale handling.
  • Pre-mapped surface optimization not yet implemented.
  • The browser window defaults to 1280×800 (adjustable in Settings) and needs WebKitGTK (see "Build"); bookmarks reuse the same window.
  • The website list does not scroll; only entries that fit in the left panel are shown.

Troubleshooting

  • "zwlr_layer_shell_v1 is not available" — you are not on a compositor that implements layer-shell (KWin does). Check with wayland-info | grep layer.
  • Shortcut never fires — check that the action appears in System Settings → Shortcuts (component "Side Panel"). You can introspect the service (read-only) with:
    gdbus introspect --session --dest org.kde.kglobalaccel --object-path /kglobalaccel
  • Shortcut conflicts — KGlobalAccel resolves clashes itself: setShortcutKeys returns the binding actually in effect, which the app logs at startup. If Meta+F was already taken, the log line shows a different code.

⚠️ Never call a KGlobalAccel method that takes a QKeySequence with a bare integer. The argument is a QKeySequence (D-Bus (ai): a struct wrapping an array of exactly four ints). Qt's demarshaller reads all four without checking the array length, so a malformed argument makes the receiver read past the end and abort on a libdbus assertion. On Plasma 6 Wayland the receiver is KWin, so a bad call takes down the entire session.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages