diff --git a/barstyles/imi/README.md b/barstyles/imi/README.md index 68a0bc5..2034410 100644 --- a/barstyles/imi/README.md +++ b/barstyles/imi/README.md @@ -13,10 +13,11 @@ Ported and adapted from the Immaterial Impulse dotfiles (end-4). - **Weather Widget** — Current conditions, hourly forecast chart, and multi-day outlook via popout card. - **Calendar Popout** — Clock + calendar grid popout with locale-aware formatting. - **Privacy Indicator** — Mic, camera, and screencast status chips that appear/disappear with smooth animations. -- **Submap Indicator** — Hyprland keybind submap pill (e.g. resize mode) with auto-hide. +- **Submap Indicator** — Hyprland keybind submap pill (e.g. resize mode) with auto-hide. Hidden on other compositors. +- **Autohide Bar** — Optional hover-reveal bar with Super-hold peek, overlay mode (windows ignore it) or push mode (windows make room), and popups that hold the bar out while open. - **System Tray** — Collapsible tray icons with overflow. - **Matugen Theming** — Full dynamic color palette from Ryoku's Matugen integration. -- **Edit Mode** — Drag-and-drop widget reordering via Bar Studio. +- **Edit Mode** — Drag-and-drop widget reordering via Bar Studio. Double-click the bar to open its own edit mode: `+` adds widgets and holds the autohide switch. - **Multi-Monitor** — Per-output Scopes with boundary-aware popout positioning. ## Configuration @@ -36,11 +37,26 @@ value changes apply without restarting the shell: | `bar.layouts.left` | Widget order for the left pill | `[...]` | | `bar.layouts.center` | Widget order for the center pill | `[...]` | | `bar.layouts.right` | Widget order for the right pill | `[...]` | +| `bar.autoHide.enable` | Slide the bar away until the pointer hits the screen edge | `false` | +| `bar.autoHide.hoverRegionWidth` | Reveal-strip height in px at the screen edge | `2` | +| `bar.autoHide.pushWindows` | Reserve bar space when shown (`false` = bar overlays windows) | `false` | +| `bar.autoHide.dismissPopups` | Keep the bar out while a bar popup is open, closing the popup on pointer-leave | `true` | +| `bar.autoHide.showWhenPressingSuper.enable` | Peek the bar while Super is held | `true` | +| `bar.autoHide.showWhenPressingSuper.delay` | Hold delay in ms before the peek shows | `140` | + +## Compositor support + +Imi reads all window-manager state (workspaces, focused window, fullscreen) +through the shell's compositor-neutral facade, so it runs on **Hyprland** and +**niri** alike. Compositor-specific extras degrade gracefully elsewhere: the +keybind submap pill and scratchpad handling only appear on Hyprland, and +focus-grab dismissal of pinned popups falls back to toggle-off on other +compositors. ## Dependencies - **Ryoku Desktop Shell** (Quickshell-based) -- **Hyprland** compositor +- **Hyprland** or **niri** compositor - **PipeWire** (for audio visualizer) - **Python 3** (installer only) diff --git a/barstyles/imi/Scene.qml b/barstyles/imi/Scene.qml index 62fa871..93e91be 100644 --- a/barstyles/imi/Scene.qml +++ b/barstyles/imi/Scene.qml @@ -20,8 +20,7 @@ Scope { screen: root.modelData color: "transparent" - exclusionMode: ExclusionMode.Normal - exclusiveZone: 48 + exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Top WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.namespace: "ryoku-imi" @@ -34,14 +33,79 @@ Scope { implicitHeight: 48 - ImiBar.BarContent { - id: barContent + readonly property bool autoHide: Config.options.bar.autoHide.enable ?? false + property bool superShow: false + property bool mustShow: !win.autoHide || hoverRegion.containsMouse || win.superShow + || GlobalStates.editMode + || ((GlobalStates.mediaControlsOpen || GlobalStates.sysTrayOverflowOpen) && (Config.options.bar.autoHide.dismissPopups ?? true)) + + property QtObject barSpaceReserver: ImiBar.BarExclusiveZoneReserver { + screen: root.modelData + barNamespace: "ryoku-imi" + farEdge: false + edgeMargin: 0 + zone: (!win.autoHide || (win.mustShow && (Config.options.bar.autoHide.pushWindows ?? false))) ? win.implicitHeight : 0 + } + + mask: Region { + item: hoverMaskRegion + } + + Timer { + id: showBarTimer + interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100) + repeat: false + onTriggered: { + win.superShow = true + } + } + Connections { + target: GlobalStates + function onSuperDownChanged() { + if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return; + if (GlobalStates.superDown) showBarTimer.restart(); + else { + showBarTimer.stop(); + win.superShow = false; + } + } + } + + MouseArea { + id: hoverRegion anchors.fill: parent + hoverEnabled: true + + Item { + id: hoverMaskRegion + readonly property real reveal: (Config.options.bar.autoHide.hoverRegionWidth ?? 2) + readonly property real rawTop: barContent.y - reveal + readonly property real rawBottom: barContent.y + barContent.height + reveal + + x: 0 + width: parent.width + y: Math.max(0, rawTop) + height: Math.max(0, Math.min(parent.height, rawBottom) - y) + } - TapHandler { - acceptedButtons: Qt.LeftButton - gesturePolicy: TapHandler.ReleaseWithinBounds - onDoubleTapped: GlobalStates.editMode = !GlobalStates.editMode + ImiBar.BarContent { + id: barContent + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: (win.autoHide && !win.mustShow) ? -height : 0 + } + height: win.implicitHeight + Behavior on anchors.topMargin { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + TapHandler { + acceptedButtons: Qt.LeftButton + gesturePolicy: TapHandler.ReleaseWithinBounds + onDoubleTapped: GlobalStates.editMode = !GlobalStates.editMode + } } } } @@ -222,6 +286,74 @@ Scope { } } } + + Rectangle { + width: pickerColumn.width + height: 1 + color: Appearance.m3colors.m3outlineVariant + opacity: 0.5 + } + + Rectangle { + id: autoHideRow + width: pickerColumn.width + height: 36 + radius: 8 + color: autoHideHover.hovered ? Appearance.m3colors.m3surfaceContainerHigh : "transparent" + + HoverHandler { id: autoHideHover } + + readonly property bool autoHideOn: Config.options.bar.autoHide.enable ?? false + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + spacing: 8 + + Text { + text: "visibility_off" + font.family: "Material Symbols Rounded" + font.pixelSize: 18 + color: Appearance.m3colors.m3onSurfaceVariant + Layout.alignment: Qt.AlignVCenter + } + Text { + text: "Autohide bar" + color: Appearance.m3colors.m3onSurface + font.pixelSize: 13 + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + Rectangle { + Layout.alignment: Qt.AlignVCenter + width: 34 + height: 20 + radius: 10 + color: autoHideRow.autoHideOn ? Appearance.m3colors.m3primary : Appearance.m3colors.m3surfaceContainerHigh + border.width: autoHideRow.autoHideOn ? 0 : 1 + border.color: Appearance.m3colors.m3outline + + Rectangle { + width: 14 + height: 14 + radius: 7 + anchors.verticalCenter: parent.verticalCenter + x: autoHideRow.autoHideOn ? parent.width - width - 3 : 3 + color: autoHideRow.autoHideOn ? Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3onSurfaceVariant + Behavior on x { + NumberAnimation { duration: 150 } + } + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Config.options.bar.autoHide.enable = !autoHideRow.autoHideOn + } + } } } } diff --git a/barstyles/imi/manifest.json b/barstyles/imi/manifest.json index 1370a10..60a7dc7 100644 --- a/barstyles/imi/manifest.json +++ b/barstyles/imi/manifest.json @@ -2,7 +2,7 @@ "schema": 1, "id": "imi", "category": "barstyles", - "version": "1.0.1", + "version": "1.1.0", "destination": "ryoku/barstyles/imi", "files": [ { @@ -41,16 +41,16 @@ "source": "README.md", "destination": "README.md", "mode": "0644", - "size": 4156, - "sha256": "93a025e3621c181c89afa1d4353c76db193753736c6571603f54f55033d8e3a3", + "size": 5490, + "sha256": "e8f8a84493ea13d7dd4e01a1685643a79a6f2a26051e28162798a46da46dfa4a", "install": true }, { "source": "Scene.qml", "destination": "Scene.qml", "mode": "0644", - "size": 8019, - "sha256": "ceea20306e1dbbebc0ab550a704f9619e9813af9b39f958b114a92355951a78a", + "size": 13552, + "sha256": "6b518b3f576236d4ee86b4302c47d862c5fb22223c3e6e54ae8d5b39e83a967c", "install": true }, { @@ -409,8 +409,8 @@ "source": "modules/common/models/WorkspaceModel.qml", "destination": "modules/common/models/WorkspaceModel.qml", "mode": "0644", - "size": 2892, - "sha256": "51f454f46980ee60ad843a7eb52aa4c6414ddc0bccd1123836fbcdf6aa3e0432", + "size": 5094, + "sha256": "e4369db17058a2f761a4e2911098d085dcd470400fb0496b4cce050c886488ae", "install": true }, { @@ -729,8 +729,8 @@ "source": "modules/common/widgets/DockContextMenu.qml", "destination": "modules/common/widgets/DockContextMenu.qml", "mode": "0644", - "size": 13668, - "sha256": "5b0a0da7e3ca350f9cae52b70e8c44f8bdd9a532007f01badba3d91af1adbc80", + "size": 13718, + "sha256": "fdd323c4b322eb194fecdf012771d802f048d22dd80bd55ab75b4c8f21ceec8c", "install": true }, { @@ -1385,16 +1385,16 @@ "source": "modules/imi/bar/ActiveWindow.qml", "destination": "modules/imi/bar/ActiveWindow.qml", "mode": "0644", - "size": 3257, - "sha256": "64702e867ff34018fdbe4a6866fa6c5a8542e43551f2b32f206ea512d8dc4736", + "size": 3533, + "sha256": "5db493446b315bded584bb84411a04cac5c1a92a9cd6176fe5c3e7d85bfbe166", "install": true }, { "source": "modules/imi/bar/Bar.qml", "destination": "modules/imi/bar/Bar.qml", "mode": "0644", - "size": 25837, - "sha256": "83c8539f3669d1270bc2490e03573299d46fb3227f7338de872d3f4a0713937a", + "size": 25844, + "sha256": "6312f4faf6563207e2ac47e08a47e5228195633af241edfb93977739acec797e", "install": true }, { @@ -1449,8 +1449,8 @@ "source": "modules/imi/bar/BarPopupOverlay.qml", "destination": "modules/imi/bar/BarPopupOverlay.qml", "mode": "0644", - "size": 32916, - "sha256": "147ccd2872c67f5dd1d974c6625dc5f42f0fc5ca1f12ffc22ad7f400888580a7", + "size": 32960, + "sha256": "072a09e6c9385ef26bca6b0bae2809d622a4da6eae9aeb561cb0e2bfa990cfd2", "install": true }, { @@ -1777,8 +1777,8 @@ "source": "modules/imi/bar/Workspaces.qml", "destination": "modules/imi/bar/Workspaces.qml", "mode": "0644", - "size": 17846, - "sha256": "8e83f1aa4c6d805a78d8e72fee52d3afc711040cb267fe2dda15530b872bdc40", + "size": 17946, + "sha256": "93546b1fa78f67eff6a25f22702c77e03206ef8f64159c67b0835d8dbfd1844f", "install": true }, { @@ -1817,8 +1817,8 @@ "source": "modules/imi/editMode/EditModeChrome.qml", "destination": "modules/imi/editMode/EditModeChrome.qml", "mode": "0644", - "size": 4919, - "sha256": "9084da67ea5e0712b1e95489178ac47abfc30687b7bf044eb549c389b7bb096f", + "size": 4914, + "sha256": "e0fff46bddb4d075cdf4b359831b4bbe3b08a21334585515facf61a6f7881a7a", "install": true }, { @@ -2089,8 +2089,8 @@ "source": "services/HyprlandData.qml", "destination": "services/HyprlandData.qml", "mode": "0644", - "size": 2375, - "sha256": "efd2f8c55320763b4bcb91ad56ab5d9b0a66ff3b6b5826cc909389caabd4dcb5", + "size": 2640, + "sha256": "c202a4da04e6dd3a3db3009e06d64a6a1ee918016ebc52ce88fd5b0e497182f3", "install": true }, { @@ -2321,8 +2321,8 @@ "source": "services/WM.qml", "destination": "services/WM.qml", "mode": "0644", - "size": 1785, - "sha256": "f90c21dbd4524f1dd04252c9fc6467bd437748bf6f7f2fe8ddadd083e2904192", + "size": 4437, + "sha256": "ddf66fb7fc7817f093b2220c792dd99e2ee284d745a6e83a1de047b5c5bb2668", "install": true }, { @@ -2357,5 +2357,6 @@ "sha256": "b91a53f73089e76988abe0c8c5731ac6302025466ef7761f824b6602416104b0", "install": true } - ] + ], + "license": "GPL-3.0-only" } diff --git a/barstyles/imi/modules/common/models/WorkspaceModel.qml b/barstyles/imi/modules/common/models/WorkspaceModel.qml index a23cf32..5c307c3 100644 --- a/barstyles/imi/modules/common/models/WorkspaceModel.qml +++ b/barstyles/imi/modules/common/models/WorkspaceModel.qml @@ -1,7 +1,6 @@ pragma ComponentBehavior: Bound import QtQuick -import Quickshell.Wayland -import Quickshell.Hyprland +import Ryoku.Ui.Singletons import "../../../services" import ".." as C @@ -11,29 +10,110 @@ NestableObject { required property var screen readonly property string monitorName: screen?.name ?? "" - readonly property var hyprMonitor: Hyprland.monitorFor(screen) - readonly property var liveMonitorData: (HyprlandData.monitors && Array.isArray(HyprlandData.monitors)) ? HyprlandData.monitors.find(m => m && m.id === hyprMonitor?.id) : null - - readonly property Toplevel activeWindow: ToplevelManager.activeToplevel readonly property int shownCount: C.Config.options.bar.workspaces.shown readonly property bool showAllMonitors: C.Config.options.bar.workspaces.showAllMonitors - readonly property int activeNumber: hyprMonitor?.activeWorkspace?.id ?? 1 + readonly property bool dynamicModel: Wm.workspaceModel === "dynamic" || !WM.isHyprland + + readonly property var liveKeys: { + const nums = []; + const named = []; + const seen = ({}); + const list = Wm.workspaces || []; + for (let i = 0; i < list.length; i++) { + const w = list[i]; + if (!w || w.special === true) + continue; + if (!root.showAllMonitors && WM.onOtherScreen(w, root.monitorName)) + continue; + const num = WM.workspaceKey(w); + const label = (num !== null) ? String(num) : String(w.name ?? ""); + if (label === "" || seen[label]) + continue; + seen[label] = true; + if (num !== null) + nums.push(label); + else + named.push(label); + } + nums.sort((a, b) => Number(a) - Number(b)); + const out = nums.concat(named); + if (root.activeKey !== "" && out.indexOf(root.activeKey) === -1) + out.push(root.activeKey); + return out; + } + + readonly property string activeKey: { + const list = Wm.workspaces || []; + for (let i = 0; i < list.length; i++) { + const w = list[i]; + if (!w || WM.onOtherScreen(w, root.monitorName)) + continue; + if (w.active === true || w.focused === true || w.isActive === true || w.isFocused === true) + return String(w.name ?? ""); + } + const focused = Wm.focusedWorkspace; + if (focused && !WM.onOtherScreen(focused, root.monitorName) + && focused.name !== undefined && focused.name !== null) + return String(focused.name); + return String(WM.activeWorkspaceNumber(root.monitorName)); + } + + readonly property int activeIndex: { + if (!root.dynamicModel) + return (root.activeNumber - 1) % root.shownCount; + return Math.max(0, root.liveKeys.indexOf(root.activeKey)); + } + + readonly property int slotCount: root.dynamicModel ? Math.max(1, root.liveKeys.length) : root.shownCount + function slotKeyAt(index) { + if (root.dynamicModel) + return root.liveKeys[index] ?? ""; + return root.getWorkspaceIdAt(index); + } - readonly property bool currentWorkspaceNotFake: activeWindow?.activated ?? false - readonly property int fakeWorkspace: currentWorkspaceNotFake ? -9999 : activeNumber + readonly property int activeNumber: WM.activeWorkspaceNumber(root.monitorName) + + readonly property bool currentWorkspaceNotFake: root.activeWindow !== null && root.activeWindow !== undefined + readonly property int fakeWorkspace: (root.dynamicModel || currentWorkspaceNotFake) ? -9999 : activeNumber readonly property int group: Math.floor((activeNumber - 1) / shownCount) - readonly property var specialWorkspace: liveMonitorData?.specialWorkspace + readonly property var liveMonitorData: HyprlandData.monitors.find(m => m && m.name === root.monitorName) ?? null + readonly property var specialWorkspace: liveMonitorData?.specialWorkspace ?? null readonly property bool specialWorkspaceActive: Boolean(specialWorkspace && specialWorkspace.id !== 0 && specialWorkspace.name && specialWorkspace.name !== "") readonly property string specialWorkspaceName: specialWorkspaceActive ? (specialWorkspace.name.replace("special:", "") || "special") : "" - property list occupied: [] - property list biggestWindow: occupied.map((_, index) => { - const number = getWorkspaceIdAt(index) - return root.biggestWindowForNumber(number) - }) + readonly property var activeWindow: Wm.focusedWindow + + readonly property list occupied: { + const list = Wm.workspaces || []; + const out = []; + for (let i = 0; i < root.shownCount; i++) { + const thisWorkspaceId = root.getWorkspaceId(root.group, i); + let found = false; + for (let k = 0; k < list.length; k++) { + const w = list[k]; + if (!w || w.special === true) + continue; + if (!root.showAllMonitors && WM.onOtherScreen(w, root.monitorName)) + continue; + if (WM.workspaceKey(w) === thisWorkspaceId) { + found = true; + break; + } + } + out.push(found); + } + return out; + } + + readonly property list biggestWindow: { + const out = []; + for (let index = 0; index < root.occupied.length; index++) + out.push(root.biggestWindowForNumber(root.getWorkspaceId(root.group, index))); + return out; + } function getWorkspaceId(group, index) { return group * root.shownCount + index + 1 @@ -48,33 +128,4 @@ NestableObject { } return null; } - - function updateWorkspaceOccupied() { - root.occupied = Array.from({ length: root.shownCount }, (_, i) => { - const thisWorkspaceId = getWorkspaceId(root.group, i) - return Hyprland.workspaces.values.some(ws => ws.id === thisWorkspaceId) - }) - } - - Component.onCompleted: updateWorkspaceOccupied() - - Connections { - target: Hyprland.workspaces - function onValuesChanged() { - root.updateWorkspaceOccupied() - } - } - Connections { - target: Hyprland - function onFocusedWorkspaceChanged() { - root.updateWorkspaceOccupied() - } - } - - onGroupChanged: { - updateWorkspaceOccupied() - } - onShowAllMonitorsChanged: { - updateWorkspaceOccupied(); - } } diff --git a/barstyles/imi/modules/common/widgets/DockContextMenu.qml b/barstyles/imi/modules/common/widgets/DockContextMenu.qml index 4bf7cfd..b4de2e5 100644 --- a/barstyles/imi/modules/common/widgets/DockContextMenu.qml +++ b/barstyles/imi/modules/common/widgets/DockContextMenu.qml @@ -43,7 +43,7 @@ Item { readonly property int workspacesShown: Math.min( (Config.options?.overview.rows ?? 2) * (Config.options?.overview.columns ?? 5), 10) readonly property int workspaceGroup: Math.floor( - ((Hyprland.focusedMonitor?.activeWorkspace?.id ?? 1) - 1) / workspacesShown) + (WM.activeWorkspaceNumber() - 1) / workspacesShown) LiveDesktopEntry { id: liveDeskEntry @@ -91,7 +91,7 @@ Item { implicitHeight: menuBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 HyprlandFocusGrab { - active: true + active: WM.isHyprland windows: [contextPopup] onCleared: root.close() } @@ -207,9 +207,11 @@ Item { onClicked: { for (const toplevel of root.appToplevel.toplevels) { const addr = toplevel.HyprlandToplevel?.address + ?? toplevel.address + ?? toplevel.id; if (!addr) continue - Hyprland.dispatch(`hl.dsp.window.move({ workspace = ${wsButton.workspaceValue}, follow = false, window = "address:0x${addr}" })`) + WM.moveWindowToWorkspace(addr, wsButton.workspaceValue) } root.close() } diff --git a/barstyles/imi/modules/imi/bar/ActiveWindow.qml b/barstyles/imi/modules/imi/bar/ActiveWindow.qml index 76fe660..c0bf2bc 100644 --- a/barstyles/imi/modules/imi/bar/ActiveWindow.qml +++ b/barstyles/imi/modules/imi/bar/ActiveWindow.qml @@ -4,6 +4,7 @@ import "../../common" import "../../common/models" import "../../common/widgets" import "../../common/functions" +import Ryoku.Ui.Singletons import QtQuick import QtQuick.Layouts import Quickshell @@ -15,16 +16,22 @@ import Qt5Compat.GraphicalEffects Item { id: root property bool vertical: false - readonly property var monitor: WM.monitorFor(root.QsWindow.window?.screen) - readonly property Toplevel activeWindow: ToplevelManager.activeToplevel - property string activeWindowAddress: activeWindow?.HyprlandToplevel?.address ? `0x${activeWindow.HyprlandToplevel.address}` : "" - property bool focusingThisMonitor: WM.focusedMonitor?.name === monitor?.name - property var biggestWindow: HyprlandData.biggestWindowForWorkspace(monitor?.activeWorkspace?.id ?? 1) + readonly property string screenName: root.QsWindow.window?.screen?.name ?? "" + readonly property var focusedWindow: Wm.focusedWindow ?? ToplevelManager.activeToplevel + readonly property bool tlActivated: ToplevelManager.activeToplevel?.activated ?? false + property bool focusingThisMonitor: { + const fout = Wm.focusedOutput ?? ""; + if (fout !== "" && root.screenName !== "") + return fout === root.screenName; + return root.tlActivated && root.screenName !== ""; + } + readonly property int screenWorkspace: WM.activeWorkspaceNumber(root.screenName) + property var biggestWindow: HyprlandData.biggestWindowForWorkspace(root.screenWorkspace) property string activeAppClass: { - if (!root.focusingThisMonitor || !root.activeWindow?.activated) + if (!root.focusingThisMonitor || !root.focusedWindow) return root.biggestWindow?.class ?? "" - return root.activeWindow?.appId ?? root.biggestWindow?.class ?? "" + return root.focusedWindow?.appId ?? root.focusedWindow?.class ?? root.biggestWindow?.class ?? "" } property var mainAppIconSource: { @@ -69,8 +76,8 @@ Item { font.pixelSize: Appearance.font.pixelSize.smaller color: Appearance.colors.colSubtext elide: Text.ElideRight - text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ? - root.activeWindow?.appId : + text: root.focusingThisMonitor && root.focusedWindow && root.biggestWindow ? + (root.focusedWindow?.appId ?? root.focusedWindow?.class ?? "") : (root.biggestWindow?.class) ?? Translation.tr("Desktop") } StyledText { @@ -78,9 +85,9 @@ Item { font.pixelSize: Appearance.font.pixelSize.small color: Appearance.colors.colOnLayer0 elide: Text.ElideRight - text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ? - root.activeWindow?.title : - (root.biggestWindow?.title) ?? `${Translation.tr("Workspace")} ${monitor?.activeWorkspace?.id ?? 1}` + text: root.focusingThisMonitor && root.focusedWindow && root.biggestWindow ? + (root.focusedWindow?.title ?? "") : + (root.biggestWindow?.title) ?? `${Translation.tr("Workspace")} ${root.screenWorkspace}` } } } diff --git a/barstyles/imi/modules/imi/bar/Bar.qml b/barstyles/imi/modules/imi/bar/Bar.qml index 48ce8cf..f4bd1e2 100644 --- a/barstyles/imi/modules/imi/bar/Bar.qml +++ b/barstyles/imi/modules/imi/bar/Bar.qml @@ -5,6 +5,7 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland +import Ryoku.Ui.Singletons import "../../.." import "../../../services" import "../../common" @@ -69,9 +70,9 @@ Scope { property bool mustShow: hoverRegion.containsMouse || superShow || GlobalStates.editMode || ((GlobalStates.mediaControlsOpen || GlobalStates.sysTrayOverflowOpen) && Config?.options.bar.autoHide.dismissPopups) - property var thisMonitorData: HyprlandData.monitors.find(m => m.name === barRoot.screen?.name) - property bool monitorHasFullscreen: HyprlandData.workspaceById[thisMonitorData?.activeWorkspace?.id]?.hasfullscreen ?? false - property bool monitorHasSpecialOpen: (thisMonitorData?.specialWorkspace?.name ?? "") !== "" + property string screenName: barRoot.screen?.name ?? "" + property bool monitorHasFullscreen: screenName !== "" && Wm.outputHasFullscreen(screenName) + property bool monitorHasSpecialOpen: WM.isHyprland && ((HyprlandData.monitors.find(m => m.name === screenName)?.specialWorkspace?.name ?? "") !== "") // The zone lives on barSpaceReserver below, so this surface // never reconfigures for it. Do not put an `exclusiveZone` // back here, not even 0: writing that property at all forces @@ -91,8 +92,8 @@ Scope { ? 0 : Appearance.sizes.baseBarHeight + (Config.options.bar.cornerStyle === 1 ? Appearance.sizes.hyprlandGapsOut : 0) } WlrLayershell.namespace: "quickshell:bar" - // Overlay layer only while special workspace sits on top of a fullscreen window on this monitor, - // else Top layer so fullscreen apps cover the bar as normal (Hyprland buries Top layer under fullscreen+special). + // Overlay layer only while a scratchpad sits on top of a fullscreen window on this monitor, + // else Top layer so fullscreen apps cover the bar as normal (the compositor buries Top under fullscreen+special). WlrLayershell.layer: (monitorHasFullscreen && monitorHasSpecialOpen) ? WlrLayer.Overlay : WlrLayer.Top // A detached bar style (cornerStyle 3) holds the surface off the // screen edge by barDetachMargin. That gap is not part of the diff --git a/barstyles/imi/modules/imi/bar/BarPopupOverlay.qml b/barstyles/imi/modules/imi/bar/BarPopupOverlay.qml index 7644d76..767e768 100644 --- a/barstyles/imi/modules/imi/bar/BarPopupOverlay.qml +++ b/barstyles/imi/modules/imi/bar/BarPopupOverlay.qml @@ -5,6 +5,7 @@ import Quickshell import Quickshell.Hyprland import Quickshell.Wayland import "../../.." +import "../../../services" import "../../common" import "../../common/widgets" import "bar_popup_unroll.js" as BarPopupUnroll @@ -364,7 +365,7 @@ Scope { // card has stopped moving and is showing content at full size. HyprlandFocusGrab { id: cardGrab - active: !!overlayWindow.current?.pinnedOpen + active: WM.isHyprland && !!overlayWindow.current?.pinnedOpen && !overlayWindow.exiting && !overlayWindow.morphing && card.width > Appearance.sizes.elevationMargin * 2 diff --git a/barstyles/imi/modules/imi/bar/Workspaces.qml b/barstyles/imi/modules/imi/bar/Workspaces.qml index 5361f79..3703fd0 100644 --- a/barstyles/imi/modules/imi/bar/Workspaces.qml +++ b/barstyles/imi/modules/imi/bar/Workspaces.qml @@ -29,7 +29,7 @@ ButtonMouseArea { property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55 property real workspaceIconOpacityShrinked: 1 property real workspaceIconMarginShrinked: -Appearance.spacing.space50 - property int workspaceIndexInGroup: (wsModel.activeNumber - 1) % wsModel.shownCount + property int workspaceIndexInGroup: wsModel.activeIndex property real specialTextSize: workspaceButtonWidth * 0.5 Layout.alignment: vertical ? Qt.AlignHCenter : Qt.AlignVCenter @@ -53,7 +53,7 @@ ButtonMouseArea { } function switchWorkspaceToHovered() { - WM.switchWorkspace(wsModel.getWorkspaceIdAt(hoverIndex)); + WM.switchWorkspace(wsModel.slotKeyAt(hoverIndex)); } onPressed: mouse => { if (mouse.button == Qt.LeftButton) @@ -102,14 +102,14 @@ ButtonMouseArea { visible: false Repeater { - model: wsModel.shownCount + model: wsModel.slotCount delegate: Item { id: wsBg required property int index - readonly property int wsId: wsModel.getWorkspaceIdAt(index) - property bool currentOccupied: wsModel.occupied[index] && wsId != wsModel.fakeWorkspace - property bool previousOccupied: index > 0 && wsModel.occupied[index - 1] && (wsId - 1) != wsModel.fakeWorkspace - property bool nextOccupied: index < wsModel.shownCount - 1 && wsModel.occupied[index + 1] && (wsId + 1) != wsModel.fakeWorkspace + readonly property var wsKey: wsModel.slotKeyAt(index) + property bool currentOccupied: wsModel.occupied[index] && wsKey != wsModel.fakeWorkspace + property bool previousOccupied: index > 0 && wsModel.occupied[index - 1] && (Number(wsKey) - 1) != wsModel.fakeWorkspace + property bool nextOccupied: index < wsModel.slotCount - 1 && wsModel.occupied[index + 1] && (Number(wsKey) + 1) != wsModel.fakeWorkspace implicitWidth: root.workspaceButtonWidth implicitHeight: root.workspaceButtonWidth @@ -183,7 +183,7 @@ ButtonMouseArea { layer.enabled: true // For the masking Repeater { - model: wsModel.shownCount + model: wsModel.slotCount delegate: NumberWorkspaceItem {} } } @@ -207,7 +207,7 @@ ButtonMouseArea { z: 6 Repeater { - model: wsModel.shownCount + model: wsModel.slotCount delegate: WorkspaceItem { id: wsApp property var biggestWindow: wsModel.biggestWindow[index] @@ -354,7 +354,7 @@ ButtonMouseArea { component WorkspaceItem: Item { required property int index - readonly property int wsId: wsModel.getWorkspaceIdAt(index) + readonly property var wsKey: wsModel.slotKeyAt(index) implicitWidth: root.vertical ? root.barThickness : root.workspaceButtonWidth implicitHeight: root.vertical ? root.workspaceButtonWidth : root.barThickness } @@ -362,11 +362,14 @@ ButtonMouseArea { component NumberWorkspaceItem: WorkspaceItem { id: wsNum property bool hasBiggestWindow: !!wsModel.biggestWindow[index] - property int wsId: wsModel.getWorkspaceIdAt(index) - property color contentColor: (wsModel.occupied[wsNum.index] && wsId !== wsModel.fakeWorkspace) ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1Inactive + readonly property int wsNumber: { + const n = parseInt(String(wsNum.wsKey), 10); + return isNaN(n) ? -1 : n; + } + property color contentColor: (wsModel.occupied[wsNum.index] && wsNum.wsKey !== wsModel.fakeWorkspace) ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1Inactive readonly property var kanjiList: ["", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"] - readonly property string kanjiText: (wsId >= 1 && wsId <= 10) ? kanjiList[wsId] : String(wsId) + readonly property string kanjiText: (wsNum.wsNumber >= 1 && wsNum.wsNumber <= 10) ? kanjiList[wsNum.wsNumber] : String(wsNum.wsKey) readonly property bool appIconDisplayed: wsNum.hasBiggestWindow && (Config.options?.bar.workspaces.showAppIcons ?? true) && !root.superPressAndHeld FadeLoader { diff --git a/barstyles/imi/modules/imi/editMode/EditModeChrome.qml b/barstyles/imi/modules/imi/editMode/EditModeChrome.qml index ef462d5..af130b9 100644 --- a/barstyles/imi/modules/imi/editMode/EditModeChrome.qml +++ b/barstyles/imi/modules/imi/editMode/EditModeChrome.qml @@ -54,7 +54,7 @@ Scope { delegate: Loader { id: surfaceLoader required property var modelData - // This screen's Hyprland record, for the special-workspace gate. + // This screen's compositor record, for the scratchpad gate. // Found by name rather than by index: `Quickshell.screens` and // `HyprlandData.monitors` are two lists that agree today and are // not promised to stay in the same order. diff --git a/barstyles/imi/services/HyprlandData.qml b/barstyles/imi/services/HyprlandData.qml index 7b401d4..8610d1f 100644 --- a/barstyles/imi/services/HyprlandData.qml +++ b/barstyles/imi/services/HyprlandData.qml @@ -4,18 +4,11 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Hyprland +import Ryoku.Ui.Singletons as Ryoku -// Live Hyprland monitor / workspace / window facts, reconstructed on the shell's -// own Quickshell.Hyprland connection. Upstream (Immaterial Impulse) carried a -// service that polled `hyprctl -j`; here the same ipc shapes are read straight -// from the shell's live model, so the bar reads Hyprland without standing up a -// second poller or a second host process. Singleton { id: root - // One flat monitor ipc record per output (name, id, x, y, width, height, - // scale, activeWorkspace{id,name}, specialWorkspace{id,name}), exactly the - // `hyprctl -j monitors` shape the vendored consumers expect. readonly property var monitors: { const out = []; const mons = Hyprland.monitors ? Hyprland.monitors.values : []; @@ -24,11 +17,28 @@ Singleton { if (o) out.push(o); } + if (out.length > 0) + return out; + const screens = Quickshell.screens; + for (let i = 0; i < screens.length; i++) { + const s = screens[i]; + if (!s) + continue; + out.push({ + "name": s.name, + "id": i, + "x": 0, + "y": 0, + "width": s.width, + "height": s.height, + "scale": 1, + "activeWorkspace": Ryoku.Wm.outputByName(s.name)?.activeWorkspace ?? null, + "specialWorkspace": { "id": 0, "name": "" } + }); + } return out; } - // Workspace ipc records keyed by id; each carries `hasfullscreen`, `name`, - // `monitor`, etc. Used for the bar's fullscreen gate. readonly property var workspaceById: { const map = ({}); const wss = Hyprland.workspaces ? Hyprland.workspaces.values : []; @@ -40,9 +50,6 @@ Singleton { return map; } - // The largest window sitting on a workspace, by pixel area, returned as its - // raw ipc object (`class`, `title`, `size`, `workspace`). Null when the - // workspace holds no window. function biggestWindowForWorkspace(workspaceId) { const tls = Hyprland.toplevels ? Hyprland.toplevels.values : []; let best = null; @@ -58,6 +65,21 @@ Singleton { best = o; } } - return best; + if (best) + return best; + const wins = Ryoku.Wm.windows || []; + for (let i = 0; i < wins.length; i++) { + const w = wins[i]; + if (!w) + continue; + const key = parseInt(String(w.workspace), 10); + if (key !== workspaceId && String(w.workspace) !== String(workspaceId)) + continue; + return { + "class": w.appId ?? w.class ?? "", + "title": w.title ?? "" + }; + } + return null; } } diff --git a/barstyles/imi/services/WM.qml b/barstyles/imi/services/WM.qml index aa24f3d..d6ee92e 100644 --- a/barstyles/imi/services/WM.qml +++ b/barstyles/imi/services/WM.qml @@ -4,37 +4,82 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Hyprland +import Ryoku.Ui.Singletons as Ryoku Singleton { id: root - // The output Hyprland currently has keyboard focus on, read from the shell's - // live connection. Widgets compare its name against their own monitor. - readonly property var focusedMonitor: Hyprland.focusedMonitor + readonly property bool isHyprland: (Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") || "") !== "" - function switchWorkspace(id) { - if (id !== undefined && id !== null) { - const cmd = "hl.dsp.focus({ workspace = " + id + " })"; - try { - Hyprland.dispatch(cmd); - } catch (e) { - Quickshell.execDetached(["hyprctl", "dispatch", cmd]); + readonly property var focusedMonitor: ({ "name": Ryoku.Wm.focusedOutput ?? "" }) + + function workspaceKey(w) { + if (!w) + return null; + const name = parseInt(String(w.name), 10); + if (!isNaN(name) && name >= 1) + return name; + const idx = parseInt(String(w.idx), 10); + if (!isNaN(idx) && idx >= 1) + return idx; + const id = parseInt(String(w.id), 10); + if (!isNaN(id) && id >= 1) + return id; + return null; + } + + function onOtherScreen(w, screenName) { + if (screenName === undefined || screenName === null || String(screenName) === "") + return false; + const out = (w && w.output !== undefined && w.output !== null) ? String(w.output) : ""; + return out !== "" && out !== String(screenName); + } + + function activeWorkspaceNumber(screenName) { + const list = Ryoku.Wm.workspaces || []; + for (let i = 0; i < list.length; i++) { + const w = list[i]; + if (!w) + continue; + if (root.onOtherScreen(w, screenName)) + continue; + if (w.active === true || w.focused === true || w.isActive === true || w.isFocused === true) { + const key = root.workspaceKey(w); + if (key !== null) + return key; } } + const focused = Ryoku.Wm.focusedWorkspace; + if (focused && !root.onOtherScreen(focused, screenName)) { + const key = root.workspaceKey(focused); + if (key !== null) + return key; + } + return 1; + } + + function switchWorkspace(id) { + if (id === undefined || id === null || id === "") + return; + if (!root.isHyprland) { + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", String(id)]); + return; + } + Ryoku.Wm.focusWorkspace(String(id)); } function switchWorkspaceRelative(direction) { - const offset = (direction === "next" ? "+1" : "-1"); - const cmd = "hl.dsp.focus({ workspace = \"r" + offset + "\" })"; - try { - Hyprland.dispatch(cmd); - } catch (e) { - Quickshell.execDetached(["hyprctl", "dispatch", cmd]); + if (!root.isHyprland) { + const step = direction === "next" ? 1 : -1; + const target = Math.max(1, root.activeWorkspaceNumber("") + step); + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", String(target)]); + return; } + Ryoku.Wm.cycleWorkspace(direction === "next" ? 1 : -1); } function focusWindow(address) { - if (address) { + if (address && root.isHyprland) { const cmd = "hl.dsp.focus({ window = \"address:" + address + "\" })"; try { Hyprland.dispatch(cmd); @@ -45,7 +90,7 @@ Singleton { } function closeWindow(address) { - if (address) { + if (address && root.isHyprland) { const cmd = "hl.dsp.window.close({ window = \"address:" + address + "\" })"; try { Hyprland.dispatch(cmd); @@ -55,7 +100,33 @@ Singleton { } } + function moveWindowToWorkspace(windowId, workspace) { + if (windowId === undefined || windowId === null || workspace === undefined || workspace === null) + return; + const addr = String(windowId).replace(/^0x/, ""); + const isAddress = /^[0-9a-fA-F]+$/.test(addr); + if (root.isHyprland && isAddress) { + const cmd = "hl.dsp.window.move({ workspace = " + workspace + ", follow = false, window = \"address:0x" + addr + "\" })"; + try { + Hyprland.dispatch(cmd); + } catch (e) { + Quickshell.execDetached(["hyprctl", "dispatch", cmd]); + } + return; + } + try { + Ryoku.Wm.moveWindowToWorkspace(String(windowId), String(workspace)); + } catch (e) { + } + } + function monitorFor(screen) { - return Hyprland.monitorFor(screen); + if (root.isHyprland) { + const mon = Hyprland.monitorFor(screen); + if (mon) + return mon; + } + const name = screen?.name ?? ""; + return name ? { "name": name } : null; } } diff --git a/barstyles/registry.json b/barstyles/registry.json index 103bdd6..698b4cc 100644 --- a/barstyles/registry.json +++ b/barstyles/registry.json @@ -48,14 +48,12 @@ { "id": "imi", "name": "Imi", - "version": "1.0.1", + "version": "1.1.0", "path": "barstyles/imi", "author": "dodo986", "upstream": "https://github.com/ryoku-dev/ryostore/tree/main/barstyles/imi", "summary": "Material 3 Island Bar", "description": "Material 3-Like design with morphing popouts, kanji workspaces, audio visualizer, and customizable widget pills.", - "windowManager": "hyprland", - "windowManagerReason": "Written against Hyprland's own window and workspace model, so it does not run on niri yet.", "tags": [ "top", "material3", @@ -69,8 +67,8 @@ "preview": "assets/preview.png", "screenshots": [], "manifest": "manifest.json", - "manifestSha256": "b9b2b59fa99ab0b02b26be3ce1905c9e71988543608f916e5d1d22c61624a3b7", - "lastUpdated": "2026-09-08", + "manifestSha256": "6fb10dd4fb4a0c59b838bd9e6079eb6f2a468395b885bc4d9770121ff005e73c", + "lastUpdated": "2026-10-07", "official": false, "license": "GPL-3.0-only" },