aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md11
-rw-r--r--desktop/Prefs.qml16
-rw-r--r--desktop/island/ControlTile.qml143
-rw-r--r--desktop/island/Controls.js107
-rw-r--r--desktop/island/ControlsPanel.qml179
-rw-r--r--desktop/island/Island.qml13
-rw-r--r--desktop/island/README.md32
-rw-r--r--desktop/island/StatusPill.qml39
-rw-r--r--desktop/island/test_controls.js98
-rw-r--r--desktop/shell.qml7
10 files changed, 21 insertions, 624 deletions
diff --git a/AGENTS.md b/AGENTS.md
index a4ac38f..f5bfd8c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,14 +6,15 @@ Guidance for agents working in this repository.
Quickshell components for a Hyprland desktop. The repo is a hybrid: `desktop/`
is the integrated process, hosting the parts that need to talk to each other,
-the drawer and the island now and a control panel later. The other directories
+the drawer, which is the control center, and the island. The other directories
stay separate shells, each complete on its own: any of them runs alone, and
running one does not require the others. `desktop/` is itself a host for
modules.
- desktop/ the drawer: sound, mail, VMs, appearance
+ desktop/ the drawer (SUPER alone): sound, mail, VMs, status, performance,
+ clipboard, appearance
island/ top-centre pills on DP-1: clock, volume, tray, layout, privacy,
- controls panel (SUPER alone), calendar, performance
+ calendar, performance
appearance/ wallpaper picker and colour scheme switcher
window-switcher/ expose: open windows scattered as live previews, on ALT+TAB
or the top-right hot corner of DP-1
@@ -26,7 +27,7 @@ for the whole session.
## Reference: Modesty
-The island, control panel and calendar work follows the model of
+The island, drawer and calendar work follows the model of
[Modesty](https://github.com/sandeepbist/Modesty), a Quickshell desktop. A
clone lives at `~/Programming/GIT/GITHUB/Modesty`; `git pull` it before
relying on it. **It is GPLv3 and this repo is GPLv2-only: read it for ideas,
@@ -151,7 +152,7 @@ changing that component. The ones that generalise:
+ Super_L", ..., { release = true })` fires on SUPER pressed and released
alone, and is cancelled by any key in between and by `SUPER`+drag and
`SUPER`+scroll; all checked by hand. That is how SUPER alone opens the
- island's controls panel.
+ drawer.
- **`FileView.setText` creates missing parent directories**, so `Prefs.qml`
has no mkdir step. `Quickshell.stateDir` is a per-shell hashed directory
(`by-shell/<hash>`), not `~/.local/state/quickshell`, which is why the
diff --git a/desktop/Prefs.qml b/desktop/Prefs.qml
index c342aba..fa0dde9 100644
--- a/desktop/Prefs.qml
+++ b/desktop/Prefs.qml
@@ -14,7 +14,6 @@ pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
-import "island/Controls.js" as Controls
// Preferences that must survive a reboot, in one JSON file under the state
// home. Only the shell writes it, which is why it is not in ~/.config.
@@ -29,9 +28,6 @@ Singleton {
(Quickshell.env("XDG_STATE_HOME") || Quickshell.env("HOME") + "/.local/state")
+ "/quickshell/desktop.json"
- // Visible control tiles, in display order.
- property var controls: Controls.defaults()
-
// The public IP the user pinned from the performance panel; "" for none.
property string pinnedIp: ""
@@ -39,11 +35,6 @@ Singleton {
// know about.
property var doc: ({})
- function setControls(list) {
- root.controls = Controls.normalize(list);
- root.write(Object.assign({}, root.doc, { controls: root.controls }));
- }
-
function setPinnedIp(ip) {
if (ip === root.pinnedIp) return;
root.pinnedIp = ip;
@@ -63,9 +54,6 @@ Singleton {
if (v && typeof v === "object" && !Array.isArray(v)) parsed = v;
} catch (e) {}
root.doc = parsed;
- const next = "controls" in parsed ? Controls.normalize(parsed.controls) : Controls.defaults();
- // The watch also fires on our own write; compare so it is a no-op.
- if (JSON.stringify(next) !== JSON.stringify(root.controls)) root.controls = next;
const pin = parsed.perf && typeof parsed.perf.pinnedIp === "string" ? parsed.perf.pinnedIp : "";
if (pin !== root.pinnedIp) root.pinnedIp = pin;
}
@@ -77,11 +65,11 @@ Singleton {
watchChanges: true
printErrors: false
// printErrors is off for the missing-file read; a failed write still
- // has to be heard, or the new order silently vanishes at restart.
+ // has to be heard, or the pin silently vanishes at restart.
onSaveFailed: error => console.warn("Prefs: writing", root.path, "failed:", error)
onFileChanged: reload()
onLoaded: root.reparse()
// Missing file: the defaults, and nothing written.
- onLoadFailed: { root.doc = {}; root.controls = Controls.defaults(); root.pinnedIp = ""; }
+ onLoadFailed: { root.doc = {}; root.pinnedIp = ""; }
}
}
diff --git a/desktop/island/ControlTile.qml b/desktop/island/ControlTile.qml
deleted file mode 100644
index 873a42e..0000000
--- a/desktop/island/ControlTile.qml
+++ /dev/null
@@ -1,143 +0,0 @@
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-import Quickshell
-import Quickshell.Widgets
-import Quickshell.Networking
-import Quickshell.Bluetooth
-import QtQuick
-import ".."
-import "Controls.js" as Controls
-import "Logic.js" as Logic
-
-// One control: icon over label. Left-click toggles, right-click opens the
-// drawer page that holds the rest, scroll changes volume on the two audio
-// tiles. Toggle tiles fill when on; audio tiles fill to their volume.
-Rectangle {
- id: tile
-
- required property string tileId
- // The sound module's Service.
- required property QtObject sound
-
- signal openPage(string page)
-
- readonly property var info: Controls.tile(tileId)
- readonly property bool isAudio: tileId === "output" || tileId === "mic"
- readonly property var audio: tileId === "output" ? (sound.sink?.audio ?? null)
- : tileId === "mic" ? (sound.source?.audio ?? null)
- : null
- readonly property var adapter: Bluetooth.defaultAdapter
- // Networking starts empty for about two seconds (AGENTS.md).
- readonly property bool wifiReady:
- (Networking.devices?.values ?? []).some(d => d.type === DeviceType.Wifi)
-
- readonly property bool ready: isAudio ? audio !== null
- : tileId === "bluetooth" ? adapter !== null
- : tileId === "wifi" ? wifiReady
- : true
-
- readonly property bool on: {
- switch (tileId) {
- case "wifi": return Networking.wifiEnabled;
- case "bluetooth": return adapter?.enabled ?? false;
- case "dnd": return Status.dnd;
- // Lit while a game holds it too; a click then clears only the manual
- // half (Status.qml), so the tile stays lit until the game ends.
- case "presentation": return Status.presentation;
- case "nolock": return !Status.nolock;
- default: return audio ? !audio.muted : false;
- }
- }
-
- function toggle() {
- switch (tileId) {
- case "wifi": Networking.wifiEnabled = !Networking.wifiEnabled; break;
- case "bluetooth": adapter.enabled = !adapter.enabled; break;
- case "dnd":
- case "presentation":
- case "nolock": Status.toggleMode(tileId); break;
- default: audio.muted = !audio.muted;
- }
- }
-
- radius: 12
- color: Qt.alpha(Theme.surface, 0.5)
- border.width: 1
- border.color: Qt.alpha(Theme.text, 0.08)
- opacity: ready ? 1 : 0.4
- clip: true
-
- // Toggle tiles: lit when on.
- Rectangle {
- anchors.fill: parent
- radius: parent.radius
- visible: !tile.isAudio && tile.on
- color: Qt.alpha(Theme.accent, 0.35)
- }
-
- // Audio tiles: a bar to the volume. Muted keeps the width in a neutral
- // colour, so the level stays readable. Not the accent as the neutral:
- // udt can snap it onto a palette colour (AGENTS.md).
- Rectangle {
- visible: tile.isAudio
- anchors { left: parent.left; top: parent.top; bottom: parent.bottom }
- width: parent.width * Controls.fill(tile.audio?.volume)
- radius: parent.radius
- color: tile.audio?.muted ? Qt.alpha(Theme.overlay, 0.6) : Qt.alpha(Theme.accent, 0.35)
- }
-
- Column {
- anchors.centerIn: parent
- width: parent.width - 8
- spacing: 4
-
- IconImage {
- anchors.horizontalCenter: parent.horizontalCenter
- implicitSize: 22
- source: Quickshell.iconPath(tile.tileId === "output"
- ? Logic.volumeIcon(tile.audio?.volume ?? 0, tile.audio?.muted ?? false)
- : Controls.icon(tile.tileId, tile.on), true)
- }
-
- Text {
- width: parent.width
- horizontalAlignment: Text.AlignHCenter
- elide: Text.ElideRight
- text: tile.info?.label ?? tile.tileId
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
- color: Theme.text
- }
- }
-
- MouseArea {
- anchors.fill: parent
- acceptedButtons: Qt.LeftButton | Qt.RightButton
- cursorShape: Qt.PointingHandCursor
- onClicked: mouse => {
- if (mouse.button === Qt.RightButton) tile.openPage(tile.info.page);
- else if (tile.ready) tile.toggle();
- }
- // One step per wheel notch (120), as the layout flag does. A
- // MouseArea, not a WheelHandler: the handler never fired in the
- // island (AGENTS.md).
- property real acc: 0
- onWheel: wheel => {
- wheel.accepted = true;
- if (!tile.isAudio || !tile.ready) return;
- acc += wheel.angleDelta.y;
- if (Math.abs(acc) < 120) return;
- const max = tile.tileId === "output" ? 1.5 : 1.0;
- tile.audio.volume = Controls.step(tile.audio.volume, acc > 0 ? 0.05 : -0.05, max);
- acc = 0;
- }
- }
-}
diff --git a/desktop/island/Controls.js b/desktop/island/Controls.js
deleted file mode 100644
index 0ca2f6c..0000000
--- a/desktop/island/Controls.js
+++ /dev/null
@@ -1,107 +0,0 @@
-.pragma library
-
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// The controls panel's tiles and the pure operations on their order. The
-// order itself lives in Prefs.qml; everything here takes a list and returns
-// a new one, so it runs under node (test_controls.js).
-
-// Table order is the default order and the order of the hidden strip.
-const TILES = {
- wifi: { label: "Wi-Fi", page: "network" },
- bluetooth: { label: "Bluetooth", page: "bluetooth" },
- dnd: { label: "DND", page: "status" },
- presentation: { label: "Presentation", page: "status" },
- nolock: { label: "Auto-lock", page: "status" },
- output: { label: "Output", page: "sound" },
- mic: { label: "Mic", page: "sound" },
-};
-
-function defaults() {
- return Object.keys(TILES);
-}
-
-function tile(id) {
- return TILES[id] || null;
-}
-
-// Icon name for a tile's state. `on` is the tile's lit state: for the mic
-// that is "not muted". Output is "" because Logic.volumeIcon owns it.
-function icon(id, on) {
- switch (id) {
- case "wifi": return on ? "network-wireless" : "network-wireless-disconnected";
- case "bluetooth": return "preferences-system-bluetooth";
- case "dnd": return on ? "notifications-disabled" : "notifications";
- case "presentation": return "video-display";
- case "nolock": return "system-lock-screen";
- case "mic": return on ? "audio-input-microphone" : "microphone-sensitivity-muted";
- default: return "";
- }
-}
-
-// A stored list made safe: unknown ids (a tile removed from the code) and
-// duplicates dropped. An empty list is a valid choice; a non-list is not.
-function normalize(list) {
- if (!Array.isArray(list)) return defaults();
- const out = [];
- for (let i = 0; i < list.length; i++) {
- const id = list[i];
- if (TILES[id] && out.indexOf(id) < 0) out.push(id);
- }
- return out;
-}
-
-function hidden(list) {
- return defaults().filter(id => list.indexOf(id) < 0);
-}
-
-function move(list, from, to) {
- const out = list.slice();
- const id = out.splice(from, 1)[0];
- out.splice(to, 0, id);
- return out;
-}
-
-function hide(list, id) {
- return list.filter(x => x !== id);
-}
-
-function show(list, id) {
- if (!TILES[id] || list.indexOf(id) >= 0) return list.slice();
- return list.concat([id]);
-}
-
-// Audio tile bar width as a fraction. Output goes to 1.5, the bar stops at
-// 1. !(> 0) also catches a volume not populated yet.
-function fill(volume) {
- if (!(volume > 0)) return 0;
- return Math.min(1, volume);
-}
-
-// One scroll step, rounded to the percent so repeated steps do not drift.
-// The cap only stops a step up: a level already above it (set elsewhere,
-// e.g. a mic at 120% from pavucontrol) is left alone by up and stepped from
-// by down, never snapped to the cap.
-function step(volume, delta, max) {
- const cur = volume > 0 ? volume : 0;
- const v = Math.round((cur + delta) * 100) / 100;
- if (delta < 0) return Math.max(0, v);
- return Math.max(cur, Math.min(max, v));
-}
-
-// The slot a dragged tile lands in, from its centre in grid coordinates.
-// Off the grid clamps to the nearest edge; past the last tile is the last.
-function slotAt(x, y, pitch, cols, count) {
- const col = Math.max(0, Math.min(cols - 1, Math.floor(x / pitch)));
- const row = Math.max(0, Math.floor(y / pitch));
- return Math.max(0, Math.min(count - 1, row * cols + col));
-}
diff --git a/desktop/island/ControlsPanel.qml b/desktop/island/ControlsPanel.qml
deleted file mode 100644
index a7872d7..0000000
--- a/desktop/island/ControlsPanel.qml
+++ /dev/null
@@ -1,179 +0,0 @@
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-import Quickshell
-import Quickshell.Widgets
-import QtQuick
-import ".."
-import "Controls.js" as Controls
-
-// The grid of controls and its edit mode. In edit mode a tile is dragged to
-// a new slot, a × hides it, and hidden tiles wait in a strip below with a +.
-// Every change is written to Prefs at once.
-Column {
- id: panel
-
- required property QtObject sound
- property bool editing: false
-
- signal openDrawer(string page)
-
- spacing: 12
-
- Item {
- width: panel.width
- height: 20
-
- Text {
- anchors.verticalCenter: parent.verticalCenter
- text: panel.editing ? "Edit controls" : "Controls"
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
- color: Theme.text
- }
-
- IconImage {
- anchors { right: parent.right; verticalCenter: parent.verticalCenter }
- implicitSize: 16
- opacity: panel.editing ? 1 : 0.6
- source: Quickshell.iconPath("document-edit", true)
-
- MouseArea {
- anchors.fill: parent
- anchors.margins: -4
- cursorShape: Qt.PointingHandCursor
- onClicked: panel.editing = !panel.editing
- }
- }
- }
-
- Item {
- id: grid
-
- readonly property int cols: 4
- readonly property real gap: 8
- readonly property real cell: (width - gap * (cols - 1)) / cols
- readonly property real pitch: cell + gap
- readonly property int rows: Math.ceil(Prefs.controls.length / cols)
-
- width: panel.width
- height: Math.max(0, rows * pitch - gap)
-
- Repeater {
- model: Prefs.controls
-
- delegate: Item {
- id: slot
- required property string modelData
- required property int index
-
- width: grid.cell
- height: grid.cell
- x: (index % grid.cols) * grid.pitch
- y: Math.floor(index / grid.cols) * grid.pitch
- z: mover.pressed ? 1 : 0
-
- ControlTile {
- width: parent.width
- height: parent.height
- x: mover.pressed ? mover.dx : 0
- y: mover.pressed ? mover.dy : 0
- tileId: slot.modelData
- sound: panel.sound
- onOpenPage: page => panel.openDrawer(page)
- }
-
- // Edit mode only, and above the tile so it takes the press
- // instead of the toggle. Offsets the tile while held; on
- // release the tile lands in the slot under its centre.
- MouseArea {
- id: mover
- anchors.fill: parent
- enabled: panel.editing
- preventStealing: true
- // Both buttons and the wheel, so in edit mode nothing
- // reaches the tile: no drawer page, no volume change.
- acceptedButtons: Qt.LeftButton | Qt.RightButton
- onWheel: wheel => wheel.accepted = true
- cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
- property real sx: 0
- property real sy: 0
- property real dx: 0
- property real dy: 0
- onPressed: mouse => { sx = mouse.x; sy = mouse.y; dx = 0; dy = 0; }
- onPositionChanged: mouse => { dx = mouse.x - sx; dy = mouse.y - sy; }
- onReleased: {
- const to = Controls.slotAt(slot.x + dx + width / 2, slot.y + dy + height / 2,
- grid.pitch, grid.cols, Prefs.controls.length);
- if (to !== slot.index)
- Prefs.setControls(Controls.move(Prefs.controls, slot.index, to));
- }
- }
-
- IconImage {
- visible: panel.editing && !mover.pressed
- anchors { top: parent.top; right: parent.right; margins: 3 }
- implicitSize: 14
- source: Quickshell.iconPath("window-close", true)
-
- MouseArea {
- anchors.fill: parent
- anchors.margins: -4
- cursorShape: Qt.PointingHandCursor
- onClicked: Prefs.setControls(Controls.hide(Prefs.controls, slot.modelData))
- }
- }
- }
- }
- }
-
- Flow {
- width: panel.width
- spacing: 6
- visible: panel.editing && Controls.hidden(Prefs.controls).length > 0
-
- Repeater {
- model: Controls.hidden(Prefs.controls)
-
- delegate: Rectangle {
- id: chip
- required property string modelData
- width: chipRow.implicitWidth + 16
- height: 28
- radius: 14
- color: Qt.alpha(Theme.surface, 0.5)
-
- Row {
- id: chipRow
- anchors.centerIn: parent
- spacing: 4
-
- IconImage {
- anchors.verticalCenter: parent.verticalCenter
- implicitSize: 14
- source: Quickshell.iconPath("list-add", true)
- }
- Text {
- anchors.verticalCenter: parent.verticalCenter
- text: Controls.tile(chip.modelData).label
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
- color: Theme.text
- }
- }
-
- MouseArea {
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- onClicked: Prefs.setControls(Controls.show(Prefs.controls, chip.modelData))
- }
- }
- }
- }
-}
diff --git a/desktop/island/Island.qml b/desktop/island/Island.qml
index d0d0a49..1807c82 100644
--- a/desktop/island/Island.qml
+++ b/desktop/island/Island.qml
@@ -38,7 +38,7 @@ PanelWindow {
WlrLayershell.namespace: "quickshell-island"
WlrLayershell.keyboardFocus: panel !== "" ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
- // "", "sound", "controls", "perf" or "calendar".
+ // "", "sound", "perf" or "calendar".
property string panel: ""
// --- reveal and hide -------------------------------------------------
@@ -64,15 +64,6 @@ PanelWindow {
else if (!hovered) hideTimer.restart();
}
- // The keybind's way in: reveal, and open or close the controls panel.
- // Closing goes through onPanelChanged, which starts the hide timer when
- // the pointer is elsewhere.
- function toggleControls() {
- hideTimer.stop();
- shown = true;
- panel = panel === "controls" ? "" : "controls";
- }
-
Timer {
id: hideTimer
interval: 400
@@ -221,8 +212,6 @@ PanelWindow {
onMenuShown: win.menuHeld = true
onOpenSound: win.panel = win.panel === "sound" ? "" : "sound"
onOpenDrawer: { win.panel = ""; win.drawer.show("sound"); }
- onOpenControls: win.toggleControls()
onOpenPerf: win.panel = win.panel === "perf" ? "" : "perf"
- onOpenPage: page => { win.panel = ""; win.drawer.show(page); }
}
}
diff --git a/desktop/island/README.md b/desktop/island/README.md
index c4cf35f..4ad1050 100644
--- a/desktop/island/README.md
+++ b/desktop/island/README.md
@@ -46,12 +46,10 @@ is open or not. Click opens the calendar panel.
## Status pill
-Left to right: performance icon, controls icon, volume icon, tray, layout
-flag.
+Left to right: performance icon, volume icon, tray, layout flag.
- **Performance icon** opens the performance panel, see below. A dot on its
corner warns when something needs attention.
-- **Controls icon** opens the controls panel, see below.
- **Volume icon** shows the default sink (muted, low, medium, high). Click
opens the sound panel.
- **Tray** shows `SystemTray` items. Left click activates, right click opens
@@ -62,27 +60,13 @@ flag.
`activelayout` event. Scroll switches layout with
`hyprctl switchxkblayout current next|prev`.
-## Controls panel
-
-Opened by the status pill's controls icon, or by SUPER pressed and released
-alone (`qs -p desktop ipc call island controls`, a Hyprland release bind;
-any key or mouse bind pressed in between cancels it). Seven tiles: wifi,
-bluetooth, DND, presentation, auto-lock, output, mic.
-
-- Click toggles. Right-click closes the panel and opens the tile's drawer
- page.
-- Output and mic fill to their volume, clamped at 100%; muted greys the bar
- instead of emptying it. Scroll steps 5%, output capped at 150%.
-- A tile whose backing object has not arrived (the first two seconds, no
- adapter, no sink) is dimmed and ignores input.
-- The pencil enters edit mode: drag a tile to another slot, × hides it, the
- strip below lists hidden tiles with a + each. Closing the panel ends it.
-
-The visible tiles and their order live in
-`$XDG_STATE_HOME/quickshell/desktop.json` (`../Prefs.qml`), written only when
-they change and keeping keys it does not know. A tile added to the code
-later starts hidden. Pure helpers in `Controls.js`, tested with
-`node island/test_controls.js`.
+## Controls panel (removed)
+
+There was one, opened by SUPER alone. Each of its seven tiles opened a drawer
+page that already existed and it went unused, so it was removed and SUPER
+alone opens the drawer instead. `Prefs.qml` keeps only the pinned IP; a
+`controls` key left in `desktop.json` by the old panel is kept on write and
+otherwise ignored.
## Performance panel
diff --git a/desktop/island/StatusPill.qml b/desktop/island/StatusPill.qml
index d62b87f..f064f98 100644
--- a/desktop/island/StatusPill.qml
+++ b/desktop/island/StatusPill.qml
@@ -17,8 +17,8 @@ import ".."
import "perf"
import "Logic.js" as Logic
-// Performance icon, controls icon, volume icon, tray, layout flag. The
-// performance, controls and volume icons open their panels in this capsule;
+// Performance icon, volume icon, tray, layout flag. The performance and
+// volume icons open their panels in this capsule;
// the tray is a plain tray; the flag switches layout on scroll and ignores
// clicks.
Capsule {
@@ -30,28 +30,21 @@ Capsule {
property string panel: ""
signal openSound()
- signal openControls()
signal openPerf()
signal openDrawer()
- // A drawer page, from a control tile's right-click.
- signal openPage(string page)
// Emitted just before a tray menu opens; the menu cannot report its close.
signal menuShown()
- expanded: panel === "sound" || panel === "controls" || panel === "perf"
+ expanded: panel === "sound" || panel === "perf"
restWidth: restRow.implicitWidth + 24
panelWidth: panel === "perf" ? 600 : 320
- panelHeight: (panel === "controls" ? controlsPanel.implicitHeight
- : panel === "perf" ? (perfPanel.item ? perfPanel.item.implicitHeight : 0)
+ panelHeight: (panel === "perf" ? (perfPanel.item ? perfPanel.item.implicitHeight : 0)
: soundPanel.implicitHeight) + 28
// The detailed sampling (per-core, network, VRAM) runs only while the
// panel shows it.
Binding { target: Perf; property: "detailed"; value: pill.panel === "perf" }
- // Edit mode never outlives its panel.
- onPanelChanged: controlsPanel.editing = false
-
Row {
id: restRow
anchors.centerIn: parent
@@ -93,18 +86,6 @@ Capsule {
IconImage {
anchors.verticalCenter: parent.verticalCenter
implicitSize: 18
- source: Quickshell.iconPath("preferences-system", true)
-
- MouseArea {
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- onClicked: pill.openControls()
- }
- }
-
- IconImage {
- anchors.verticalCenter: parent.verticalCenter
- implicitSize: 18
source: Quickshell.iconPath(Logic.volumeIcon(pill.sound.volume, pill.sound.muted), true)
MouseArea {
@@ -179,18 +160,6 @@ Capsule {
onOpenDrawer: pill.openDrawer()
}
- ControlsPanel {
- id: controlsPanel
- x: 14
- y: 14
- width: pill.panelWidth - 28
- sound: pill.sound
- opacity: pill.panel === "controls" ? 1 : 0
- visible: opacity > 0
- Behavior on opacity { NumberAnimation { duration: 160 } }
- onOpenDrawer: page => pill.openPage(page)
- }
-
// Loaded only while open: its models are fresh arrays every sample, and
// a hidden instance would rebuild its rows every 2s for the whole session.
Loader {
diff --git a/desktop/island/test_controls.js b/desktop/island/test_controls.js
deleted file mode 100644
index b2db873..0000000
--- a/desktop/island/test_controls.js
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// Run: node desktop/island/test_controls.js
-// Controls.js is a QML library, so its .pragma line is stripped before node
-// evaluates it, as test_logic.js does for Logic.js.
-
-const fs = require("fs");
-const assert = require("assert");
-
-const src = fs.readFileSync(__dirname + "/Controls.js", "utf8")
- .replace(/^\.pragma library$/m, "");
-const C = new Function(src +
- "\nreturn { defaults, tile, icon, normalize, hidden, move, hide, show, fill, step, slotAt };")();
-
-const ALL = ["wifi", "bluetooth", "dnd", "presentation", "nolock", "output", "mic"];
-
-// Defaults: all seven, a fresh copy each time.
-assert.deepStrictEqual(C.defaults(), ALL);
-const d = C.defaults(); d.pop();
-assert.deepStrictEqual(C.defaults(), ALL);
-
-// Table: label and drawer page; unknown is null.
-assert.strictEqual(C.tile("wifi").page, "network");
-assert.strictEqual(C.tile("nolock").label, "Auto-lock");
-assert.strictEqual(C.tile("mic").page, "sound");
-assert.strictEqual(C.tile("nope"), null);
-
-// Icons by state. Output is Logic.volumeIcon's job, so "" here.
-assert.strictEqual(C.icon("wifi", true), "network-wireless");
-assert.strictEqual(C.icon("wifi", false), "network-wireless-disconnected");
-assert.strictEqual(C.icon("dnd", true), "notifications-disabled");
-assert.strictEqual(C.icon("dnd", false), "notifications");
-assert.strictEqual(C.icon("mic", true), "audio-input-microphone");
-assert.strictEqual(C.icon("mic", false), "microphone-sensitivity-muted");
-assert.strictEqual(C.icon("bluetooth", false), "preferences-system-bluetooth");
-assert.strictEqual(C.icon("output", true), "");
-
-// normalize: drops unknown ids and duplicates, keeps order; an empty list
-// is valid (everything hidden); a non-array is the default.
-assert.deepStrictEqual(C.normalize(["mic", "gone", "wifi", "mic"]), ["mic", "wifi"]);
-assert.deepStrictEqual(C.normalize([]), []);
-assert.deepStrictEqual(C.normalize("wifi"), ALL);
-assert.deepStrictEqual(C.normalize(undefined), ALL);
-
-// hidden: known ids not in the list, in table order.
-assert.deepStrictEqual(C.hidden(["wifi", "mic"]), ["bluetooth", "dnd", "presentation", "nolock", "output"]);
-assert.deepStrictEqual(C.hidden(ALL), []);
-
-// move: from index to index, input untouched.
-const src3 = ["a", "b", "c"];
-assert.deepStrictEqual(C.move(src3, 0, 2), ["b", "c", "a"]);
-assert.deepStrictEqual(C.move(src3, 2, 0), ["c", "a", "b"]);
-assert.deepStrictEqual(C.move(src3, 1, 1), ["a", "b", "c"]);
-assert.deepStrictEqual(src3, ["a", "b", "c"]);
-
-// hide / show.
-assert.deepStrictEqual(C.hide(["wifi", "mic"], "wifi"), ["mic"]);
-assert.deepStrictEqual(C.show(["mic"], "wifi"), ["mic", "wifi"]);
-assert.deepStrictEqual(C.show(["mic"], "mic"), ["mic"]);
-assert.deepStrictEqual(C.show(["mic"], "nope"), ["mic"]);
-
-// fill: volume clamped to 0..1; unpopulated reads as empty, not full.
-assert.strictEqual(C.fill(0.4), 0.4);
-assert.strictEqual(C.fill(1.5), 1);
-assert.strictEqual(C.fill(-1), 0);
-assert.strictEqual(C.fill(undefined), 0);
-assert.strictEqual(C.fill(NaN), 0);
-
-// step: 5% steps, rounded to the percent, clamped to 0..max.
-assert.strictEqual(C.step(0.5, 0.05, 1.5), 0.55);
-assert.strictEqual(C.step(0.02, -0.05, 1.5), 0);
-assert.strictEqual(C.step(1.48, 0.05, 1.5), 1.5);
-assert.strictEqual(C.step(0.98, 0.05, 1.0), 1);
-assert.strictEqual(C.step(0.1 + 0.2, 0.05, 1), 0.35);
-assert.strictEqual(C.step(undefined, 0.05, 1), 0.05);
-// Above the cap (set elsewhere): up leaves it, down steps from where it is.
-assert.strictEqual(C.step(1.2, 0.05, 1.0), 1.2);
-assert.strictEqual(C.step(1.2, -0.05, 1.0), 1.15);
-
-// slotAt: pitch 70, 4 columns, 7 tiles.
-assert.strictEqual(C.slotAt(35, 35, 70, 4, 7), 0);
-assert.strictEqual(C.slotAt(175, 35, 70, 4, 7), 2);
-assert.strictEqual(C.slotAt(105, 105, 70, 4, 7), 5);
-assert.strictEqual(C.slotAt(-50, -50, 70, 4, 7), 0); // off the top left
-assert.strictEqual(C.slotAt(500, 35, 70, 4, 7), 3); // past the right edge
-assert.strictEqual(C.slotAt(275, 105, 70, 4, 7), 6); // empty 8th slot -> last
-assert.strictEqual(C.slotAt(35, 900, 70, 4, 7), 6); // below the grid -> last
-
-console.log("test_controls: ok");
diff --git a/desktop/shell.qml b/desktop/shell.qml
index d70b338..fd99f11 100644
--- a/desktop/shell.qml
+++ b/desktop/shell.qml
@@ -86,11 +86,4 @@ ShellRoot {
function toggle() { drawer.toggle(""); }
function close() { drawer.close(); }
}
-
- // The bare SUPER release bind reaches this:
- // qs -p <this dir> ipc call island controls
- IpcHandler {
- target: "island"
- function controls() { island.toggleControls(); }
- }
}