diff --git a/README.md b/README.md index d84f5cb..afdfd19 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A bottom desktop-environment panel for wlroots-based compositors, built on | Section | Contents | | ------- | ----------------------------------------------------------------- | -| Left | Application menu built from `.desktop` entries, with search | +| Left | Application menu built from `.desktop` entries, with search, and pinned quick-launch icons | | Middle | Open windows (task list), click to focus, middle-click to close | | Right | On-screen keyboard, wifi, bluetooth, and a session menu: log out, suspend, restart, shut down | @@ -122,6 +122,12 @@ The launcher is a full-screen overlay layer surface rather than an `xdg-popup`, which is what makes click-away dismissal and keyboard focus work on any wlroots compositor without compositor-specific focus grabs. +**Quick launch.** The ids in `quickLaunch` are pinned as icons next to the app +menu, in the order listed, and clicking one launches that entry exactly as the +menu would — field codes stripped, `Terminal=true` honoured, and its own systemd +scope when the panel is a unit. An id that is not installed is skipped rather +than left as a dead button, so the same list can be carried between machines. + **On-screen keyboard.** One button shows squeekboard, another click hides it, and the button is accented while the keyboard is up. squeekboard runs as a systemd user unit (`oskUnit`, `squeekboard.service` by default): showing starts @@ -196,6 +202,9 @@ running with `qs --path .`). small output still gets a usable menu. - `terminal` — argv prefix for entries with `Terminal=true`. Honours `$TERMINAL`; otherwise change the `["foot"]` fallback to your terminal. + - `quickLaunch` — desktop entry ids pinned next to the app menu, in order. + Defaults to `["firefox", "org.kde.dolphin"]`; set `[]` for none. The ids are + desktop file names without the extension (`ls /usr/share/applications`). - `showOsk` — set `false` to drop the on-screen keyboard button entirely. - `oskUnit` — the systemd user unit squeekboard runs as. - `showWifi` — set `false` to drop the wifi button entirely. @@ -223,6 +232,7 @@ modules/Bar.qml The panel window and its three sections modules/AppMenu.qml Left: launcher button modules/Launcher.qml The application menu overlay modules/AppEntry.qml One row in the menu +modules/QuickLaunch.qml Left: pinned app icons modules/WindowList.qml Middle: task list modules/WindowButton.qml One task button modules/OskMenu.qml Right: on-screen keyboard button @@ -287,6 +297,11 @@ components/ToggleSwitch.qml On/off switch how the panel notices squeekboard going away, since it does not announce that itself — would never arrive. Showing retries the call for a few seconds because the unit goes active a moment before squeekboard claims its name. +- The quick-launch icons resolve their ids by scanning `DesktopEntries` rather + than calling `byId`. A function call in a binding creates no dependency on the + model, so it would be evaluated once against a still-empty entry list and stay + empty; scanning `applications.values` makes the binding re-run when the scan + finishes, and when an app is installed or removed later. - `qs ipc call` cannot reach a handler function named `show`: the name collides with the `ipc show` subcommand and the call silently turns into a listing. The `osk` target uses `open` and `close` instead, which matches the menu targets diff --git a/config/Config.qml b/config/Config.qml index 3d95bba..d034e28 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -57,6 +57,14 @@ Singleton { // is not already up; hiding it leaves the unit running. readonly property string oskUnit: "squeekboard.service" + // ---- Quick launch ---------------------------------------------------- + + // Desktop entry ids pinned next to the app menu, in the order given. An id + // this machine does not have installed is skipped rather than left as a + // dead button, so one list can be carried between machines. A trailing + // ".desktop" is accepted. + readonly property var quickLaunch: ["firefox", "org.kde.dolphin"] + // ---- Wifi ------------------------------------------------------------ // Set false to drop the wifi button. It also hides itself where there is no diff --git a/modules/Bar.qml b/modules/Bar.qml index 9dce3bf..7b5839d 100644 --- a/modules/Bar.qml +++ b/modules/Bar.qml @@ -53,6 +53,10 @@ PanelWindow { Layout.fillHeight: true } + QuickLaunch { + Layout.fillHeight: true + } + WindowList { bar: bar Layout.fillWidth: true diff --git a/modules/QuickLaunch.qml b/modules/QuickLaunch.qml new file mode 100644 index 0000000..0108de5 --- /dev/null +++ b/modules/QuickLaunch.qml @@ -0,0 +1,59 @@ +import Quickshell +import QtQuick +import qs.config +import qs.components +import qs.services + +// Left section, right of the app menu: one icon per pinned desktop entry. +Item { + id: root + + // Resolved from the ids in Config.quickLaunch, in the order given, minus + // whatever this machine does not have installed. + // + // A scan rather than DesktopEntries.byId: the binding then depends on the + // model itself, so the icons appear when the initial scan finishes and + // follow an app being installed or removed. A function call would be + // evaluated once against an empty model and stay that way. + readonly property var entries: { + const apps = [...DesktopEntries.applications.values]; + + return Config.quickLaunch.map(id => { + const wanted = id.replace(/\.desktop$/, "").toLowerCase(); + return apps.find(e => (e.id || "").toLowerCase() === wanted) ?? null; + }).filter(e => e !== null); + } + + visible: root.entries.length > 0 + implicitWidth: row.implicitWidth + + Row { + id: row + + anchors.centerIn: parent + spacing: Theme.spacing + + Repeater { + model: root.entries + + PanelButton { + id: button + + required property var modelData + + height: root.height - Theme.spacing + minContentWidth: Theme.iconSize + + onClicked: AppLauncher.launch(button.modelData) + + // Content is reparented into the button's holder, so the entry + // is reached through the button's id rather than `parent`. + AppIcon { + icon: button.modelData.icon + fallbackLabel: button.modelData.name + size: Theme.iconSize + } + } + } + } +}