diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml index 91ff4e4..09f812b 100644 --- a/.github/workflows/package-release.yml +++ b/.github/workflows/package-release.yml @@ -24,6 +24,7 @@ jobs: set -euo pipefail bash -n packaging/aur/PKGBUILD bash -n packaging/shibumi-shell + python3 tests/production-boundary-regression.py python3 tests/test_package_release.py python3 tests/test_shibumi_manager.py python3 tests/test_shibumi_suite.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e5f35de..16adf0a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -560,12 +560,12 @@ Current Phase 2 foundation: feature data and workers live in independently validated plugins; - fail-closed schema-1 parser for V1 group order, splits, and resource-bounded widget settings; -- one root-owned procfs telemetry service shared by every output; +- one process-wide `hancore.shibumi.telemetry` procfs service shared by every output; - internal CPU and memory widgets with compact/full horizontal presentations; - screen-local, on-demand CPU and memory panels; - GPU telemetry is inactive unless a CPU panel is open and tolerates missing or unusable `nvidia-smi` by falling back to DRM sysfs or no GPU row. -- one root-owned Hyprland workspace model with V1 persist-10, persist-5, active, +- one process-wide `hancore.shibumi.workspaces` Hyprland model with V1 persist-10, persist-5, active, default, numbers, and magic presentation contracts; - a validated Quattro workspace action adapter, per-screen Shibumi workspace widget, and lifecycle-lazy keyboard panel whose backend-free content is @@ -573,8 +573,8 @@ Current Phase 2 foundation: - the Control Center owns Shibumi settings while Omarchy exclusively owns the application launcher menu. Disabled widget loaders and the default-off G7 service construct no backend work; -- one root-owned minute-precision clock service and one root-owned weather - service feed a single V1-compatible G8 center composite on every output; +- one process-wide `hancore.shibumi.center` clock and weather service feeds a + single V1-compatible G8 center composite on every output; the composite owns weather, clock/date/calendar, and active-only status presentation while retaining the official weather detail workflow and official Omarchy idle/notification state owners; @@ -615,7 +615,7 @@ Current Phase 2 foundation: registration, model release, and teardown. Real Wayland panel mapping remains an acceptance gate. - G7 replaces the stock AI presentation with one selected-provider Shibumi - pill and one lazy local panel. A single root-owned `AiUsageService` consumes + pill and one lazy local panel. The process-wide `hancore.shibumi.ai` service consumes the primitive schema-v1 records produced by current `omarchy.agents`; it never loads the host Agents panel or exposes host backend objects to views. The service owns one bounded update process and two watched Claude/Codex @@ -649,7 +649,7 @@ Current Phase 2 foundation: paths per frame. the validation system accepts the real-player, unavailable/crash, retry/cleanup, Top/Bottom, single-output visual, and resource slices. Multiple real players and physical multi-output acceptance remain gates. -- G11 uses one root-owned `NetworkService`, regardless of output count. It +- G11 uses one process-wide `hancore.shibumi.network` service, regardless of output count. It hosts the registered `omarchy.network` component as the authoritative `Quickshell.Networking`, status, scan, DNS, and visible-network action owner while suppressing its stock button, popup, and IPC handler; @@ -666,8 +666,8 @@ Current Phase 2 foundation: is not restored. Top Wayland mapping and cleanup pass on the validation system; the new direct speed-test path, bottom, mutation, and physical multi-output remain runtime gates. -- G13 has one root-owned `MonitorService` around the registered - `omarchy.monitor` component. That hidden component remains the only +- G13 has one process-wide `hancore.shibumi.brightness` service around the + registered `omarchy.monitor` component. That hidden component remains the only brightness, display, scale, IPC, poller, and command owner; - each output owns only its V1 brightness presentation and a lazy local Shibumi panel. The panel delegates brightness, scale, and display mutations to the @@ -676,20 +676,20 @@ Current Phase 2 foundation: mapping, reversible laptop brightness mutation, and a real `1.0 -> 1.25 -> 1.0` scale round trip pass on the validation system; physical display enable/disable and multi-output behavior remain gates. -- G12 and G14 are separate V1 battery and power-profile presentations over one - root-owned `PowerService`. Battery state stays event-driven through the +- G12 and G14 are separate V1 battery and power-profile presentations over the + process-wide `hancore.shibumi.power-state` service. Battery state stays event-driven through the shared UPower singleton, battery details are panel-lifecycle gated, and one profile refresh/set owner serves every output. The combined `omarchy.power` alias is consumed so it cannot run beside the split views; G14 remains available on batteryless desktops. the validation system passes a real discharging-to-charging transition with matching kernel, UPower, helper, widget, and panel state. -- G15 has one root-owned `BluetoothService` and one native +- G15 has one process-wide `hancore.shibumi.bluetooth` service and one native `BluetoothBackendAdapter`. The adapter owns Quickshell's BlueZ/PipeWire models, pairing/device actions, pending state, and Bluetooth-audio handoff; no complete Omarchy Bluetooth UI component is instantiated as a backend; - each output owns only its V1 Bluetooth presentation and lazy Shibumi device - panel. The root service leases discovery across open panels and owns one + panel. The process-wide service leases discovery across open panels and owns one symmetric six-method `omarchy.bluetooth` IPC target. Presentation has no Bluetooth/PipeWire import, process, timer, or file watcher; the service facade owns one bounded discovery-reconciliation timer and the adapter owns four @@ -700,7 +700,7 @@ Current Phase 2 foundation: lifecycle pass on the validation system; real device/audio, bottom, and physical multi-output gates remain. - the interaction foundation has a pure fixed-group layout model, one - root-owned persistent controller, and one transient drag session per output; + bar-owned persistent controller, and one transient drag session per output; - the group renderer resolves Shibumi and official Quattro widgets without duplicate state owners, preserves custom host-layout extras, omits empty groups without reserving space, and renders persisted within-section split diff --git a/adapters/AudioPanelBridge.qml b/adapters/AudioPanelBridge.qml deleted file mode 100644 index 9bea85b..0000000 --- a/adapters/AudioPanelBridge.qml +++ /dev/null @@ -1,301 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick - -// Keeps Quattro's audio component as the single PipeWire/device/action owner. -// Shibumi renders its own bar and popup presentation, while this bridge exposes -// only state and commands from the hidden official instance. -Item { - id: root - - required property var bar - required property Item ownerWidget - property Component panelComponent: null - property url panelSource: "" - property var panelSettings: ({}) - - readonly property var panel: panelLoader.item - readonly property bool ready: panel !== null - readonly property var sink: ready && panel.sink !== undefined ? panel.sink : null - readonly property var source: ready && panel.source !== undefined ? panel.source : null - readonly property var audioSinks: ready && panel.audioSinks !== undefined - ? groupedDeviceNodes(panel.audioSinks) : [] - readonly property var audioSources: ready && panel.audioSources !== undefined - ? panel.audioSources : [] - readonly property var audioStreams: ready && panel.audioStreams !== undefined - ? panel.audioStreams : [] - readonly property real outputVolume: ready - && panel.outputVolume !== undefined ? Number(panel.outputVolume) : 0 - readonly property bool outputMuted: ready - && panel.outputMuted !== undefined ? panel.outputMuted === true : false - readonly property real inputVolume: ready - && panel.inputVolume !== undefined ? Number(panel.inputVolume) : 0 - readonly property bool inputMuted: ready - && panel.inputMuted !== undefined ? panel.inputMuted === true : false - - function toggleOutputMute() { - if (!ready || typeof panel.toggleOutputMute !== "function") return false - panel.toggleOutputMute() - return true - } - - function setOutputVolume(value) { - if (!ready || typeof panel.setOutputVolume !== "function") return false - panel.setOutputVolume(Math.max(0, Math.min(1, Number(value) || 0))) - return true - } - - function toggleInputMute() { - if (!ready || typeof panel.toggleInputMute !== "function") return false - panel.toggleInputMute() - return true - } - - function setInputVolume(value) { - if (!ready || typeof panel.setInputVolume !== "function") return false - panel.setInputVolume(Math.max(0, Math.min(1, Number(value) || 0))) - return true - } - - function setDefaultSink(node) { - if (!ready || !node || typeof panel.setDefaultSink !== "function") return false - panel.setDefaultSink(node) - return true - } - - function setDefaultSource(node) { - if (!ready || !node || typeof panel.setDefaultSource !== "function") return false - panel.setDefaultSource(node) - return true - } - - function setStreamVolume(node, value) { - if (!node || !node.audio) return false - node.audio.volume = Math.max(0, Math.min(1.5, Number(value) || 0)) - return true - } - - function toggleStreamMute(node) { - if (!node || !node.audio) return false - node.audio.muted = !node.audio.muted - return true - } - - function callLabelHelper(name, node, fallback) { - if (!ready || typeof panel[name] !== "function") return String(fallback || "") - return String(panel[name](node) || fallback || "") - } - - function usableDeviceLabel(value) { - const label = String(value || "").trim() - return !label || label === "(null)" ? "" : label - } - - function nodeProperties(node) { - return node && node.ready !== false && node.properties - ? node.properties : ({}) - } - - function nodeDeviceKey(node) { - const properties = nodeProperties(node) - const deviceId = usableDeviceLabel(properties["device.id"]) - if (deviceId) return "id:" + deviceId - - const deviceName = usableDeviceLabel(properties["device.name"]) - if (deviceName) return "device:" + deviceName.toLowerCase() - - const nodeName = usableDeviceLabel(node && node.name) - const profileSeparator = nodeName.lastIndexOf(".") - return "node:" + (profileSeparator > 0 - ? nodeName.slice(0, profileSeparator) : nodeName).toLowerCase() - } - - function nodeProfileKey(node) { - const properties = nodeProperties(node) - return usableDeviceLabel( - properties["device.profile.description"]).toLowerCase() - } - - function groupedDeviceNodes(values) { - const groups = [] - if (!values) return [] - - for (let i = 0; i < values.length; i++) { - const node = values[i] - if (!node) continue - const key = nodeDeviceKey(node) || "index:" + i - let group = null - for (let j = 0; j < groups.length; j++) { - if (groups[j].key === key) { - group = groups[j] - break - } - } - if (!group) { - group = { key: key, nodes: [] } - groups.push(group) - } - group.nodes.push(node) - } - - const result = [] - for (let i = 0; i < groups.length; i++) { - groups[i].nodes.sort(function(left, right) { - const leftProfile = nodeProfileKey(left) - const rightProfile = nodeProfileKey(right) - return leftProfile < rightProfile ? -1 - : leftProfile > rightProfile ? 1 : 0 - }) - for (let j = 0; j < groups[i].nodes.length; j++) - result.push(groups[i].nodes[j]) - } - return result - } - - function descriptiveNodeLabel(node) { - if (!node) return "" - const properties = nodeProperties(node) - const profile = usableDeviceLabel(properties["device.profile.description"]) - let label = usableDeviceLabel(node.description) - if (label && profile - && label.toLowerCase().indexOf(profile.toLowerCase()) < 0) - return label + " " + profile - if (label) return label - - label = usableDeviceLabel(properties["node.description"]) - if (label && profile - && label.toLowerCase().indexOf(profile.toLowerCase()) < 0) - return label + " " + profile - if (label) return label - - const device = usableDeviceLabel(properties["device.description"]) - if (device && profile - && device.toLowerCase().indexOf(profile.toLowerCase()) < 0) - return device + " " + profile - return device || profile - } - - function nodeLabel(node) { - return descriptiveNodeLabel(node) - || callLabelHelper("nodeLabel", node, "Audio device") - } - function sinkGlyph(node) { return callLabelHelper("sinkGlyph", node, "speaker") } - function sourceGlyph(node) { return callLabelHelper("sourceGlyph", node, "mic") } - function streamLabel(node) { return callLabelHelper("streamLabel", node, "Application") } - - function suppressBackendKeyboardPanel() { - if (!panel || !panel.data || panel.data.length === undefined) return false - let suppressed = false - for (let index = 0; index < panel.data.length; index++) { - const candidate = panel.data[index] - if (!candidate || typeof candidate.beginFocusPrime !== "function" - || !("anchorItem" in candidate) || !("open" in candidate) - || !("visible" in candidate) || !("owner" in candidate) - || candidate.owner !== panel) continue - candidate.open = false - candidate.visible = false - suppressed = true - } - return suppressed - } - - function injectPanel() { - if (!panel) return - suppressBackendKeyboardPanel() - if ("bar" in panel) panel.bar = hostProxy - if ("moduleName" in panel) panel.moduleName = "omarchy.audio" - if ("settings" in panel) panel.settings = panelSettings - if ("manageIpc" in panel) panel.manageIpc = false - if (panel.opened === true && typeof panel.close === "function") panel.close() - // The official button and popup remain instantiated for their backend - // state, but Shibumi owns the only visible presentation and click target. - panel.opacity = 0 - } - - function syncPanelSource() { - panelLoader.sourceComponent = null - panelLoader.source = "" - if (panelComponent !== null) { - panelLoader.sourceComponent = panelComponent - } else if (String(panelSource)) { - panelLoader.setSource(panelSource, { - bar: hostProxy, - moduleName: "omarchy.audio", - manageIpc: false, - settings: panelSettings - }) - } - } - - onPanelSettingsChanged: injectPanel() - onBarChanged: injectPanel() - onPanelComponentChanged: Qt.callLater(syncPanelSource) - onPanelSourceChanged: Qt.callLater(syncPanelSource) - Component.onCompleted: Qt.callLater(syncPanelSource) - Component.onDestruction: { - const currentPanel = panel - if (currentPanel && currentPanel.opened === true - && typeof currentPanel.close === "function") currentPanel.close() - // Destroy the official panel while its host facade is still valid. Setting - // panel.bar to null first reevaluates unguarded upstream bindings. - panelLoader.active = false - } - - QtObject { - id: hostProxy - - readonly property var realBar: root.bar - readonly property bool vertical: realBar ? realBar.vertical === true : false - readonly property int barSize: realBar ? Number(realBar.barSize) : 35 - readonly property int sizeHorizontal: realBar && realBar.sizeHorizontal !== undefined - ? Number(realBar.sizeHorizontal) : barSize - readonly property string position: realBar ? String(realBar.position || "top") : "top" - readonly property string fontFamily: realBar ? String(realBar.fontFamily || "monospace") : "monospace" - readonly property color background: realBar && realBar.background !== undefined - ? realBar.background : "#111111" - readonly property color barBackground: background - readonly property color foreground: realBar ? realBar.foreground : "#ffffff" - readonly property color barForeground: foreground - readonly property color urgent: realBar ? realBar.urgent : foreground - readonly property bool foregroundAnimationEnabled: realBar - ? realBar.foregroundAnimationEnabled !== false : false - readonly property var shell: realBar ? realBar.shell : null - readonly property var activePopout: realBar ? realBar.activePopout : null - readonly property var clickTargets: realBar ? realBar.clickTargets : [] - - // The hidden official button must not become a second click target. - function registerClickTarget(_target) {} - function unregisterClickTarget(_target) {} - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - - function requestPopout(owner) { - if (realBar && typeof realBar.requestPopout === "function") - realBar.requestPopout(owner) - } - - function releasePopout(owner) { - if (realBar && typeof realBar.releasePopout === "function") - realBar.releasePopout(owner) - } - - function switchPanelFrom(_owner, direction) { - return realBar && typeof realBar.switchPanelFrom === "function" - ? realBar.switchPanelFrom(root.ownerWidget, direction) : false - } - - function targetBelongsToWindow(target, window) { - return realBar && typeof realBar.targetBelongsToWindow === "function" - ? realBar.targetBelongsToWindow(target, window) : false - } - } - - Loader { - id: panelLoader - anchors.fill: parent - onLoaded: { - root.injectPanel() - Qt.callLater(root.injectPanel) - } - } -} diff --git a/adapters/BluetoothBackendAdapter.qml b/adapters/BluetoothBackendAdapter.qml deleted file mode 100644 index c4375bd..0000000 --- a/adapters/BluetoothBackendAdapter.qml +++ /dev/null @@ -1,535 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Bluetooth -import Quickshell.Services.Pipewire -import "." as Local -import "BluetoothModel.js" as Model - -// The process-wide Bluetooth state and action owner. This deliberately uses -// Quickshell's data APIs directly; no foreign UI component is instantiated. -Item { - id: root - - // Runtime tests inject a side-effect-free backend. Production leaves this - // null and therefore uses the native BlueZ/PipeWire implementation below. - property var backendOverride: null - property var adapterOverride: null - // Lets the component smoke test drive native device property transitions - // without touching the host's real Bluetooth devices. - property var nativeDevicesOverride: null - property var pipewireNodesOverride: null - property var commandRunnerOverride: null - property var audioOutputOverride: null - property int audioSwitchInterval: 500 - property int audioIntentTimeoutInterval: 60000 - property int discoveryRequestTimeoutInterval: 1500 - property int discoveryDestructionGuardInterval: 30000 - property bool discoveryOwned: false - property var discoveryOwnerAdapter: null - property var discoveryRequestedAdapter: null - property var retiredDiscoveryAdapter: null - property bool discoveryDesired: false - property var nativePendingActions: ({}) - property var audioHandoffIntent: null - property var pendingAudioOutputDevice: null - property int pendingAudioOutputAttempts: 0 - - readonly property var backend: backendOverride !== null ? backendOverride : root - readonly property bool ready: true - readonly property var adapter: backendOverride !== null - ? ("adapter" in backendOverride ? backendOverride.adapter : null) - : (adapterOverride !== null ? adapterOverride : Bluetooth.defaultAdapter) - readonly property bool adapterAvailable: adapter !== null - readonly property bool radioEnabled: adapterAvailable - && adapter.enabled !== undefined && adapter.enabled === true - readonly property bool discovering: adapterAvailable - && adapter.discovering !== undefined && adapter.discovering === true - readonly property var nativeDevices: nativeDevicesOverride !== null - ? nativeDevicesOverride - : (backendOverride === null && Bluetooth.devices ? Bluetooth.devices.values : []) - readonly property var pipewireNodes: pipewireNodesOverride !== null - ? pipewireNodesOverride - : (backendOverride === null && Pipewire.nodes ? Pipewire.nodes.values : []) - readonly property var nativeDeviceGroups: Model.deviceLists(nativeDevices) - readonly property var connectedDevices: backendOverride !== null - ? backendList("connectedDevices") : nativeDeviceGroups.connected - readonly property var knownDevices: backendOverride !== null - ? backendList("knownDevices") : nativeDeviceGroups.known - readonly property var discoveredDevices: backendOverride !== null - ? backendList("discoveredDevices") : nativeDeviceGroups.discovered - readonly property var pendingActions: backendOverride !== null - && "pendingActions" in backendOverride - ? backendOverride.pendingActions : nativePendingActions - - visible: false - width: 0 - height: 0 - - function backendList(name) { - if (backendOverride === null || !(name in backendOverride)) return [] - const values = backendOverride[name] - return Array.isArray(values) ? values : [] - } - - function deviceLabel(device) { - if (backendOverride !== null - && typeof backendOverride.deviceLabel === "function") - return String(backendOverride.deviceLabel(device) || "") - return Model.deviceLabel(device) - } - - function pendingAction(address) { - const key = String(address || "") - if (backendOverride !== null - && typeof backendOverride.pendingAction === "function") - return String(backendOverride.pendingAction(key) || "") - return key && nativePendingActions[key] ? nativePendingActions[key] : "" - } - - // discoveryOwned is only set if Shibumi changed false -> true. An already - // active external scan is observed but never claimed or stopped by Shibumi. - function startDiscovery() { - if (!adapterAvailable || !radioEnabled) return false - // A replacement service must first retire a teardown guard from its own - // predecessor. If that pending start has already completed, inherit the - // proven Shibumi request instead of misclassifying it as external and - // letting the old guard stop it. - const target = adapter - const inheritedRequest = Local.BluetoothDiscoveryGuard.disarm(target) - // Read the native property directly: the derived QML binding may still - // hold the previous value inside the adapter's discoveringChanged turn. - if (target && !!target.discovering) { - if (inheritedRequest) { - discoveryRequestedAdapter = null - if (retiredDiscoveryAdapter === target) - retiredDiscoveryAdapter = null - discoveryOwned = true - discoveryOwnerAdapter = target - if (!retiredDiscoveryAdapter) discoveryRequestTimeout.stop() - } - return true - } - if (discoveryRequestedAdapter === target) return true - if (retiredDiscoveryAdapter === target) retiredDiscoveryAdapter = null - discoveryRequestedAdapter = target - discoveryRequestTimeout.restart() - requestAdapterDiscovery(target) - return true - } - - function requestAdapterDiscovery(target) { - if (backendOverride !== null - && typeof backendOverride.requestDiscovery === "function") - backendOverride.requestDiscovery(target) - else target.discovering = true - } - - function confirmRequestedDiscovery() { - const requested = discoveryRequestedAdapter - if (!requested || !requested.discovering) return - discoveryRequestedAdapter = null - if (!discoveryDesired || requested !== adapter) { - retiredDiscoveryAdapter = requested - requested.discovering = false - return - } - discoveryOwned = true - discoveryOwnerAdapter = requested - if (!retiredDiscoveryAdapter) discoveryRequestTimeout.stop() - } - - function retirePendingDiscovery() { - const requested = discoveryRequestedAdapter - if (!requested) return - discoveryRequestedAdapter = null - retiredDiscoveryAdapter = requested - discoveryRequestTimeout.restart() - if (requested.discovering !== undefined && requested.discovering) - requested.discovering = false - } - - function expireDiscoveryRequestWindow() { - confirmRequestedDiscovery() - if (discoveryRequestedAdapter && !discoveryRequestedAdapter.discovering) - discoveryRequestedAdapter = null - const retired = retiredDiscoveryAdapter - retiredDiscoveryAdapter = null - if (retired && retired.discovering !== undefined && retired.discovering) - retired.discovering = false - } - - function stopDiscovery() { - retirePendingDiscovery() - const owner = discoveryOwnerAdapter - if (discoveryOwned && owner && owner.discovering !== undefined - && owner.discovering) - owner.discovering = false - discoveryOwned = false - discoveryOwnerAdapter = null - return true - } - - function destroyDiscovery() { - const requested = discoveryRequestedAdapter - const retired = retiredDiscoveryAdapter - if (requested) - Local.BluetoothDiscoveryGuard.arm(requested, discoveryDestructionGuardInterval) - if (retired && retired !== requested) - Local.BluetoothDiscoveryGuard.arm(retired, discoveryDestructionGuardInterval) - stopDiscovery() - } - - function restartDiscovery() { - if (!adapterAvailable || !radioEnabled) return false - // A scan that was already active before Shibumi opened is externally - // owned. Refresh must observe it without toggling or claiming it. - if (discovering && !discoveryOwned) return true - if (discovering) return stopDiscovery() - return startDiscovery() - } - - function toggleBluetooth() { - if (!adapterAvailable) return false - if (backendOverride !== null) { - if (typeof backendOverride.toggleBluetooth !== "function") return false - backendOverride.toggleBluetooth() - return true - } - if (radioEnabled) stopDiscovery() - adapter.enabled = !adapter.enabled - return true - } - - function setNativePendingAction(address, action) { - if (!address) return - nativePendingActions = Model.withPendingAction( - nativePendingActions, String(address), String(action || "")) - if (action) pendingTimeout.restart() - } - - function rememberAudioHandoffIntent(device) { - if (!device || !device.address) return - // There can only be one default sink. A newer explicit connect request - // therefore replaces every older, not-yet-consumed handoff intent. - cancelPendingAudioOutput("") - audioHandoffIntent = { - address: String(device.address), - name: device.name ? String(device.name) : "", - deviceName: device.deviceName ? String(device.deviceName) : "" - } - audioIntentTimeout.restart() - } - - function clearAudioHandoffIntent(address) { - const key = String(address || "") - if (!audioHandoffIntent || !key - || String(audioHandoffIntent.address || "") !== key) return - audioHandoffIntent = null - audioIntentTimeout.stop() - } - - function cancelPendingAudioOutput(address) { - const key = String(address || "") - if (pendingAudioOutputDevice && (!key - || String(pendingAudioOutputDevice.address || "") === key)) { - pendingAudioOutputDevice = null - pendingAudioOutputAttempts = 0 - audioSwitchTimer.stop() - } - } - - function cancelAudioHandoff(address) { - clearAudioHandoffIntent(address) - cancelPendingAudioOutput(address) - } - - function cancelAllAudioHandoffs() { - audioHandoffIntent = null - audioIntentTimeout.stop() - cancelPendingAudioOutput("") - } - - function deviceCommand(action, address) { - return ["omarchy-bluetooth-device", String(action), String(address)] - } - - function executeDeviceCommand(command) { - if (commandRunnerOverride !== null - && typeof commandRunnerOverride.run === "function") { - commandRunnerOverride.run(command) - return - } - Quickshell.execDetached(command) - } - - function runNativeDeviceAction(device, action, pending) { - if (!device || !device.address) return false - setNativePendingAction(device.address, pending) - executeDeviceCommand(deviceCommand(action, device.address)) - return true - } - - function connectDevice(device) { - if (!device || device.connected) return false - if (backendOverride !== null) { - if (typeof backendOverride.connectDevice !== "function") return false - backendOverride.connectDevice(device) - return true - } - const action = device.paired || device.bonded || device.trusted - ? "connect" : "pair" - rememberAudioHandoffIntent(device) - return runNativeDeviceAction(device, action, "connecting") - } - - function disconnectDevice(device) { - if (!device || !device.address || !device.connected) return false - if (backendOverride !== null) { - if (typeof backendOverride.disconnectDevice !== "function") return false - backendOverride.disconnectDevice(device) - return true - } - cancelAudioHandoff(device.address) - setNativePendingAction(device.address, "disconnecting") - if (typeof device.disconnect === "function") device.disconnect() - executeDeviceCommand(deviceCommand("disconnect", device.address)) - return true - } - - function forgetDevice(device) { - if (!device || !device.address) return false - if (backendOverride !== null) { - if (typeof backendOverride.forgetDevice !== "function") return false - backendOverride.forgetDevice(device) - return true - } - cancelAudioHandoff(device.address) - return runNativeDeviceAction(device, "forget", "forgetting") - } - - function audioSinks() { - const sinks = [] - for (let i = 0; i < pipewireNodes.length; i++) { - const node = pipewireNodes[i] - if (node && node.isSink && !node.isStream) sinks.push(node) - } - return sinks - } - - function bluetoothAudioSink(device) { - const sinks = audioSinks() - for (let i = 0; i < sinks.length; i++) - if (Model.bluetoothSinkMatchesDevice(sinks[i], device)) return sinks[i] - return null - } - - function setDefaultAudioSink(sink) { - if (!sink) return - if (audioOutputOverride !== null - && typeof audioOutputOverride.setDefaultSink === "function") { - audioOutputOverride.setDefaultSink(sink) - return - } - Pipewire.preferredDefaultAudioSink = sink - if (sink.id === undefined || !sink.name) return - Quickshell.execDetached([ - "omarchy-audio-output-set-default", - String(sink.id), - String(sink.name) - ]) - } - - function scheduleAudioOutputSwitch(device) { - if (!device || !device.address || !device.connected) return - pendingAudioOutputDevice = { - address: device && device.address ? device.address : "", - name: device && device.name ? device.name : "", - deviceName: device && device.deviceName ? device.deviceName : "" - } - pendingAudioOutputAttempts = 0 - audioSwitchTimer.restart() - } - - function nativeDeviceByAddress(address) { - const key = String(address || "") - for (let i = 0; i < nativeDevices.length; i++) { - const device = nativeDevices[i] - if (device && String(device.address || "") === key) return device - } - return null - } - - function deviceUsesCurrentAdapter(device) { - return device && (device.adapter === undefined || device.adapter === null - || device.adapter === adapter) - } - - function validatePendingAudioOutput() { - if (!pendingAudioOutputDevice) return false - const device = nativeDeviceByAddress(pendingAudioOutputDevice.address) - if (!radioEnabled || !device || !device.connected - || !deviceUsesCurrentAdapter(device)) { - cancelPendingAudioOutput("") - return false - } - return true - } - - function switchPendingAudioOutput() { - if (!validatePendingAudioOutput()) return - const device = nativeDeviceByAddress(pendingAudioOutputDevice.address) - const sink = bluetoothAudioSink(device) - if (sink) { - setDefaultAudioSink(sink) - pendingAudioOutputDevice = null - audioSwitchTimer.stop() - return - } - pendingAudioOutputAttempts++ - if (pendingAudioOutputAttempts >= 8) { - pendingAudioOutputDevice = null - return - } - audioSwitchTimer.restart() - } - - function syncNativeAudioHandoffIntents() { - if (backendOverride !== null) return - const intent = audioHandoffIntent - if (intent) { - const device = nativeDeviceByAddress(intent.address) - if (device && device.connected && deviceUsesCurrentAdapter(device)) { - scheduleAudioOutputSwitch(device) - audioHandoffIntent = null - audioIntentTimeout.stop() - } - } - validatePendingAudioOutput() - } - - function syncNativePendingActions() { - if (backendOverride !== null) return - const next = Model.cloneMap(nativePendingActions) - let changed = false - - for (const address in next) { - const action = next[address] - let found = null - for (let i = 0; i < nativeDevices.length; i++) { - const device = nativeDevices[i] - if (device && device.address === address) { - found = device - break - } - } - - const finishedConnecting = action === "connecting" && found && found.connected - if (finishedConnecting - || (action === "disconnecting" && found && !found.connected) - || (action === "forgetting" && (!found - || (!found.paired && !found.bonded && !found.trusted)))) { - delete next[address] - changed = true - } - } - if (changed) nativePendingActions = next - } - - onNativeDevicesChanged: { - syncNativePendingActions() - syncNativeAudioHandoffIntents() - } - // ScriptModel.values changes when the collection changes, while a device's - // connection/pairing flags have their own signals. The derived group signals - // cover both paths and complete pending actions as soon as state settles. - onConnectedDevicesChanged: { - syncNativePendingActions() - syncNativeAudioHandoffIntents() - } - onKnownDevicesChanged: { - syncNativePendingActions() - syncNativeAudioHandoffIntents() - } - onDiscoveredDevicesChanged: { - syncNativePendingActions() - syncNativeAudioHandoffIntents() - } - onDiscoveryDesiredChanged: if (!discoveryDesired) stopDiscovery() - onRadioEnabledChanged: if (!radioEnabled) cancelAllAudioHandoffs() - onAdapterChanged: { - // Ownership is tied to the adapter instance on which Shibumi started the - // scan. Stop that scan before observing a replacement adapter as external. - retirePendingDiscovery() - const owner = discoveryOwnerAdapter - if (discoveryOwned && owner && owner !== adapter - && owner.discovering !== undefined && owner.discovering) - owner.discovering = false - discoveryOwned = false - discoveryOwnerAdapter = null - cancelAllAudioHandoffs() - } - Component.onDestruction: destroyDiscovery() - - Timer { - id: pendingTimeout - interval: 20000 - repeat: false - onTriggered: root.nativePendingActions = ({}) - } - - Timer { - id: audioSwitchTimer - interval: root.audioSwitchInterval - repeat: false - onTriggered: root.switchPendingAudioOutput() - } - - Timer { - id: audioIntentTimeout - interval: root.audioIntentTimeoutInterval - repeat: false - onTriggered: root.audioHandoffIntent = null - } - - Timer { - id: discoveryRequestTimeout - interval: root.discoveryRequestTimeoutInterval - repeat: false - onTriggered: root.expireDiscoveryRequestWindow() - } - - Connections { - target: root.discoveryRequestedAdapter - ignoreUnknownSignals: true - function onDiscoveringChanged() { root.confirmRequestedDiscovery() } - } - - Connections { - target: root.discoveryOwnerAdapter - ignoreUnknownSignals: true - function onDiscoveringChanged() { - const owner = root.discoveryOwnerAdapter - if (owner && !owner.discovering) { - root.discoveryOwned = false - root.discoveryOwnerAdapter = null - } - } - } - - Connections { - target: root.retiredDiscoveryAdapter - ignoreUnknownSignals: true - function onDiscoveringChanged() { - const retired = root.retiredDiscoveryAdapter - if (!retired) return - if (retired.discovering) retired.discovering = false - else { - root.retiredDiscoveryAdapter = null - if (!root.discoveryRequestedAdapter) - discoveryRequestTimeout.stop() - } - } - } -} diff --git a/adapters/BluetoothDiscoveryGuard.qml b/adapters/BluetoothDiscoveryGuard.qml deleted file mode 100644 index 8799e27..0000000 --- a/adapters/BluetoothDiscoveryGuard.qml +++ /dev/null @@ -1,166 +0,0 @@ -pragma Singleton - -import QtQuick - -QtObject { - id: root - - readonly property int initialRetryInterval: 250 - readonly property int maximumRetryInterval: 4000 - readonly property int settledStopInterval: 100 - property var guards: [] - - function guardIndex(adapter) { - for (let index = 0; index < guards.length; index++) - if (guards[index].adapter === adapter) return index - return -1 - } - - function isArmed(adapter) { - return guardIndex(adapter) >= 0 - } - - function guardCount() { - return guards.length - } - - function removeGuard(entry) { - if (!entry) return - const index = guards.indexOf(entry) - if (index < 0) return - guards.splice(index, 1) - const adapter = entry.adapter - entry.adapter = null - if (adapter) { - try { - adapter.discoveringChanged.disconnect(entry.handler) - } catch (_error) { - // The native adapter may disappear with the QML engine. There is then - // no BlueZ session left for this guard to reconcile. - } - } - const timer = entry.retryTimer - entry.retryTimer = null - if (timer) timer.destroy() - const watcher = entry.adapterWatcher - entry.adapterWatcher = null - if (watcher) { - watcher.armed = false - watcher.destroy() - } - } - - function disarm(adapter) { - const index = guardIndex(adapter) - if (index < 0) return false - removeGuard(guards[index]) - return true - } - - function adapterDestroyed(entry) { - if (!entry || guards.indexOf(entry) < 0) return - entry.adapter = null - entry.adapterWatcher = null - removeGuard(entry) - } - - function schedule(entry, interval) { - if (!entry || guards.indexOf(entry) < 0) return - const remaining = entry.expiresAt - Date.now() - if (remaining <= 0) { - removeGuard(entry) - return - } - entry.retryTimer.interval = Math.max(1, Math.min(interval, remaining)) - entry.retryTimer.restart() - } - - function reconcile(entry) { - if (!entry || guards.indexOf(entry) < 0) return - if (Date.now() >= entry.expiresAt) { - removeGuard(entry) - return - } - const adapter = entry.adapter - if (!adapter) { - removeGuard(entry) - return - } - let discovering = false - try { - discovering = !!adapter.discovering - } catch (_error) { - entry.adapter = null - removeGuard(entry) - return - } - if (discovering) { - entry.stopRequested = true - entry.stopSettlePending = false - try { - adapter.discovering = false - discovering = !!adapter.discovering - } catch (_error) { - entry.adapter = null - removeGuard(entry) - return - } - const retryInterval = entry.retryInterval - entry.retryInterval = Math.min(maximumRetryInterval, retryInterval * 2) - schedule(entry, discovering ? retryInterval : settledStopInterval) - } else if (entry.stopRequested) { - // StopDiscovery is asynchronous. Require the stopped state to survive a - // later event-loop turn before releasing the guard. - if (entry.stopSettlePending) removeGuard(entry) - else { - entry.stopSettlePending = true - schedule(entry, settledStopInterval) - } - } else { - // No delayed start has arrived yet. The signal handles it immediately; - // this wakeup exists only to enforce the hard deadline. - schedule(entry, entry.expiresAt - Date.now()) - } - } - - function arm(adapter, timeoutMs) { - if (!adapter || adapter.discovering === undefined - || !adapter.discoveringChanged) return false - disarm(adapter) - - const requestedInterval = Number(timeoutMs) - const interval = isFinite(requestedInterval) && requestedInterval > 0 - ? requestedInterval : 30000 - const entry = { - "adapter": adapter, - "expiresAt": Date.now() + interval, - "stopRequested": false, - "stopSettlePending": false, - "retryInterval": initialRetryInterval, - "handler": null, - "destroyedHandler": null, - "retryTimer": null, - "adapterWatcher": null - } - entry.handler = function() { root.reconcile(entry) } - entry.destroyedHandler = function() { root.adapterDestroyed(entry) } - entry.retryTimer = Qt.createQmlObject( - 'import QtQuick; Timer { running: false; repeat: false }', - root, - "ShibumiBluetoothDiscoveryGuardTimer" - ) - entry.adapterWatcher = Qt.createQmlObject( - 'import QtQuick; QtObject {' - + ' property bool armed: true; signal watchedDestroyed();' - + ' Component.onDestruction: if (armed) watchedDestroyed() }', - adapter, - "ShibumiBluetoothDiscoveryAdapterWatcher" - ) - entry.adapterWatcher.watchedDestroyed.connect(entry.destroyedHandler) - entry.retryTimer.triggered.connect(function() { root.reconcile(entry) }) - guards.push(entry) - adapter.discoveringChanged.connect(entry.handler) - reconcile(entry) - return true - } -} diff --git a/adapters/BluetoothModel.js b/adapters/BluetoothModel.js deleted file mode 100644 index 6608ab7..0000000 --- a/adapters/BluetoothModel.js +++ /dev/null @@ -1,115 +0,0 @@ -function deviceLabel(device) { - if (!device) return "" - return String(device.deviceName || device.name || "").trim() -} - -function toArray(values) { - if (!values) return [] - if (Array.isArray(values)) return values.slice() - - var length = Number(values.length || 0) - if (!isFinite(length) || length <= 0) return [] - - var list = [] - for (var i = 0; i < length; i++) list.push(values[i]) - return list -} - -function isUuidLike(value) { - var text = String(value || "").trim() - if (text === "") return false - return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(text) - || /^[0-9a-f]{32}$/i.test(text) - || /^0x[0-9a-f]{4,32}$/i.test(text) - || /^0000[0-9a-f]{4}-0000-1000-8000-00805f9b34fb$/i.test(text) -} - -function isAddressLike(value) { - return /^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(String(value || "").trim()) -} - -function hasHumanName(device) { - var label = deviceLabel(device) - return label !== "" && !isUuidLike(label) && !isAddressLike(label) -} - -function sortedByLabel(devices) { - var list = toArray(devices) - list.sort(function(a, b) { return deviceLabel(a).localeCompare(deviceLabel(b)) }) - return list -} - -function deviceLists(devices) { - var values = toArray(devices) - var connected = [] - var known = [] - var discovered = [] - - for (var i = 0; i < values.length; i++) { - var device = values[i] - if (!device || !hasHumanName(device)) continue - if (device.connected) connected.push(device) - else if (device.paired || device.bonded || device.trusted) known.push(device) - else discovered.push(device) - } - - return { - connected: sortedByLabel(connected), - known: sortedByLabel(known), - discovered: sortedByLabel(discovered) - } -} - -function normalizedAddress(value) { - return String(value || "").trim().toLowerCase().replace(/[^0-9a-f]/g, "") -} - -function nodeProperties(node) { - return node && node.ready && node.properties ? node.properties : ({}) -} - -function nodeText(node) { - var properties = nodeProperties(node) - return [ - node ? node.name : "", - node ? node.description : "", - node ? node.nickname : "", - node ? node.nick : "", - properties["node.name"], - properties["node.description"], - properties["node.nick"], - properties["device.name"], - properties["device.description"], - properties["device.product.name"], - properties["device.alias"], - properties["device.string"], - properties["api.bluez5.address"], - properties["bluez5.address"], - properties["media.name"] - ].join(" ").toLowerCase() -} - -function bluetoothSinkMatchesDevice(node, device) { - if (!node || !node.isSink || node.isStream || !device) return false - - var address = normalizedAddress(device.address) - var text = nodeText(node) - if (address !== "" && normalizedAddress(text).indexOf(address) !== -1) return true - - var label = deviceLabel(device).toLowerCase() - return label !== "" && text.indexOf(label) !== -1 -} - -function cloneMap(map) { - var next = ({}) - for (var key in map || {}) next[key] = map[key] - return next -} - -function withPendingAction(actions, address, action) { - var next = cloneMap(actions) - if (!address) return next - if (action) next[address] = action - else delete next[address] - return next -} diff --git a/adapters/MonitorPanelBridge.qml b/adapters/MonitorPanelBridge.qml deleted file mode 100644 index 5bc7f96..0000000 --- a/adapters/MonitorPanelBridge.qml +++ /dev/null @@ -1,220 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick - -// Hosts Quattro's monitor component as the single process-wide state/action -// owner. Shibumi owns only screen-local presentation and forwards mutations. -Item { - id: root - - required property var bar - required property Item ownerWidget - property Component panelComponent: null - property url panelSource: "" - property var panelSettings: ({}) - - readonly property var panel: panelLoader.item - readonly property bool ready: panel !== null - readonly property bool opened: ready && panel.opened === true - readonly property bool brightnessAvailable: ready - && panel.brightnessAvailable !== undefined - ? panel.brightnessAvailable === true : false - readonly property int brightnessPercent: ready - && panel.brightnessPercent !== undefined - ? Math.max(0, Math.min(100, Number(panel.brightnessPercent) || 0)) : 0 - readonly property int displayCount: ready && panel.displays !== undefined - && Array.isArray(panel.displays) ? panel.displays.length : 0 - readonly property string internalMonitor: ready - && panel.internalMonitor !== undefined ? String(panel.internalMonitor || "") : "" - readonly property string externalMonitor: ready - && panel.externalMonitor !== undefined ? String(panel.externalMonitor || "") : "" - readonly property string focusedMonitor: ready - && panel.focusedMonitor !== undefined ? String(panel.focusedMonitor || "") : "" - readonly property bool internalEnabled: ready - && panel.internalEnabled !== undefined ? panel.internalEnabled === true : false - readonly property bool mirrorEnabled: ready - && panel.mirrorEnabled !== undefined ? panel.mirrorEnabled === true : false - readonly property string monitorScale: ready - && panel.monitorScale !== undefined ? String(panel.monitorScale || "") : "" - readonly property var displays: ready && panel.displays !== undefined - && Array.isArray(panel.displays) ? panel.displays : [] - readonly property int enabledDisplayCount: ready - && panel.enabledDisplayCount !== undefined - ? Math.max(0, Number(panel.enabledDisplayCount) || 0) : 0 - readonly property bool textSizeAvailable: ready - && Array.isArray(panel.textSizeStops) && panel.textSizeStops.length > 0 - && typeof panel.currentTextIndex === "function" - && typeof panel.displayedTextPx === "function" - && typeof panel.setTextSize === "function" - readonly property var textSizeStops: textSizeAvailable - ? panel.textSizeStops : [] - readonly property int textSizeIndex: textSizeAvailable - ? Math.max(0, Math.min(textSizeStops.length - 1, - Number(panel.currentTextIndex()) || 0)) : 0 - readonly property real textSizePx: textSizeAvailable - ? Number(panel.displayedTextPx()) || textSizeStops[textSizeIndex] : 0 - - function refresh() { - if (!ready || typeof panel.refresh !== "function") return false - panel.refresh() - return true - } - - function setBrightness(value) { - if (!ready || !brightnessAvailable - || typeof panel.setBrightness !== "function") return false - panel.setBrightness(Math.max(1, Math.min(100, Number(value) || 1))) - return true - } - - function previewBrightness(value) { - if (!ready || !brightnessAvailable) return false - if (typeof panel.previewBrightness === "function") - panel.previewBrightness(Math.max(1, Math.min(100, Number(value) || 1))) - else return setBrightness(value) - return true - } - - function setScale(value) { - if (!ready || typeof panel.setScale !== "function") return false - panel.setScale(String(value || "")) - return true - } - - function setTextSize(value) { - if (!textSizeAvailable) return false - const px = Math.round(Number(value) || 0) - if (textSizeStops.indexOf(px) < 0) return false - panel.setTextSize(px) - return true - } - - function adjustTextSize(delta) { - if (!textSizeAvailable) return false - const next = Math.max(0, Math.min(textSizeStops.length - 1, - textSizeIndex + Math.sign(Number(delta) || 0))) - return setTextSize(textSizeStops[next]) - } - - function toggleDisplay(name, enabled) { - if (!ready || !String(name || "") - || typeof panel.toggleDisplay !== "function") return false - panel.toggleDisplay(String(name), enabled === true) - return true - } - - function normalizeScale(value) { - if (ready && typeof panel.normalizeScale === "function") - return String(panel.normalizeScale(value) || "") - const number = parseFloat(String(value || "")) - return isFinite(number) ? String(Math.round(number * 100) / 100) : "" - } - - function brightnessName(value) { - if (ready && typeof panel.brightnessName === "function") - return String(panel.brightnessName(value) || "") - const percent = Math.round(Number(value) || 0) - if (percent >= 80) return "Bright" - if (percent >= 45) return "Balanced" - if (percent >= 20) return "Dim" - return "Low light" - } - - function injectPanel() { - if (!panel) return - if ("bar" in panel) panel.bar = hostProxy - if ("moduleName" in panel) panel.moduleName = "omarchy.monitor" - if ("settings" in panel) panel.settings = panelSettings - if ("manageIpc" in panel) panel.manageIpc = false - if (panel.opened === true && typeof panel.close === "function") panel.close() - panel.opacity = 0 - } - - function syncPanelSource() { - panelLoader.sourceComponent = null - panelLoader.source = "" - if (panelComponent !== null) { - panelLoader.sourceComponent = panelComponent - } else if (String(panelSource)) { - panelLoader.setSource(panelSource, { - bar: hostProxy, - moduleName: "omarchy.monitor", - manageIpc: false, - settings: panelSettings - }) - } - } - - onPanelSettingsChanged: injectPanel() - onBarChanged: injectPanel() - onPanelComponentChanged: Qt.callLater(syncPanelSource) - onPanelSourceChanged: Qt.callLater(syncPanelSource) - Component.onCompleted: Qt.callLater(syncPanelSource) - Component.onDestruction: { - if (!panel || !bar) return - if (panel.opened === true && typeof panel.close === "function") panel.close() - } - - // The official component owns the legacy IPC target. Redirect an IPC open - // to Shibumi's screen-local presentation and never leave the stock popup - // mapped behind it. - Connections { - target: root.panel - function onOpenedChanged() { - if (!root.panel || root.panel.opened !== true) return - if (root.bar && typeof root.bar.summonBarWidget === "function") - root.bar.summonBarWidget("omarchy.monitor") - Qt.callLater(function() { - if (root.panel && root.panel.opened === true - && typeof root.panel.close === "function") root.panel.close() - }) - } - } - - QtObject { - id: hostProxy - - readonly property var realBar: root.bar - readonly property bool vertical: realBar ? realBar.vertical === true : false - readonly property int barSize: realBar ? Number(realBar.barSize) : 35 - readonly property int sizeHorizontal: realBar && realBar.sizeHorizontal !== undefined - ? Number(realBar.sizeHorizontal) : barSize - readonly property string position: realBar ? String(realBar.position || "top") : "top" - readonly property string fontFamily: realBar ? String(realBar.fontFamily || "monospace") : "monospace" - readonly property color background: realBar && realBar.background !== undefined - ? realBar.background : "#111111" - readonly property color barBackground: background - readonly property color foreground: realBar ? realBar.foreground : "#ffffff" - readonly property color barForeground: foreground - readonly property color urgent: realBar ? realBar.urgent : foreground - readonly property bool foregroundAnimationEnabled: realBar - ? realBar.foregroundAnimationEnabled !== false : false - readonly property var shell: realBar ? realBar.shell : null - readonly property var activePopout: realBar ? realBar.activePopout : null - readonly property var clickTargets: realBar ? realBar.clickTargets : [] - - function registerClickTarget(_target) {} - function unregisterClickTarget(_target) {} - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function requestPopout(_owner) {} - function releasePopout(_owner) {} - function switchPanelFrom(_owner, direction) { - return realBar && typeof realBar.switchPanelFrom === "function" - ? realBar.switchPanelFrom(root.ownerWidget, direction) : false - } - function targetBelongsToWindow(target, window) { - return realBar && typeof realBar.targetBelongsToWindow === "function" - ? realBar.targetBelongsToWindow(target, window) : false - } - } - - Loader { - id: panelLoader - anchors.fill: parent - onLoaded: { - root.injectPanel() - Qt.callLater(root.injectPanel) - } - } -} diff --git a/adapters/NetworkPanelBridge.qml b/adapters/NetworkPanelBridge.qml deleted file mode 100644 index 7e11cf8..0000000 --- a/adapters/NetworkPanelBridge.qml +++ /dev/null @@ -1,218 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick - -// Host Quattro's complete network component as a hidden backend. This bridge -// is instantiated once by NetworkService, never once per output. -Item { - id: root - - required property var bar - required property Item ownerWidget - property var networkService: null - property Component panelComponent: null - property url panelSource: "" - property var panelSettings: ({}) - - readonly property var panel: panelLoader.item - readonly property bool ready: panel !== null - readonly property bool opened: ready && panel.opened === true - readonly property bool backendAvailable: ready - && panel.networkManagerAvailable !== undefined - ? panel.networkManagerAvailable === true : false - readonly property string kind: ready && panel.kind !== undefined - ? String(panel.kind || "disconnected") : "disconnected" - // Quattro exposed `label` in early panel revisions, then moved the live - // Wi-Fi name to the native connected network object. Prefer the current - // event-driven source and retain narrow fallbacks for older hosts and the - // panel-only details sample. - readonly property string label: !ready ? "" - : kind === "wifi" && connectedWifiLabel() !== "" - ? connectedWifiLabel() - : panel.label !== undefined && String(panel.label || "") !== "" - ? String(panel.label) - : kind === "ethernet" && panel.info && panel.info.iface !== undefined - ? String(panel.info.iface || "") : "" - readonly property int signalStrength: ready - && panel.signalStrength !== undefined - ? Math.max(0, Math.min(100, Number(panel.signalStrength) || 0)) : 0 - readonly property bool scanning: ready && panel.scanning !== undefined - ? panel.scanning === true : false - readonly property bool busy: ready && panel.busy !== undefined - ? panel.busy === true : false - - function connectedWifiLabel() { - if (!ready || kind !== "wifi") return "" - - // `wifiNetworks` is the panel's normalized, event-driven public model and - // carries the same connected row that the official panel renders. Prefer - // it because some Qt NetworkManager objects expose `name` only after their - // wrapper has emitted a later property change. - const rows = panel.wifiNetworks - if (Array.isArray(rows)) { - for (let i = 0; i < rows.length; i++) { - if (rows[i] && rows[i].connected === true - && String(rows[i].ssid || "") !== "") - return String(rows[i].ssid) - } - } - - const current = panel.connectedWifiNetwork - if (current && current.name !== undefined - && String(current.name || "") !== "") - return String(current.name) - if (panel.info && panel.info.ssid !== undefined) - return String(panel.info.ssid || "") - return "" - } - - function open(scanWifi) { - if (!ready || typeof panel.open !== "function") return false - panel.open() - if (scanWifi === true && typeof panel.refresh === "function") panel.refresh(true) - return true - } - - function close() { - if (!ready || typeof panel.close !== "function") return false - panel.close() - return true - } - - function toggle() { - if (!ready) return false - if (opened) return close() - return open(false) - } - - function suppressBackendKeyboardPanel() { - if (!panel || !panel.data || panel.data.length === undefined) return false - let suppressed = false - for (let index = 0; index < panel.data.length; index++) { - const candidate = panel.data[index] - if (!candidate || typeof candidate.beginFocusPrime !== "function" - || !("anchorItem" in candidate) || !("open" in candidate) - || !("visible" in candidate) || !("owner" in candidate) - || candidate.owner !== panel) continue - candidate.open = false - candidate.visible = false - suppressed = true - } - return suppressed - } - - function injectPanel() { - if (!panel) return - suppressBackendKeyboardPanel() - if ("bar" in panel) panel.bar = hostProxy - if ("moduleName" in panel) panel.moduleName = "omarchy.network" - if ("settings" in panel) panel.settings = panelSettings - if ("manageIpc" in panel) panel.manageIpc = false - if (panel.opened === true && typeof panel.close === "function") panel.close() - panel.opacity = 0 - } - - function syncPanelSource() { - panelLoader.sourceComponent = null - panelLoader.source = "" - if (panelComponent !== null) { - panelLoader.sourceComponent = panelComponent - } else if (String(panelSource)) { - panelLoader.setSource(panelSource, { - bar: hostProxy, - moduleName: "omarchy.network", - manageIpc: false, - settings: panelSettings - }) - } - } - - onPanelSettingsChanged: injectPanel() - onBarChanged: injectPanel() - onPanelComponentChanged: Qt.callLater(syncPanelSource) - onPanelSourceChanged: Qt.callLater(syncPanelSource) - Component.onCompleted: Qt.callLater(syncPanelSource) - Component.onDestruction: { - if (!panel || !bar) return - if (panel.opened === true && typeof panel.close === "function") panel.close() - } - - QtObject { - id: hostProxy - - readonly property var realBar: root.bar - readonly property bool vertical: realBar ? realBar.vertical === true : false - readonly property int barSize: realBar ? Number(realBar.barSize) : 35 - readonly property int sizeHorizontal: realBar && realBar.sizeHorizontal !== undefined - ? Number(realBar.sizeHorizontal) : barSize - readonly property string position: realBar ? String(realBar.position || "top") : "top" - readonly property string fontFamily: realBar ? String(realBar.fontFamily || "monospace") : "monospace" - readonly property color foreground: realBar ? realBar.foreground : "#ffffff" - readonly property color barForeground: foreground - readonly property color urgent: realBar ? realBar.urgent : foreground - readonly property bool foregroundAnimationEnabled: realBar - ? realBar.foregroundAnimationEnabled !== false : false - readonly property var shell: hostShellProxy - readonly property var activePopout: realBar ? realBar.activePopout : null - readonly property var clickTargets: realBar ? realBar.clickTargets : [] - - function registerClickTarget(_target) {} - function unregisterClickTarget(_target) {} - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - - function run(command) { - if (realBar && typeof realBar.run === "function") realBar.run(command) - } - - function requestPopout(owner) { - if (realBar && typeof realBar.requestPopout === "function") - realBar.requestPopout(owner) - } - - function releasePopout(owner) { - if (realBar && typeof realBar.releasePopout === "function") - realBar.releasePopout(owner) - } - - function switchPanelFrom(_owner, direction) { - return realBar && typeof realBar.switchPanelFrom === "function" - ? realBar.switchPanelFrom(root.ownerWidget, direction) : false - } - - function targetBelongsToWindow(target, window) { - return realBar && typeof realBar.targetBelongsToWindow === "function" - ? realBar.targetBelongsToWindow(target, window) : false - } - } - - QtObject { - id: hostShellProxy - - readonly property var realShell: root.bar ? root.bar.shell : null - - function summon(id, payloadJson) { - if (String(id || "") === "omarchy.speedtest") { - return root.networkService - && typeof root.networkService.runSpeedTest === "function" - && root.networkService.runSpeedTest() ? "ok" : "unknown" - } - return realShell && typeof realShell.summon === "function" - ? realShell.summon(id, payloadJson) : "unknown" - } - - function hide(id) { - return realShell && typeof realShell.hide === "function" - ? realShell.hide(id) : undefined - } - } - - Loader { - id: panelLoader - anchors.fill: parent - onLoaded: { - root.injectPanel() - Qt.callLater(root.injectPanel) - } - } -} diff --git a/adapters/SystemActions.qml b/adapters/SystemActions.qml deleted file mode 100644 index 2555d1b..0000000 --- a/adapters/SystemActions.qml +++ /dev/null @@ -1,13 +0,0 @@ -import QtQuick - -QtObject { - id: root - - property var bar: null - - function openSystemMonitor() { - if (!bar || typeof bar.run !== "function") return false - bar.run("omarchy-launch-floating-terminal-with-presentation 'btop'") - return true - } -} diff --git a/adapters/WorkspaceActions.qml b/adapters/WorkspaceActions.qml deleted file mode 100644 index d7db969..0000000 --- a/adapters/WorkspaceActions.qml +++ /dev/null @@ -1,19 +0,0 @@ -import QtQuick - -Item { - id: root - - property var bar: null - - visible: false - width: 0 - height: 0 - - function focusWorkspace(value) { - const id = Number(value) - if (!Number.isInteger(id) || id <= 0 || id > 9999) return false - if (!bar || typeof bar.run !== "function") return false - bar.run("hyprctl dispatch 'hl.dsp.focus({ workspace = \"" + id + "\" })'") - return true - } -} diff --git a/adapters/qmldir b/adapters/qmldir deleted file mode 100644 index 6b87e89..0000000 --- a/adapters/qmldir +++ /dev/null @@ -1,7 +0,0 @@ -singleton BluetoothDiscoveryGuard 1.0 BluetoothDiscoveryGuard.qml -AudioPanelBridge 1.0 AudioPanelBridge.qml -BluetoothBackendAdapter 1.0 BluetoothBackendAdapter.qml -MonitorPanelBridge 1.0 MonitorPanelBridge.qml -NetworkPanelBridge 1.0 NetworkPanelBridge.qml -SystemActions 1.0 SystemActions.qml -WorkspaceActions 1.0 WorkspaceActions.qml diff --git a/assets/arch-header-arch.png b/assets/arch-header-arch.png deleted file mode 100644 index f87d1df..0000000 Binary files a/assets/arch-header-arch.png and /dev/null differ diff --git a/assets/arch-header-linux.png b/assets/arch-header-linux.png deleted file mode 100644 index 813a621..0000000 Binary files a/assets/arch-header-linux.png and /dev/null differ diff --git a/assets/bob2.png b/assets/bob2.png deleted file mode 100644 index 83a77ce..0000000 Binary files a/assets/bob2.png and /dev/null differ diff --git a/assets/bob3.png b/assets/bob3.png deleted file mode 100644 index ac7afa6..0000000 Binary files a/assets/bob3.png and /dev/null differ diff --git a/assets/codex.svg b/assets/codex.svg deleted file mode 100644 index b40dd62..0000000 --- a/assets/codex.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/logo-tint.frag.qsb b/assets/logo-tint.frag.qsb deleted file mode 100644 index 520a691..0000000 Binary files a/assets/logo-tint.frag.qsb and /dev/null differ diff --git a/assets/omacom-text.png b/assets/omacom-text.png deleted file mode 100644 index 48e52a0..0000000 Binary files a/assets/omacom-text.png and /dev/null differ diff --git a/assets/opencode-mark.svg b/assets/opencode-mark.svg deleted file mode 100644 index 290cc4e..0000000 --- a/assets/opencode-mark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/contracts/v1-embedded-v2-differences.json b/contracts/v1-embedded-v2-differences.json index 9d49262..cf6bfeb 100644 --- a/contracts/v1-embedded-v2-differences.json +++ b/contracts/v1-embedded-v2-differences.json @@ -47,7 +47,7 @@ "impact": "cava-process-lifecycle", "decision": "adapt-safer", "implementation": ["hancore.shibumi.media/Service.qml", "hancore.shibumi.media/BarWidget.qml"], - "evidence": ["tests/audio-media-plugins-regression.sh", "tests/media-widget-smoke.qml"] + "evidence": ["tests/audio-media-plugins-regression.sh", "tests/media-plugin-smoke.qml"] }, { "source": "modules/WorkspaceWidget.qml", diff --git a/contracts/v1-feature-evidence.json b/contracts/v1-feature-evidence.json index e283d85..362e746 100644 --- a/contracts/v1-feature-evidence.json +++ b/contracts/v1-feature-evidence.json @@ -37,7 +37,7 @@ "strategy": "adapt", "sources": ["IconMap.js"], "implementation": ["shared/presentation/IconText.qml", "hancore.shibumi.control-center/ControlSettings.qml"], - "evidence": ["tests/shibumi-presentation-smoke.qml", "tests/control-center-smoke.qml"] + "evidence": ["tests/group-renderer-regression.qml", "tests/control-center-smoke.qml"] }, { "id": "power-actions", @@ -86,7 +86,7 @@ "strategy": "port", "sources": ["modules/BatteryWidget.qml", "panels/BatteryPanel.qml"], "implementation": ["hancore.shibumi.battery/BarWidget.qml", "hancore.shibumi.battery/BatteryPanel.qml"], - "evidence": ["tests/power-plugins-regression.sh", "tests/power-widgets-smoke.qml"] + "evidence": ["tests/power-plugins-regression.sh", "tests/power-plugins-smoke.qml"] }, { "id": "bluetooth", @@ -107,7 +107,7 @@ "strategy": "port", "sources": ["modules/ClockWidget.qml", "panels/CalendarPopup.qml"], "implementation": ["hancore.shibumi.center/ClockWidget.qml", "hancore.shibumi.center/CalendarPanel.qml"], - "evidence": ["tests/center-plugin-regression.sh", "tests/clock-widget-smoke.qml"] + "evidence": ["tests/center-plugin-regression.sh", "tests/center-plugin-smoke.qml"] }, { "id": "ai-usage", @@ -128,7 +128,7 @@ "strategy": "port", "sources": ["modules/IconText.qml", "modules/UiText.qml"], "implementation": ["shared/presentation/IconText.qml"], - "evidence": ["tests/shibumi-presentation-smoke.qml", "tests/style-contract-regression.sh"] + "evidence": ["tests/group-renderer-regression.qml", "tests/style-contract-regression.sh"] }, { "id": "status-indicators", @@ -191,7 +191,7 @@ "strategy": "port", "sources": ["modules/PillShadow.qml"], "implementation": ["shared/presentation/PillSurface.qml", "styles/shibumi/RunChrome.qml"], - "evidence": ["tests/shibumi-presentation-smoke.qml", "tests/run-geometry-regression.qml"] + "evidence": ["tests/group-renderer-regression.qml", "tests/run-geometry-regression.qml"] }, { "id": "power-profile", @@ -212,7 +212,7 @@ "strategy": "port", "sources": ["modules/TooltipMixin.qml", "panels/TooltipOverlay.qml"], "implementation": ["shared/presentation/ShibumiPanelToolTip.qml", "hancore.shibumi.bar/Bar.qml"], - "evidence": ["tests/panel-surface-smoke.qml", "tests/shibumi-presentation-smoke.qml"] + "evidence": ["tests/panel-surface-smoke.qml", "tests/group-renderer-regression.qml"] }, { "id": "tray", diff --git a/contracts/v2-feature-evidence.json b/contracts/v2-feature-evidence.json index 539d6dc..0ce0d52 100644 --- a/contracts/v2-feature-evidence.json +++ b/contracts/v2-feature-evidence.json @@ -3,13 +3,13 @@ "referenceBaseline": "contracts/baselines/quickshell-dots-d0896fc-v2-deec8103.json", "referenceRootId": "v2", "features": [ - {"id":"bar-shell.full","implementation":["styles/shibumi/RunChrome.qml","styles/shibumi/BarSurface.qml"],"tests":["tests/shibumi-presentation-smoke.qml","tests/style-contract-regression.sh"]}, - {"id":"bar-shell.fit","implementation":["styles/shibumi/RunChrome.qml","styles/shibumi/BarSurface.qml"],"tests":["tests/shibumi-presentation-smoke.qml","tests/style-contract-regression.sh"]}, - {"id":"bar-shell.dock","implementation":["styles/shibumi/RunChrome.qml","styles/shibumi/BarSurface.qml"],"tests":["tests/shibumi-presentation-smoke.qml","tests/style-contract-regression.sh"]}, - {"id":"bar-shell.notch","implementation":["styles/shibumi/RunChrome.qml","styles/shibumi/BarSurface.qml"],"tests":["tests/shibumi-presentation-smoke.qml","tests/style-contract-regression.sh"]}, + {"id":"bar-shell.full","implementation":["styles/shibumi/RunChrome.qml","styles/shibumi/BarSurface.qml"],"tests":["tests/group-renderer-regression.qml","tests/style-contract-regression.sh"]}, + {"id":"bar-shell.fit","implementation":["styles/shibumi/RunChrome.qml","styles/shibumi/BarSurface.qml"],"tests":["tests/group-renderer-regression.qml","tests/style-contract-regression.sh"]}, + {"id":"bar-shell.dock","implementation":["styles/shibumi/RunChrome.qml","styles/shibumi/BarSurface.qml"],"tests":["tests/group-renderer-regression.qml","tests/style-contract-regression.sh"]}, + {"id":"bar-shell.notch","implementation":["styles/shibumi/RunChrome.qml","styles/shibumi/BarSurface.qml"],"tests":["tests/group-renderer-regression.qml","tests/style-contract-regression.sh"]}, {"id":"bar.responsive-staging","implementation":["core/ResponsiveLayout.js","styles/shibumi/BarSurface.qml"],"tests":["tests/responsive-layout-regression.qml"]}, {"id":"bar.inline-slot-controls","implementation":["styles/shibumi/GroupSection.qml","core/LayoutController.qml"],"tests":["tests/style-contract-regression.sh","tests/shibumi-config-regression.qml"]}, - {"id":"bar.border-toggle","implementation":["styles/shibumi/VisualTokens.qml","styles/shibumi/RunChrome.qml"],"tests":["tests/shibumi-presentation-smoke.qml","tests/state-service-smoke.qml"]}, + {"id":"bar.border-toggle","implementation":["styles/shibumi/VisualTokens.qml","styles/shibumi/RunChrome.qml"],"tests":["tests/group-renderer-regression.qml","tests/state-service-smoke.qml"]}, {"id":"panel.border-toggle","implementation":["styles/shibumi/VisualTokens.qml","shared/presentation/ShibumiPanel.qml"],"tests":["tests/panel-surface-smoke.qml","tests/state-service-smoke.qml"]}, {"id":"panel.connected-silhouette","implementation":["styles/shibumi/RunChrome.qml","shared/presentation/ShibumiPanel.qml"],"tests":["tests/panel-surface-smoke.qml","tests/panel-routing-regression.qml"]}, {"id":"panel.pointer","implementation":["shared/presentation/ShibumiPanel.qml"],"tests":["tests/panel-surface-smoke.qml"]}, @@ -19,18 +19,18 @@ {"id":"picker.tanzaku","implementation":["hancore.shibumi.quick-access/TanzakuPickerView.qml"],"tests":["tests/quick-access-plugin-smoke.qml","tests/quick-access-plugin-regression.sh"]}, {"id":"picker.hearthstone","implementation":["hancore.shibumi.quick-access/HearthstonePickerView.qml"],"tests":["tests/quick-access-plugin-smoke.qml","tests/quick-access-plugin-regression.sh"]}, {"id":"picker.carousel","implementation":["hancore.shibumi.quick-access/CarouselPickerView.qml","hancore.shibumi.quick-access/Service.qml"],"tests":["tests/quick-access-plugin-regression.sh","tests/shibumi-config-regression.qml"]}, - {"id":"widget.palette","implementation":["styles/shibumi/VisualTokens.qml","core/GroupSlot.qml"],"tests":["tests/shibumi-presentation-smoke.qml","tests/state-service-smoke.qml"]}, - {"id":"widget.fill-border-modes","implementation":["styles/shibumi/VisualTokens.qml","core/GroupSlot.qml"],"tests":["tests/shibumi-presentation-smoke.qml","tests/group-renderer-regression.qml"]}, - {"id":"widget.separators","implementation":["styles/shibumi/VisualTokens.qml","core/GroupSlot.qml"],"tests":["tests/shibumi-presentation-smoke.qml","tests/group-renderer-regression.qml"]}, - {"id":"widget.compact-modes","implementation":["hancore.shibumi.control-center/BarFunctionsPage.qml","hancore.shibumi.audio/BarWidget.qml"],"tests":["tests/control-center-smoke.qml","tests/audio-widget-smoke.qml"]}, - {"id":"widget.icon-only-modes","implementation":["hancore.shibumi.control-center/BarFunctionsPage.qml","hancore.shibumi.status/BarWidget.qml"],"tests":["tests/control-center-smoke.qml","tests/status-widget-smoke.qml"]}, + {"id":"widget.palette","implementation":["styles/shibumi/VisualTokens.qml","core/GroupSlot.qml"],"tests":["tests/group-renderer-regression.qml","tests/state-service-smoke.qml"]}, + {"id":"widget.fill-border-modes","implementation":["styles/shibumi/VisualTokens.qml","core/GroupSlot.qml"],"tests":["tests/group-renderer-regression.qml","tests/style-contract-regression.sh"]}, + {"id":"widget.separators","implementation":["styles/shibumi/VisualTokens.qml","core/GroupSlot.qml"],"tests":["tests/group-renderer-regression.qml","tests/style-contract-regression.sh"]}, + {"id":"widget.compact-modes","implementation":["hancore.shibumi.control-center/BarFunctionsPage.qml","hancore.shibumi.audio/BarWidget.qml"],"tests":["tests/control-center-smoke.qml","tests/audio-plugin-smoke.qml"]}, + {"id":"widget.icon-only-modes","implementation":["hancore.shibumi.control-center/BarFunctionsPage.qml","hancore.shibumi.status/BarWidget.qml"],"tests":["tests/control-center-smoke.qml","tests/status-plugin-smoke.qml"]}, {"id":"temperature.widget","implementation":["hancore.shibumi.temperature/BarWidget.qml"],"tests":["tests/telemetry-plugins-smoke.qml","tests/telemetry-plugins-regression.sh"]}, {"id":"temperature.source-selection","implementation":["hancore.shibumi.telemetry/ThermalTelemetry.qml"],"tests":["tests/telemetry-plugins-smoke.qml","tests/telemetry-plugins-regression.sh"]}, {"id":"thermals.panel","implementation":["hancore.shibumi.temperature/TemperaturePanel.qml"],"tests":["tests/telemetry-plugins-smoke.qml","tests/telemetry-plugins-regression.sh"]}, {"id":"gpu.widget-panel","implementation":["hancore.shibumi.gpu/BarWidget.qml","hancore.shibumi.gpu/GpuPanel.qml"],"tests":["tests/telemetry-plugins-smoke.qml","tests/telemetry-plugins-regression.sh"]}, {"id":"storage.widget-panel","implementation":["hancore.shibumi.storage/BarWidget.qml","hancore.shibumi.storage/StoragePanel.qml","hancore.shibumi.storage/StorageTelemetry.qml"],"tests":["tests/storage-plugin-smoke.qml","tests/storage-plugin-regression.sh"]}, - {"id":"media.full-muse","implementation":["hancore.shibumi.media/BarWidget.qml","hancore.shibumi.media/Service.qml"],"tests":["tests/media-plugin-smoke.qml","tests/media-widget-smoke.qml"]}, - {"id":"media.artwork","implementation":["hancore.shibumi.media/MediaPanel.qml"],"tests":["tests/media-plugin-smoke.qml","tests/media-widget-smoke.qml"]}, + {"id":"media.full-muse","implementation":["hancore.shibumi.media/BarWidget.qml","hancore.shibumi.media/Service.qml"],"tests":["tests/media-plugin-smoke.qml","tests/audio-media-plugins-regression.sh"]}, + {"id":"media.artwork","implementation":["hancore.shibumi.media/MediaPanel.qml"],"tests":["tests/media-plugin-smoke.qml","tests/audio-media-plugins-regression.sh"]}, {"id":"tray.drawer","implementation":["hancore.shibumi.status/TrayDrawerPanel.qml"],"tests":["tests/status-plugin-smoke.qml","tests/status-plugin-regression.sh"]}, {"id":"tray.item-menu","implementation":["hancore.shibumi.status/TrayAppMenuPanel.qml"],"tests":["tests/tray-app-menu-smoke.qml","tests/status-plugin-regression.sh"]}, {"id":"launcher.logo-variants","implementation":["hancore.shibumi.state/ShibumiConfig.js","hancore.shibumi.control-center/BarWidget.qml"],"tests":["tests/shibumi-config-regression.qml","tests/control-center-smoke.qml"]}, diff --git a/contracts/v2-source-evidence.json b/contracts/v2-source-evidence.json index 5e97726..d8cf2b6 100644 --- a/contracts/v2-source-evidence.json +++ b/contracts/v2-source-evidence.json @@ -30,7 +30,7 @@ "strategy": "adapt", "sources": ["IconMap.js"], "implementation": ["shared/presentation/IconText.qml", "hancore.shibumi.control-center/ControlSettings.qml"], - "evidence": ["tests/shibumi-presentation-smoke.qml", "tests/control-center-smoke.qml"] + "evidence": ["tests/group-renderer-regression.qml", "tests/control-center-smoke.qml"] }, { "id": "power-actions", @@ -44,7 +44,7 @@ "strategy": "port", "sources": ["modules/AiBarInset.qml", "modules/AiPanelSurface.qml", "modules/ConnectedBarInset.qml", "modules/ConnectedPanelSurface.qml", "modules/PanelPointer.qml"], "implementation": ["styles/shibumi/RunChrome.qml", "shared/presentation/ShibumiPanel.qml"], - "evidence": ["tests/shibumi-presentation-smoke.qml", "tests/panel-surface-smoke.qml", "tests/panel-routing-regression.qml"] + "evidence": ["tests/group-renderer-regression.qml", "tests/panel-surface-smoke.qml", "tests/panel-routing-regression.qml"] }, { "id": "control-center", @@ -79,7 +79,7 @@ "strategy": "port", "sources": ["modules/BatteryWidget.qml", "panels/BatteryPanel.qml"], "implementation": ["hancore.shibumi.battery/BarWidget.qml", "hancore.shibumi.battery/BatteryPanel.qml"], - "evidence": ["tests/power-plugins-regression.sh", "tests/power-widgets-smoke.qml"] + "evidence": ["tests/power-plugins-regression.sh", "tests/power-plugins-smoke.qml"] }, { "id": "bluetooth", @@ -100,7 +100,7 @@ "strategy": "port", "sources": ["modules/ClockWidget.qml", "panels/CalendarPopup.qml"], "implementation": ["hancore.shibumi.center/ClockWidget.qml", "hancore.shibumi.center/CalendarPanel.qml"], - "evidence": ["tests/center-plugin-regression.sh", "tests/clock-widget-smoke.qml"] + "evidence": ["tests/center-plugin-regression.sh", "tests/center-plugin-smoke.qml"] }, { "id": "ai-usage", @@ -121,7 +121,7 @@ "strategy": "port", "sources": ["modules/IconText.qml", "modules/UiText.qml"], "implementation": ["shared/presentation/IconText.qml"], - "evidence": ["tests/shibumi-presentation-smoke.qml", "tests/style-contract-regression.sh"] + "evidence": ["tests/group-renderer-regression.qml", "tests/style-contract-regression.sh"] }, { "id": "status-indicators", @@ -198,7 +198,7 @@ "strategy": "port", "sources": ["modules/PillShadow.qml"], "implementation": ["shared/presentation/PillSurface.qml", "styles/shibumi/RunChrome.qml"], - "evidence": ["tests/shibumi-presentation-smoke.qml", "tests/run-geometry-regression.qml"] + "evidence": ["tests/group-renderer-regression.qml", "tests/run-geometry-regression.qml"] }, { "id": "power-profile", @@ -219,7 +219,7 @@ "strategy": "port", "sources": ["modules/TooltipMixin.qml", "panels/TooltipOverlay.qml"], "implementation": ["shared/presentation/ShibumiPanelToolTip.qml", "hancore.shibumi.bar/Bar.qml"], - "evidence": ["tests/panel-surface-smoke.qml", "tests/shibumi-presentation-smoke.qml"] + "evidence": ["tests/panel-surface-smoke.qml", "tests/group-renderer-regression.qml"] }, { "id": "tray", diff --git a/docs/bluetooth-ipc-root-cause-plan.md b/docs/bluetooth-ipc-root-cause-plan.md index 54e996d..f7c0d92 100644 --- a/docs/bluetooth-ipc-root-cause-plan.md +++ b/docs/bluetooth-ipc-root-cause-plan.md @@ -269,24 +269,21 @@ ein Modellmodul begrenzt. ### Phase 4 – Implementierung ausschließlich in Shibumi Abhängig von Phase 3 werden nur die betroffenen Shibumi-Quellen geändert. -Wegen der parallelen V1-/Plugin-Struktur sind mindestens diese Spiegel zu -prüfen und konsistent zu halten: +Die Implementierung wurde in diesen ausgelieferten Plugin-Quellen geprüft: - `hancore.shibumi.bluetooth/Service.qml` - `hancore.shibumi.bluetooth/BluetoothBackendAdapter.qml` - `hancore.shibumi.bluetooth/BluetoothModel.js` - `hancore.shibumi.bluetooth/BarWidget.qml` -- `services/BluetoothService.qml` -- `adapters/BluetoothBackendAdapter.qml` -- `adapters/BluetoothModel.js` -- `widgets/BluetoothWidget.qml` + +Die damaligen parallelen Root-Spiegel wurden in Step 0 nach dem Retargeting der +Assertions entfernt und sind keine aktuelle Autorenquelle mehr. Außerdem werden die falschen Verträge und Dokumentationsaussagen korrigiert: - `tests/contract-regression.sh` - `tests/bluetooth-plugin-regression.sh` - `tests/bluetooth-plugin-smoke.qml` -- `tests/bluetooth-widget-smoke.qml` - Bluetooth-Fixtures unter `tests/fixtures/` - `docs/phase2-validation.md` - `docs/phase2-ownership-map.md` @@ -294,7 +291,8 @@ Außerdem werden die falschen Verträge und Dokumentationsaussagen korrigiert: - [x] Keine Datei unter `/usr/share/omarchy` verändern. - [x] Keine Runtime-Warnung unterdrücken. - [x] Keine reale Bluetooth-Funkmutation in automatischen Produktionstests. -- [x] Quell- und Plugin-Spiegel über denselben Contract und denselben Smoke prüfen. +- [x] Ausgelieferte Pluginquellen und isolierte Fixtures über denselben Contract + und denselben Smoke prüfen. ### Phase 5 – Testpyramide diff --git a/docs/phase2-ownership-map.md b/docs/phase2-ownership-map.md index 5aa7af9..b781d04 100644 --- a/docs/phase2-ownership-map.md +++ b/docs/phase2-ownership-map.md @@ -30,20 +30,20 @@ Every feature has exactly one state owner and one action boundary: | Group | V1 contents | Authoritative Shibumi state/action owner | Shibumi responsibility | | --- | --- | --- | --- | | G1 | Shibumi wordmark and Control Center | Independent `hancore.shibumi.control-center` widget/panel over host-owned Shibumi settings | Preserve the V1 wordmark/icon, screen-local Control Center, lifecycle-gated feedback, colors 01-07 plus foreground, and complete bar-settings contract without opening or owning the App Menu; the current eight-choice palette is implemented and accepted on the validation system | -| G2 | Workspaces and workspace panel | One root-owned `Quickshell.Hyprland` model plus validated Quattro focus adapter | Shibumi modes, three presentation styles and lazy screen-local panel implemented locally; real-Wayland acceptance remains | +| G2 | Workspaces and workspace panel | One process-wide `hancore.shibumi.workspaces` service over `Quickshell.Hyprland` plus its validated focus adapter | Shibumi modes, three presentation styles and lazy screen-local panel implemented locally; real-Wayland acceptance remains | | G3 | Tray and notifications | Complete official `omarchy.tray` and `omarchy.notifications` components | One V1 status pill groups both components without copying SystemTray, notification, DND, history, menu, or action ownership; Omarchy update presentation is restored to G8 and no V1 self-updater exists in Shibumi | | G4 | Memory | Shibumi singleton system telemetry | Compact/full widget and lazy detail panel | | G5 | CPU | Shibumi singleton system telemetry | Compact/full widget and lazy detail panel | | G6 | Volume | Official `omarchy.audio` state/actions through a narrow bridge; one open-only local peak monitor | V1 full/compact pill and lazy V1 mixer panel cover descriptively labeled output/input devices, per-app streams, mute, and meter; real Wayland/top-bottom/multi-output acceptance remains | -| G7 | Claude, Codex, and OpenCode usage | One root-owned Shibumi AI usage service over primitive current `omarchy.agents` schema-v1 records; pinned older hosts exclusively fall back to their official Claude/Codex `omarchy.model-usage` providers, and one read-only OpenCode SQLite provider fills the remaining host gap | One selected-provider pill, V1 usage fill, local lazy panel, provider cycling, refresh, current `omarchy.agents` and legacy `omarchy.model-usage` routing, and no duplicate visible host panel, cache, or screen-local poller ownership; OpenCode is accepted on the validation system while current-host Claude/Codex real-account and multi-output gates remain | +| G7 | Claude, Codex, and OpenCode usage | One process-wide `hancore.shibumi.ai` service over primitive current `omarchy.agents` schema-v1 records; pinned older hosts exclusively fall back to their official Claude/Codex `omarchy.model-usage` providers, and one read-only OpenCode SQLite provider fills the remaining host gap | One selected-provider pill, V1 usage fill, local lazy panel, provider cycling, refresh, current `omarchy.agents` and legacy `omarchy.model-usage` routing, and no duplicate visible host panel, cache, or screen-local poller ownership; OpenCode is accepted on the validation system while current-host Claude/Codex real-account and multi-output gates remain | | G8 | Weather, clock, date, status indicators, Omarchy update | Shared Shibumi clock/status adapters plus official Quattro weather-detail, idle, notification, and update owners | Single V1 pill, lazy routed calendar, local weather/status/update facades, child settings, and monitor-local normal/compact/minimal stages are implemented. Top weather, update-available, idle/DND, the complete recording-indicator lifecycle, and a real Voxtype recording/transcription cycle pass on the validation system; bottom and physical multi-output acceptance remain. | | G9 | MPRIS | Keep-loaded official `omarchy.media` service plus one lazy process-wide Shibumi Cava service | Preserve the default row and current V1 FULL/muse 24-band spectrum, vinyl, transport, click, and wheel outcome; implementation plus real-player/failure/resource/Top-Bottom single-output acceptance pass, while multiple-real-player and physical multi-output gates remain | -| G10 | Idle inhibitor, media browser, theme/wallpaper picker | Surface-bound Quickshell idle inhibitor, Omarchy theme/background actions, and one root-owned Shibumi picker/media controller | V1 quick-tools pill plus Tanzaku and Hearthstone presentation-only views; Quattro keeps its native carousel; basic focused-output runtime and worker cleanup accepted on the validation system | -| G11 | Network | One root-owned service around the official `omarchy.network` state/action backend, plus Shibumi-owned inline speed-test and saved-profile workers | V1 `NET`/SSID/signal and compact views plus a lazy local details/DNS/speed-test/available/saved/connect/forget panel; top Wayland mapping and cleanup pass on the validation system, while mutation, bottom, and physical multi-output acceptance remain | -| G12 | Battery | One root-owned Shibumi power service over Quickshell's UPower singleton; Quattro battery helper only while the detail panel is open | V1 compact/full battery view and lazy battery panel; hidden on batteryless desktops; a real discharging-to-charging transition passes on the validation system | -| G13 | Brightness | One root-owned service around the complete official `omarchy.monitor` backend | V1 `BRI`/sun/percentage and compact views plus a lazy, screen-local Shibumi brightness/scale/display panel; top/Bottom mapping, reversible laptop brightness mutation, and a real `1.0 -> 1.25 -> 1.0` scale round trip pass on the validation system, while physical display enable/disable and multi-output acceptance remain | -| G14 | Power profile | Same root-owned Shibumi power service; one Quattro profile-list poller and one validated setter | V1 compact/full profile view and lazy profile panel; remains available without a battery | -| G15 | Bluetooth | One root-owned Shibumi service and native Quickshell BlueZ/PipeWire adapter; no complete Omarchy Bluetooth component is loaded | V1 full/compact widget and lazy per-output Shibumi device panel; confirmed discovery ownership with bounded reconciliation, latest-only audio handoff intent, and one symmetric six-method legacy IPC target; top mapping and real adapter/radio/discovery lifecycle pass on the validation system, while real device/audio, bottom, and physical multi-output acceptance remain | +| G10 | Idle inhibitor, media browser, theme/wallpaper picker | Surface-bound Quickshell idle inhibitor, Omarchy theme/background actions, and one process-wide `hancore.shibumi.quick-access` picker/media controller | V1 quick-tools pill plus Tanzaku and Hearthstone presentation-only views; Quattro keeps its native carousel; basic focused-output runtime and worker cleanup accepted on the validation system | +| G11 | Network | One process-wide `hancore.shibumi.network` service around the official `omarchy.network` state/action backend, plus Shibumi-owned inline speed-test and saved-profile workers | V1 `NET`/SSID/signal and compact views plus a lazy local details/DNS/speed-test/available/saved/connect/forget panel; top Wayland mapping and cleanup pass on the validation system, while mutation, bottom, and physical multi-output acceptance remain | +| G12 | Battery | One process-wide `hancore.shibumi.power-state` service over Quickshell's UPower singleton; Quattro battery helper only while the detail panel is open | V1 compact/full battery view and lazy battery panel; hidden on batteryless desktops; a real discharging-to-charging transition passes on the validation system | +| G13 | Brightness | One process-wide `hancore.shibumi.brightness` service around the complete official `omarchy.monitor` backend | V1 `BRI`/sun/percentage and compact views plus a lazy, screen-local Shibumi brightness/scale/display panel; top/Bottom mapping, reversible laptop brightness mutation, and a real `1.0 -> 1.25 -> 1.0` scale round trip pass on the validation system, while physical display enable/disable and multi-output acceptance remain | +| G14 | Power profile | Same process-wide `hancore.shibumi.power-state` service; one Quattro profile-list poller and one validated setter | V1 compact/full profile view and lazy profile panel; remains available without a battery | +| G15 | Bluetooth | One process-wide `hancore.shibumi.bluetooth` service and native Quickshell BlueZ/PipeWire adapter; no complete Omarchy Bluetooth component is loaded | V1 full/compact widget and lazy per-output Shibumi device panel; confirmed discovery ownership with bounded reconciliation, latest-only audio handoff intent, and one symmetric six-method legacy IPC target; top mapping and real adapter/radio/discovery lifecycle pass on the validation system, while real device/audio, bottom, and physical multi-output acceptance remain | ## Shared Runtime Modules @@ -174,11 +174,12 @@ single-output lifecycle, real-player, degraded, Top/Bottom, visual, and resource slices; this does not waive physical multi-output acceptance. Network uses the same state/action delegation principle as audio, but the owner -must be root-scoped: Quattro's component includes a permanent three-second bar +must be process-wide: Quattro's component includes a permanent three-second bar status process, so instantiating it inside every output would duplicate work. -One `NetworkService` hosts that component, and all G11 widgets consume it. -Screen-local popups delegate visible-network and DNS mutations to the official -backend. The root service runs `omarchy-network-speedtest` itself for bounded +The `hancore.shibumi.network` service hosts that component, and all G11 widgets +consume it. Screen-local popups delegate visible-network and DNS mutations to +the official backend. The process-wide service runs +`omarchy-network-speedtest` itself for bounded download/upload phases and publishes progress only to Shibumi's inline panel; it never loads Omarchy's standalone speed-test panel. A lifecycle-only `nmcli` query/action path adds saved profiles that are not currently visible because diff --git a/docs/plugin-suite-inventory.md b/docs/plugin-suite-inventory.md index 293d053..b0829e1 100644 --- a/docs/plugin-suite-inventory.md +++ b/docs/plugin-suite-inventory.md @@ -374,26 +374,25 @@ These are small host-neutral helpers used by several feature plugins. Reviewed copies are vendored into each consuming plugin and checked byte-for-byte by the sync test. They are not runtime plugins and are never imported from `shared/`. -### Transitional files +### Resolved transitional files -```text -manifest.json combined root manifest; delete after - independent manifests exist -widgets/WidgetRegistry.qml internal component switch; replace - with Omarchy's BarWidgetRegistry -``` +The combined root manifest and `widgets/WidgetRegistry.qml` were retired after +the independent manifests and Quattro registry integration became authoritative. +Step 0 later removed the remaining unguarded historical root implementations. The root `README.md`, `ARCHITECTURE.md`, `LICENSE`, `config/`, `docs/`, and repository scripts remain repository-level development and distribution files. `styles/README.md` moves into the bar plugin's developer documentation; it is not installed as a runtime dependency. -## Test Ownership +## Historical Test Ownership -Tests move with their owning plugin while repository-wide contract tests stay at -the root. Paths in the table are relative to `tests/`. +This table freezes extraction-era test ownership; its filenames are historical +and are not the current executable inventory. Current plugin regressions, +machine-readable evidence contracts, and `tests/contract-regression.sh` are +authoritative. Paths in the table are relative to `tests/`. -| Owner | Current tests | +| Owner | Extraction-era tests | | --- | --- | | Repository/installer | `contract-regression.sh`, future manifest/dependency/self-containment/install/rollback tests | | Bar | `group-interaction-regression.qml`, `group-registry-regression.qml`, `group-renderer-regression.qml`, `host-widget-resolver-regression.qml`, `layout-controller-regression.qml`, `layout-model-regression.qml`, `panel-routing-regression.qml`, `qsrise-presentation-smoke.qml`, `reactor-renderer-regression.qml`, `run-geometry-regression.qml`, `style-contract-regression.sh`, `fixtures/ResolverTestWidget.qml` | diff --git a/docs/v1-presentation-contract.md b/docs/v1-presentation-contract.md index e18c78a..1e7b544 100644 --- a/docs/v1-presentation-contract.md +++ b/docs/v1-presentation-contract.md @@ -150,7 +150,9 @@ bounded retry and cleanup, Top/Bottom rendering, and the focused single-output resource slice. Multiple-real-player source switching, remaining media-panel states, and physical multi-output remain acceptance gates. -`tests/shibumi-presentation-smoke.qml` proves at component-load time that: +`tests/group-renderer-regression.qml` and +`tests/style-contract-regression.sh` prove at component-load and contract time +that: - the 35/38/32/24 px geometry contract is active; - CPU and memory follow the Shibumi bar height; diff --git a/docs/v1-widget-parity-audit.md b/docs/v1-widget-parity-audit.md index 2be3572..6101bb2 100644 --- a/docs/v1-widget-parity-audit.md +++ b/docs/v1-widget-parity-audit.md @@ -34,9 +34,9 @@ Status values: | G8 | Weather, clock, date, idle, DND, recording, Voxtype, Omarchy update | Local V1 weather, clock/date/calendar, active-only status, and update facades over process-wide services and official owners | Adapted, incomplete | Weather has one process-wide source, honors Quattro's saved location, and lazy-loads the V1 panel without the stock Weather poller. the validation system confirms one shared live Voxtype stream plus a real recording/transcription/idle cycle with text insertion. A real no-audio Quattro recording was detected, timed, stopped by physically clicking the visible G8 indicator, finalized as a valid MP4, and cleaned up while G8 expanded and contracted correctly. Complete mixed-scale and physical multi-output acceptance. Idle, notification, and update owners remain authoritative. | | G9 | MPRIS controls, marquee, pulse, and player panel, including selectable FULL/muse | Local default row, lazy panel, and current V1 24-band vinyl/waveform/transport row over the official keep-loaded media service and one lazy process-wide spectrum owner | Ported | Real `mpv`, two-player source switching, the actual empty/populated Top/Bottom MediaPanel, available/unavailable/crash states, two-client/one-worker lifecycle, 20 pause/resume cycles, resource characterization, and Top/Bottom bar visuals pass on the validation system. Complete degraded/failure presentation and physical multi-output acceptance. | | G10 | Idle inhibitor, screenshot/video browser, theme/wallpaper picker | One V1 quick-access pill, a true surface-bound idle inhibitor, and one process-wide picker/media controller with V1-faithful Tanzaku and Hearthstone views; use Quattro's native image picker for its carousel | Ported | Basic open/close, all four modes, style persistence, focused-output routing, worker cleanup, 20-cycle closed-PSS retention, real theme/wallpaper success, controlled theme/wallpaper failure feedback, and screenshot/video copy/open/two-step trash are accepted on the validation system. Tanzaku and the revised Hearthstone wallpaper view are visually accepted; keyboard navigation, warm close cleanup, native carousel coexistence, V1 idle-inhibitor presentation, and a real 4K cold-wallpaper/warm-reopen gate pass. Complete remaining Hearthstone theme/media rendering and physical multi-output acceptance. | -| G11 | Network state, Wi-Fi/ethernet presentation, throughput, network panel | One root-owned official NetworkManager backend plus lazy Shibumi panels, a Shibumi-owned inline speed-test process, and a lifecycle-only saved-profile adapter | Ported, acceptance incomplete | Top and bottom panel placement, expanded metadata, saved-profile discovery, alias routing, and backend refresh pass on the validation system. The direct inline speed-test path has fixture-backed phase, failure, and crash-cleanup coverage; repeat real output acceptance for this new owner before release. A stale saved-only profile was forgotten through the visible two-step action and stayed absent after reopen/rescan. A real `DHCP -> Cloudflare -> DHCP` DNS round trip passed through the panel and Polkit with the original connection and resolver state restored. Validate a new protected-network connection/passphrase and physical multi-output before release. | +| G11 | Network state, Wi-Fi/ethernet presentation, throughput, network panel | One process-wide `hancore.shibumi.network` service around the official NetworkManager backend plus lazy Shibumi panels, a Shibumi-owned inline speed-test process, and a lifecycle-only saved-profile adapter | Ported, acceptance incomplete | Top and bottom panel placement, expanded metadata, saved-profile discovery, alias routing, and backend refresh pass on the validation system. The direct inline speed-test path has fixture-backed phase, failure, and crash-cleanup coverage; repeat real output acceptance for this new owner before release. A stale saved-only profile was forgotten through the visible two-step action and stayed absent after reopen/rescan. A real `DHCP -> Cloudflare -> DHCP` DNS round trip passed through the panel and Polkit with the original connection and resolver state restored. Validate a new protected-network connection/passphrase and physical multi-output before release. | | G12 | Battery widget and battery detail panel | Local view and panel over the shared UPower service | Ported, state accepted | the validation system reports the physical BAT0 as present and healthy. A real discharging-to-charging transition produced matching kernel, UPower, helper, widget, and panel state. Complete physical multi-output acceptance. | -| G13 | Brightness widget and panel | One official root backend plus V1-style per-output widget and lazy local Shibumi display panel | Ported, acceptance incomplete | Top and Bottom mapping, reversible laptop brightness mutation, and a real `1.0 -> 1.25 -> 1.0` scale round trip pass on the validation system. Validate no-backlight hardware, physical display enable/disable, and physical multi-output ownership. | +| G13 | Brightness widget and panel | One process-wide `hancore.shibumi.brightness` service around the official backend plus a V1-style per-output widget and lazy local Shibumi display panel | Ported, acceptance incomplete | Top and Bottom mapping, reversible laptop brightness mutation, and a real `1.0 -> 1.25 -> 1.0` scale round trip pass on the validation system. Validate no-backlight hardware, physical display enable/disable, and physical multi-output ownership. | | G14 | Power-profile widget and panel | Local view and panel over the shared power-profile service | Ported, action accepted | the validation system completed and restored a real performance-to-balanced-to-performance change from `app-graphical.slice`; the product panel now passes Top/Bottom routing and loader diagnostics. Complete physical multi-output acceptance. | | G15 | Bluetooth widget and panel | One shared Shibumi-native BlueZ/PipeWire adapter with local V1-style widgets and lazy Shibumi device panels | Ported, acceptance incomplete | Top/Bottom mapping, real adapter/radio reactivity, confirmed discovery ownership/reconciliation, multi-panel lease accounting, and the symmetric six-method legacy IPC pass on the validation system. Validate pair/connect/disconnect/forget with real devices, Bluetooth audio, and physical multi-output. | diff --git a/hancore.shibumi.cpu/GpuTelemetry.qml b/hancore.shibumi.cpu/GpuTelemetry.qml index b8b5d50..948b799 100644 --- a/hancore.shibumi.cpu/GpuTelemetry.qml +++ b/hancore.shibumi.cpu/GpuTelemetry.qml @@ -7,7 +7,7 @@ Scope { property int consumers: 0 property string helperPath: String(Qt.resolvedUrl( - "../scripts/shibumi-gpu-probe")).replace("file://", "") + "scripts/shibumi-gpu-probe")).replace("file://", "") property var devices: [] property string backend: "" property string name: "" diff --git a/scripts/check-production-boundary b/scripts/check-production-boundary new file mode 100755 index 0000000..539a767 --- /dev/null +++ b/scripts/check-production-boundary @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import os +import posixpath +import re +import sys +from collections import Counter +from pathlib import Path + + +CODE_SUFFIXES = {"", ".bash", ".js", ".mjs", ".py", ".qml", ".sh"} + +# Exact, reviewed compatibility debt. A capability cutover removes entries; it +# must not broaden them. This is a repository policy gate, not a hostile-code +# sandbox: deliberate lexical obfuscation is outside its threat model. +TOKEN_ALLOWANCES = { + "hancore.shibumi.ai/Service.qml": Counter({ + "entryPointUrl": 2, + "registeredWidgetSource": 2, + "standardWidgetSource": 3, + }), + "hancore.shibumi.audio/BarWidget.qml": Counter({ + "entryPointUrl": 2, + "registeredComponent": 2, + "registeredSource": 2, + "registeredWidgetComponent": 2, + "registeredWidgetSource": 2, + }), + "hancore.shibumi.bar/Bar.qml": Counter({ + "entryPointUrl": 3, + "firstPartyServiceFor": 2, + "registeredWidgetComponent": 1, + "registeredWidgetSource": 1, + }), + "hancore.shibumi.bar/core/WidgetSlot.qml": Counter({ + "registeredWidgetComponent": 4, + }), + "hancore.shibumi.bar/services/HostWidgetResolver.qml": Counter({ + "Qt.createComponent": 1, + "entryPointUrl": 5, + }), + "hancore.shibumi.brightness/Service.qml": Counter({ + "entryPointUrl": 2, + "registeredComponent": 2, + "registeredSource": 2, + "registeredWidgetComponent": 2, + "registeredWidgetSource": 2, + }), + "hancore.shibumi.center/BarWidget.qml": Counter({ + "entryPointUrl": 2, + "registeredComponent": 2, + "registeredSource": 2, + "registeredWidgetComponent": 2, + "registeredWidgetSource": 2, + }), + "hancore.shibumi.center/SystemUpdateWidget.qml": Counter({ + "firstPartyServiceFor": 2, + }), + "hancore.shibumi.media/BarWidget.qml": Counter({ + "firstPartyServiceFor": 2, + }), + "hancore.shibumi.media/Service.qml": Counter({ + "firstPartyServiceFor": 2, + }), + "hancore.shibumi.network/Service.qml": Counter({ + "entryPointUrl": 2, + "registeredComponent": 2, + "registeredSource": 2, + "registeredWidgetComponent": 2, + "registeredWidgetSource": 2, + }), + "hancore.shibumi.reactor/ReactorService.qml": Counter({ + "firstPartyService": 2, + "firstPartyServiceFor": 2, + }), + "hancore.shibumi.status/BarWidget.qml": Counter({ + "entryPointUrl": 2, + "firstPartyServiceFor": 5, + "registeredComponent": 3, + "registeredSource": 3, + "registeredWidgetComponent": 2, + "registeredWidgetSource": 2, + }), + "hancore.shibumi.status/Service.qml": Counter({ + "firstPartyServiceFor": 4, + }), +} + +PROVIDER_ALLOWANCES = { + "hancore.shibumi.ai/Service.qml": { + "omarchy.agents", "omarchy.model-usage", + }, + "hancore.shibumi.audio/BarWidget.qml": {"omarchy.audio"}, + "hancore.shibumi.brightness/Service.qml": {"omarchy.monitor"}, + "hancore.shibumi.center/BarWidget.qml": {"omarchy.system-update"}, + "hancore.shibumi.network/Service.qml": {"omarchy.network"}, + "hancore.shibumi.status/BarWidget.qml": {"omarchy.tray"}, +} + +FIRST_PARTY_ALLOWANCES = { + "hancore.shibumi.bar/Bar.qml": {"omarchy.idle"}, + "hancore.shibumi.center/SystemUpdateWidget.qml": { + "omarchy.system-update", + }, + "hancore.shibumi.media/BarWidget.qml": {"omarchy.media"}, + "hancore.shibumi.media/Service.qml": {"omarchy.media"}, + "hancore.shibumi.reactor/ReactorService.qml": {"omarchy.media"}, + "hancore.shibumi.status/BarWidget.qml": {"omarchy.notifications"}, + "hancore.shibumi.status/Service.qml": { + "omarchy.idle", "omarchy.notifications", + }, +} + +HELPER_ALLOWANCES = { + "hancore.shibumi.network/Service.qml": Counter({ + "omarchy-network-status": 1, + }), + "hancore.shibumi.power-state/Service.qml": Counter({ + "omarchy-battery-status": 1, + "omarchy-powerprofiles-list": 1, + }), + "hancore.shibumi.status/Service.qml": Counter({ + "/tmp/omarchy-screenrecord-filename": 1, + "omarchy-voxtype-status": 1, + "pgrep": 1, + }), + "hancore.shibumi.quick-access/scripts/shibumi-picker": Counter({ + "pgrep": 1, + }), +} + +PROTECTED_TOKENS = { + token for allowance in TOKEN_ALLOWANCES.values() for token in allowance +} +PRIVATE_PATHS = ( + re.compile(r"/usr/share/omarchy/(?:plugins|shell)(?:/|$)"), + re.compile(r"(?:^|[/\"'\s])shell/plugins/", re.MULTILINE), + re.compile(r"\.config/omarchy/plugins/omarchy\."), +) +PROVIDER_CALL = re.compile( + r"\b(?:registeredSource|registeredComponent|registeredWidgetSource|" + r"registeredWidgetComponent|standardWidgetSource)\s*\(\s*" + r"[\"'](omarchy\.[A-Za-z0-9_.-]+)[\"']" +) +FIRST_PARTY_CALL = re.compile( + r"\b(?:firstPartyServiceFor|firstPartyService)\s*\(\s*" + r"[\"'](omarchy\.[A-Za-z0-9_.-]+)[\"']" +) +HELPER_MARKER = re.compile( + r"/tmp/omarchy-[A-Za-z0-9_.-]+|" + r"\bomarchy-[A-Za-z0-9-]*(?:status|state|list)\b|" + r"\b(?:pgrep|pidof)\b" +) +RELATIVE_LITERAL = re.compile(r"[\"']([^\"']*\.\.[^\"']*)[\"']") + + +def add(findings: list[str], path: str, kind: str, detail: str) -> None: + findings.append(f"{path}: {kind}: {detail}") + + +def production_files(root: Path) -> list[tuple[Path, Path]]: + files: list[tuple[Path, Path]] = [] + for plugin in sorted(root.glob("hancore.shibumi.*")): + if plugin.is_symlink(): + files.append((plugin, plugin)) + continue + if not plugin.is_dir(): + continue + for path in sorted(plugin.rglob("*")): + if path.is_symlink() or ( + path.is_file() + and (path.suffix.lower() in CODE_SUFFIXES or os.access(path, os.X_OK)) + ): + files.append((path, plugin)) + return files + + +def compare_exact( + findings: list[str], relative: str, kind: str, + observed: Counter[str], allowed: Counter[str], +) -> None: + for token, count in sorted((observed - allowed).items()): + add(findings, relative, kind, f"{token} occurs {count} unapproved time(s)") + for token, count in sorted((allowed - observed).items()): + add( + findings, + relative, + kind, + f"approved {token} occurrence disappeared {count} time(s); " + "remove or update its narrow allowance", + ) + + +def check_tree(root: Path) -> list[str]: + root = root.resolve() + findings: list[str] = [] + visited: set[str] = set() + present_plugins = { + path.name for path in root.glob("hancore.shibumi.*") if path.is_dir() + } + + for path, plugin in production_files(root): + relative = path.relative_to(root).as_posix() + visited.add(relative) + resolved_plugin = plugin.resolve() + if path.is_symlink(): + kind = "package escape" \ + if not path.resolve().is_relative_to(resolved_plugin) \ + else "production symlink" + add(findings, relative, kind, "production symlinks are not allowed") + continue + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + add(findings, relative, "undecodable production source", "not UTF-8") + continue + + for pattern in PRIVATE_PATHS: + if pattern.search(text): + add(findings, relative, "private component path", pattern.pattern) + + observed_tokens: Counter[str] = Counter() + for token in PROTECTED_TOKENS: + count = text.count(token) if "." in token else len(re.findall( + rf"(? int: + parser = argparse.ArgumentParser( + description="Reject undeclared private backends in shipped Shibumi plugins." + ) + parser.add_argument( + "--root", + type=Path, + default=Path(__file__).resolve().parents[1], + help="tree containing hancore.shibumi.* plugin directories", + ) + args = parser.parse_args() + if not args.root.is_dir(): + parser.error(f"production root is not a directory: {args.root}") + findings = check_tree(args.root) + if findings: + for finding in findings: + print(f"production boundary error: {finding}", file=sys.stderr) + return 1 + print("Shibumi production boundary check passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/opencode-usage b/scripts/opencode-usage deleted file mode 100755 index 8212787..0000000 --- a/scripts/opencode-usage +++ /dev/null @@ -1,292 +0,0 @@ -#!/usr/bin/env python3 -""" -OpenCode local usage for the Shibumi Omarchy plugin. - -OpenCode does not expose a Claude/Codex-style 5h/7d quota locally. This script -reads OpenCode's local SQLite messages and prints the V1-compatible JSON shape -consumed by the plugin, using configurable soft caps for the visual usage bars. -It never reads API tokens, writes cache files, or makes network calls. -""" -import json -import os -import sqlite3 -import time -from datetime import datetime, timedelta -from pathlib import Path - -DATA_DIR = Path(os.environ.get("OPENCODE_DATA_DIR") or (Path.home() / ".local" / "share" / "opencode")) -DB_FILE = DATA_DIR / "opencode.db" - -DEFAULT_5H_LIMIT = 1_000_000 -DEFAULT_7D_LIMIT = 20_000_000 - - -def env_int(name: str, default: int) -> int: - try: - value = int(os.environ.get(name, "")) - return value if value > 0 else default - except Exception: - return default - - -LIMIT_5H = env_int("OPENCODE_USAGE_5H_LIMIT", DEFAULT_5H_LIMIT) -LIMIT_7D = env_int("OPENCODE_USAGE_7D_LIMIT", DEFAULT_7D_LIMIT) - - -def unavailable() -> None: - print(json.dumps({"ready": False, "status": "unavailable"})) - - -def connect() -> sqlite3.Connection: - con = sqlite3.connect(f"file:{DB_FILE}?mode=ro", uri=True, timeout=2.0) - con.execute("pragma busy_timeout=2000") - return con - - -def as_int(value) -> int: - try: - return int(value or 0) - except Exception: - return 0 - - -def as_float(value) -> float: - try: - return float(value or 0.0) - except Exception: - return 0.0 - - -def model_label(model_id: str, provider_id: str = "") -> str: - model_id = str(model_id or "") - provider_id = str(provider_id or "") - return model_id + (f" ({provider_id})" if provider_id else "") - - -def assistant_messages( - con: sqlite3.Connection, since_ms: int, before_ms: int -) -> list[dict]: - rows = con.execute( - """ - select session_id, time_created, time_updated, data - from message - where time_updated >= ? and time_updated < ? - order by time_updated asc - """, - (since_ms, before_ms), - ).fetchall() - out = [] - for session_id, time_created, time_updated, raw in rows: - try: - data = json.loads(raw or "{}") - except Exception: - continue - if data.get("role") != "assistant": - continue - model_id = data.get("modelID") or "" - if not model_id: - continue - tokens = data.get("tokens") or {} - cache = tokens.get("cache") or {} - inp = as_int(tokens.get("input")) - output = as_int(tokens.get("output")) - reasoning = as_int(tokens.get("reasoning")) - cache_read = as_int(cache.get("read")) - cache_write = as_int(cache.get("write")) - # cache_read is server-side context replay. Keep it visible in the model - # breakdown, but exclude it from usage caps so the 5h bar can move. - total = inp + output + reasoning + cache_write - out.append({ - "session_id": session_id, - "model_id": model_id, - "provider_id": data.get("providerID") or "", - "input": inp, - "output": output, - "reasoning": reasoning, - "cache_read": cache_read, - "cache_write": cache_write, - "total": total, - "total_with_cache_read": total + cache_read, - "cost": as_float(data.get("cost")), - "time": as_int(time_updated or time_created), - }) - return out - - -def sum_since(messages: list[dict], since_ms: int) -> tuple[int, float, int]: - usage = 0 - cost = 0.0 - with_cache_read = 0 - for msg in messages: - if msg["time"] < since_ms: - continue - usage += msg["total"] - with_cache_read += msg["total_with_cache_read"] - cost += msg["cost"] - return usage, cost, with_cache_read - - -def newest_model(messages: list[dict], today_ms: int, tomorrow_ms: int) -> str: - msgs = [ - msg for msg in messages - if today_ms <= msg["time"] < tomorrow_ms and msg["total"] > 0 - ] - if not msgs: - return "" - msg = max(msgs, key=lambda item: item["time"]) - return model_label(msg["model_id"], msg["provider_id"]) - - -def fmt_tokens(value: int) -> str: - if value >= 1_000_000: - return f"{value / 1_000_000:.1f}M" - if value >= 1_000: - return f"{value / 1_000:.1f}K" - return str(value) - - -def model_rows( - messages: list[dict], today_ms: int, tomorrow_ms: int, limit: int = 6 -) -> list[dict]: - by_model = {} - for msg in messages: - if not today_ms <= msg["time"] < tomorrow_ms or msg["total"] <= 0: - continue - key = (msg["provider_id"], msg["model_id"]) - cur = by_model.setdefault(key, { - "name": model_label(msg["model_id"], msg["provider_id"]), - "session_ids": set(), - "input": 0, - "output": 0, - "reasoning": 0, - "cache_read": 0, - "cache_write": 0, - "cost": 0.0, - "last_seen": 0, - "today": 0, - "total": 0, - "total_with_cache_read": 0, - }) - cur["session_ids"].add(msg["session_id"]) - cur["input"] += msg["input"] - cur["output"] += msg["output"] - cur["reasoning"] += msg["reasoning"] - cur["cache_read"] += msg["cache_read"] - cur["cache_write"] += msg["cache_write"] - cur["cost"] += msg["cost"] - cur["last_seen"] = max(cur["last_seen"], msg["time"]) - cur["total"] += msg["total"] - cur["total_with_cache_read"] += msg["total_with_cache_read"] - cur["today"] += msg["total"] - - rows = sorted(by_model.values(), key=lambda item: item["last_seen"], reverse=True)[:limit] - totals = [] - for row in rows: - total = int(row["total"]) - cost = float(row["cost"]) - totals.append({ - "name": row["name"], - "sessions": len(row["session_ids"]), - "total": total, - "totalLabel": fmt_tokens(total), - "inputLabel": fmt_tokens(int(row["input"])), - "outputLabel": fmt_tokens(int(row["output"])), - "reasoningLabel": fmt_tokens(int(row["reasoning"])), - "cacheReadLabel": fmt_tokens(int(row["cache_read"])), - "cacheWriteLabel": fmt_tokens(int(row["cache_write"])), - # Every model component in this row already has today's scope. - # Keep the legacy field inert rather than repeating the total. - "todayLabel": "0", - "costLabel": f"${cost:.2f}" if cost > 0 else "", - "lastSeen": int(row["last_seen"]), - "totalWithCacheRead": int(row["total_with_cache_read"]), - "totalWithCacheReadLabel": fmt_tokens(int(row["total_with_cache_read"])), - }) - max_total = max((row["total"] for row in totals), default=0) - for row in totals: - row["pct"] = int(round(row["total"] * 100 / max_total)) if max_total else 0 - return totals - - -def pct(tokens: int, limit: int) -> float: - if limit <= 0: - return 0.0 - return min(1.0, max(0.0, tokens / float(limit))) - - -def local_day_bounds(now_value: datetime | None = None) -> tuple[int, int, str]: - current = now_value or datetime.now() - start_today = current.replace(hour=0, minute=0, second=0, microsecond=0) - start_tomorrow = start_today + timedelta(days=1) - return ( - int(start_today.timestamp() * 1000), - int(start_tomorrow.timestamp() * 1000), - start_today.date().isoformat(), - ) - - -def main() -> None: - if not DB_FILE.exists(): - unavailable() - return - - now = time.time() - five_h_ms = int((now - 5 * 3600) * 1000) - seven_d_ms = int((now - 7 * 24 * 3600) * 1000) - hour_ms = int((now - 3600) * 1000) - today_ms, tomorrow_ms, local_day = local_day_bounds() - current_ms_exclusive = int(now * 1000) + 1 - - try: - con = connect() - with con: - # Explicitly pin every aggregate to one SQLite read snapshot. A - # concurrent OpenCode write cannot split latestModel, model rows, - # and token totals across different database states. - con.execute("begin") - messages = assistant_messages( - con, min(seven_d_ms, today_ms), - min(tomorrow_ms, current_ms_exclusive), - ) - tok_5h, cost_5h, tok_5h_with_cache = sum_since(messages, five_h_ms) - tok_7d, cost_7d, tok_7d_with_cache = sum_since(messages, seven_d_ms) - tok_1h, _, tok_1h_with_cache = sum_since(messages, hour_ms) - tok_today, cost_today, tok_today_with_cache = sum_since(messages, today_ms) - model = newest_model(messages, today_ms, tomorrow_ms) - models = model_rows(messages, today_ms, tomorrow_ms) - except sqlite3.Error: - unavailable() - return - - out = { - "5h-utilization": f"{pct(tok_5h, LIMIT_5H):.4f}", - "5h-reset": "0", - "7d-utilization": f"{pct(tok_7d, LIMIT_7D):.4f}", - "7d-reset": "0", - "updated_at": datetime.now().strftime("%H:%M:%S"), - "_day": local_day, - "_source": "sqlite", - "_plan": "local messages", - "_model": model, - "_models": models, - "_tokens_used": tok_5h, - "_tokens_used_with_cache_read": tok_5h_with_cache, - "_window_limit": LIMIT_5H, - "_rate_per_hour": tok_1h, - "_rate_per_hour_with_cache_read": tok_1h_with_cache, - "_today_tokens": tok_today, - "_today_tokens_with_cache_read": tok_today_with_cache, - "_7d_tokens": tok_7d, - "_7d_tokens_with_cache_read": tok_7d_with_cache, - "_7d_limit": LIMIT_7D, - "_cost_5h": cost_5h, - "_cost_7d": cost_7d, - "_cost_today": cost_today, - "status": "allowed_warning" if tok_5h >= LIMIT_5H * 0.9 else "allowed", - } - out["ready"] = True - print(json.dumps(out)) - - -if __name__ == "__main__": - main() diff --git a/scripts/shibumi-gpu-probe b/scripts/shibumi-gpu-probe deleted file mode 100755 index c093ddd..0000000 --- a/scripts/shibumi-gpu-probe +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env bash - -set -uo pipefail -shopt -s extglob nullglob - -sys_root=${SHIBUMI_GPU_SYS_ROOT:-/sys} -found=0 -legacy_emitted=0 -declare -A seen_devices=() - -clean_field() { - local value=${1:-} - value=${value//$'\n'/ } - value=${value//$'\r'/ } - value=${value//|/ } - value=${value##+([[:space:]])} - value=${value%%+([[:space:]])} - printf '%s' "$value" -} - -integer_field() { - local value=${1:-} - if [[ $value =~ ^[[:space:]]*([0-9]+)[[:space:]]*$ ]]; then - printf '%s' "${BASH_REMATCH[1]}" - return 0 - fi - return 1 -} - -normalize_pci_slot() { - local value - value=$(clean_field "${1:-}") - value=${value,,} - if [[ $value =~ ^[[:xdigit:]]{8}: ]]; then - value=${value:4} - fi - [[ $value =~ ^[[:xdigit:]]{4}:[[:xdigit:]]{2}:[[:xdigit:]]{2}\.[[:xdigit:]]$ ]] \ - || return 1 - printf '%s' "$value" -} - -emit_device() { - local id backend util temp used total name driver version node - id=$(clean_field "${1:-}") - backend=$(clean_field "${2:-}") - util=$(integer_field "${3:-}") || util=0 - temp=$(integer_field "${4:-}") || temp=0 - used=$(integer_field "${5:-}") || used=0 - total=$(integer_field "${6:-}") || total=0 - name=$(clean_field "${7:-}") - driver=$(clean_field "${8:-}") - version=$(clean_field "${9:-}") - node=$(clean_field "${10:-}") - [[ -n $id && -n $backend ]] || return 1 - (( util > 100 )) && util=100 - [[ -n $name ]] || name="$driver GPU" - # Keep the first two records compatible with a pre-upgrade telemetry parser - # while a transactional update replaces the helper before loaded QML. - if (( legacy_emitted == 0 )); then - printf '%s|%s|%s|%s|%s\n' "$backend" "$util" "$temp" "$used" "$total" - printf 'meta|%s|%s|%s\n' "$name" "$driver" "$version" - legacy_emitted=1 - fi - printf 'device|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \ - "$id" "$backend" "$util" "$temp" "$used" "$total" \ - "$name" "$driver" "$version" "$node" - seen_devices["$id"]=1 - found=$((found + 1)) -} - -probe_nvidia() { - [[ ${SHIBUMI_GPU_DISABLE_NVIDIA:-0} != 1 ]] || return 0 - - local nvidia_bin output summary slot name version util temp used total id - nvidia_bin=$(command -v nvidia-smi 2>/dev/null) || return 0 - output=$("$nvidia_bin" \ - --query-gpu=pci.bus_id,name,driver_version,utilization.gpu,temperature.gpu,memory.used,memory.total \ - --format=csv,noheader,nounits 2>/dev/null) || return 0 - - while IFS= read -r summary; do - [[ -n $summary ]] || continue - IFS=, read -r slot name version util temp used total <<<"$summary" - slot=$(normalize_pci_slot "$slot" 2>/dev/null || true) - # A runtime index is not stable enough for persisted selection. Let the - # DRM pass recover this device by its PCI slot when NVIDIA omits one. - [[ -n $slot ]] || continue - id="pci:$slot" - [[ -z ${seen_devices[$id]+x} ]] || continue - util=$(integer_field "$util") || continue - temp=$(integer_field "$temp") || continue - used=$(integer_field "$used") || continue - total=$(integer_field "$total") || continue - emit_device "$id" nvidia "$util" "$temp" "$used" "$total" \ - "$name" nvidia "$version" "" - done <<<"$output" -} - -probe_drm() { - local device card_node resolved slot id driver_path driver util temp raw sensor - local total_bytes used_bytes total_mib used_mib model model_line lspci_bin - local version - - for device in "$sys_root"/class/drm/card+([0-9])/device; do - [[ -d $device ]] || continue - card_node=${device%/device} - card_node=${card_node##*/} - resolved=$(readlink -f "$device" 2>/dev/null) || continue - slot=${resolved##*/} - slot=$(normalize_pci_slot "$slot" 2>/dev/null || true) - [[ -n $slot ]] || continue - id="pci:$slot" - [[ -z ${seen_devices[$id]+x} ]] || continue - driver_path=$(readlink -f "$device/driver" 2>/dev/null) || continue - driver=${driver_path##*/} - - util=0 - if [[ -r $device/gpu_busy_percent ]]; then - IFS= read -r raw <"$device/gpu_busy_percent" - util=$(integer_field "$raw") || util=0 - fi - (( util > 100 )) && util=100 - - temp=0 - for sensor in "$device"/hwmon/hwmon*/temp1_input; do - [[ -r $sensor ]] || continue - IFS= read -r raw <"$sensor" - raw=$(integer_field "$raw") || continue - temp=$((raw / 1000)) - break - done - - used_mib=0 - total_mib=0 - if [[ -r $device/mem_info_vram_used ]]; then - IFS= read -r used_bytes <"$device/mem_info_vram_used" - used_bytes=$(integer_field "$used_bytes") || used_bytes=0 - used_mib=$((used_bytes / 1048576)) - fi - if [[ -r $device/mem_info_vram_total ]]; then - IFS= read -r total_bytes <"$device/mem_info_vram_total" - total_bytes=$(integer_field "$total_bytes") || total_bytes=0 - total_mib=$((total_bytes / 1048576)) - fi - - model="" - lspci_bin=$(command -v lspci 2>/dev/null || true) - if [[ -n $lspci_bin ]]; then - IFS= read -r model_line < <("$lspci_bin" -D -s "$slot" 2>/dev/null) - model_line=${model_line#* } - [[ $model_line == *": "* ]] && model=${model_line#*: } - fi - model=$(clean_field "$model") - [[ -n $model ]] || model="$driver GPU" - - version="" - [[ -r $sys_root/module/$driver/version ]] \ - && IFS= read -r version <"$sys_root/module/$driver/version" - version=$(clean_field "$version") - - emit_device "$id" sysfs "$util" "$temp" "$used_mib" "$total_mib" \ - "$model" "$driver" "$version" "$card_node" - done -} - -probe_nvidia -probe_drm -if (( found == 0 )); then - printf 'none|0|0|0|0\n' -fi -printf 'status|ok\n' diff --git a/scripts/sync-shared.sh b/scripts/sync-shared.sh index e0ebce3..03a5cfd 100755 --- a/scripts/sync-shared.sh +++ b/scripts/sync-shared.sh @@ -24,7 +24,6 @@ mappings=( "shared/telemetry/SystemTelemetry.qml:hancore.shibumi.telemetry/SystemTelemetry.qml" "shared/telemetry/ThermalTelemetry.qml:services/ThermalTelemetry.qml" "shared/telemetry/ThermalTelemetry.qml:hancore.shibumi.telemetry/ThermalTelemetry.qml" - "shared/telemetry/GpuTelemetry.qml:services/GpuTelemetry.qml" "shared/telemetry/GpuTelemetry.qml:hancore.shibumi.cpu/GpuTelemetry.qml" "shared/power-state/Service.qml:services/PowerService.qml" "shared/power-state/Service.qml:hancore.shibumi.power-state/Service.qml" @@ -124,7 +123,6 @@ mappings=( ) executable_mappings=( - "shared/telemetry/shibumi-gpu-probe:scripts/shibumi-gpu-probe" "shared/telemetry/shibumi-gpu-probe:hancore.shibumi.cpu/scripts/shibumi-gpu-probe" "shared/quick-access/shibumi-picker:scripts/shibumi-picker" "shared/quick-access/shibumi-picker:hancore.shibumi.quick-access/scripts/shibumi-picker" diff --git a/services/AiUsageService.qml b/services/AiUsageService.qml deleted file mode 100644 index b8634ee..0000000 --- a/services/AiUsageService.qml +++ /dev/null @@ -1,270 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell.Io - -// One process-wide provider owner. Official Claude/Codex scanners are loaded -// only when their local data exists; OpenCode uses the plugin-local read-only -// SQLite adapter. Per-output widgets render this shared state only. -Item { - id: root - - required property var bar - - property bool detectionReady: false - property bool claudeAvailable: false - property bool codexAvailable: false - property bool openCodeAvailable: false - property int providerRevision: 0 - - readonly property string modelUsageSource: bar - && typeof bar.registeredWidgetSource === "function" - ? String(bar.registeredWidgetSource("omarchy.model-usage") || "") : "" - readonly property url claudeSource: providerUrl("Claude.qml") - readonly property url codexSource: providerUrl("Codex.qml") - readonly property var settings: bar - && typeof bar.widgetSettings === "function" - ? bar.widgetSettings("G7", "hancore.shibumi.ai") : ({}) - readonly property string requestedTool: String( - settings.aiTool || settings.tool || "claude").toLowerCase() - readonly property var providers: { - void(providerRevision) - const result = [] - if (claudeLoader.item) result.push(claudeLoader.item) - if (codexLoader.item) result.push(codexLoader.item) - if (openCodeProvider.enabled) result.push(openCodeProvider) - return result - } - readonly property string selectedTool: resolveTool(requestedTool) - readonly property var selectedProvider: providerFor(selectedTool) - readonly property bool refreshing: providers.some(function(provider) { - return provider && provider.refreshing === true - }) - - function providerUrl(fileName) { - const source = String(modelUsageSource || "") - const slash = source.lastIndexOf("/") - if (slash < 0) return "" - return source.slice(0, slash + 1) + "providers/" + fileName - } - - function providerFor(id) { - const target = String(id || "") - for (let index = 0; index < providers.length; index++) { - if (String(providers[index].providerId || "") === target) - return providers[index] - } - return null - } - - function resolveTool(preferred) { - const requested = String(preferred || "") - if (providerFor(requested)) return requested - const order = ["claude", "codex", "opencode"] - for (let index = 0; index < order.length; index++) { - if (providerFor(order[index])) return order[index] - } - return "" - } - - function selectTool(id) { - const target = String(id || "") - if (!providerFor(target) || !bar - || typeof bar.setWidgetSetting !== "function") return false - return bar.setWidgetSetting("G7", "hancore.shibumi.ai", "aiTool", target) - } - - function cycleTool(direction) { - if (providers.length < 2) return false - let index = providers.indexOf(selectedProvider) - if (index < 0) index = 0 - const step = Number(direction) < 0 ? -1 : 1 - const next = providers[(index + step + providers.length) % providers.length] - return selectTool(next.providerId) - } - - function refreshAll(force) { - for (let index = 0; index < providers.length; index++) { - const provider = providers[index] - if (provider && typeof provider.refresh === "function") provider.refresh(force === true) - } - } - - function usagePercent(provider) { - if (!provider) return -1 - const primary = displayPercent(provider, provider.rateLimitPercent) - const secondary = displayPercent(provider, provider.secondaryRateLimitPercent) - if (isFinite(primary) && primary >= 0) return Math.round(primary) - if (isFinite(secondary) && secondary >= 0) return Math.round(secondary) - return -1 - } - - function displayPercent(provider, value) { - const number = Number(value) - if (!isFinite(number) || number < 0) return -1 - const providerId = provider ? String(provider.providerId || "") : "" - // The official Claude and Codex providers expose utilization as a 0..1 - // fraction. OpenCode is normalized to 0..100 by its local adapter. - if ((providerId === "claude" || providerId === "codex") && number <= 1) - return Math.min(100, number * 100) - return Math.min(100, number) - } - - function displayTierLabel(value) { - const label = String(value || "") - if (label.toLowerCase() === "prolite") return "Pro Lite" - return label - } - - function resetText(provider, timestamp) { - if (!provider || !timestamp || typeof provider.formatResetTime !== "function") return "" - return String(provider.formatResetTime(timestamp) || "") - } - - function providerReportsFiveHour(provider) { - if (!provider) return false - const labels = [provider.rateLimitLabel, provider.secondaryRateLimitLabel] - for (let index = 0; index < labels.length; index++) { - const label = String(labels[index] || "").toLowerCase() - if (label.indexOf("5h") >= 0 - || (label.indexOf("5") >= 0 && label.indexOf("hour") >= 0)) - return true - } - return false - } - - function tooltipText() { - if (providers.length === 0) return "No AI usage providers detected" - const lines = [] - for (let index = 0; index < providers.length; index++) { - const provider = providers[index] - if (lines.length) lines.push("") - let heading = String(provider.providerName || provider.providerId || "AI") - if (displayTierLabel(provider.tierLabel)) - heading += " (" + displayTierLabel(provider.tierLabel) + ")" - lines.push(heading) - if (Number(provider.rateLimitPercent) >= 0) { - const reset = resetText(provider, provider.rateLimitResetAt) - lines.push(String(provider.rateLimitLabel || "Primary") + ": " - + Math.round(displayPercent(provider, provider.rateLimitPercent)) + "%" - + (reset ? " · resets in " + reset : "")) - } - if (Number(provider.secondaryRateLimitPercent) >= 0) { - const reset = resetText(provider, provider.secondaryRateLimitResetAt) - lines.push(String(provider.secondaryRateLimitLabel || "Secondary") + ": " - + Math.round(displayPercent(provider, - provider.secondaryRateLimitPercent)) + "%" - + (reset ? " · resets in " + reset : "")) - } - if (String(provider.providerId || "") === "codex" - && !providerReportsFiveHour(provider)) - lines.push("5h: not reported by Codex RPC") - if (String(provider.usageStatusText || "") - && String(provider.providerId || "") === "codex") - lines.push("General limit: " + String(provider.usageStatusText)) - const providerId = String(provider.providerId || "") - if (providerId === "opencode") { - if (Number(provider.windowTokens) > 0) - lines.push("5h tokens: " + formatTokens(provider.windowTokens)) - if (Number(provider.hourlyTokens) > 0) - lines.push("1h rate: " + formatTokens(provider.hourlyTokens) + "/h") - } else if (Number(provider.windowTokens) > 0) { - let activity = formatTokens(provider.windowTokens) + " tokens" - if (Number(provider.hourlyTokens) > 0) - activity += " · " + formatTokens(provider.hourlyTokens) + "/h" - lines.push(activity) - } else if (Number(provider.hourlyTokens) > 0) { - lines.push("Rate: " + formatTokens(provider.hourlyTokens) + "/h") - } - if (provider.todayTotalTokens !== undefined - && Number(provider.todayTotalTokens) > 0) - lines.push("Today: " + formatTokens(provider.todayTotalTokens) + " tokens") - if (String(provider.latestModel || "")) - lines.push((providerId === "opencode" ? "Latest today: " : "") - + String(provider.latestModel)) - } - return lines.join("\n") - } - - function formatTokens(value) { - const count = Math.max(0, Number(value) || 0) - if (count >= 1000000) return (count / 1000000).toFixed(2) + "M" - if (count >= 1000) return (count / 1000).toFixed(1) + "K" - return String(Math.round(count)) - } - - function applyDetection(raw) { - try { - const result = JSON.parse(String(raw || "{}")) - claudeAvailable = result.claude === true - codexAvailable = result.codex === true - openCodeAvailable = result.opencode === true - detectionReady = true - providerRevision++ - } catch (_error) { - detectionReady = true - } - } - - Process { - id: detectionProc - command: ["bash", "-c", [ - "home=${HOME:?}", - "claude=false", - "codex=false", - "opencode=false", - "[[ -s \"$home/.claude/.credentials.json\" || -s \"$home/.claude/stats-cache.json\" || -s \"$home/.claude/history.jsonl\" ]] && claude=true", - "[[ -d \"$home/.codex/sessions\" || -s \"$home/.codex/auth.json\" ]] && codex=true", - "[[ -s \"$home/.local/share/opencode/opencode.db\" ]] && opencode=true", - "printf '{\"claude\":%s,\"codex\":%s,\"opencode\":%s}\\n' \"$claude\" \"$codex\" \"$opencode\"" - ].join("; ")] - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: root.applyDetection(text) - } - } - - Timer { - interval: 60 * 1000 - running: true - repeat: true - triggeredOnStart: true - onTriggered: if (!detectionProc.running) detectionProc.running = true - } - - Loader { - id: claudeLoader - active: root.detectionReady && root.claudeAvailable && String(root.claudeSource) - source: active ? root.claudeSource : "" - onLoaded: { - if (item) { - item.providerSettings = root.settings.providers - && root.settings.providers.claude ? root.settings.providers.claude : ({}) - item.enabled = true - } - root.providerRevision++ - } - onItemChanged: root.providerRevision++ - } - - Loader { - id: codexLoader - active: root.detectionReady && root.codexAvailable && String(root.codexSource) - source: active ? root.codexSource : "" - onLoaded: { - if (item) { - item.providerSettings = root.settings.providers - && root.settings.providers.codex ? root.settings.providers.codex : ({}) - item.enabled = true - } - root.providerRevision++ - } - onItemChanged: root.providerRevision++ - } - - OpenCodeProvider { - id: openCodeProvider - enabled: root.detectionReady && root.openCodeAvailable - onReadyChanged: root.providerRevision++ - } -} diff --git a/services/BluetoothService.qml b/services/BluetoothService.qml deleted file mode 100644 index 47ab1bd..0000000 --- a/services/BluetoothService.qml +++ /dev/null @@ -1,111 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell.Io -import "../adapters" as Adapters - -// One process-wide owner for Quattro's Bluetooth and Bluetooth-audio state. -// Screen-local widgets and panels consume this facade without constructing -// additional BlueZ or PipeWire models. -Item { - id: root - - required property var bar - property var backendOverride: null - property var sessionOwners: [] - property int discoveryRetryInterval: 1000 - property int discoveryRequestTimeoutInterval: 1500 - - readonly property var backend: adapter.backend - readonly property bool ready: adapter.ready - readonly property bool adapterAvailable: adapter.adapterAvailable - readonly property bool radioEnabled: adapter.radioEnabled - readonly property bool discovering: adapter.discovering - readonly property var connectedDevices: adapter.connectedDevices - readonly property var knownDevices: adapter.knownDevices - readonly property var discoveredDevices: adapter.discoveredDevices - readonly property var pendingActions: adapter.pendingActions - readonly property int connectedCount: connectedDevices.length - readonly property int sessionCount: sessionOwners.length - - visible: false - width: 0 - height: 0 - - function beginSession(owner) { - if (!owner) return false - if (sessionOwners.indexOf(owner) < 0) - sessionOwners = sessionOwners.concat([owner]) - if (sessionOwners.length === 1 && radioEnabled) adapter.startDiscovery() - return true - } - - function endSession(owner) { - sessionOwners = sessionOwners.filter(item => item !== owner) - if (sessionOwners.length === 0) adapter.stopDiscovery() - } - - function restartDiscovery() { - return sessionOwners.length > 0 && adapter.restartDiscovery() - } - - function openPanel() { - return bar && typeof bar.summonBarWidget === "function" - ? bar.summonBarWidget("omarchy.bluetooth") : false - } - - function closePanel() { - return bar && typeof bar.hideBarWidget === "function" - ? bar.hideBarWidget("omarchy.bluetooth") : false - } - - function togglePanel() { - return bar && typeof bar.isBarWidgetOpen === "function" - && bar.isBarWidgetOpen("omarchy.bluetooth") - ? closePanel() : openPanel() - } - - function toggleBluetooth() { - return adapter.toggleBluetooth() - } - function connectDevice(device) { return adapter.connectDevice(device) } - function disconnectDevice(device) { return adapter.disconnectDevice(device) } - function forgetDevice(device) { return adapter.forgetDevice(device) } - function pendingAction(address) { return adapter.pendingAction(address) } - function deviceLabel(device) { return adapter.deviceLabel(device) } - - onRadioEnabledChanged: { - if (!radioEnabled) adapter.stopDiscovery() - } - - Component.onDestruction: adapter.stopDiscovery() - - // Preserve Quattro's public contract with one process-wide Shibumi owner. - IpcHandler { - target: "omarchy.bluetooth" - - function open(): void { root.openPanel() } - function show(): void { root.openPanel() } - function toggle(): void { root.togglePanel() } - function close(): void { root.closePanel() } - function hide(): void { root.closePanel() } - function toggleBluetooth(): void { root.toggleBluetooth() } - } - - Adapters.BluetoothBackendAdapter { - id: adapter - backendOverride: root.backendOverride - discoveryDesired: root.sessionCount > 0 - discoveryRequestTimeoutInterval: root.discoveryRequestTimeoutInterval - } - - Timer { - id: discoveryRetry - interval: root.discoveryRetryInterval - repeat: true - triggeredOnStart: true - running: root.sessionCount > 0 && root.adapterAvailable - && root.radioEnabled && !root.discovering - onTriggered: adapter.startDiscovery() - } -} diff --git a/services/ClockService.qml b/services/ClockService.qml deleted file mode 100644 index 96477bb..0000000 --- a/services/ClockService.qml +++ /dev/null @@ -1,11 +0,0 @@ -import QtQuick -import Quickshell - -Scope { - readonly property date date: systemClock.date - - SystemClock { - id: systemClock - precision: SystemClock.Minutes - } -} diff --git a/services/GpuTelemetry.qml b/services/GpuTelemetry.qml deleted file mode 100644 index b8b5d50..0000000 --- a/services/GpuTelemetry.qml +++ /dev/null @@ -1,196 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Io - -Scope { - id: root - - property int consumers: 0 - property string helperPath: String(Qt.resolvedUrl( - "../scripts/shibumi-gpu-probe")).replace("file://", "") - property var devices: [] - property string backend: "" - property string name: "" - property string driverName: "" - property string driverVersion: "" - property int utilization: 0 - property int temperatureC: 0 - property int memoryUsedMiB: 0 - property int memoryTotalMiB: 0 - property bool probeFailed: false - readonly property bool available: backend !== "" - readonly property int deviceCount: Array.isArray(devices) ? devices.length : 0 - readonly property var defaultDevice: deviceCount > 0 ? devices[0] : null - readonly property int intervalMs: 1500 - readonly property int probeTimeoutSeconds: 2 - - function acquire() { - consumers++ - if (consumers === 1) refresh() - } - - function release() { - consumers = Math.max(0, consumers - 1) - if (consumers === 0) probe.running = false - } - - function refresh() { - if (consumers <= 0 || probe.running) return - probe.running = true - } - - function boundedPercent(value) { - return Math.max(0, Math.min(100, parseInt(value) || 0)) - } - - function nonNegativeInteger(value) { - return Math.max(0, parseInt(value) || 0) - } - - function deviceForId(deviceId) { - const target = String(deviceId || "").trim() - if (target === "" || target === "auto") return defaultDevice - const rows = Array.isArray(devices) ? devices : [] - for (let index = 0; index < rows.length; index++) { - if (String(rows[index].id || "") === target) return rows[index] - } - return null - } - - function applySummary(device) { - const value = device || null - backend = value ? String(value.backend || "") : "" - name = value ? String(value.name || "") : "" - driverName = value ? String(value.driverName || "") : "" - driverVersion = value ? String(value.driverVersion || "") : "" - utilization = value ? boundedPercent(value.utilization) : 0 - temperatureC = value ? nonNegativeInteger(value.temperatureC) : 0 - memoryUsedMiB = value ? nonNegativeInteger(value.memoryUsedMiB) : 0 - memoryTotalMiB = value ? nonNegativeInteger(value.memoryTotalMiB) : 0 - } - - function publishDevices(rows) { - devices = rows - applySummary(rows.length > 0 ? rows[0] : null) - } - - function parse(output) { - const lines = String(output || "").trim().split("\n").filter( - function(value) { return String(value || "").trim() !== "" }) - const completed = lines.some(function(value) { - const parts = String(value || "").split("|") - return parts[0] === "status" && parts[1] === "ok" - }) - if (lines.length === 0 || !completed) { - probeFailed = true - return - } - - const rows = [] - let noneReported = false - let deviceRecordSeen = false - for (let index = 0; index < lines.length; index++) { - const fields = lines[index].split("|") - if (fields[0] === "none") { - noneReported = true - continue - } - if (fields[0] !== "device") continue - deviceRecordSeen = true - if (fields.length < 11) continue - const deviceId = String(fields[1] || "").trim() - const deviceBackend = String(fields[2] || "").trim() - if (deviceId === "" || deviceBackend === "") continue - let duplicate = false - for (let existing = 0; existing < rows.length; existing++) { - if (rows[existing].id === deviceId) { - duplicate = true - break - } - } - if (duplicate) continue - rows.push({ - id: deviceId, - backend: deviceBackend, - utilization: boundedPercent(fields[3]), - temperatureC: nonNegativeInteger(fields[4]), - memoryUsedMiB: nonNegativeInteger(fields[5]), - memoryTotalMiB: nonNegativeInteger(fields[6]), - name: String(fields[7] || "").trim(), - driverName: String(fields[8] || "").trim(), - driverVersion: String(fields[9] || "").trim(), - drmNode: String(fields[10] || "").trim(), - available: true - }) - } - - if (rows.length > 0) { - publishDevices(rows) - probeFailed = false - return - } - if (deviceRecordSeen) { - probeFailed = true - return - } - if (noneReported) { - publishDevices([]) - probeFailed = false - return - } - - // Accept the original single-device protocol for compatibility with - // staged older helpers during transactional upgrades. - const fields = lines[0].split("|") - if (fields.length < 5 || fields[0] === "status") { - probeFailed = true - return - } - let legacyName = "" - let legacyDriver = "" - let legacyVersion = "" - for (let index = 1; index < lines.length; index++) { - const parts = lines[index].split("|") - if (parts[0] !== "meta" || parts.length < 4) continue - legacyName = String(parts[1] || "").trim() - legacyDriver = String(parts[2] || "").trim() - legacyVersion = String(parts[3] || "").trim() - } - const legacyBackend = String(fields[0] || "").trim() - if (legacyBackend === "") { - probeFailed = true - return - } - publishDevices([{ - id: "legacy:" + legacyBackend, - backend: legacyBackend, - utilization: boundedPercent(fields[1]), - temperatureC: nonNegativeInteger(fields[2]), - memoryUsedMiB: nonNegativeInteger(fields[3]), - memoryTotalMiB: nonNegativeInteger(fields[4]), - name: legacyName, - driverName: legacyDriver, - driverVersion: legacyVersion, - drmNode: "", - available: true - }]) - probeFailed = false - } - - Process { - id: probe - command: ["timeout", "--signal=TERM", - String(root.probeTimeoutSeconds), root.helperPath] - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: root.parse(text) - } - } - - Timer { - interval: root.intervalMs - running: root.consumers > 0 - repeat: true - onTriggered: root.refresh() - } -} diff --git a/services/MonitorService.qml b/services/MonitorService.qml deleted file mode 100644 index 1bf193d..0000000 --- a/services/MonitorService.qml +++ /dev/null @@ -1,65 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import "../adapters" as Adapters - -// One process-wide owner for Quattro's monitor state and actions. The official -// component keeps hardware, scale, display, OSD, and IPC ownership; every -// output consumes this service through its own lazy Shibumi panel. -Item { - id: root - - required property var bar - readonly property url panelSource: bar - ? bar.registeredWidgetSource("omarchy.monitor") : "" - property Component panelComponent: String(panelSource) ? null - : bar ? bar.registeredWidgetComponent("omarchy.monitor") : null - - readonly property var backend: bridge.panel - readonly property bool ready: bridge.ready - readonly property bool brightnessAvailable: bridge.brightnessAvailable - readonly property int brightnessPercent: bridge.brightnessPercent - readonly property string internalMonitor: bridge.internalMonitor - readonly property string externalMonitor: bridge.externalMonitor - readonly property string focusedMonitor: bridge.focusedMonitor - readonly property bool internalEnabled: bridge.internalEnabled - readonly property bool mirrorEnabled: bridge.mirrorEnabled - readonly property string monitorScale: bridge.monitorScale - readonly property var displays: bridge.displays - readonly property int enabledDisplayCount: bridge.enabledDisplayCount - readonly property var scaleValues: ["1", "1.25", "1.6", "2", "3", "4"] - readonly property bool textSizeAvailable: bridge.textSizeAvailable - readonly property var textSizeStops: bridge.textSizeStops - readonly property int textSizeIndex: bridge.textSizeIndex - readonly property real textSizePx: bridge.textSizePx - - visible: false - width: 0 - height: 0 - - function officialSettings() { - return bar && typeof bar.widgetSettings === "function" - ? bar.widgetSettings("G13", "omarchy.monitor") : ({}) - } - - function refresh() { return bridge.refresh() } - function setBrightness(value) { return bridge.setBrightness(value) } - function previewBrightness(value) { return bridge.previewBrightness(value) } - function setScale(value) { return bridge.setScale(value) } - function setTextSize(value) { return bridge.setTextSize(value) } - function adjustTextSize(delta) { return bridge.adjustTextSize(delta) } - function toggleDisplay(name, enabled) { - return bridge.toggleDisplay(name, enabled) - } - function normalizeScale(value) { return bridge.normalizeScale(value) } - function brightnessName(value) { return bridge.brightnessName(value) } - - Adapters.MonitorPanelBridge { - id: bridge - bar: root.bar - ownerWidget: root.bar - panelComponent: root.panelComponent - panelSource: root.panelSource - panelSettings: root.officialSettings() - } -} diff --git a/services/NetworkService.qml b/services/NetworkService.qml deleted file mode 100644 index d4354a3..0000000 --- a/services/NetworkService.qml +++ /dev/null @@ -1,798 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell.Io -import Quickshell.Networking -import "../adapters" as Adapters - -// One process-wide NetworkManager owner for every Shibumi output. Quattro's -// official network component remains authoritative for live status, details, -// DNS, and visible-network actions. Shibumi owns the session-scoped scanner -// lease required by its hidden backend, its inline speed-test process, and the -// saved-profile view missing from the host API. -Item { - id: root - - required property var bar - readonly property url panelSource: bar - ? bar.registeredWidgetSource("omarchy.network") : "" - property Component panelComponent: String(panelSource) ? null - : bar ? bar.registeredWidgetComponent("omarchy.network") : null - readonly property var backend: bridge.panel - readonly property url speedTestRunnerSource: - Qt.resolvedUrl("../hancore.shibumi.network/InlineSpeedTestRunner.py") - property string speedTestExecutable: "omarchy-network-speedtest" - property int speedTestPhaseDuration: 5000 - readonly property bool speedTestReady: String(speedTestRunnerSource) !== "" - && String(speedTestExecutable || "") !== "" - property bool speedTestRunning: false - readonly property bool speedTestHasRun: speedTestDownloadMbps !== "" - || speedTestUploadMbps !== "" - property string speedTestPhase: "" - property string speedTestDownloadMbps: "" - property string speedTestUploadMbps: "" - property string speedTestError: "" - property bool speedTestExpectedStop: false - property bool speedTestPendingRun: false - property bool speedTestProcessStarted: false - property string speedTestStderr: "" - readonly property bool ready: backend !== null - readonly property bool backendAvailable: ready && bridge.backendAvailable - readonly property string kind: bridge.kind - readonly property string label: bridge.label - readonly property int signalStrength: bridge.signalStrength - property bool scanPending: false - property var scannerDevice: null - readonly property bool scanning: bridge.scanning || scanPending - readonly property bool busy: bridge.busy || profileAction.running - readonly property bool wifiEnabled: Networking.wifiEnabled - readonly property bool wifiAvailable: backend - ? backend.wifiStationAvailable === true : false - readonly property var info: backend && backend.info ? backend.info : ({}) - readonly property real downloadRate: backend ? Number(backend.downloadRate || 0) : 0 - readonly property real uploadRate: backend ? Number(backend.uploadRate || 0) : 0 - readonly property real routerPingLatency: backend - ? Number(backend.routerPingLatency === undefined ? -1 : backend.routerPingLatency) : -1 - readonly property real internetPingLatency: backend - ? Number(backend.internetPingLatency === undefined ? -1 : backend.internetPingLatency) : -1 - readonly property int internetPingPacketLoss: backend - ? Number(backend.internetPingPacketLoss || 0) : 0 - readonly property string dnsProvider: backend - ? String(backend.dnsProvider || "DHCP") : "DHCP" - readonly property var dnsProviders: backend && backend.dnsProviders - ? backend.dnsProviders : ["DHCP", "Cloudflare", "Google", "Custom"] - readonly property string actionSsid: backend ? String(backend.actionSsid || "") : "" - readonly property string actionKind: backend ? String(backend.actionKind || "") : "" - readonly property string failureSsid: backend ? String(backend.failureSsid || "") : "" - readonly property string failureReason: backend ? String(backend.failureReason || "") : "" - readonly property var visibleNetworks: backend && backend.wifiNetworks - ? backend.wifiNetworks : [] - property var savedProfiles: [] - property bool profilesLoaded: false - property string profileError: "" - property var sessionOwners: [] - readonly property int sessionCount: sessionOwners.length - readonly property var networks: mergedNetworks(visibleNetworks, savedProfiles) - - function activeWifiDevice() { - return ready && backend && backend.wifiDevice - ? backend.wifiDevice : null - } - - function setSessionScannerEnabled(enabled) { - const nextDevice = sessionCount > 0 ? activeWifiDevice() : null - if (scannerDevice && scannerDevice !== nextDevice) - scannerDevice.scannerEnabled = false - scannerDevice = nextDevice - if (!scannerDevice) return false - scannerDevice.scannerEnabled = enabled === true - return true - } - - function releaseWifiScanner() { - scanRestart.stop() - scanDone.stop() - scanPending = false - const currentDevice = activeWifiDevice() - if (scannerDevice) scannerDevice.scannerEnabled = false - if (currentDevice && currentDevice !== scannerDevice) - currentDevice.scannerEnabled = false - scannerDevice = null - } - - function requestWifiScan() { - scanRestart.stop() - scanDone.stop() - if (sessionCount <= 0 || !activeWifiDevice()) { - releaseWifiScanner() - return false - } - scanPending = true - setSessionScannerEnabled(false) - scanRestart.restart() - return true - } - - function completeWifiScan() { - scanPending = false - if (backend && typeof backend.syncWifiNetworks === "function") - backend.syncWifiNetworks() - } - - visible: false - width: 0 - height: 0 - - onReadyChanged: { - if (!ready || sessionCount === 0) releaseWifiScanner() - else requestWifiScan() - } - - Connections { - target: root.backend - function onWifiDeviceChanged() { - if (root.sessionCount > 0) root.requestWifiScan() - else root.releaseWifiScanner() - } - } - - function officialSettings() { - return bar && typeof bar.widgetSettings === "function" - ? bar.widgetSettings("G11", "omarchy.network") : ({}) - } - - function beginSession(owner) { - if (!owner || sessionOwners.indexOf(owner) >= 0) return - const next = sessionOwners.slice() - next.push(owner) - sessionOwners = next - if (sessionCount === 1) { - refresh(true) - } - } - - function endSession(owner) { - if (!owner) return - const nextOwners = sessionOwners.filter(candidate => candidate !== owner) - if (nextOwners.length === 0) stopSpeedTest() - sessionOwners = nextOwners - if (sessionCount !== 0) return - detailsPoll.stop() - detailsProc.running = false - profileList.running = false - releaseWifiScanner() - } - - function refresh(scanWifi) { - if (!ready || typeof backend.refresh !== "function") return false - const shouldScanWifi = scanWifi === true && activeWifiDevice() !== null - // The hidden backend may gate its own scanner on `opened`, while older - // hosts enable it synchronously even for refresh(false). A requested scan - // therefore defers every backend refresh until our scanner lease starts. - if (shouldScanWifi) { - requestWifiScan() - if (sessionCount > 0 && !profileList.running) refreshProfiles() - return true - } - if (scanPending) return true - backend.refresh(false) - if (sessionCount > 0) setSessionScannerEnabled(true) - else releaseWifiScanner() - return true - } - - function toggleWifi() { - if (!backendAvailable || !wifiAvailable) return false - Networking.wifiEnabled = !Networking.wifiEnabled - if (Networking.wifiEnabled) Qt.callLater(function() { root.refresh(true) }) - return true - } - - function securityKind(security) { - switch (security) { - case WifiSecurityType.Open: return "open" - case WifiSecurityType.WpaPsk: - case WifiSecurityType.Wpa2Psk: - case WifiSecurityType.Sae: return "psk" - case WifiSecurityType.Owe: return "owe" - case WifiSecurityType.StaticWep: - case WifiSecurityType.DynamicWep: return "wep" - case WifiSecurityType.WpaEap: - case WifiSecurityType.Wpa2Eap: return "enterprise" - case WifiSecurityType.Wpa3SuiteB192: - case WifiSecurityType.Leap: return "unsupported" - default: return "unknown" - } - } - - function securityLabel(security) { - switch (security) { - case WifiSecurityType.Open: return "Open" - case WifiSecurityType.WpaPsk: return "WPA Personal" - case WifiSecurityType.Wpa2Psk: return "WPA2 Personal" - case WifiSecurityType.Sae: return "WPA3 Personal" - case WifiSecurityType.Owe: return "Enhanced Open (OWE)" - case WifiSecurityType.StaticWep: - case WifiSecurityType.DynamicWep: return "WEP" - case WifiSecurityType.WpaEap: return "WPA Enterprise" - case WifiSecurityType.Wpa2Eap: return "WPA2 Enterprise" - case WifiSecurityType.Wpa3SuiteB192: return "WPA3 Enterprise" - case WifiSecurityType.Leap: return "LEAP" - default: return "Unknown" - } - } - - function profileSecurityLabel(keyManagement, authAlgorithm) { - switch (String(keyManagement || "").toLowerCase()) { - case "wpa-psk": return "WPA Personal profile" - case "sae": return "WPA3 Personal profile" - case "owe": return "Enhanced Open profile" - case "wpa-eap": return "WPA Enterprise profile" - case "wpa-eap-suite-b-192": return "WPA3 Suite B profile" - case "ieee8021x": return String(authAlgorithm || "").toLowerCase() - === "leap" ? "LEAP profile" : "802.1X profile" - case "open": return "Open profile" - case "none": return "WEP profile" - default: return "Saved Wi-Fi profile" - } - } - - function profileMatchesSecurity(profile, security) { - const keyManagement = String(profile && profile.keyManagement || "") - .toLowerCase() - switch (keyManagement) { - case "wpa-psk": - return security === WifiSecurityType.WpaPsk - || security === WifiSecurityType.Wpa2Psk - case "sae": return security === WifiSecurityType.Sae - case "owe": return security === WifiSecurityType.Owe - case "wpa-eap": - return security === WifiSecurityType.WpaEap - || security === WifiSecurityType.Wpa2Eap - case "wpa-eap-suite-b-192": - return security === WifiSecurityType.Wpa3SuiteB192 - case "ieee8021x": - return String(profile.authAlgorithm || "").toLowerCase() === "leap" - ? security === WifiSecurityType.Leap - : security === WifiSecurityType.DynamicWep - case "open": return security === WifiSecurityType.Open - case "none": return security === WifiSecurityType.StaticWep - default: return false - } - } - - function profileVisibleMatchCount(profile, visible) { - if (!profile || !Array.isArray(visible)) return 0 - let count = 0 - for (let i = 0; i < visible.length; i++) { - const candidate = visible[i] - if (candidate && candidate.ssid === profile.ssid - && profileMatchesSecurity(profile, candidate.security)) count++ - } - return count - } - - function mergedNetworks(visible, profiles) { - const rows = [] - const visibleRows = Array.isArray(visible) ? visible : [] - const savedRows = Array.isArray(profiles) ? profiles : [] - const representedProfileUuids = ({}) - - for (let i = 0; i < visibleRows.length; i++) { - const source = visibleRows[i] - if (!source) continue - const matchingProfiles = [] - for (let p = 0; p < savedRows.length; p++) { - const candidate = savedRows[p] - if (candidate && candidate.ssid === source.ssid - && profileMatchesSecurity(candidate, source.security)) - matchingProfiles.push(candidate) - } - const profile = matchingProfiles.length === 1 - && profileVisibleMatchCount(matchingProfiles[0], visibleRows) === 1 - ? matchingProfiles[0] : null - if (profile && String(profile.uuid || "")) - representedProfileUuids[String(profile.uuid)] = true - rows.push({ - connected: source.connected === true, - known: source.known === true || profile !== null, - ssid: String(source.ssid || ""), - signal: Math.max(0, Math.min(100, Number(source.signal || 0))), - security: source.security, - securityKind: securityKind(source.security), - securityLabel: securityLabel(source.security), - visible: true, - profileUuid: profile ? String(profile.uuid || "") : "", - lastSuccessful: profile ? Number(profile.lastSuccessful || 0) : 0, - entryKey: profile ? "profile:" + profile.uuid - : "network:" + String(source.ssid || "") + ":" + securityKind(source.security) - }) - } - - for (let p = 0; p < savedRows.length; p++) { - const profile = savedRows[p] - if (!profile || representedProfileUuids[String(profile.uuid || "")]) - continue - rows.push({ - network: null, - connected: false, - known: true, - ssid: String(profile.ssid || ""), - signal: 0, - security: null, - securityKind: "saved", - securityLabel: profileSecurityLabel(profile.keyManagement, - profile.authAlgorithm), - visible: false, - profileUuid: String(profile.uuid || ""), - lastSuccessful: Number(profile.lastSuccessful || 0), - entryKey: "profile:" + String(profile.uuid || "") - }) - } - - rows.sort(function(left, right) { - if (left.connected !== right.connected) return left.connected ? -1 : 1 - if (left.known !== right.known) return left.known ? -1 : 1 - if (left.visible !== right.visible) return left.visible ? -1 : 1 - return right.signal - left.signal - }) - return rows - } - - function visibleIdentityMatchCount(entry) { - if (!entry || entry.visible === false) return 0 - const ssid = String(entry.ssid || "") - if (!ssid) return 0 - const expectedKind = String(entry.securityKind - || securityKind(entry.security)) - const hasExactSecurity = entry.security !== undefined - && entry.security !== null - const rows = Array.isArray(visibleNetworks) ? visibleNetworks : [] - let count = 0 - for (let i = 0; i < rows.length; i++) { - const candidate = rows[i] - if (!candidate || String(candidate.ssid || "") !== ssid) continue - if (hasExactSecurity && candidate.security !== entry.security) continue - if (!hasExactSecurity - && securityKind(candidate.security) !== expectedKind) continue - count++ - } - return count - } - - function hasUniqueVisibleIdentity(entry) { - return visibleIdentityMatchCount(entry) === 1 - } - - function currentVisibleEntry(entry) { - if (!entry || entry.visible === false) return null - const ssid = String(entry.ssid || "") - const hasExactSecurity = entry.security !== undefined - && entry.security !== null - const expectedKind = String(entry.securityKind - || securityKind(entry.security)) - const rows = mergedNetworks(visibleNetworks, savedProfiles) - let result = null - let count = 0 - for (let i = 0; i < rows.length; i++) { - const candidate = rows[i] - if (!candidate || candidate.visible === false - || String(candidate.ssid || "") !== ssid) continue - if (hasExactSecurity && candidate.security !== entry.security) continue - if (!hasExactSecurity && candidate.securityKind !== expectedKind) continue - result = candidate - count++ - } - return count === 1 ? result : null - } - - function savedProfileMatchCount(entry) { - if (!entry || !Array.isArray(savedProfiles)) return 0 - let count = 0 - for (let i = 0; i < savedProfiles.length; i++) { - const profile = savedProfiles[i] - if (profile && profile.ssid === entry.ssid - && profileMatchesSecurity(profile, entry.security)) count++ - } - return count - } - - function visibleSsidMatchCount(entry) { - const ssid = String(entry && entry.ssid || "") - if (!ssid || !Array.isArray(visibleNetworks)) return 0 - let count = 0 - for (let i = 0; i < visibleNetworks.length; i++) { - const candidate = visibleNetworks[i] - if (candidate && String(candidate.ssid || "") === ssid) count++ - } - return count - } - - function hasUnambiguousVisibleSsid(entry) { - return hasUniqueVisibleIdentity(entry) - && visibleSsidMatchCount(entry) === 1 - } - - function connect(entry) { - if (!entry || busy) return false - profileError = "" - if (entry.profileUuid && entry.visible === false) - return runProfileAction("connect", entry.profileUuid) - if (!ready || !hasUniqueVisibleIdentity(entry)) return false - if (entry.connected) return disconnect(entry) - if (entry.profileUuid) - return runProfileAction("connect", entry.profileUuid) - if (hasUnambiguousVisibleSsid(entry) - && (entry.known || entry.securityKind === "open") - && typeof backend.connectKnown === "function") { - backend.connectKnown(String(entry.ssid || "")) - return true - } - return false - } - - function connectWithPassphrase(entry, passphrase) { - const current = currentVisibleEntry(entry) - if (!entry || entry.securityKind !== "psk" - || !String(passphrase || "") || !ready || busy - || !hasUnambiguousVisibleSsid(entry) || !current - || current.connected === true || current.known === true - || savedProfileMatchCount(entry) > 0 - || typeof backend.connectWithPassphrase !== "function") return false - profileError = "" - backend.connectWithPassphrase(String(entry.ssid || ""), String(passphrase)) - return true - } - - function connectEnterprise(entry, identity, passphrase) { - const current = currentVisibleEntry(entry) - if (!entry || entry.securityKind !== "enterprise" - || !String(identity || "") || !String(passphrase || "") - || !ready || busy || !hasUnambiguousVisibleSsid(entry) || !current - || current.connected === true || current.known === true - || savedProfileMatchCount(entry) > 0 - || typeof backend.connectEnterprise !== "function") return false - profileError = "" - backend.connectEnterprise(String(entry.ssid || ""), - String(identity), String(passphrase)) - return true - } - - function disconnect(entry) { - if (!entry || entry.connected !== true || !ready || busy - || !hasUnambiguousVisibleSsid(entry) - || typeof backend.disconnectRow !== "function") return false - backend.disconnectRow(String(entry.ssid || "")) - return true - } - - function forget(entry) { - if (!entry || !entry.known || busy) return false - profileError = "" - if (entry.profileUuid && entry.visible === false) - return runProfileAction("forget", entry.profileUuid) - if (!ready || !hasUniqueVisibleIdentity(entry)) return false - if (entry.profileUuid) - return runProfileAction("forget", entry.profileUuid) - if (!hasUnambiguousVisibleSsid(entry) - || typeof backend.forget !== "function") return false - backend.forget(entry) - return true - } - - function setDns(provider) { - if (!ready || typeof backend.setDns !== "function") return false - backend.setDns(provider) - return true - } - - function speedTestCommandFor(phaseValue) { - return [String(speedTestRunnerSource), - String(speedTestExecutable || ""), String(phaseValue || "")] - } - - function updateSpeedTestLine(line) { - const raw = String(line || "").trim() - if (!/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(raw)) return - const value = Number(raw) - if (!isFinite(value) || value < 0) return - const normalized = String(value) - if (speedTestPhase === "down") speedTestDownloadMbps = normalized - else if (speedTestPhase === "up") speedTestUploadMbps = normalized - } - - function startSpeedTestPhase(phaseValue) { - speedTestExpectedStop = false - speedTestProcessStarted = false - speedTestPhase = phaseValue - speedTestStderr = "" - speedTestProc.command = speedTestCommandFor(phaseValue) - speedTestProc.running = true - speedTestPhaseTimer.restart() - } - - function speedTestPhaseHasSample() { - return speedTestPhase === "down" - ? speedTestDownloadMbps !== "" : speedTestUploadMbps !== "" - } - - function failSpeedTest(message) { - speedTestPhaseTimer.stop() - speedTestError = String(message || "Speed test failed") - speedTestPhase = "" - speedTestRunning = false - speedTestExpectedStop = false - speedTestProcessStarted = false - } - - function finishSpeedTestPhase() { - if (!speedTestRunning) return - if (speedTestPhase === "down") { - startSpeedTestPhase("up") - return - } - speedTestPhase = "" - speedTestRunning = false - speedTestExpectedStop = false - } - - function stopSpeedTestPhase() { - speedTestPhaseTimer.stop() - if (!speedTestRunning) return - if (speedTestProc.running) { - speedTestExpectedStop = true - speedTestProc.running = false - return - } - finishSpeedTestPhase() - } - - function handleSpeedTestExit(exitCode) { - speedTestPhaseTimer.stop() - const expected = speedTestExpectedStop - speedTestExpectedStop = false - speedTestProcessStarted = false - if (speedTestPendingRun) { - speedTestPendingRun = false - speedTestRunning = false - if (sessionCount > 0) Qt.callLater(function() { - if (root.sessionCount > 0) root.runSpeedTest() - }) - return - } - if (!speedTestRunning) return - if (!expected && exitCode !== 0) { - failSpeedTest(speedTestStderr || "Speed test failed") - return - } - if (!speedTestPhaseHasSample()) { - failSpeedTest(speedTestStderr || "Speed test produced no " - + speedTestPhase + "load data") - return - } - finishSpeedTestPhase() - } - - function stopSpeedTest() { - const active = speedTestRunning || speedTestProc.running - speedTestPhaseTimer.stop() - speedTestPendingRun = false - speedTestPhase = "" - speedTestRunning = false - if (speedTestProc.running) { - speedTestExpectedStop = true - speedTestProc.running = false - } else { - speedTestExpectedStop = false - speedTestProcessStarted = false - } - return active - } - - function runSpeedTest() { - if (!ready || !speedTestReady || speedTestRunning) return false - speedTestError = "" - speedTestDownloadMbps = "" - speedTestUploadMbps = "" - if (speedTestProc.running) { - if (sessionCount <= 0) return false - speedTestPendingRun = true - speedTestRunning = true - speedTestPhase = "" - return true - } - speedTestPendingRun = false - speedTestRunning = true - startSpeedTestPhase("down") - return true - } - - function formatRate(value) { - return ready && typeof backend.formatRate === "function" - ? backend.formatRate(value) : "0 B/s" - } - - function formatPing(value) { - return ready && typeof backend.formatPingLatency === "function" - ? backend.formatPingLatency(value) : "--" - } - - function formatSpeed(value) { - const speed = Number(value) - if (!(speed > 0)) return "—" - return (speed >= 100 ? speed.toFixed(0) : speed.toFixed(1)) + " Mbps" - } - - function refreshProfiles() { - if (sessionCount <= 0 || profileList.running) return false - profilesLoaded = false - profileList.running = true - return true - } - - function runProfileAction(action, uuid) { - const id = String(uuid || "") - if (!id || profileAction.running) return false - profileAction.action = action - profileAction.command = action === "connect" - ? ["nmcli", "--wait", "20", "connection", "up", "uuid", id] - : ["nmcli", "--wait", "20", "connection", "delete", "uuid", id] - profileAction.running = true - return true - } - - Adapters.NetworkPanelBridge { - id: bridge - bar: root.bar - ownerWidget: root.bar - networkService: root - panelComponent: root.panelComponent - panelSource: root.panelSource - panelSettings: root.officialSettings() - } - - Timer { - id: scanRestart - interval: 100 - repeat: false - onTriggered: { - if (root.sessionCount > 0 - && root.setSessionScannerEnabled(true)) { - if (root.backend && typeof root.backend.refresh === "function") - root.backend.refresh(false) - scanDone.restart() - } else { - root.scanPending = false - } - } - } - - Timer { - id: scanDone - interval: 1500 - repeat: false - onTriggered: root.completeWifiScan() - } - - Timer { - id: speedTestPhaseTimer - interval: root.speedTestPhaseDuration - repeat: false - onTriggered: root.stopSpeedTestPhase() - } - - Process { - id: speedTestProc - onStarted: root.speedTestProcessStarted = true - onRunningChanged: { - if (!running && root.speedTestRunning - && !root.speedTestProcessStarted) - root.failSpeedTest("Unable to start network speed test") - } - stdout: SplitParser { - onRead: function(line) { root.updateSpeedTestLine(line) } - } - stderr: StdioCollector { - waitForEnd: true - onStreamFinished: { - root.speedTestStderr = String(text || "").trim() - if (root.speedTestError !== "" && root.speedTestStderr !== "") - root.speedTestError = root.speedTestStderr - } - } - onExited: function(exitCode, _exitStatus) { - root.handleSpeedTestExit(exitCode) - } - } - - Timer { - id: detailsPoll - interval: 1500 - repeat: true - running: root.sessionCount > 0 && root.ready - onTriggered: { - if (!detailsProc.running) detailsProc.running = true - } - } - - Process { - id: detailsProc - command: ["omarchy-network-status", "--verbose"] - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: { - if (root.backend && typeof root.backend.updateDetails === "function") - root.backend.updateDetails(text) - } - } - } - - Process { - id: profileList - command: ["bash", "-c", - "connections=$(nmcli -t -f UUID,TYPE connection show) || exit 1; " - + "printf '__READY__\\n'; " - + "while IFS=: read -r uuid type; do " - + "case \"$type\" in 802-11-wireless|wifi) ;; *) continue ;; esac; " - + "mapfile -t details < <(nmcli --escape no -g 802-11-wireless.ssid,connection.timestamp,802-11-wireless-security.key-mgmt,802-11-wireless-security.auth-alg connection show uuid \"$uuid\"); " - + "ssid=${details[0]-}; timestamp=${details[1]:-0}; key_mgmt=${details[2]-}; auth_alg=${details[3]-}; " - + "[ -n \"$ssid\" ] || continue; " - + "case \"$timestamp\" in ''|*[!0-9]*) timestamp=0 ;; esac; " - + "[ -n \"$key_mgmt\" ] || key_mgmt=open; [ -n \"$auth_alg\" ] || auth_alg=unknown; " - + "printf '%s\\t%s\\t%s\\t%s\\t%s\\n' \"$uuid\" \"$ssid\" \"$timestamp\" \"$key_mgmt\" \"$auth_alg\"; " - + "done <<< \"$connections\""] - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: { - const lines = String(text || "").trim().split("\n") - const ready = lines.length > 0 && lines[0] === "__READY__" - root.profilesLoaded = ready - if (!ready) { - root.profileError = "Saved networks unavailable" - return - } - const profiles = [] - for (let i = 1; i < lines.length; i++) { - if (!lines[i]) continue - const fields = lines[i].split("\t") - if (fields.length < 5) continue - profiles.push({ - uuid: fields[0], - ssid: fields.slice(1, fields.length - 3).join("\t"), - lastSuccessful: parseInt(fields[fields.length - 3]) || 0, - keyManagement: fields[fields.length - 2], - authAlgorithm: fields[fields.length - 1] - }) - } - root.profileError = "" - root.savedProfiles = profiles - } - } - } - - Process { - id: profileAction - property string action: "" - stderr: StdioCollector { - id: profileActionError - waitForEnd: true - } - onExited: function(exitCode, _exitStatus) { - root.profileError = exitCode === 0 ? "" - : String(profileActionError.text || "Network action failed").trim() - action = "" - if (root.sessionCount > 0) { - root.refresh(false) - root.refreshProfiles() - } - } - } - - Component.onDestruction: { - sessionOwners = [] - releaseWifiScanner() - stopSpeedTest() - detailsProc.running = false - profileList.running = false - } -} diff --git a/services/OpenCodeProvider.qml b/services/OpenCodeProvider.qml deleted file mode 100644 index 4fce3f8..0000000 --- a/services/OpenCodeProvider.qml +++ /dev/null @@ -1,201 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell.Io - -Item { - id: root - visible: false - - property string providerId: "opencode" - property string providerName: "OpenCode" - property bool enabled: false - property var scannerCommandOverride: [] - property int scanGeneration: 0 - property bool ready: false - property bool refreshing: false - property bool midnightRefreshPending: false - property double lastRefreshedAtMs: 0 - - property real rateLimitPercent: -1 - property string rateLimitLabel: "5h soft cap" - property string rateLimitResetAt: "" - property real secondaryRateLimitPercent: -1 - property string secondaryRateLimitLabel: "7d soft cap" - property string secondaryRateLimitResetAt: "" - - property real todayTotalTokens: 0 - property real windowTokens: 0 - property real hourlyTokens: 0 - property var models: [] - property string latestModel: "" - property string dataDay: "" - property string tierLabel: "Local messages" - property string usageStatusText: "" - property string authHelpText: "OpenCode usage is read from its local database." - property bool hasLocalStats: true - - readonly property string scannerPath: String( - Qt.resolvedUrl("../scripts/opencode-usage")).replace("file://", "") - readonly property bool scannerRunning: usageScanner.running - - function clampPercent(value) { - const number = Number(value) - if (!isFinite(number)) return -1 - return Math.max(0, Math.min(100, number * 100)) - } - - function localDay() { - return Qt.formatDateTime(new Date(), "yyyy-MM-dd") - } - - function millisecondsUntilNextLocalMidnight() { - const now = new Date() - const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1) - return Math.max(1, next.getTime() - now.getTime()) - } - - function clearCurrentDayData() { - todayTotalTokens = 0 - latestModel = "" - models = [] - dataDay = "" - } - - function clearScannerData() { - ready = false - rateLimitPercent = -1 - secondaryRateLimitPercent = -1 - windowTokens = 0 - hourlyTokens = 0 - clearCurrentDayData() - } - - function scheduleMidnightInvalidation() { - if (!enabled) { - midnightTimer.stop() - return - } - midnightTimer.interval = millisecondsUntilNextLocalMidnight() - midnightTimer.restart() - } - - function handleLocalMidnight() { - clearCurrentDayData() - if (usageScanner.running) midnightRefreshPending = true - else refresh(true) - scheduleMidnightInvalidation() - } - - function refresh(_force) { - if (!enabled || usageScanner.running) return - refreshing = true - usageScanner.running = true - scanGeneration++ - } - - function stopScanner() { - if (usageScanner.running) usageScanner.running = false - refreshing = false - } - - function finishScanner() { - refreshing = false - lastRefreshedAtMs = Date.now() - if (midnightRefreshPending) { - midnightRefreshPending = false - Qt.callLater(function() { root.refresh(true) }) - } - } - - function parseScannerOutput(output) { - const raw = String(output || "").trim() - if (!raw) { - clearScannerData() - usageStatusText = "OpenCode data unavailable" - return - } - try { - const data = JSON.parse(raw.split("\n").pop()) - ready = data.ready === true - if (!ready) { - clearScannerData() - usageStatusText = "OpenCode data unavailable" - return - } - rateLimitPercent = clampPercent(data["5h-utilization"]) - secondaryRateLimitPercent = clampPercent(data["7d-utilization"]) - windowTokens = Math.max(0, Number(data._tokens_used) || 0) - hourlyTokens = Math.max(0, Number(data._rate_per_hour) || 0) - const scanDay = String(data._day || "") - if (scanDay === localDay()) { - todayTotalTokens = Math.max(0, Number(data._today_tokens) || 0) - latestModel = String(data._model || "") - models = Array.isArray(data._models) ? data._models : [] - dataDay = scanDay - } else { - clearCurrentDayData() - } - tierLabel = String(data._plan || "Local messages") - usageStatusText = String(data.status || "allowed") === "allowed_warning" - ? "Approaching local soft cap" : "Local activity" - } catch (error) { - clearScannerData() - usageStatusText = "OpenCode scan failed" - authHelpText = String(error) - } - } - - function formatResetTime(_timestamp) { return "" } - - Process { - id: usageScanner - command: Array.isArray(root.scannerCommandOverride) - && root.scannerCommandOverride.length > 0 - ? root.scannerCommandOverride : ["python3", root.scannerPath] - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: root.parseScannerOutput(text) - } - stderr: StdioCollector { - waitForEnd: true - onStreamFinished: if (String(text || "").trim()) - console.warn("shibumi/opencode", String(text).trim()) - } - onExited: root.finishScanner() - } - - Timer { - id: refreshTimer - interval: 5 * 60 * 1000 - running: root.enabled - repeat: true - triggeredOnStart: true - onTriggered: root.refresh(false) - } - - // UI-only day data expires exactly at the next local midnight. This timer - // exists only while OpenCode is enabled and is rescheduled after each fire; - // it owns no worker and the shared five-minute scanner remains unchanged. - Timer { - id: midnightTimer - repeat: false - onTriggered: root.handleLocalMidnight() - } - - onEnabledChanged: { - if (!enabled) { - midnightRefreshPending = false - stopScanner() - midnightTimer.stop() - } else { - scheduleMidnightInvalidation() - } - } - Component.onCompleted: scheduleMidnightInvalidation() - Component.onDestruction: { - midnightRefreshPending = false - midnightTimer.stop() - stopScanner() - } -} diff --git a/services/PickerService.qml b/services/PickerService.qml deleted file mode 100644 index 62971ef..0000000 --- a/services/PickerService.qml +++ /dev/null @@ -1,495 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Hyprland -import Quickshell.Io -import qs.Commons as Commons -import "PickerModel.js" as PickerModel - -Item { - id: root - - property var bar: null - readonly property string home: Quickshell.env("HOME") - readonly property string scriptPath: String( - Qt.resolvedUrl("../scripts/shibumi-picker")).replace("file://", "") - readonly property bool available: bar !== null && bar.hostReady - && bar.omarchyPath !== "" - - property bool opened: false - property string mode: "wallpaper" - property string pickerStyle: "tanzaku" - property var activeScreen: null - property string activeScreenName: activeScreen ? String(activeScreen.name || "") : "" - property var entries: [] - readonly property var filteredEntries: PickerModel.filtered(entries, filterText) - property int selectedIndex: 0 - readonly property var selectedEntry: filteredEntries.length > 0 - ? filteredEntries[PickerModel.clampIndex(selectedIndex, filteredEntries.length)] : null - property string filterText: "" - property bool loading: false - property bool scanComplete: false - property bool confirmDelete: false - property string currentSelection: "" - property string statusText: "" - property int requestSerial: 0 - property bool cachedRowsApplied: false - property string selectedThemeAuthor: "" - property string selectedThemeRepo: "" - property var selectedThemePalette: [] - - readonly property bool imageMode: mode === "theme" || mode === "wallpaper" - readonly property bool mediaMode: mode === "screenshots" || mode === "videos" - readonly property bool videoMode: mode === "videos" - readonly property string title: mode === "theme" ? "Themes" - : mode === "wallpaper" ? "Wallpapers" - : mode === "videos" ? "Videos" : "Screenshots" - readonly property string emptyText: loading ? "Loading " + title.toLowerCase() + "..." - : "No " + title.toLowerCase() + " found" - readonly property string currentThemeNamePath: - home + "/.local/state/omarchy/current/theme.name" - - function normalizeStyle(value) { - const candidate = String(value || "") - return ["tanzaku", "hearthstone", "carousel"].indexOf(candidate) >= 0 - ? candidate : "tanzaku" - } - - function resolveTargetScreen(preferred) { - if (preferred && String(preferred.name || "") !== "") return preferred - const focusedName = Hyprland.focusedMonitor - ? String(Hyprland.focusedMonitor.name || "") : "" - return bar && typeof bar.screenForName === "function" - ? bar.screenForName(focusedName) : null - } - - function cycleStyle(direction) { - const styles = ["tanzaku", "hearthstone", "carousel"] - const current = Math.max(0, styles.indexOf(normalizeStyle(pickerStyle))) - const step = Number(direction) < 0 ? -1 : 1 - const next = styles[(current + step + styles.length) % styles.length] - if (!bar || typeof bar.mutateShibumiConfig !== "function") return false - return bar.mutateShibumiConfig(function(config) { - if (!config.picker || typeof config.picker !== "object") config.picker = {} - config.picker.style = next - }) - } - - function openMode(nextMode, screen) { - const candidate = String(nextMode || "") - if (["theme", "wallpaper", "screenshots", "videos"].indexOf(candidate) < 0) - return false - requestSerial++ - stopForegroundWorkers() - mode = candidate - activeScreen = resolveTargetScreen(screen) - filterText = "" - selectedIndex = 0 - confirmDelete = false - currentSelection = "" - statusText = "" - clearThemeMetadata() - entries = [] - cachedRowsApplied = false - scanComplete = false - loading = true - opened = true - if (mode === "theme" || mode === "wallpaper") { - currentProc.activeSerial = requestSerial - currentProc.command = mode === "theme" - ? ["cat", currentThemeNamePath] - : ["readlink", "-f", home + "/.local/state/omarchy/current/background"] - currentProc.running = true - } else { - beginLoads(requestSerial) - } - return true - } - - function toggleMode(nextMode, screen) { - if (opened && mode === nextMode) { - close() - return true - } - return openMode(nextMode, screen) - } - - function close() { - if (!opened) return - requestSerial++ - opened = false - confirmDelete = false - filterText = "" - stopForegroundWorkers() - loading = false - } - - function stopForegroundWorkers() { - currentProc.running = false - cacheProc.running = false - scanProc.running = false - priorityWarmProc.running = false - warmProc.running = false - warmDelay.stop() - clearThemeMetadata() - } - - function beginLoads(serial) { - if (!opened || serial !== requestSerial) return - cacheProc.activeSerial = serial - cacheProc.command = [scriptPath, "cached", mode] - cacheProc.running = true - scanProc.activeSerial = serial - scanProc.command = [scriptPath, "scan", mode, bar.omarchyPath] - scanProc.running = true - } - - function applyRows(text, fromCache, serial) { - if (!opened || serial !== requestSerial) return - const parsed = PickerModel.parseRows(text) - if (fromCache && (cachedRowsApplied || parsed.length === 0)) return - if (fromCache && entries.length > 0) return - if (!fromCache) scanComplete = true - if (parsed.length === 0 && fromCache) return - entries = parsed - cachedRowsApplied = fromCache - selectedIndex = selectedIndexForCurrent(parsed) - loading = fromCache ? true : false - Qt.callLater(function() { - if (!root.opened || serial !== root.requestSerial) return - root.warmVisible() - warmDelay.restart() - }) - } - - function selectedIndexForCurrent(nextEntries) { - if (mode === "theme") { - for (let i = 0; i < nextEntries.length; i++) { - if (String(nextEntries[i].label || "") === currentSelection) return i - } - return 0 - } - return PickerModel.indexForSource(nextEntries, currentSelection) - } - - function moveSelection(delta) { - selectedIndex = PickerModel.clampIndex(selectedIndex + Number(delta), - filteredEntries.length) - confirmDelete = false - warmVisible() - } - - function clearThemeMetadata() { - themeMetaDelay.stop() - themeMetaProc.running = false - selectedThemeAuthor = "" - selectedThemeRepo = "" - selectedThemePalette = [] - } - - function requestThemeMetadata() { - clearThemeMetadata() - if (mode === "theme" && selectedEntry && selectedEntry.directory) - themeMetaDelay.restart() - } - - function openSelectedThemeRepo() { - if (!selectedThemeRepo) return false - let url = String(selectedThemeRepo) - .replace(/^git@github\.com:/, "https://github.com/") - .replace(/\.git$/, "") - if (!/^https?:\/\//.test(url)) return false - Quickshell.execDetached(["xdg-open", url]) - return true - } - - function selectIndex(index) { - selectedIndex = PickerModel.clampIndex(index, filteredEntries.length) - confirmDelete = false - warmVisible() - } - - function updateFilter(text) { - filterText = String(text || "") - selectedIndex = 0 - confirmDelete = false - warmVisible() - } - - function thumbnailUrl(entry) { - if (!entry || !entry.thumbnailReady || !entry.thumbnailPath) return "" - return Commons.Util.fileUrl(entry.thumbnailPath) - } - - function sourcePaths(visibleOnly) { - const result = [] - const seen = {} - if (visibleOnly) { - for (let distance = 0; distance <= 5; distance++) { - const indexes = distance === 0 ? [selectedIndex] - : [selectedIndex - distance, selectedIndex + distance] - for (let j = 0; j < indexes.length; j++) { - const entry = filteredEntries[indexes[j]] - if (!entry || !entry.sourcePath || entry.thumbnailReady - || seen[entry.sourcePath]) continue - seen[entry.sourcePath] = true - result.push(entry.sourcePath) - } - } - return result - } - for (let i = 0; i < entries.length; i++) { - const entry = entries[i] - if (!entry || !entry.sourcePath || entry.thumbnailReady - || seen[entry.sourcePath]) continue - seen[entry.sourcePath] = true - result.push(entry.sourcePath) - } - return result - } - - function startWarm(process, sources, niceLevel) { - if (!opened || !Array.isArray(sources) || sources.length === 0) return - process.activeSerial = requestSerial - process.command = [scriptPath, "warm", mode, String(niceLevel)].concat(sources) - process.running = true - } - - function warmVisible() { - startWarm(priorityWarmProc, sourcePaths(true), 10) - } - - function warmAll() { - startWarm(warmProc, sourcePaths(false), 19) - } - - function noteThumbnailReady(path, serial) { - if (!opened || serial !== requestSerial) return - entries = PickerModel.replaceThumbnailReady(entries, String(path || "").trim()) - } - - function activateSelected() { - const entry = selectedEntry - if (!entry || !entry.sourcePath) return false - if (mode === "theme") { - actionProc.command = ["env", "OMARCHY_PATH=" + bar.omarchyPath, - "omarchy-theme-set", entry.label] - } else if (mode === "wallpaper") { - actionProc.command = ["omarchy-theme-bg-set", entry.sourcePath] - } else { - Quickshell.execDetached(["xdg-open", entry.sourcePath]) - close() - return true - } - actionProc.running = true - close() - return true - } - - function copySelected() { - const entry = selectedEntry - if (!mediaMode || !entry || !entry.sourcePath) return false - copyProc.command = videoMode - ? ["bash", "-c", "printf '%s' \"$1\" | wl-copy", "shibumi-copy", entry.sourcePath] - : ["bash", "-c", [ - "mime=$(file -Lb --mime-type -- \"$1\") || exit 1;", - "[[ $mime == image/* ]] || exit 1;", - "wl-copy --type \"$mime\" < \"$1\"" - ].join(" "), "shibumi-copy", entry.sourcePath] - statusText = "Copying..." - copyProc.running = true - return true - } - - function requestDeleteSelected() { - const entry = selectedEntry - if (!mediaMode || !entry || !entry.sourcePath) return false - if (!confirmDelete) { - confirmDelete = true - deleteConfirmTimeout.restart() - return true - } - deleteConfirmTimeout.stop() - confirmDelete = false - deleteProc.command = ["bash", "-c", - "gio trash -- \"$1\" 2>/dev/null || trash-put -- \"$1\" 2>/dev/null", - "shibumi-trash", entry.sourcePath] - deleteProc.running = true - return true - } - - onSelectedIndexChanged: confirmDelete = false - onSelectedEntryChanged: requestThemeMetadata() - onAvailableChanged: if (available) initialPrewarmDelay.restart() - Component.onDestruction: { - stopForegroundWorkers() - wallpaperPrewarmProc.running = false - themePrewarmProc.running = false - initialPrewarmDelay.stop() - wallpaperPrewarmDelay.stop() - } - - Process { - id: currentProc - property int activeSerial: 0 - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: { - if (currentProc.activeSerial !== root.requestSerial || !root.opened) return - root.currentSelection = String(text || "").trim() - root.beginLoads(currentProc.activeSerial) - } - } - onExited: { - if (activeSerial === root.requestSerial && root.opened && !cacheProc.running - && !scanProc.running) root.beginLoads(activeSerial) - } - } - - Process { - id: cacheProc - property int activeSerial: 0 - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: root.applyRows(text, true, cacheProc.activeSerial) - } - } - - Process { - id: scanProc - property int activeSerial: 0 - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: root.applyRows(text, false, scanProc.activeSerial) - } - onExited: function(code) { - if (activeSerial !== root.requestSerial || !root.opened) return - root.loading = false - if (code !== 0) root.statusText = "Scan failed" - } - } - - Process { - id: priorityWarmProc - property int activeSerial: 0 - stdout: SplitParser { - onRead: line => root.noteThumbnailReady(line, priorityWarmProc.activeSerial) - } - } - - Process { - id: warmProc - property int activeSerial: 0 - stdout: SplitParser { - onRead: line => root.noteThumbnailReady(line, warmProc.activeSerial) - } - } - - Process { id: actionProc } - Process { - id: copyProc - onExited: function(code) { - root.statusText = code === 0 ? "Copied" : "Copy failed" - } - } - - Process { - id: themeMetaProc - property string requestedDirectory: "" - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: { - if (!root.selectedEntry - || String(root.selectedEntry.directory || "") !== themeMetaProc.requestedDirectory) - return - const parts = String(text || "").replace(/\n+$/, "").split("\t") - root.selectedThemeAuthor = parts[0] || "" - root.selectedThemeRepo = parts[1] || "" - root.selectedThemePalette = parts[2] ? parts[2].split(",") : [] - } - } - } - - Process { - id: deleteProc - onExited: function(code) { - if (code !== 0) { - root.statusText = "Delete failed; file kept" - return - } - root.statusText = "Moved to trash" - if (root.opened) root.beginLoads(root.requestSerial) - } - } - - Process { - id: wallpaperPrewarmProc - } - - Process { - id: themePrewarmProc - } - - Timer { - id: warmDelay - interval: 450 - onTriggered: root.warmAll() - } - - Timer { - id: deleteConfirmTimeout - interval: 3500 - onTriggered: root.confirmDelete = false - } - - Timer { - id: themeMetaDelay - interval: 70 - onTriggered: { - if (!root.selectedEntry || !root.selectedEntry.directory) return - const directory = String(root.selectedEntry.directory) - themeMetaProc.requestedDirectory = directory - themeMetaProc.command = ["bash", "-c", [ - "d=$1; repo=''; author='';", - "if [[ -f $d/.git/config ]]; then", - "repo=$(sed -nE 's#^[[:space:]]*url = (.*)$#\\1#p' \"$d/.git/config\" | head -1);", - "author=$(printf '%s' \"$repo\" | sed -nE 's#.*github\\.com[:/]+([^/]+)/.*#\\1#p'); fi;", - "palette=$(awk -F'\"' '$2 ~ /^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/ { if (out != \"\") out=out \",\"; out=out $2; if (++n == 6) exit } END { print out }' \"$d/colors.toml\" 2>/dev/null);", - "printf '%s\\t%s\\t%s\\n' \"$author\" \"$repo\" \"$palette\"" - ].join(" "), "shibumi-theme-meta", directory] - themeMetaProc.running = true - } - } - - Timer { - id: wallpaperPrewarmDelay - interval: 120 - onTriggered: { - if (!root.available) return - wallpaperPrewarmProc.running = false - wallpaperPrewarmProc.command = [root.scriptPath, "prewarm", "wallpaper", - root.bar.omarchyPath] - wallpaperPrewarmProc.running = true - } - } - - Timer { - id: initialPrewarmDelay - interval: 900 - onTriggered: { - if (!root.available) return - themePrewarmProc.command = [root.scriptPath, "prewarm", "theme", - root.bar.omarchyPath] - themePrewarmProc.running = true - wallpaperPrewarmDelay.restart() - } - } - - FileView { - path: root.available ? root.currentThemeNamePath : "" - watchChanges: true - printErrors: false - onFileChanged: wallpaperPrewarmDelay.restart() - } -} diff --git a/services/QuoteService.qml b/services/QuoteService.qml deleted file mode 100644 index 266dd04..0000000 --- a/services/QuoteService.qml +++ /dev/null @@ -1,105 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io -import "QuoteDefaults.js" as QuoteDefaults -import "ReactorModel.js" as ReactorModel - -Item { - id: root - - required property var bar - readonly property bool active: bar && bar.reactorMode === 8 - readonly property string quotesPath: Quickshell.env("HOME") - + "/.config/shibumi/quotes.txt" - property var quotes: QuoteDefaults.values() - property int currentIndex: -1 - property int serial: 0 - property int eventCount: 0 - property bool armed: false - - signal eventRaised(var event) - signal cleared() - - visible: false - width: 0 - height: 0 - - function publishNext() { - if (!active || quotes.length === 0) return false - currentIndex = (currentIndex + 1) % quotes.length - const choices = [] - for (let offset = 0; offset < quotes.length && choices.length < 64; offset++) { - const entry = quotes[(currentIndex + offset) % quotes.length] - const quote = ReactorModel.sanitize(entry.quote, 160) - const author = ReactorModel.sanitize(entry.author, 36) - if (quote) choices.push({ quote: quote, author: author }) - } - if (choices.length === 0) return false - const selected = choices[0] - eventCount++ - eventRaised({ - serial: ++serial, - timestamp: Date.now(), - kind: "text", - direction: 1, - left: selected.quote, - right: selected.author ? "-" + selected.author : "", - choices: choices, - profile: "quote", - screen: "", - count: 1, - gain: 1 - }) - return true - } - - function reloadQuotes() { - let parsed = [] - try { parsed = ReactorModel.parseQuotes(quotesReader.text()) } - catch (_error) {} - if (parsed.length === 0) return false - quotes = parsed - currentIndex = -1 - if (armed) { - cleared() - cycle.interval = 250 - cycle.restart() - } - return true - } - - function runTest(kindValue, _argumentValue) { - const kind = String(kindValue || "").toLowerCase() - if (kind === "clear") { - cleared() - return true - } - if (kind === "quote" || kind === "next" || kind === "text") - return publishNext() - return false - } - - FileView { - id: quotesReader - path: root.quotesPath - watchChanges: true - printErrors: false - onFileChanged: reload() - onLoaded: root.reloadQuotes() - } - - Timer { - id: cycle - interval: 450 - onTriggered: { - root.armed = true - root.publishNext() - interval = 16000 - restart() - } - } - - Component.onCompleted: cycle.start() -} diff --git a/services/ReactorService.qml b/services/ReactorService.qml deleted file mode 100644 index 21b032b..0000000 --- a/services/ReactorService.qml +++ /dev/null @@ -1,576 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Hyprland -import Quickshell.Io -import "ReactorModel.js" as ReactorModel - -Item { - id: root - - required property var bar - readonly property bool active: bar && bar.reactorMode === 7 - readonly property string home: Quickshell.env("HOME") - readonly property var workspaceService: bar ? bar.workspaceService : null - readonly property var statusService: bar ? bar.statusService : null - readonly property var powerService: bar ? bar.powerService : null - readonly property var aiUsageService: bar ? bar.aiUsageService : null - readonly property var notificationService: statusService - ? statusService.notificationService : null - readonly property var mediaService: bar && bar.shell - && typeof bar.shell.firstPartyServiceFor === "function" - ? bar.shell.firstPartyServiceFor("omarchy.media") : null - readonly property var networkWidget: moduleItem("hancore.shibumi.network") - readonly property var audioWidget: moduleItem("hancore.shibumi.audio") - readonly property var centerWidget: moduleItem("hancore.shibumi.center") - readonly property var updateWidget: centerWidget ? centerWidget.updateWidget : null - readonly property string networkMode: networkWidget - ? String(networkWidget.mode || "none") : "" - readonly property bool outputMuted: audioWidget - ? audioWidget.muted === true : false - readonly property bool updateAvailable: updateWidget - ? updateWidget.updateAvailable === true : false - readonly property int notificationCount: notificationService - && notificationService.pendingModel - ? Number(notificationService.pendingModel.count) || 0 : 0 - readonly property string mediaTitle: mediaService - ? String(mediaService.title || "") : "" - readonly property string mediaArtist: mediaService - ? String(mediaService.artist || "") : "" - readonly property string mediaAlbum: mediaService - ? String(mediaService.album || "") : "" - readonly property bool batteryLow: powerService - && powerService.hasBattery && powerService.discharging - && powerService.percent <= 20 - - property bool armed: false - property int serial: 0 - property var eventHistory: [] - property double lastExternalTimestamp: 0 - property string lastThemeName: "" - property string lastNetworkMode: "" - property bool muteBaselined: false - property bool dndBaselined: false - property bool updateBaselined: false - property int lastNotificationCount: -1 - property string lastTrackSignature: "" - property var recentOpen: ({}) - property string activeAddress: "" - property string pendingUrgentAddress: "" - property string pendingUrgentClass: "" - property string urgentAddress: "" - property string urgentClass: "" - property int urgentRepeats: 0 - property var warningNext: ({ offline: 0, battery: 0, urgent: 0 }) - property var quotaState: ({}) - property var warningQueue: [] - - signal eventRaised(var event) - signal cleared() - - visible: false - width: 0 - height: 0 - - function moduleItem(moduleName) { - const slots = bar && Array.isArray(bar.moduleSlots) ? bar.moduleSlots : [] - for (let index = 0; index < slots.length; index++) { - const slot = slots[index] - if (slot && String(slot.moduleName || "") === moduleName && slot.activeItem) - return slot.activeItem - } - return null - } - - function publish(kind, options) { - if (!active) return false - const source = options || ({}) - const event = { - serial: ++serial, - timestamp: Date.now(), - kind: String(kind || "text"), - direction: Number(source.direction) < 0 ? -1 : 1, - left: ReactorModel.sanitize(source.left, 64), - right: ReactorModel.sanitize(source.right, 28), - profile: ReactorModel.profile(source.profile), - screen: String(source.screen || ""), - count: Math.max(1, Math.min(36, Number(source.count) || 1)), - gain: Math.max(0.1, Math.min(1, Number(source.gain) || 1)) - } - if (event.kind === "text" && !event.left && !event.right) return false - eventHistory = ReactorModel.boundedAppend(eventHistory, event, 8) - eventRaised(event) - return true - } - - function pushText(left, right, profile, force, screen) { - if (!active || (!force && statusService - && statusService.notificationsSilenced)) return false - return publish("text", { - left: left, - right: right, - profile: profile || "long", - screen: screen || "" - }) - } - - function pushPulse(kind, direction, screen, options) { - const source = options || ({}) - return publish(kind, { - direction: direction, - screen: screen || "", - count: source.count, - gain: source.gain, - profile: "short" - }) - } - - function clear() { - eventHistory = [] - warningQueue = [] - warningDrain.stop() - clearUrgent() - serial++ - cleared() - } - - function workspaceChanged() { - if (!armed || !workspaceService) return - const id = Number(workspaceService.focusedId) || 0 - if (id <= 0) return - const state = workspaceService.workspaceState(id) - pushText("WS " + id, ReactorModel.workspaceLabel(state.windowCount), - "short", true, focusedScreenName()) - } - - function focusedScreenName() { - const monitor = Hyprland.focusedMonitor - return monitor ? String(monitor.name || "") : "" - } - - function classForAddress(address) { - const target = String(address || "") - if (!target) return "" - try { - const toplevels = Hyprland.toplevels.values || [] - for (let index = 0; index < toplevels.length; index++) { - const object = toplevels[index] ? toplevels[index].lastIpcObject : null - const candidate = ReactorModel.parseAddress(object ? object.address : "") - if (candidate === target) - return String(object.class || object.initialClass || "") - } - } catch (_error) {} - return "" - } - - function rememberOpen(address) { - if (!address) return - const next = recentOpen - const now = Date.now() - next[address] = now - for (const key in next) { - if (now - next[key] > 5000) delete next[key] - } - recentOpen = next - } - - function commitUrgent() { - const address = pendingUrgentAddress - let appClass = pendingUrgentClass - pendingUrgentAddress = "" - pendingUrgentClass = "" - if (!armed || !active || !address || address === activeAddress) return - const openedAt = recentOpen[address] || 0 - if (openedAt > 0 && Date.now() - openedAt < 2500) { - const next = recentOpen - delete next[address] - recentOpen = next - return - } - appClass = appClass || classForAddress(address) - if (!appClass) return - urgentAddress = address - urgentClass = ReactorModel.sanitize(appClass, 28) - urgentRepeats = 0 - const nextWarning = warningNext - nextWarning.urgent = 0 - warningNext = nextWarning - checkWarnings() - } - - function clearUrgent() { - urgentAddress = "" - urgentClass = "" - urgentRepeats = 0 - const next = warningNext - next.urgent = 0 - warningNext = next - } - - function handleHyprlandEvent(event) { - if (!active || !event) return - const name = String(event.name || "") - const address = ReactorModel.parseAddress(event.data) - if (name === "openwindow") { - rememberOpen(address) - pushPulse("win", 1, focusedScreenName()) - } else if (name === "closewindow") { - pushPulse("win", -1, "") - const next = recentOpen - if (address && next[address] !== undefined) delete next[address] - recentOpen = next - if (address && address === urgentAddress) clearUrgent() - if (address && address === pendingUrgentAddress) { - pendingUrgentAddress = "" - pendingUrgentClass = "" - urgentDelay.stop() - } - } else if (name === "fullscreen") { - pushText("FULLSCREEN", String(event.data).trim() === "1" ? "ON" : "OFF", - "short", false, focusedScreenName()) - } else if (name === "urgent" && address) { - pendingUrgentAddress = address - pendingUrgentClass = classForAddress(address) - urgentDelay.restart() - } else if (name === "activewindowv2") { - activeAddress = address - if (address && address === urgentAddress) clearUrgent() - if (address && address === pendingUrgentAddress) { - pendingUrgentAddress = "" - pendingUrgentClass = "" - urgentDelay.stop() - } - } - } - - function themeChanged() { - themeNameReader.reload() - themeDelay.restart() - } - - function handleThemeText() { - const name = String(themeNameReader.text() || "").trim() - if (!lastThemeName) { - lastThemeName = name - return - } - if (!armed || !name || name === lastThemeName) return - lastThemeName = name - pushText(name, "THEME LOADED", "long", false, "") - } - - function baselineExternal() { - if (lastExternalTimestamp > 0) return - let parsed = null - try { parsed = ReactorModel.parseExternal(externalEventReader.text(), 0, Date.now()) } - catch (_error) {} - lastExternalTimestamp = parsed ? Math.min(parsed.timestamp, Date.now()) : Date.now() - } - - function handleExternal() { - if (lastExternalTimestamp <= 0) { - baselineExternal() - return - } - let parsed = null - try { - parsed = ReactorModel.parseExternal(externalEventReader.text(), - lastExternalTimestamp, Date.now()) - } catch (_error) {} - if (!parsed) return - lastExternalTimestamp = parsed.timestamp - if (parsed.left === "AI") return - pushText(parsed.left, parsed.right, "long", false, "") - } - - function announceNotification() { - if (!armed || !notificationService || notificationCount <= lastNotificationCount - || (statusService && statusService.notificationsSilenced)) { - lastNotificationCount = notificationCount - return - } - lastNotificationCount = notificationCount - const model = notificationService.pendingModel - if (!model || model.count < 1) return - const entry = model.get(0) - if (!entry) return - const summary = String(entry.summary || "") - const body = String(entry.body || "") - const message = summary && body && body !== summary - ? summary + " - " + body : summary || body - pushText(message, entry.app || "NOTIFY", "long", false, "") - } - - function scheduleTrack() { - if (armed) trackDelay.restart() - } - - function announceTrack() { - if (!armed || !mediaTitle) return - const signature = mediaTitle + "|" + mediaArtist + "|" + mediaAlbum - if (signature === lastTrackSignature) return - if (pushText(mediaTitle, - mediaArtist + (mediaAlbum ? " - " + mediaAlbum : ""), - "long", false, "")) lastTrackSignature = signature - } - - function handleNetwork() { - const current = networkMode - if (!lastNetworkMode) { - lastNetworkMode = current - return - } - if (!armed || !current || current === lastNetworkMode) return - if (current === "none") checkWarnings() - else if (lastNetworkMode === "none") - pushText("ONLINE", "NETWORK", "short", false, "") - lastNetworkMode = current - } - - function handleMute() { - if (!muteBaselined) { - muteBaselined = true - return - } - if (armed) pushText("VOLUME", outputMuted ? "MUTED" : "UNMUTED", - "short", false, "") - } - - function handleDnd() { - if (!dndBaselined) { - dndBaselined = true - return - } - if (armed) pushText(statusService.notificationsSilenced ? "DND ON" : "DND OFF", - "", "short", true, "") - } - - function handleUpdate() { - if (!updateBaselined) { - updateBaselined = true - return - } - if (armed && updateAvailable) - pushText("UPDATE READY", "OMARCHY", "long", false, "") - } - - function queueWarning(left, right) { - if (!armed || !active) return false - for (let index = 0; index < warningQueue.length; index++) { - if (warningQueue[index].left === left && warningQueue[index].right === right) - return false - } - const next = warningQueue.slice(-4) - next.push({ left: left, right: right }) - warningQueue = next - if (!warningDrain.running) warningDrain.restart() - return true - } - - function checkQuotaWarnings(now) { - if (!aiUsageService) return - const providers = aiUsageService.providers || [] - const state = quotaState - for (let index = 0; index < providers.length; index++) { - const provider = providers[index] - const id = String(provider.providerId || "ai") - const percent = aiUsageService.usagePercent(provider) - const previous = state[id] || ({ hot: false, next: 0 }) - if (percent < 75) previous.hot = false - if (percent >= 80) { - previous.hot = true - if (now >= previous.next && queueWarning( - String(provider.providerName || id) + " " + percent, "AI QUOTA!")) - previous.next = now + 300000 - } else if (!previous.hot) previous.next = 0 - state[id] = previous - } - quotaState = state - } - - function checkWarnings() { - if (!armed || !active) return - const now = Date.now() - const next = warningNext - if (networkMode === "none") { - if (now >= next.offline && queueWarning("OFFLINE!", "NETWORK")) - next.offline = now + 120000 - } else next.offline = 0 - if (batteryLow) { - if (now >= next.battery && queueWarning( - "BATTERY " + powerService.percent, "PLUG IN!")) - next.battery = now + 120000 - } else next.battery = 0 - if (urgentAddress && urgentClass && now >= next.urgent && urgentRepeats < 3 - && queueWarning(urgentClass, "WANTS YOU!")) { - next.urgent = now + 60000 * Math.pow(3, urgentRepeats) - urgentRepeats++ - } - checkQuotaWarnings(now) - warningNext = next - } - - function runTest(kindValue, argumentValue) { - if (!active) return false - const kind = String(kindValue || "").toLowerCase() - const argument = String(argumentValue || "") - const parts = argument.split("|") - if (kind === "text" || kind === "text-long") - return pushText(parts[0] || "THIS IS A LONG REACTOR TEST MESSAGE", - parts.slice(1).join("|") || "SOURCE", "long", true, "") - if (kind === "text-short") - return pushText(parts[0] || "REACTOR", parts.slice(1).join("|") || "TEST", - "short", true, "") - if (kind === "warn" || kind === "text-warn") - return pushText(parts[0] || "OFFLINE!", parts.slice(1).join("|") || "NETWORK", - "warn", true, "") - if (kind === "win" || kind === "win-open") - return pushPulse("win", argument === "-1" || argument === "close" ? -1 : 1, - focusedScreenName()) - if (kind === "win-close") return pushPulse("win", -1, focusedScreenName()) - if (kind === "monsweep" || kind === "sweep") - return pushPulse("monsweep", argument === "-1" ? -1 : 1, - focusedScreenName(), { count: 36, gain: 0.82 }) - if (kind === "workspace" || kind === "ws") { - const workspace = Math.max(1, Number(argument) || 1) - return pushText("WS " + workspace, "TEST APPS", "short", true, "") - } - if (kind === "clear") { - clear() - return true - } - return pushText("REACTOR TEST", kind || "EVENT", "short", true, "") - } - - onNetworkModeChanged: handleNetwork() - onOutputMutedChanged: handleMute() - onUpdateAvailableChanged: handleUpdate() - onNotificationCountChanged: announceNotification() - onMediaTitleChanged: scheduleTrack() - onMediaArtistChanged: scheduleTrack() - onMediaAlbumChanged: scheduleTrack() - onBatteryLowChanged: if (batteryLow) checkWarnings() - - Connections { - target: Hyprland - function onFocusedMonitorChanged() { - if (root.armed) root.pushPulse("monsweep", 1, root.focusedScreenName(), - { count: 36, gain: 0.82 }) - } - function onRawEvent(event) { root.handleHyprlandEvent(event) } - } - - Connections { - target: root.workspaceService - function onFocusedIdChanged() { root.workspaceChanged() } - } - - Connections { - target: root.statusService - function onNotificationsSilencedChanged() { root.handleDnd() } - function onVoxtypeStateChanged() { - if (root.armed && root.statusService.voxtypeState === "recording") - root.pushText("VOXTYPE", "REC", "short", false, "") - } - } - - FileView { - id: themeNameReader - path: root.home + "/.local/state/omarchy/current/theme.name" - watchChanges: true - printErrors: false - onFileChanged: root.themeChanged() - onLoaded: root.handleThemeText() - } - - Timer { - id: themeDelay - interval: 180 - onTriggered: root.handleThemeText() - } - - FileView { - id: externalEventReader - path: root.home + "/.cache/qs-reactor-event" - watchChanges: true - printErrors: false - onFileChanged: reload() - onLoaded: root.handleExternal() - onLoadFailed: root.baselineExternal() - } - - Timer { - id: urgentDelay - interval: 700 - onTriggered: root.commitUrgent() - } - - Timer { - id: trackDelay - interval: 800 - onTriggered: root.announceTrack() - } - - Timer { - id: warningDrain - interval: 1 - onTriggered: { - if (root.warningQueue.length === 0) return - const next = root.warningQueue.slice() - const warning = next.shift() - root.warningQueue = next - root.pushText(warning.left, warning.right, "warn", true, "") - if (next.length > 0) { - interval = 4200 - warningDrain.restart() - } else interval = 1 - } - } - - Timer { - interval: 30000 - repeat: true - running: root.armed - onTriggered: root.checkWarnings() - } - - Process { - id: pacmanTail - property int changedPackages: 0 - running: root.active - command: ["bash", "-c", "exec tail -n 0 -F /var/log/pacman.log 2>/dev/null"] - stdout: SplitParser { - onRead: function(line) { - if (line.indexOf("transaction started") >= 0) pacmanTail.changedPackages = 0 - else if (line.indexOf("] upgraded ") >= 0 - || line.indexOf("] installed ") >= 0 - || line.indexOf("] removed ") >= 0) pacmanTail.changedPackages++ - else if (line.indexOf("transaction completed") >= 0 - && pacmanTail.changedPackages > 0) { - const count = pacmanTail.changedPackages - root.pushText(count + (count === 1 ? " PACKAGE" : " PACKAGES") - + " CHANGED", "PACMAN", "long", false, "") - pacmanTail.changedPackages = 0 - } - } - } - } - - Timer { - interval: 3000 - running: true - onTriggered: { - root.lastNetworkMode = root.networkMode - root.lastNotificationCount = root.notificationCount - root.lastTrackSignature = root.mediaTitle + "|" + root.mediaArtist - + "|" + root.mediaAlbum - root.muteBaselined = true - root.dndBaselined = true - root.updateBaselined = true - root.armed = true - root.checkWarnings() - root.pushText("REACTOR", "ARMED", "short", true, "") - } - } -} diff --git a/services/StatusService.qml b/services/StatusService.qml deleted file mode 100644 index 1b88ec1..0000000 --- a/services/StatusService.qml +++ /dev/null @@ -1,192 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io - -// One process-wide adapter over Quattro-owned status backends. Bar views never -// instantiate their own pollers, so multiple monitors consume the same state. -Item { - id: root - - required property var bar - - readonly property var idleService: bar && bar.shell - && typeof bar.shell.firstPartyServiceFor === "function" - ? bar.shell.firstPartyServiceFor("omarchy.idle") : null - readonly property var notificationService: bar && bar.shell - && typeof bar.shell.firstPartyServiceFor === "function" - ? bar.shell.firstPartyServiceFor("omarchy.notifications") : null - readonly property bool stayAwake: idleService - ? idleService.stayAwake === true : false - readonly property bool notificationsSilenced: notificationService - ? notificationService.doNotDisturb === true : false - - property string recordingPid: "" - property int recordingElapsed: 0 - property int recordingBaseElapsed: 0 - property double recordingBaseMs: 0 - property bool recordingRefreshPending: false - readonly property bool recording: recordingPid !== "" - - property string voxtypeState: "idle" - property string voxtypeHint: "" - property bool voxtypeAvailable: false - readonly property bool voxtypeActive: voxtypeState === "recording" - || voxtypeState === "transcribing" - - function toggleStayAwake() { - if (!idleService || typeof idleService.setIdleEnabled !== "function") return false - idleService.setIdleEnabled(stayAwake) - return true - } - - function toggleNotifications() { - if (!notificationService - || typeof notificationService.setDoNotDisturb !== "function") return false - notificationService.setDoNotDisturb(!notificationsSilenced) - return true - } - - function stopRecording() { - if (!recording || !bar) return false - bar.run("omarchy-capture-screenrecording --stop-recording") - recordingRetry.restart() - return true - } - - function openVoxtypeModel() { - if (!bar) return false - bar.run("omarchy-voxtype-model") - return true - } - - function openVoxtypeConfig() { - if (!bar) return false - bar.run("omarchy-voxtype-config") - return true - } - - function refreshRecording() { - if (recordingProbe.running) { - recordingRefreshPending = true - return - } - recordingProbe.running = true - } - - function setRecordingPid(value) { - const next = String(value || "").trim() - if (next === recordingPid) return - recordingPid = next - recordingElapsed = 0 - recordingBaseElapsed = 0 - recordingBaseMs = next ? Date.now() : 0 - if (!next) return - recordingElapsedProbe.command = ["ps", "-o", "etimes=", "-p", next] - recordingElapsedProbe.running = true - } - - function updateRecordingElapsed() { - if (!recording || recordingBaseMs <= 0) return - recordingElapsed = recordingBaseElapsed - + Math.floor((Date.now() - recordingBaseMs) / 1000) - } - - function updateVoxtype(raw) { - try { - const data = JSON.parse(String(raw || "{}")) - const next = String(data.alt || data.class || "idle") - voxtypeState = next === "recording" || next === "transcribing" - ? next : "idle" - voxtypeHint = String(data.tooltip || "").split("\n")[0] - voxtypeAvailable = true - } catch (error) { - voxtypeState = "idle" - voxtypeHint = "" - } - } - - FileView { - id: recordingState - path: "/tmp/omarchy-screenrecord-filename" - watchChanges: true - printErrors: false - onFileChanged: { - reload() - root.refreshRecording() - } - onLoaded: root.refreshRecording() - onLoadFailed: root.refreshRecording() - } - - Process { - id: recordingProbe - command: ["pgrep", "-o", "-f", "^gpu-screen-recorder"] - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: root.setRecordingPid(text) - } - onExited: { - if (!root.recordingRefreshPending) return - root.recordingRefreshPending = false - Qt.callLater(root.refreshRecording) - } - } - - Process { - id: recordingElapsedProbe - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: { - const seconds = Math.max(0, parseInt(String(text || "").trim()) || 0) - root.recordingBaseElapsed = seconds - root.recordingElapsed = seconds - root.recordingBaseMs = Date.now() - } - } - } - - Timer { - interval: 45000 - running: true - repeat: true - triggeredOnStart: true - onTriggered: root.refreshRecording() - } - - Timer { - id: recordingRetry - interval: 900 - onTriggered: root.refreshRecording() - } - - Timer { - interval: 1000 - running: root.recording - repeat: true - triggeredOnStart: true - onTriggered: root.updateRecordingElapsed() - } - - Process { - id: voxtypeStatus - command: ["bash", "-lc", "exec omarchy-voxtype-status"] - running: true - stdout: SplitParser { - onRead: function(data) { root.updateVoxtype(data) } - } - onExited: { - root.voxtypeAvailable = false - root.voxtypeState = "idle" - root.voxtypeHint = "" - } - } - - Timer { - interval: 60000 - running: !voxtypeStatus.running - repeat: true - onTriggered: voxtypeStatus.running = true - } -} diff --git a/services/WeatherService.qml b/services/WeatherService.qml deleted file mode 100644 index 6f78a51..0000000 --- a/services/WeatherService.qml +++ /dev/null @@ -1,221 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io - -Item { - id: root - - property string icon: "·" - property string place: "" - property string tempC: "" - property string tempF: "" - property string feelsC: "" - property string feelsF: "" - property string description: "" - property string country: "" - property string humidity: "" - property string windKmh: "" - property string windMph: "" - property var forecastDays: [] - property var configuredLocation: ({ - name: "", - latitude: null, - longitude: null - }) - property bool loaded: false - property bool unavailable: false - property bool refreshPending: false - readonly property bool refreshing: weatherProc.running - readonly property string locationQuery: { - const latitude = parseFloat(String(configuredLocation.latitude)) - const longitude = parseFloat(String(configuredLocation.longitude)) - if (!isNaN(latitude) && !isNaN(longitude)) - return latitude + "," + longitude - const name = String(configuredLocation.name || "").trim() - return name ? encodeURIComponent(name) : "" - } - readonly property string requestUrl: "https://wttr.in/" - + locationQuery + "?format=j1" - - function parseLocation(raw) { - try { - const value = JSON.parse(String(raw || "")) - const latitude = parseFloat(value && value.latitude) - const longitude = parseFloat(value && value.longitude) - configuredLocation = { - name: value && typeof value.name === "string" - ? value.name.trim() : "", - latitude: !isNaN(latitude) ? latitude : null, - longitude: !isNaN(longitude) ? longitude : null - } - } catch (_error) { - configuredLocation = { name: "", latitude: null, longitude: null } - } - } - - function chanceOfRain(day) { - const hourly = day && day.hourly ? day.hourly : [] - let maximum = 0 - for (let index = 0; index < hourly.length; index++) { - const chance = parseFloat(hourly[index].chanceofrain) - if (!isNaN(chance) && chance > maximum) maximum = chance - } - return maximum - } - - function forecastCode(day) { - const hourly = day && day.hourly ? day.hourly : [] - const representative = hourly.length > 4 - ? hourly[4] : hourly.length > 0 ? hourly[0] : null - return representative ? String(representative.weatherCode || "") : "" - } - - function minutesForClock(value) { - const match = String(value || "").match(/^(\d{1,2}):(\d{2})\s*([AP]M)$/i) - if (!match) return -1 - let hours = parseInt(match[1]) || 0 - const minutes = parseInt(match[2]) || 0 - const suffix = match[3].toUpperCase() - if (suffix === "PM" && hours !== 12) hours += 12 - if (suffix === "AM" && hours === 12) hours = 0 - return hours * 60 + minutes - } - - function isNight(sunrise, sunset) { - const rise = minutesForClock(sunrise) - const set = minutesForClock(sunset) - if (rise < 0 || set < 0) return false - const now = new Date() - const minutes = now.getHours() * 60 + now.getMinutes() - return minutes < rise || minutes >= set - } - - function glyphForCode(code, night) { - const value = parseInt(code) || 0 - if (value === 113) return night ? "\ue32b" : "\ue30d" - if (value === 116) return night ? "\ue32e" : "\ue302" - if (value === 119 || value === 122) return "\ue33d" - if (value === 143 || value === 248 || value === 260) return "\ue313" - if ([176, 263, 266, 293, 296, 353].indexOf(value) >= 0) - return night ? "\ue333" : "\ue308" - if ([179, 227, 230, 323, 326, 368].indexOf(value) >= 0) - return night ? "\ue327" : "\ue30a" - if ([182, 185, 281, 284, 311, 314, 317, 320, 350, 362, 365, 374, 377] - .indexOf(value) >= 0) return "\ue3ad" - if ([200, 386, 389, 392, 395].indexOf(value) >= 0) return "\ue31d" - if ([299, 302, 305, 308, 356, 359].indexOf(value) >= 0) return "\ue318" - if ([329, 332, 335, 338, 371].indexOf(value) >= 0) return "\ue31a" - return "\ue33d" - } - - function refresh(force) { - if (!enabled) return - if (weatherProc.running) { - if (force === true) refreshPending = true - return - } - weatherProc.running = true - } - - function parseReport(raw) { - const text = String(raw || "").trim() - if (!text) { - unavailable = true - return - } - - try { - const report = JSON.parse(text) - const current = report.current_condition && report.current_condition[0] - ? report.current_condition[0] : null - const area = report.nearest_area && report.nearest_area[0] - ? report.nearest_area[0] : null - const astronomy = report.weather && report.weather[0] - && report.weather[0].astronomy && report.weather[0].astronomy[0] - ? report.weather[0].astronomy[0] : null - if (!current) { - unavailable = true - return - } - - icon = glyphForCode(current.weatherCode, - isNight(astronomy ? astronomy.sunrise : "", astronomy ? astronomy.sunset : "")) - tempC = String(current.temp_C || "") - tempF = String(current.temp_F || "") - feelsC = String(current.FeelsLikeC || "") - feelsF = String(current.FeelsLikeF || "") - description = current.weatherDesc && current.weatherDesc[0] - ? String(current.weatherDesc[0].value || "") : "" - place = area && area.areaName && area.areaName[0] - ? String(area.areaName[0].value || "") : "" - country = area && area.country && area.country[0] - ? String(area.country[0].value || "") : "" - humidity = String(current.humidity || "") - windKmh = String(current.windspeedKmph || "") - windMph = String(current.windspeedMiles || "") - - const days = [] - const reportDays = report.weather || [] - for (let index = 0; index < reportDays.length && index < 3; index++) { - const day = reportDays[index] - days.push({ - date: String(day.date || ""), - minC: String(day.mintempC || ""), - maxC: String(day.maxtempC || ""), - minF: String(day.mintempF || ""), - maxF: String(day.maxtempF || ""), - code: forecastCode(day), - rain: chanceOfRain(day) - }) - } - forecastDays = days - loaded = true - unavailable = false - } catch (_error) { - unavailable = true - } - } - - Process { - id: weatherProc - command: ["curl", "-fsS", "--max-time", "5", root.requestUrl] - stdout: StdioCollector { - waitForEnd: true - onStreamFinished: root.parseReport(text) - } - onRunningChanged: { - if (running || !root.refreshPending) return - root.refreshPending = false - Qt.callLater(function() { root.refresh(false) }) - } - } - - FileView { - id: locationFile - path: Quickshell.env("HOME") - + "/.local/state/omarchy/settings/weather.json" - watchChanges: true - printErrors: false - onFileChanged: reload() - onLoaded: root.parseLocation(text()) - onLoadFailed: root.parseLocation("") - } - - Timer { - interval: 1500 - running: root.enabled - onTriggered: locationFile.reload() - } - - onLocationQueryChanged: refresh(true) - - Timer { - interval: 15 * 60 * 1000 - running: root.enabled - repeat: true - triggeredOnStart: true - onTriggered: root.refresh(false) - } -} diff --git a/services/WorkspaceModel.js b/services/WorkspaceModel.js deleted file mode 100644 index 9508310..0000000 --- a/services/WorkspaceModel.js +++ /dev/null @@ -1,91 +0,0 @@ -.pragma library - -function positiveId(value) { - var id = Number(value) - return Number.isInteger(id) && id > 0 && id <= 9999 ? id : 0 -} - -function toplevelCount(workspace) { - if (!workspace || !workspace.toplevels) return 0 - var values = workspace.toplevels.values - return values && typeof values.length === "number" - ? Math.max(0, values.length) : 0 -} - -function snapshot(source, focusedValue) { - var values = source && typeof source.length === "number" ? source : [] - var focusedId = positiveId(focusedValue) - var byId = {} - for (var i = 0; i < values.length; i++) { - var id = positiveId(values[i] ? values[i].id : 0) - if (!id) continue - var count = toplevelCount(values[i]) - if (!byId[id] || count > byId[id].windowCount) - byId[id] = { id: id, windowCount: count } - } - if (focusedId && !byId[focusedId]) - byId[focusedId] = { id: focusedId, windowCount: 0 } - - var ids = Object.keys(byId).map(Number) - ids.sort(function(left, right) { return left - right }) - var result = [] - for (var j = 0; j < ids.length; j++) { - var entry = byId[ids[j]] - result.push({ - id: entry.id, - windowCount: entry.windowCount, - focused: entry.id === focusedId, - occupied: entry.windowCount > 0 - }) - } - return result -} - -function visibleIds(modeValue, entries, focusedValue) { - var mode = ["10", "5", "active"].indexOf(String(modeValue || "")) !== -1 - ? String(modeValue) : "10" - var focusedId = positiveId(focusedValue) - var source = entries && typeof entries.length === "number" ? entries : [] - var ids = [] - - if (mode === "active") { - for (var i = 0; i < source.length; i++) { - var activeId = positiveId(source[i] ? source[i].id : 0) - if (activeId && ids.indexOf(activeId) === -1) ids.push(activeId) - } - if (focusedId && ids.indexOf(focusedId) === -1) ids.push(focusedId) - ids.sort(function(left, right) { return left - right }) - return ids - } - - var limit = mode === "5" ? 5 : 10 - for (var id = 1; id <= limit; id++) ids.push(id) - // Persist modes guarantee a minimum range; they are not a visibility cap. - // Keep every real positive Hyprland workspace reachable even when it sits - // beyond that range, including occupied workspaces on another monitor. - for (var entryIndex = 0; entryIndex < source.length; entryIndex++) { - var existingId = positiveId(source[entryIndex] ? source[entryIndex].id : 0) - if (existingId && ids.indexOf(existingId) === -1) ids.push(existingId) - } - if (focusedId && ids.indexOf(focusedId) === -1) ids.push(focusedId) - ids.sort(function(left, right) { return left - right }) - return ids -} - -function stateFor(idValue, entries, focusedValue) { - var id = positiveId(idValue) - var focusedId = positiveId(focusedValue) - var source = entries && typeof entries.length === "number" ? entries : [] - var count = 0 - for (var i = 0; i < source.length; i++) { - if (positiveId(source[i] ? source[i].id : 0) !== id) continue - count = Math.max(0, Number(source[i].windowCount) || 0) - break - } - return { - id: id, - focused: id !== 0 && id === focusedId, - occupied: count > 0, - windowCount: count - } -} diff --git a/services/WorkspaceService.qml b/services/WorkspaceService.qml deleted file mode 100644 index dd05762..0000000 --- a/services/WorkspaceService.qml +++ /dev/null @@ -1,56 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell.Hyprland -import "WorkspaceModel.js" as WorkspaceModel - -Item { - id: root - - property var bar: null - property var actionAdapter: null - property var config: ({}) - property var workspaceSource: Hyprland.workspaces.values || [] - property var focusedWorkspaceSource: Hyprland.focusedWorkspace - readonly property int focusedId: WorkspaceModel.positiveId( - focusedWorkspaceSource ? focusedWorkspaceSource.id : 0) - readonly property var entries: WorkspaceModel.snapshot( - workspaceSource, focusedId) - readonly property string mode: config && ["10", "5", "active"].indexOf( - String(config.mode || "")) !== -1 ? String(config.mode) : "10" - readonly property string style: config - && ["default", "numbers", "magic", "kanji", "rings", "aurora", "pacman"].indexOf( - String(config.style || "")) !== -1 ? String(config.style) : "default" - readonly property var visibleWorkspaceIds: WorkspaceModel.visibleIds( - mode, entries, focusedId) - - visible: false - width: 0 - height: 0 - - function workspaceState(id) { - return WorkspaceModel.stateFor(id, entries, focusedId) - } - - function focusWorkspace(id) { - return actionAdapter - && typeof actionAdapter.focusWorkspace === "function" - ? actionAdapter.focusWorkspace(id) : false - } - - function setPreference(name, value) { - const key = String(name || "") - const next = String(value || "") - if (key === "mode" && ["10", "5", "active"].indexOf(next) < 0) - return false - if (key === "style" - && ["default", "numbers", "magic", "kanji", "rings", "aurora", "pacman"].indexOf(next) < 0) return false - if (key !== "mode" && key !== "style") return false - if (!bar || typeof bar.mutateShibumiConfig !== "function") return false - return bar.mutateShibumiConfig(function(shibumi) { - if (!shibumi.workspace || typeof shibumi.workspace !== "object") - shibumi.workspace = { version: 1, mode: "10", style: "default" } - shibumi.workspace[key] = next - }) - } -} diff --git a/shared/telemetry/GpuTelemetry.qml b/shared/telemetry/GpuTelemetry.qml index b8b5d50..948b799 100644 --- a/shared/telemetry/GpuTelemetry.qml +++ b/shared/telemetry/GpuTelemetry.qml @@ -7,7 +7,7 @@ Scope { property int consumers: 0 property string helperPath: String(Qt.resolvedUrl( - "../scripts/shibumi-gpu-probe")).replace("file://", "") + "scripts/shibumi-gpu-probe")).replace("file://", "") property var devices: [] property string backend: "" property string name: "" diff --git a/tests/ai-plugin-regression.sh b/tests/ai-plugin-regression.sh index 4598c82..e668700 100755 --- a/tests/ai-plugin-regression.sh +++ b/tests/ai-plugin-regression.sh @@ -293,14 +293,9 @@ fi if rg -q 'CACHE_FILE|stale_last' "$repo_root/hancore.shibumi.ai/scripts/opencode-usage"; then fail "OpenCode provider persists a usage cache" fi -cmp -s "$repo_root/scripts/opencode-usage" \ - "$repo_root/hancore.shibumi.ai/scripts/opencode-usage" \ - || fail "canonical and vendored OpenCode scanners drifted" -diff -u \ - <(sed 's#\.\./scripts/opencode-usage#scripts/opencode-usage#' \ - "$repo_root/services/OpenCodeProvider.qml") \ - "$repo_root/hancore.shibumi.ai/OpenCodeProvider.qml" >/dev/null \ - || fail "canonical and vendored OpenCode providers drifted" +rg -Fq 'Qt.resolvedUrl("scripts/opencode-usage")' \ + "$repo_root/hancore.shibumi.ai/OpenCodeProvider.qml" \ + || fail "OpenCode provider does not resolve its shipped scanner" rg -q 'con\.execute\("begin"\)' \ "$repo_root/hancore.shibumi.ai/scripts/opencode-usage" \ || fail "OpenCode aggregates do not share one SQLite read snapshot" diff --git a/tests/ai-usage-widget-smoke.qml b/tests/ai-usage-widget-smoke.qml deleted file mode 100644 index e6785b8..0000000 --- a/tests/ai-usage-widget-smoke.qml +++ /dev/null @@ -1,194 +0,0 @@ -import QtQuick -import Quickshell -import "widgets" as Widgets - -ShellRoot { - id: root - property int phase: 0 - property real stableProviderWidth: 0 - property var clickTargets: [] - - function fail(message) { - console.error("ai-usage-widget-smoke:", message) - Qt.exit(1) - } - - QtObject { - id: claudeProvider - property string providerId: "claude" - property string providerName: "Claude" - property real rateLimitPercent: 17 - property string rateLimitLabel: "Weekly" - property string rateLimitResetAt: "" - property real secondaryRateLimitPercent: -1 - property string secondaryRateLimitLabel: "" - property string secondaryRateLimitResetAt: "" - property real todayTotalTokens: 900 - property string tierLabel: "Pro" - property string usageStatusText: "Allowed" - property string latestModel: "claude-test" - } - - QtObject { - id: codexProvider - property string providerId: "codex" - property string providerName: "Codex" - property real rateLimitPercent: 13 - property string rateLimitLabel: "Weekly" - property string rateLimitResetAt: "" - property real secondaryRateLimitPercent: -1 - property string secondaryRateLimitLabel: "" - property string secondaryRateLimitResetAt: "" - property real todayTotalTokens: 1200 - property string tierLabel: "Plus" - property string usageStatusText: "Allowed" - property string latestModel: "gpt-test" - } - - QtObject { - id: openCodeProvider - property string providerId: "opencode" - property string providerName: "OpenCode" - property real rateLimitPercent: 22 - property string rateLimitLabel: "5h soft cap" - property string rateLimitResetAt: "" - property real secondaryRateLimitPercent: 8 - property string secondaryRateLimitLabel: "7d soft cap" - property string secondaryRateLimitResetAt: "" - property real todayTotalTokens: 400 - property string tierLabel: "Local messages" - property string usageStatusText: "Local activity" - property string latestModel: "local-test" - } - - QtObject { - id: sharedAi - property var providers: [claudeProvider, codexProvider, openCodeProvider] - property string selectedTool: "codex" - readonly property var selectedProvider: selectedTool === "opencode" - ? openCodeProvider : selectedTool === "claude" - ? claudeProvider : codexProvider - property bool refreshing: false - property int refreshCount: 0 - property int cycleCount: 0 - function usagePercent(provider) { return Math.round(provider.rateLimitPercent) } - function tooltipText() { return "Codex\nWeekly: 13%\n\nOpenCode\n5h: 22%" } - function refreshAll(_force) { refreshCount++ } - function cycleTool(direction) { - cycleCount++ - selectedTool = direction < 0 ? "codex" : "opencode" - return true - } - function selectTool(tool) { selectedTool = tool; return true } - function resetText(_provider, _timestamp) { return "" } - function formatTokens(value) { return String(value) } - } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: 35 - property string position: "top" - property string fontFamily: "monospace" - property color foreground: "#eeeeee" - property color barForeground: foreground - property color background: "#111111" - property color urgent: "#dd7788" - property bool foregroundAnimationEnabled: false - property var aiUsageService: sharedAi - property var clickTargets: root.clickTargets - property var visualTokens: ({ - slotHeight: 28, - pillHeight: 24, - pillRadius: 12, - pillPaddingX: 9, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false, - compactGap: 5, - labelSize: 12 - }) - function registerClickTarget(target) { - if (root.clickTargets.indexOf(target) < 0) - root.clickTargets = root.clickTargets.concat([target]) - } - function unregisterClickTarget(target) { - root.clickTargets = root.clickTargets.filter(item => item !== target) - } - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(_owner, _direction) { return false } - function targetBelongsToWindow(_target, _window) { return false } - property var activePopout: null - } - - Widgets.AiUsageWidget { - id: aiWidget - bar: fakeBar - aiServiceOverride: sharedAi - panelSource: Qt.resolvedUrl("AiTestPanel.qml") - } - - Timer { - interval: 100 - repeat: true - running: true - onTriggered: { - if (root.phase === 0) { - if (!aiWidget.visible || aiWidget.providerId !== "codex" - || aiWidget.usagePercent !== 13 || aiWidget.implicitWidth <= 0 - || aiWidget.providerIconSlotWidth !== 20 - || aiWidget.providerIconSlotHeight !== 16 - || aiWidget.claudeGlyphPixelSize !== 15 - || aiWidget.providerGlyphWidth !== 14 - || aiWidget.providerGlyphHeight !== 14 - || aiWidget.providerGlyphHorizontalOffset !== 0 - || aiWidget.providerContentHorizontalOffset !== -1 - || aiWidget.providerGlyphHorizontalOffset - + aiWidget.providerContentHorizontalOffset !== -1 - || aiWidget.childPanelWidget("omarchy.agents") !== aiWidget - || aiWidget.childPanelWidget("omarchy.model-usage") !== aiWidget) - return root.fail("single-provider facade") - root.stableProviderWidth = aiWidget.implicitWidth - sharedAi.cycleTool(1) - sharedAi.refreshAll(true) - aiWidget.open() - } else if (root.phase === 1) { - if (aiWidget.providerId !== "opencode" || sharedAi.cycleCount !== 1 - || sharedAi.refreshCount !== 1 || !aiWidget.opened - || !aiWidget.panelLoaded || !aiWidget.panelItem.ready - || aiWidget.implicitWidth !== root.stableProviderWidth - || aiWidget.providerGlyphWidth !== 20 - || aiWidget.providerGlyphHeight !== 12 - || aiWidget.providerGlyphHorizontalOffset !== 0 - || aiWidget.providerContentHorizontalOffset !== 0 - || aiWidget.providerGlyphHorizontalOffset - + aiWidget.providerContentHorizontalOffset !== 0) - return root.fail("provider switch/panel lifecycle") - sharedAi.selectTool("claude") - } else if (root.phase === 2) { - if (aiWidget.providerId !== "claude" - || aiWidget.implicitWidth !== root.stableProviderWidth - || aiWidget.providerGlyphWidth !== 15 - || aiWidget.providerGlyphHeight !== 15 - || aiWidget.providerGlyphHorizontalOffset !== 0 - || aiWidget.providerContentHorizontalOffset !== 0 - || aiWidget.providerGlyphHorizontalOffset - + aiWidget.providerContentHorizontalOffset !== 0) - return root.fail("stable provider icon slot") - aiWidget.close() - } else { - if (aiWidget.opened || aiWidget.panelLoaded) - return root.fail("panel cleanup") - stop() - console.log("ai usage widget smoke passed") - Qt.quit() - } - root.phase++ - } - } -} diff --git a/tests/audio-media-plugins-regression.sh b/tests/audio-media-plugins-regression.sh index e0afa82..4edfa0c 100755 --- a/tests/audio-media-plugins-regression.sh +++ b/tests/audio-media-plugins-regression.sh @@ -109,25 +109,21 @@ if rg -q 'Quickshell\.Services\.(Pipewire|Mpris)|MprisPlayer' \ fi audio_widget="$repo_root/hancore.shibumi.audio/BarWidget.qml" -embedded_audio_widget="$repo_root/widgets/AudioWidget.qml" audio_panel="$repo_root/hancore.shibumi.audio/AudioPanel.qml" audio_bridge="$repo_root/hancore.shibumi.audio/AudioPanelBridge.qml" -for volume_widget in "$audio_widget" "$embedded_audio_widget"; do - full_content=$(sed -n \ - '/id: fullHorizontalContent/,/id: compactHorizontalContent/p' \ - "$volume_widget") - compact_content=$(sed -n \ - '/id: compactHorizontalContent/,/id: verticalContent/p' \ - "$volume_widget") - grep -Fq 'horizontalAlignment: Text.AlignRight' <<<"$full_content" \ - || fail "full audio value does not preserve its fixed right edge" - grep -Fq 'horizontalAlignment: Text.AlignLeft' <<<"$compact_content" \ - || fail "compact audio value does not keep a fixed icon gap" - if grep -Fq 'horizontalAlignment: Text.AlignRight' \ - <<<"$compact_content"; then - fail "compact audio value still shifts inside its fixed-width field" - fi -done +full_content=$(sed -n \ + '/id: fullHorizontalContent/,/id: compactHorizontalContent/p' \ + "$audio_widget") +compact_content=$(sed -n \ + '/id: compactHorizontalContent/,/id: verticalContent/p' \ + "$audio_widget") +grep -Fq 'horizontalAlignment: Text.AlignRight' <<<"$full_content" \ + || fail "full audio value does not preserve its fixed right edge" +grep -Fq 'horizontalAlignment: Text.AlignLeft' <<<"$compact_content" \ + || fail "compact audio value does not keep a fixed icon gap" +if grep -Fq 'horizontalAlignment: Text.AlignRight' <<<"$compact_content"; then + fail "compact audio value still shifts inside its fixed-width field" +fi rg -q 'registeredWidgetSource' "$audio_widget" \ || fail "audio bridge does not resolve the official Omarchy source" rg -q 'registeredWidgetComponent' "$audio_widget" \ @@ -234,12 +230,9 @@ fi rg -q 'readonly property bool spectrumRequested: open && active && spectrumEnabled' \ "$repo_root/hancore.shibumi.media/MediaPanel.qml" \ || fail "media spectrum work is not panel-lifecycle bounded" -for media_panel in \ - "$repo_root/hancore.shibumi.media/MediaPanel.qml" \ - "$repo_root/widgets/MediaPanel.qml"; do - rg -q 'property Timer positionTimer: Timer \{' "$media_panel" \ - || fail "$(basename "$media_panel") assigns a non-visual Timer to ShibumiPanel content" -done +rg -q 'property Timer positionTimer: Timer \{' \ + "$repo_root/hancore.shibumi.media/MediaPanel.qml" \ + || fail "MediaPanel.qml assigns a non-visual Timer to ShibumiPanel content" rg -q 'current/theme/cava_theme' \ "$repo_root/hancore.shibumi.media/Service.qml" \ || fail "media spectrum does not consume the active Cava theme" diff --git a/tests/audio-network-ipc-contract-regression.sh b/tests/audio-network-ipc-contract-regression.sh index 2185929..a0a1a9a 100755 --- a/tests/audio-network-ipc-contract-regression.sh +++ b/tests/audio-network-ipc-contract-regression.sh @@ -17,8 +17,6 @@ audio_bridge="$repo_root/hancore.shibumi.audio/AudioPanelBridge.qml" network_service="$repo_root/hancore.shibumi.network/Service.qml" network_widget="$repo_root/hancore.shibumi.network/BarWidget.qml" network_bridge="$repo_root/hancore.shibumi.network/NetworkPanelBridge.qml" -embedded_audio_bridge="$repo_root/adapters/AudioPanelBridge.qml" -embedded_network_bridge="$repo_root/adapters/NetworkPanelBridge.qml" host_network="$OMARCHY_PATH/shell/plugins/panels/network/Panel.qml" [[ $(rg -l 'target: "omarchy\.audio"' \ @@ -34,8 +32,7 @@ rg -Fq 'manageIpc: false' "$audio_widget" \ || fail 'visible Audio widget can duplicate direct IPC ownership' rg -Fq 'manageIpc = false' "$audio_bridge" \ || fail 'hidden official Audio backend can duplicate direct IPC ownership' -for bridge in "$audio_bridge" "$network_bridge" \ - "$embedded_audio_bridge" "$embedded_network_bridge"; do +for bridge in "$audio_bridge" "$network_bridge"; do rg -Fq 'function suppressBackendKeyboardPanel()' "$bridge" \ || fail "hidden official backend lacks KeyboardPanel suppression: $bridge" rg -Fq 'typeof candidate.beginFocusPrime !== "function"' "$bridge" \ diff --git a/tests/audio-widget-smoke.qml b/tests/audio-widget-smoke.qml deleted file mode 100644 index 34078d3..0000000 --- a/tests/audio-widget-smoke.qml +++ /dev/null @@ -1,213 +0,0 @@ -import QtQuick -import Quickshell -import "widgets" as Widgets -import "fixtures" as Fixtures - -ShellRoot { - id: root - - property int phase: 0 - property int phaseTicks: 0 - property real fullWidth: 0 - property int wheelCallsBefore: 0 - property var clickTargets: [] - - function fail(message) { - console.error("audio-widget-smoke:", message) - Qt.exit(1) - } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: 35 - property int sizeHorizontal: 35 - property string position: "top" - property string fontFamily: "monospace" - property color foreground: "#eeeeee" - property color barForeground: foreground - property color urgent: "#88bbee" - property bool foregroundAnimationEnabled: false - property var activePopout: null - property var clickTargets: root.clickTargets - property var shell: null - property var barWidgetRegistry: null - property var visualTokens: ({ - pillHeight: 24, - pillRadius: 12, - pillPaddingX: 9, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false, - slotHeight: 28, - contentGap: 5, - compactGap: 4, - labelSize: 12, - iconSize: 15 - }) - - function widgetSettings(group, module) { - return group === "G6" && module === "omarchy.audio" - ? ({ testSetting: "retained" }) : ({}) - } - function registerClickTarget(target) { - if (root.clickTargets.indexOf(target) < 0) - root.clickTargets = root.clickTargets.concat([target]) - } - function unregisterClickTarget(target) { - root.clickTargets = root.clickTargets.filter(item => item !== target) - } - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(_owner, _direction) { return false } - function targetBelongsToWindow(_target, _window) { return true } - } - - Loader { - id: audioLoader - active: true - sourceComponent: Component { - Widgets.AudioWidget { - bar: fakeBar - settings: ({ compact: false }) - panelComponent: audioPanelComponent - popupSource: Qt.resolvedUrl("fixtures/AudioTestView.qml") - } - } - } - - Component { - id: audioPanelComponent - Fixtures.AudioTestPanel {} - } - - Widgets.AudioWidget { - id: unavailableAudio - bar: fakeBar - panelComponent: null - } - - Timer { - interval: 80 - repeat: true - running: true - onTriggered: { - root.phaseTicks++ - const audio = audioLoader.item - if (root.phase === 0) { - if (!audio || !audio.audioReady || root.phaseTicks < 3) return - if (!audio || !audio.audioReady || audio.volume !== 42 || audio.muted - || audio.implicitHeight !== 35 || unavailableAudio.visible) - return root.fail("backend readiness/state/geometry") - if (audio.compact) - return root.fail("explicit full presentation resolved compact") - if (!audio.audioPanel || audio.audioPanel.opacity !== 0 - || audio.audioPanel.settings.testSetting !== "retained" - || audio.audioPanel.manageIpc - || audio.audioPanel.opened || audio.audioPanel.openCount !== 0 - || audio.audioPanel.backendKeyboardPanelOpen - || audio.audioPanel.backendKeyboardPanelVisible - || audio.childPanelWidget("omarchy.audio") !== audio - || !audio.ownsPanelWidget(audio) - || !audio.ownsPanelWidget(audio.audioPanel)) - return root.fail("official panel bridge/routing/settings") - if (root.clickTargets.length !== 1 - || audio.audioPanel.internalButton.registeredBar === fakeBar) - return root.fail("duplicate official click target") - audio.audioPanel.open() - if (!audio.audioPanel.opened - || audio.audioPanel.backendKeyboardPanelOpen - || audio.audioPanel.backendKeyboardPanelVisible) - return root.fail("hidden official KeyboardPanel became visible") - audio.audioPanel.close() - - root.fullWidth = audio.implicitWidth - audio.settings = ({ compact: true }) - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 1) { - if (root.phaseTicks < 3) return - if (!audio.compact) - return root.fail("compact setting did not update") - if (audio.implicitWidth >= root.fullWidth) - return root.fail("compact presentation did not reduce width: " - + audio.implicitWidth + " >= " + root.fullWidth) - - audio.interactionTarget.triggerPress(Qt.RightButton) - if (!audio.muted) return root.fail("right-click mute forwarding") - root.wheelCallsBefore = audio.audioPanel.outputVolumeChanges - audio.interactionTarget.wheelMoved(120) - audio.interactionTarget.wheelMoved(120) - audio.interactionTarget.wheelMoved(0) - if (audio.volume !== 52) - return root.fail("wheel target did not accumulate immediately") - if (audio.audioPanel.outputVolumeChanges !== root.wheelCallsBefore) - return root.fail("wheel burst wrote the backend before settling") - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 2) { - if (root.phaseTicks < 3) return - if (audio.audioPanel.outputVolumeChanges !== root.wheelCallsBefore + 1 - || Math.abs(audio.audioPanel.outputVolume - 0.52) > 0.001) - return root.fail("wheel burst was not coalesced into one final write") - audio.interactionTarget.triggerPress(Qt.LeftButton) - if (!audio.opened || audio.audioPanel.opened - || audio.audioPanel.openCount !== 1) - return root.fail("local mixer lifecycle/official panel isolation") - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 3) { - if (!audio.panelLoaded || !audio.panelItem) return - if (fakeBar.activePopout !== audio - || audio.panelItem.renderedSinkCount !== 3 - || audio.panelItem.renderedSourceCount !== 2 - || audio.panelItem.renderedStreamCount !== 2) - return root.fail("local mixer model/popout ownership") - if (audio.panelItem.firstSinkLabel() - !== "SteelSeries Arctis 7 Chat" - || audio.panelItem.secondSinkLabel() - !== "SteelSeries Arctis 7 Game") - return root.fail("description-first output device labels") - if (!audio.panelItem.selectSecondSink() - || audio.audioPanel.defaultSinkChanges !== 1 - || audio.audioPanel.sink.id !== 2) - return root.fail("output device forwarding") - if (!audio.panelItem.selectSecondSource() - || audio.audioPanel.defaultSourceChanges !== 1 - || audio.audioPanel.source.id !== 4) - return root.fail("input device forwarding") - if (!audio.panelItem.setFirstStreamVolume(0.75) - || audio.audioPanel.audioStreams[0].audio.volume !== 0.75) - return root.fail("stream volume forwarding") - if (!audio.panelItem.toggleFirstStreamMute() - || !audio.audioPanel.audioStreams[0].audio.muted) - return root.fail("stream mute forwarding") - if (!audio.panelItem.setInputVolume(0.35) - || audio.audioPanel.inputVolumeChanges !== 1 - || Math.abs(audio.audioPanel.inputVolume - 0.35) > 0.001) - return root.fail("input volume forwarding") - if (!audio.panelItem.toggleInputMute() || !audio.audioPanel.inputMuted) - return root.fail("input mute forwarding fixture") - audio.close() - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 4) { - if (audio.opened || audio.panelLoaded || fakeBar.activePopout !== null) - return root.fail("lazy local mixer close") - audioLoader.active = false - root.phase++ - root.phaseTicks = 0 - } else { - if (root.clickTargets.length !== 0 || fakeBar.activePopout !== null) - return root.fail("destruction cleanup") - stop() - console.log("audio widget smoke passed") - Qt.quit() - } - } - } -} diff --git a/tests/bluetooth-plugin-regression.sh b/tests/bluetooth-plugin-regression.sh index 0adee13..4a1533d 100755 --- a/tests/bluetooth-plugin-regression.sh +++ b/tests/bluetooth-plugin-regression.sh @@ -149,12 +149,7 @@ rg -Fq 'if (discovering && !discoveryOwned) return true' "$adapter" \ rg -q 'property var discoveryOwnerAdapter: null' "$adapter" \ || fail "Bluetooth discovery ownership is not tied to its adapter instance" [[ -f $model ]] || fail "Bluetooth native model is missing" -cmp -s "$repo_root/adapters/BluetoothBackendAdapter.qml" "$adapter" \ - || fail "root and plugin Bluetooth adapters drifted" -cmp -s "$repo_root/adapters/BluetoothModel.js" "$model" \ - || fail "root and plugin Bluetooth models drifted" -cmp -s "$repo_root/adapters/BluetoothDiscoveryGuard.qml" "$discovery_guard" \ - || fail "root and plugin Bluetooth discovery guards drifted" +[[ -f $discovery_guard ]] || fail "Bluetooth discovery guard is missing" if rg -q 'IpcHandler \{' "$adapter"; then fail "Bluetooth backend adapter must not register a second IPC owner" fi diff --git a/tests/bluetooth-widget-smoke.qml b/tests/bluetooth-widget-smoke.qml deleted file mode 100644 index 7c36759..0000000 --- a/tests/bluetooth-widget-smoke.qml +++ /dev/null @@ -1,307 +0,0 @@ -import QtQuick -import Quickshell -import "services" as Services -import "widgets" as Widgets -import "adapters" as Adapters -import "fixtures" as Fixtures - -ShellRoot { - id: root - - property int phase: 0 - property int phaseTicks: 0 - property real fullWidth: 0 - property var clickTargets: [] - property int summonCount: 0 - property bool radioBeforeToggle: false - property bool discoveryBeforeToggle: false - - function fail(message) { - console.error("bluetooth-widget-smoke:", message) - Qt.exit(1) - } - - Item { - id: fakeBar - visible: false - width: 0 - height: 0 - property bool vertical: false - property int barSize: 35 - property int sizeHorizontal: 35 - property string position: "top" - property string fontFamily: "monospace" - property color foreground: "#eeeeee" - property color barForeground: foreground - property color urgent: "#88bbee" - property bool foregroundAnimationEnabled: false - property var activePopout: null - property var clickTargets: root.clickTargets - property var shell: null - property var barWidgetRegistry: null - property var bluetoothService: sharedBluetoothService - property var visualTokens: ({ - pillHeight: 24, - pillRadius: 12, - pillPaddingX: 9, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false, - slotHeight: 28, - contentGap: 5, - compactGap: 4, - labelSize: 12, - iconSize: 15 - }) - - function registeredWidgetSource(_id) { return "" } - function registeredWidgetComponent(_id) { return null } - function widgetSettings(group, module) { - return group === "G15" && module === "omarchy.bluetooth" - ? ({ testSetting: "retained" }) : ({}) - } - function registerClickTarget(target) { - if (root.clickTargets.indexOf(target) < 0) - root.clickTargets = root.clickTargets.concat([target]) - } - function unregisterClickTarget(target) { - root.clickTargets = root.clickTargets.filter(item => item !== target) - } - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(_owner, _direction) { return false } - function targetBelongsToWindow(_target, _window) { return true } - function summonBarWidget(id) { - if (id !== "omarchy.bluetooth") return false - root.summonCount++ - return true - } - } - - Fixtures.BluetoothTestBackend { - id: testBackend - } - - QtObject { - id: nativePendingDevice - property string address: "FE:DC:BA:98:76:54" - property string name: "Shibumi Pending Test Device" - property string deviceName: name - property bool connected: false - property bool paired: true - property bool bonded: true - property bool trusted: true - } - - Adapters.BluetoothBackendAdapter { - id: nativeSignalAdapter - nativeDevicesOverride: [nativePendingDevice] - } - - Services.BluetoothService { - id: sharedBluetoothService - bar: fakeBar - backendOverride: testBackend - } - - QtObject { - id: unavailableService - property bool ready: false - property bool adapterAvailable: false - property bool radioEnabled: false - property int connectedCount: 0 - } - - Loader { - id: firstLoader - active: true - sourceComponent: Component { - Widgets.BluetoothWidget { - bar: fakeBar - settings: ({ compact: false }) - bluetoothServiceOverride: sharedBluetoothService - popupSource: Qt.resolvedUrl("fixtures/BluetoothTestView.qml") - } - } - } - - Loader { - id: secondLoader - active: true - sourceComponent: Component { - Widgets.BluetoothWidget { - bar: fakeBar - settings: ({ compact: false }) - bluetoothServiceOverride: sharedBluetoothService - popupSource: Qt.resolvedUrl("fixtures/BluetoothTestView.qml") - } - } - } - - Widgets.BluetoothWidget { - id: unavailableBluetooth - bar: fakeBar - bluetoothServiceOverride: unavailableService - } - - Timer { - interval: 80 - repeat: true - running: true - onTriggered: { - root.phaseTicks++ - const first = firstLoader.item - const second = secondLoader.item - const backend = sharedBluetoothService.backend - - if (root.phase === 0) { - if (!first || !second || !backend || !sharedBluetoothService.ready - || root.phaseTicks < 3) return - if (first.bluetoothService !== second.bluetoothService - || first.bluetoothService !== sharedBluetoothService - || !first.adapterAvailable || !first.radioEnabled - || first.connectedCount !== 1 || first.implicitHeight !== 35 - || unavailableBluetooth.visible) - return root.fail("shared backend readiness/state/geometry") - if (backend !== testBackend) - return root.fail("native adapter fixture boundary") - if (first.childPanelWidget("omarchy.bluetooth") !== first - || second.childPanelWidget("omarchy.bluetooth") !== second - || !first.ownsPanelWidget(first)) - return root.fail("screen-local alias routing") - if (root.clickTargets.length !== 2 || sharedBluetoothService.sessionCount !== 0) - return root.fail("duplicate target or closed lifecycle") - - nativeSignalAdapter.setNativePendingAction( - nativePendingDevice.address, "connecting") - nativePendingDevice.connected = true - - root.fullWidth = first.implicitWidth - first.settings = ({ compact: true }) - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 1) { - if (root.phaseTicks < 3) return - if (!first.compact || first.implicitWidth >= root.fullWidth) - return root.fail("compact presentation width") - if (!first.showConnectedCount || first.connectedCount !== 1) - return root.fail("connected count hidden in compact presentation") - if (nativeSignalAdapter.pendingAction(nativePendingDevice.address) !== "") - return root.fail("native connect transition did not clear pending state") - - nativeSignalAdapter.setNativePendingAction( - nativePendingDevice.address, "disconnecting") - nativePendingDevice.connected = false - - root.radioBeforeToggle = sharedBluetoothService.radioEnabled - root.discoveryBeforeToggle = sharedBluetoothService.discovering - first.toggleBluetooth() - first.toggleBluetooth() - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 2) { - if (root.phaseTicks < 4) return - if (backend.toggleCount !== 2 - || first.radioEnabled !== root.radioBeforeToggle - || sharedBluetoothService.discovering !== root.discoveryBeforeToggle) - return root.fail("radio/discovery rollback after toggle round trip") - if (nativeSignalAdapter.pendingAction(nativePendingDevice.address) !== "") - return root.fail("native disconnect transition did not clear pending state") - - nativeSignalAdapter.setNativePendingAction( - nativePendingDevice.address, "forgetting") - nativePendingDevice.paired = false - nativePendingDevice.bonded = false - nativePendingDevice.trusted = false - - backend.fakeAdapter.discovering = true - sharedBluetoothService.beginSession(root) - sharedBluetoothService.restartDiscovery() - sharedBluetoothService.endSession(root) - - sharedBluetoothService.openPanel() - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 3) { - if (root.phaseTicks < 2) return - if (nativeSignalAdapter.pendingAction(nativePendingDevice.address) !== "") - return root.fail("native forget transition did not clear pending state") - if (!sharedBluetoothService.discovering) - return root.fail("external discovery refresh was claimed or stopped") - backend.fakeAdapter.discovering = root.discoveryBeforeToggle - - first.interactionTarget.triggerPress(Qt.MiddleButton) - if (root.summonCount !== 1 - || backend.toggleCount !== 2 || !first.radioEnabled || !first.opened) - return root.fail("service IPC route, radio, or panel forwarding") - - second.open() - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 4) { - if (root.phaseTicks < 3) return - if (!first.opened || !second.opened || !first.panelLoaded - || !second.panelLoaded || sharedBluetoothService.sessionCount !== 2 - || backend.viewLoadCount !== 2 || !sharedBluetoothService.discovering - || fakeBar.activePopout !== second) - return root.fail("two-output local panel sessions") - - sharedBluetoothService.connectDevice(backend.knownDevices[0]) - sharedBluetoothService.disconnectDevice(backend.connectedDevices[0]) - sharedBluetoothService.forgetDevice(backend.knownDevices[0]) - if (backend.connectCount !== 1 || backend.disconnectCount !== 1 - || backend.forgetCount !== 1 - || sharedBluetoothService.pendingAction("66:77:88:99:AA:BB") !== "forgetting") - return root.fail("backend device action forwarding") - - sharedBluetoothService.restartDiscovery() - first.close() - secondLoader.active = false - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 5) { - if (root.phaseTicks < 3) return - if (sharedBluetoothService.sessionCount !== 0 - || sharedBluetoothService.discovering || first.panelLoaded - || root.clickTargets.length !== 1 || fakeBar.activePopout !== null) - return root.fail("discovery, session, or local panel teardown") - - backend.selectedAdapter = backend.fakeAdapter - backend.fakeAdapter.discovering = false - sharedBluetoothService.beginSession(root) - backend.alternateAdapter.discovering = true - backend.selectedAdapter = backend.alternateAdapter - sharedBluetoothService.endSession(root) - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 6) { - if (root.phaseTicks < 2) return - if (backend.fakeAdapter.discovering - || !backend.alternateAdapter.discovering - || sharedBluetoothService.sessionCount !== 0) - return root.fail("adapter replacement discovery ownership") - - backend.selectedAdapter = backend.fakeAdapter - backend.alternateAdapter.discovering = false - backend.adapterPresent = false - root.phase++ - root.phaseTicks = 0 - } else { - if (root.phaseTicks < 2) return - if (first.visible) return root.fail("missing-adapter fallback visibility") - firstLoader.active = false - Qt.callLater(function() { - if (root.clickTargets.length !== 0) - return root.fail("click target destruction cleanup") - console.log("bluetooth widget smoke passed") - Qt.quit() - }) - stop() - } - } - } -} diff --git a/tests/brightness-widget-smoke.qml b/tests/brightness-widget-smoke.qml deleted file mode 100644 index a5984c2..0000000 --- a/tests/brightness-widget-smoke.qml +++ /dev/null @@ -1,239 +0,0 @@ -import QtQuick -import Quickshell -import "services" as Services -import "widgets" as Widgets -import "fixtures" as Fixtures - -ShellRoot { - id: root - - property int phase: 0 - property int phaseTicks: 0 - property real fullWidth: 0 - property var clickTargets: [] - property int summonCount: 0 - - function fail(message) { - console.error("brightness-widget-smoke:", message) - Qt.exit(1) - } - - Item { - id: fakeBar - visible: false - width: 0 - height: 0 - property bool vertical: false - property int barSize: 35 - property int sizeHorizontal: 35 - property string position: "top" - property string fontFamily: "monospace" - property color foreground: "#eeeeee" - property color barForeground: foreground - property color urgent: "#88bbee" - property bool foregroundAnimationEnabled: false - property var activePopout: null - property var clickTargets: root.clickTargets - property var shell: null - property var barWidgetRegistry: null - property var monitorService: sharedMonitorService - property var visualTokens: ({ - pillHeight: 24, - pillRadius: 12, - pillPaddingX: 9, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false, - slotHeight: 28, - contentGap: 5, - compactGap: 4, - labelSize: 12, - iconSize: 15 - }) - - function registeredWidgetSource(_id) { return "" } - function registeredWidgetComponent(_id) { return null } - function widgetSettings(group, module) { - return group === "G13" && module === "omarchy.monitor" - ? ({ testSetting: "retained" }) : ({}) - } - function registerClickTarget(target) { - if (root.clickTargets.indexOf(target) < 0) - root.clickTargets = root.clickTargets.concat([target]) - } - function unregisterClickTarget(target) { - root.clickTargets = root.clickTargets.filter(item => item !== target) - } - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(_owner, _direction) { return false } - function targetBelongsToWindow(_target, _window) { return true } - function summonBarWidget(id) { - if (id !== "omarchy.monitor") return false - root.summonCount++ - return true - } - } - - Component { - id: monitorPanelComponent - Fixtures.MonitorTestPanel {} - } - - Services.MonitorService { - id: sharedMonitorService - bar: fakeBar - panelComponent: monitorPanelComponent - } - - QtObject { - id: unavailableService - property bool ready: false - property bool brightnessAvailable: false - property int brightnessPercent: 0 - property var displays: [] - } - - Loader { - id: firstLoader - active: true - sourceComponent: Component { - Widgets.BrightnessWidget { - bar: fakeBar - settings: ({ compact: false }) - monitorServiceOverride: sharedMonitorService - popupSource: Qt.resolvedUrl("fixtures/MonitorTestView.qml") - } - } - } - - Loader { - id: secondLoader - active: true - sourceComponent: Component { - Widgets.BrightnessWidget { - bar: fakeBar - settings: ({ compact: false }) - monitorServiceOverride: sharedMonitorService - popupSource: Qt.resolvedUrl("fixtures/MonitorTestView.qml") - } - } - } - - Widgets.BrightnessWidget { - id: unavailableBrightness - bar: fakeBar - monitorServiceOverride: unavailableService - } - - Timer { - interval: 80 - repeat: true - running: true - onTriggered: { - root.phaseTicks++ - const first = firstLoader.item - const second = secondLoader.item - const backend = sharedMonitorService.backend - - if (root.phase === 0) { - if (!first || !second || !backend || !sharedMonitorService.ready - || root.phaseTicks < 3) return - if (first.monitorService !== second.monitorService - || first.monitorService !== sharedMonitorService - || !first.brightnessAvailable || first.percent !== 64 - || first.displayCount !== 2 || first.implicitHeight !== 35 - || !sharedMonitorService.textSizeAvailable - || sharedMonitorService.textSizePx !== 12 - || sharedMonitorService.textSizeIndex !== 3 - || unavailableBrightness.visible) - return root.fail("shared backend readiness/state/geometry") - if (backend.opacity !== 0 || backend.manageIpc !== false - || backend.settings.testSetting !== "retained") - return root.fail("hidden official owner/settings") - if (first.childPanelWidget("omarchy.monitor") !== first - || second.childPanelWidget("omarchy.monitor") !== second - || !first.ownsPanelWidget(first)) - return root.fail("screen-local alias routing") - if (root.clickTargets.length !== 2) - return root.fail("duplicate official click target") - - root.fullWidth = first.implicitWidth - first.settings = ({ compact: true }) - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 1) { - if (root.phaseTicks < 3) return - if (!first.compact || first.implicitWidth >= root.fullWidth) - return root.fail("compact presentation width") - - backend.open() - if (root.summonCount !== 1) - return root.fail("legacy monitor IPC redirect") - first.interactionTarget.wheelMoved(120) - if (sharedMonitorService.brightnessPercent !== 69 || backend.setCount !== 1) - return root.fail("wheel brightness forwarding") - sharedMonitorService.previewBrightness(71) - sharedMonitorService.setScale("1.6") - sharedMonitorService.setTextSize(14) - sharedMonitorService.adjustTextSize(1) - sharedMonitorService.toggleDisplay("DP-1", true) - if (backend.previewCount !== 1 || backend.scaleCount !== 1 - || backend.textSizeSetCount !== 2 || backend.textSizePx !== 16 - || backend.toggleCount !== 1 || sharedMonitorService.monitorScale !== "1.6" - || sharedMonitorService.enabledDisplayCount !== 1) - return root.fail("monitor action forwarding") - - first.interactionTarget.triggerPress(Qt.LeftButton) - second.open() - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 2) { - if (root.phaseTicks < 3) return - if (!first.opened || !second.opened || !first.panelLoaded - || !second.panelLoaded || backend.viewLoadCount !== 2 - || backend.refreshCount !== 2 || fakeBar.activePopout !== second) - return root.fail("two-output lazy local panels: first=" + first.opened - + " second=" + second.opened + " firstLoaded=" + first.panelLoaded - + " secondLoaded=" + second.panelLoaded + " views=" - + backend.viewLoadCount + " refreshes=" + backend.refreshCount - + " activeSecond=" + (fakeBar.activePopout === second)) - if (backend.opened) - return root.fail("hidden official popup remained open") - - backend.brightnessAvailable = false - backend.internalMonitor = "" - backend.displays = [ - { name: "DP-1", enabled: true, focused: true } - ] - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 3) { - if (root.phaseTicks < 2) return - if (!first.visible || first.brightnessAvailable || first.internalDisplay - || first.tooltipText !== "Display controls" || first.implicitWidth <= 0) - return root.fail("display controls fallback without backlight") - first.close() - secondLoader.active = false - root.phase++ - root.phaseTicks = 0 - } else { - if (first.panelLoaded || root.clickTargets.length !== 1 - || fakeBar.activePopout !== null) - return root.fail("local panel teardown") - firstLoader.active = false - Qt.callLater(function() { - if (root.clickTargets.length !== 0) - return root.fail("click target destruction cleanup") - console.log("brightness widget smoke passed") - Qt.quit() - }) - stop() - } - } - } -} diff --git a/tests/center-model-regression.qml b/tests/center-model-regression.qml index ac0635f..ce5b0bf 100644 --- a/tests/center-model-regression.qml +++ b/tests/center-model-regression.qml @@ -1,6 +1,6 @@ import QtQuick -import "../widgets/CenterLayout.js" as CenterLayout -import "../widgets/CalendarModel.js" as CalendarModel +import "../hancore.shibumi.center/CenterLayout.js" as CenterLayout +import "../hancore.shibumi.center/CalendarModel.js" as CalendarModel import "../hancore.shibumi.center/WeatherLocationModel.js" as WeatherLocationModel QtObject { diff --git a/tests/center-widget-smoke.qml b/tests/center-widget-smoke.qml deleted file mode 100644 index d9ea2a2..0000000 --- a/tests/center-widget-smoke.qml +++ /dev/null @@ -1,254 +0,0 @@ -import QtQuick -import Quickshell -import "widgets" as Widgets - -ShellRoot { - id: root - - property int phase: 0 - property var clickTargets: [] - - function fail(message) { - console.error("center-widget-smoke:", message) - Qt.exit(1) - } - - component FakeWeather: Item { - property var bar: null - property string moduleName: "" - property var settings: ({}) - property bool opened: false - implicitWidth: 44 - implicitHeight: 35 - function open() { opened = true } - function close() { opened = false } - function togglePanel() { opened = !opened } - function refresh() {} - } - - component FakeUpdate: Item { - property var bar: null - property string moduleName: "" - property var settings: ({}) - property bool updateAvailable: true - property int runCount: 0 - visible: updateAvailable - implicitWidth: 24 - implicitHeight: 35 - function runUpdate() { runCount++ } - } - - Component { id: weatherComponent; FakeWeather {} } - Component { id: updateComponent; FakeUpdate {} } - - QtObject { - id: registry - property int revision: 1 - property var widgets: ({ - "omarchy.weather": { component: weatherComponent }, - "omarchy.system-update": { component: updateComponent } - }) - } - - QtObject { - id: sharedClock - property date date: new Date(2026, 6, 16, 13, 5, 0) - } - - QtObject { - id: sharedStatus - property bool stayAwake: true - property bool notificationsSilenced: false - property bool recording: false - property int recordingElapsed: 0 - property bool voxtypeActive: false - property string voxtypeState: "idle" - property string voxtypeHint: "" - property int stopCount: 0 - property int modelCount: 0 - property int configCount: 0 - function toggleStayAwake() { stayAwake = !stayAwake; return true } - function toggleNotifications() { notificationsSilenced = !notificationsSilenced; return true } - function stopRecording() { stopCount++; return true } - function openVoxtypeModel() { modelCount++; return true } - function openVoxtypeConfig() { configCount++; return true } - } - - QtObject { - id: sharedWeather - property string icon: "W" - property string place: "Berlin" - property string tempC: "24" - property string tempF: "75" - property string description: "Clear" - property bool loaded: true - property bool unavailable: false - function refresh(_force) {} - } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: 35 - property string position: "top" - property string fontFamily: "monospace" - property color foreground: "#eeeeee" - property color barForeground: foreground - property color urgent: "#88bbee" - property bool foregroundAnimationEnabled: false - property var activePopout: null - property var barWidgetRegistry: registry - property var clockService: sharedClock - property var statusService: sharedStatus - property var weatherService: sharedWeather - property var clickTargets: root.clickTargets - property var visualTokens: ({ - pillHeight: 24, - pillRadius: 12, - pillPaddingX: 9, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false, - labelSize: 12, - captionSize: 10, - mutedInk: "#999999" - }) - - function widgetSettings(_group, module) { - if (module === "omarchy.weather") return ({ unit: "C" }) - if (module === "omarchy.system-update") return ({ marker: "G8-update" }) - return ({ clock12h: false }) - } - function setWidgetSetting(_group, _module, _key, _value) { return true } - function run(_command) {} - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function registerClickTarget(target) { - if (root.clickTargets.indexOf(target) < 0) - root.clickTargets = root.clickTargets.concat([target]) - } - function unregisterClickTarget(target) { - root.clickTargets = root.clickTargets.filter(item => item !== target) - } - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(_owner, _direction) { return false } - } - - Widgets.CenterWidget { - id: center - bar: fakeBar - availableWidth: 500 - calendarSource: Qt.resolvedUrl("CenterTestCalendar.qml") - } - - Timer { - interval: 130 - repeat: true - running: true - onTriggered: { - if (root.phase === 0) { - if (center.stage !== 0 || center.dateText !== "Thu 16" - || !center.weatherWidget || !center.indicatorWidget || !center.updateWidget - || center.weatherWidget.settings.unit !== "C" - || center.weatherWidget.weatherService !== sharedWeather - || center.weatherWidget.implicitWidth !== 20 - || center.indicatorWidget.statusService !== sharedStatus - || center.indicatorWidget.dndOpticalCenterOffset !== 1 - || center.indicatorWidget.implicitWidth <= 0 - || center.updateWidget.settings.marker !== "G8-update" - || center.updateWidget.moduleName !== "omarchy.system-update" - || center.updateWidget.implicitWidth !== 20 - || center.updateWidget.opticalCenterOffset !== 1 - || center.weatherWidget.implicitWidth - !== center.updateWidget.implicitWidth) - return root.fail("center children/settings/presentation" - + " stage=" + center.stage - + " date=" + center.dateText - + " weather=" + Boolean(center.weatherWidget) - + " indicator=" + Boolean(center.indicatorWidget) - + " update=" + Boolean(center.updateWidget) - + " indicatorWidth=" + (center.indicatorWidget - ? center.indicatorWidget.implicitWidth : -1) - + " hasActive=" + (center.indicatorWidget - ? center.indicatorWidget.hasActive : false) - + " updateModule=" + (center.updateWidget - ? center.updateWidget.moduleName : "missing")) - if (!center.updateBackend - || center.updateBackend.settings.marker !== "G8-update" - || center.updateWidget.backendWidget !== center.updateBackend - || center.updateWidget.tooltipText !== "Omarchy update available" - || center.updateWidget.iconFamily !== "Material Symbols Rounded" - || center.updateWidget.width <= 0 - || center.updateWidget.height <= 0 - || !center.updateWidget.interactionTarget - || center.updateWidget.interactionTarget.width <= 0 - || center.updateWidget.interactionTarget.height <= 0 - || !center.updateWidget.activate() - || center.updateBackend.runCount !== 1) - return root.fail("update facade/backend contract") - if (center.childPanelWidget("omarchy.weather") !== center.weatherWidget - || center.childPanelWidget("omarchy.clock") !== center - || center.childPanelWidget("omarchy.indicators") !== null) - return root.fail("nested panel routing contract") - center.weatherWidget.panelSource = Qt.resolvedUrl( - "WeatherPanelTestView.qml") - center.weatherWidget.togglePanel() - if (!center.weatherWidget.opened) - return root.fail("weather panel contract") - center.open() - } else if (root.phase === 1) { - if (!center.weatherWidget.panelLoaded - || center.weatherWidget.panelItem.weatherService !== sharedWeather) - return root.fail("lazy weather panel") - if (!center.opened || !center.calendarLoaded - || !center.calendarLoaderReady) - return root.fail("lazy calendar open") - center.weatherWidget.close() - center.close() - center.availableWidth = 80 - } else if (root.phase === 2) { - if (center.opened || center.calendarLoaded || center.stage !== 2) - return root.fail("calendar close/minimal stage") - sharedStatus.notificationsSilenced = true - sharedStatus.recording = true - sharedStatus.recordingElapsed = 3661 - sharedStatus.voxtypeActive = true - sharedStatus.voxtypeState = "recording" - center.availableWidth = 500 - } else if (root.phase === 3) { - if (!center.indicatorWidget.hasActive - || center.indicatorWidget.elapsedText() !== "1:01:01" - || center.indicatorWidget.idleIconFamily !== center.bar.fontFamily - || center.indicatorWidget.dndIconFamily !== "Material Symbols Rounded" - || center.indicatorWidget.recordingIconFamily !== center.bar.fontFamily - || center.indicatorWidget.recordingIconGlyph !== "󰻂" - || center.indicatorWidget.recordingIconColor !== center.bar.urgent - || center.indicatorWidget.voxtypeIconFamily !== "Material Symbols Rounded" - || !center.indicatorWidget.toggleStayAwake() - || sharedStatus.stayAwake - || !center.indicatorWidget.toggleNotifications() - || sharedStatus.notificationsSilenced - || !center.indicatorWidget.stopRecording() - || !center.indicatorWidget.activateVoxtype(Qt.LeftButton) - || !center.indicatorWidget.activateVoxtype(Qt.RightButton) - || sharedStatus.stopCount !== 1 - || sharedStatus.modelCount !== 1 - || sharedStatus.configCount !== 1) - return root.fail("status indicator state/action contract") - sharedStatus.recording = false - sharedStatus.voxtypeActive = false - sharedStatus.voxtypeState = "idle" - } else { - if (center.stage !== 0 || center.implicitHeight !== 35) - return root.fail("normal stage restore/geometry") - stop() - console.log("center widget smoke passed") - Qt.quit() - } - root.phase++ - } - } -} diff --git a/tests/clock-widget-smoke.qml b/tests/clock-widget-smoke.qml deleted file mode 100644 index 43af885..0000000 --- a/tests/clock-widget-smoke.qml +++ /dev/null @@ -1,94 +0,0 @@ -import QtQuick -import Quickshell -import "widgets" as Widgets - -ShellRoot { - id: root - - property string changedGroup: "" - property string changedModule: "" - property string changedKey: "" - property var changedValue: null - property string command: "" - - function fail(message) { - console.error("clock widget smoke failed: " + message) - Qt.exit(1) - } - - QtObject { - id: sharedClock - property date date: new Date(2026, 6, 16, 13, 5, 0) - } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: 35 - property string fontFamily: "monospace" - property color foreground: "#f0f0f0" - property color barForeground: foreground - property color urgent: "#ff7a90" - property bool foregroundAnimationEnabled: false - property var clockService: sharedClock - property var visualTokens: ({ labelSize: 12 }) - - function setWidgetSetting(group, module, key, value) { - root.changedGroup = group - root.changedModule = module - root.changedKey = key - root.changedValue = value - return true - } - function run(value) { root.command = value } - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function registerClickTarget(_target) {} - function unregisterClickTarget(_target) {} - } - - Widgets.ClockWidget { - id: firstClock - bar: fakeBar - settings: ({ clock12h: false }) - } - - Widgets.ClockWidget { - id: secondClock - bar: fakeBar - settings: ({ clock12h: false }) - } - - Timer { - interval: 50 - running: true - onTriggered: { - if (firstClock.clock !== sharedClock || secondClock.clock !== sharedClock) - return root.fail("widgets do not share the root clock") - if (firstClock.timeText !== "13:05" || firstClock.implicitHeight !== 35) - return root.fail("V1 24-hour presentation") - if (firstClock.tooltipText.indexOf("2026") < 0) - return root.fail("date tooltip") - if (!firstClock.toggleFormat() - || root.changedGroup !== "G8" - || root.changedModule !== "omarchy.clock" - || root.changedKey !== "clock12h" - || root.changedValue !== true) - return root.fail("host-owned format persistence") - - firstClock.settings = ({ clock12h: true }) - if (firstClock.timeText !== "1:05 PM") - return root.fail("V1 12-hour presentation") - if (!firstClock.openTimezoneMenu() - || root.command !== "omarchy-menu-timezone") - return root.fail("Quattro timezone action") - - fakeBar.vertical = true - if (firstClock.implicitWidth !== 35) - return root.fail("vertical defensive geometry") - - console.log("clock widget smoke passed") - Qt.quit() - } - } -} diff --git a/tests/contract-regression.sh b/tests/contract-regression.sh index 083bda8..82212ee 100755 --- a/tests/contract-regression.sh +++ b/tests/contract-regression.sh @@ -19,6 +19,7 @@ command -v python3 >/dev/null 2>&1 || fail "python3 is required" "$repo_root/tests/baseline-contract-regression.sh" "$repo_root/tests/documentation-regression.py" +python3 "$repo_root/tests/production-boundary-regression.py" python3 "$repo_root/tests/test_package_release.py" python3 "$repo_root/tests/test_shibumi_manager.py" python3 "$repo_root/tests/test_shibumi_suite.py" @@ -47,6 +48,24 @@ fi [[ ! -e manifest.json ]] \ || fail "repository root must not masquerade as one native Omarchy plugin" +for retired_root_copy in \ + adapters/BluetoothBackendAdapter.qml \ + adapters/BluetoothDiscoveryGuard.qml \ + adapters/BluetoothModel.js \ + adapters/WorkspaceActions.qml \ + services/GpuTelemetry.qml \ + services/OpenCodeProvider.qml \ + scripts/opencode-usage \ + scripts/shibumi-gpu-probe; do + [[ ! -e $retired_root_copy ]] \ + || fail "plugin-canonical source regained a root copy: $retired_root_copy" +done +unexpected_root_sources=$(find adapters assets services widgets -type f 2>/dev/null \ + | grep -Ev '^(services/(HostWidgetResolver\.qml|PickerModel\.js|PowerModel\.js|PowerService\.qml|QuoteDefaults\.js|ReactorModel\.js|SystemTelemetry\.qml|ThemePalette\.qml|ThemePaletteModel\.js|ThermalTelemetry\.qml)|widgets/(IconText\.qml|PacmanWorkspaceMarker\.qml|PillSurface\.qml|ShibumiButtonGroup\.qml|ShibumiPanel\.qml|ShibumiPanelToolTip\.qml|ShibumiSlider\.qml))$' \ + || true) +[[ -z $unexpected_root_sources ]] || fail \ + "historical root implementation remains outside declared vendoring maps: $unexpected_root_sources" + jq -e ' .schemaVersion == 1 and .id == "hancore.shibumi.bar" and @@ -55,7 +74,7 @@ jq -e ' ' hancore.shibumi.bar/manifest.json >/dev/null \ || fail "invalid Shibumi bar manifest" -[[ -s assets/logo-tint.frag.qsb ]] \ +[[ -s hancore.shibumi.control-center/assets/logo-tint.frag.qsb ]] \ || fail "G1 exact flat-tint shader is missing" [[ ! -e hancore.shibumi.menu && ! -e menu ]] \ || fail "retired Shibumi App Menu source is still present" @@ -160,14 +179,16 @@ if rg -q 'Services\.(SystemTelemetry|GpuTelemetry|PowerService|StatusService|Wea fi rg -q 'Qt\.createComponent\(url, Component\.PreferSynchronous\)' services/HostWidgetResolver.qml \ || fail "host widget resolver does not load official manifest entry points" -[[ $(rg -l 'bar\.registeredWidgetComponent' widgets/{Audio,Status,Center}Widget.qml | wc -l) -eq 3 ]] \ +[[ $(rg -l 'bar\.registeredWidgetComponent' \ + hancore.shibumi.{audio,status,center}/BarWidget.qml | wc -l) -eq 3 ]] \ || fail "host-backed composites bypass the stable widget resolver" -rg -q 'registeredWidgetComponent\("omarchy\.network"\)' services/NetworkService.qml \ - || fail "root network owner bypasses the stable widget resolver" -rg -q 'registeredWidgetComponent\("omarchy\.monitor"\)' services/MonitorService.qml \ - || fail "root monitor owner bypasses the stable widget resolver" -if rg -q 'registeredWidgetComponent\("omarchy\.bluetooth"\)' services/BluetoothService.qml; then - fail "root Bluetooth owner must not instantiate the complete host widget" +rg -q 'registeredComponent\("omarchy\.network"\)' hancore.shibumi.network/Service.qml \ + || fail "shipped network owner bypasses the stable widget resolver" +rg -q 'registeredComponent\("omarchy\.monitor"\)' hancore.shibumi.brightness/Service.qml \ + || fail "shipped monitor owner bypasses the stable widget resolver" +if rg -q 'registeredWidgetComponent\("omarchy\.bluetooth"\)' \ + hancore.shibumi.bluetooth/Service.qml; then + fail "shipped Bluetooth owner must not instantiate the complete host widget" fi if rg -U -q 'visible: root\.anchorIndex < 0\n[[:space:]]*bar: root\.bar\n[[:space:]]*region: "center"\n[[:space:]]*entries: root\.entries' core/CenterSection.qml; then fail "inactive center fallback must not instantiate duplicate widgets" @@ -383,7 +404,7 @@ rg -q 'screenName: root\.screenName' core/GroupSlot.qml core/BarSection.qml \ rg -Fq 'screenName: root ? root.screenName : ""' \ styles/shibumi/GroupSection.qml \ || fail "group renderer does not propagate guarded output identity" -rg -q 'function childPanelWidget\(pluginId\)' widgets/CenterWidget.qml \ +rg -q 'function childPanelWidget\(pluginId\)' hancore.shibumi.center/BarWidget.qml \ || fail "center composite does not expose nested weather routing" rg -q 'G3: \["hancore.shibumi.status"\]' core/GroupRegistry.js \ || fail "G3 is not owned by the Shibumi status composite" @@ -393,125 +414,125 @@ for consumed_alias in omarchy.audio omarchy.clock omarchy.network omarchy.power; done rg -Fq 'entry.shibumiModule === true' core/GroupRegistry.js \ || fail "explicit Quattro modules cannot opt into the Shibumi bar" -rg -q 'hancore\.shibumi\.status' widgets/WidgetRegistry.qml \ +rg -q 'hancore\.shibumi\.status' contracts/plugin-suite-v1.json \ || fail "Shibumi status composite is not registered" -rg -q 'hancore\.shibumi\.update-center' widgets/StatusWidget.qml \ +rg -q 'hancore\.shibumi\.update-center' hancore.shibumi.status/BarWidget.qml \ || fail "G3 status presentation does not own the Shibumi update center" -if rg -q 'omarchy\.system-update' widgets/StatusWidget.qml; then +if rg -q 'omarchy\.system-update' hancore.shibumi.status/BarWidget.qml; then fail "G3 status presentation must not own the Omarchy update widget" fi -rg -q 'omarchy\.system-update' widgets/CenterWidget.qml \ +rg -q 'omarchy\.system-update' hancore.shibumi.center/BarWidget.qml \ || fail "G8 center presentation does not own the Omarchy update widget" -rg -q 'StatusIndicators' widgets/CenterWidget.qml \ +rg -q 'StatusIndicators' hancore.shibumi.center/BarWidget.qml \ || fail "G8 does not render the V1 status indicator facade" -rg -q 'SystemUpdateWidget' widgets/CenterWidget.qml \ +rg -q 'SystemUpdateWidget' hancore.shibumi.center/BarWidget.qml \ || fail "G8 does not render the V1 Omarchy-update facade" -rg -q '^ width: implicitWidth$' widgets/SystemUpdateWidget.qml \ +rg -q '^ width: implicitWidth$' hancore.shibumi.center/SystemUpdateWidget.qml \ || fail "G8 update indicator does not expose its visual width as a hit target" -rg -q '^ height: implicitHeight$' widgets/SystemUpdateWidget.qml \ +rg -q '^ height: implicitHeight$' hancore.shibumi.center/SystemUpdateWidget.qml \ || fail "G8 update indicator does not expose its visual height as a hit target" -if rg -q 'Process \{|Timer \{|IpcHandler \{' widgets/SystemUpdateWidget.qml; then +if rg -q 'Process \{|Timer \{|IpcHandler \{' hancore.shibumi.center/SystemUpdateWidget.qml; then fail "Omarchy-update facade duplicates the official backend owner" fi -rg -q 'onTabRequested' widgets/CalendarPanel.qml \ +rg -q 'onTabRequested' hancore.shibumi.center/CalendarPanel.qml \ || fail "G8 calendar is missing sibling-panel keyboard routing" -rg -q 'ShibumiPanelToolTip \{' widgets/CalendarPanel.qml \ +rg -q 'ShibumiPanelToolTip \{' hancore.shibumi.center/CalendarPanel.qml \ || fail "G8 calendar navigation bypasses the Shibumi tooltip" -if rg -q 'registeredSource\("omarchy\.indicators"\)' widgets/CenterWidget.qml; then +if rg -q 'registeredSource\("omarchy\.indicators"\)' hancore.shibumi.center/BarWidget.qml; then fail "G8 must not instantiate Quattro's stock indicator presentation" fi rg -q '"service": "Service.qml"' hancore.shibumi.status/manifest.json \ || fail "status backend is not process-wide" rg -q 'WeatherService \{' hancore.shibumi.center/Service.qml \ || fail "weather backend is not process-wide" -rg -q 'WeatherWidget' widgets/CenterWidget.qml \ +rg -q 'WeatherWidget' hancore.shibumi.center/BarWidget.qml \ || fail "G8 does not render the V1 weather facade" if rg -q 'SystemTray\.items|NotificationServer|Process \{|FileView \{' \ - widgets/StatusWidget.qml widgets/TrayStatusView.qml \ - widgets/NotificationStatusView.qml widgets/TrayDrawerPanel.qml; then + hancore.shibumi.status/BarWidget.qml hancore.shibumi.status/TrayStatusView.qml \ + hancore.shibumi.status/NotificationStatusView.qml hancore.shibumi.status/TrayDrawerPanel.qml; then fail "status composite must not duplicate official service or polling ownership" fi -[[ $(rg -c 'Timer \{' widgets/StatusWidget.qml) -eq 1 ]] \ +[[ $(rg -c 'Timer \{' hancore.shibumi.status/BarWidget.qml) -eq 1 ]] \ || fail "status composite must own exactly one lifecycle timer" rg -U -q 'Timer \{\n[[:space:]]*id: childSyncTimer\n[[:space:]]*interval: 0\n' \ - widgets/StatusWidget.qml \ + hancore.shibumi.status/BarWidget.qml \ || fail "status lifecycle timer must be the zero-delay child loader sync" -if rg -q 'Timer \{' widgets/TrayStatusView.qml \ - widgets/NotificationStatusView.qml widgets/TrayDrawerPanel.qml; then +if rg -q 'Timer \{' hancore.shibumi.status/TrayStatusView.qml \ + hancore.shibumi.status/NotificationStatusView.qml hancore.shibumi.status/TrayDrawerPanel.qml; then fail "status child presentations must not own timers" fi -rg -q 'trayBackend: root\.trayWidget' widgets/StatusWidget.qml \ +rg -q 'trayBackend: root\.trayWidget' hancore.shibumi.status/BarWidget.qml \ || fail "G3 tray presentation is not bound to the official tray owner" -rg -q 'notificationService: root\.notificationService' widgets/StatusWidget.qml \ +rg -q 'notificationService: root\.notificationService' hancore.shibumi.status/BarWidget.qml \ || fail "G3 notification presentation is not bound to the official service" rg -q 'G6: \["hancore.shibumi.audio"\]' core/GroupRegistry.js \ || fail "G6 is not owned by the Shibumi audio composite" -rg -q 'hancore\.shibumi\.audio' widgets/WidgetRegistry.qml \ +rg -q 'hancore\.shibumi\.audio' contracts/plugin-suite-v1.json \ || fail "Shibumi audio composite is not registered" -rg -q 'Adapters\.AudioPanelBridge' widgets/AudioWidget.qml \ +rg -q 'AudioPanelBridge' hancore.shibumi.audio/BarWidget.qml \ || fail "audio view does not preserve the official panel owner" -rg -q 'popupSource: Qt\.resolvedUrl\("AudioPanel\.qml"\)' widgets/AudioWidget.qml \ +rg -q 'popupSource: Qt\.resolvedUrl\("AudioPanel\.qml"\)' hancore.shibumi.audio/BarWidget.qml \ || fail "audio widget does not lazy-load the Shibumi mixer panel" rg -q 'return String\(pluginId \|\| ""\) === "omarchy\.audio" \? root : null' \ - widgets/AudioWidget.qml \ + hancore.shibumi.audio/BarWidget.qml \ || fail "official audio routing is not redirected to the Shibumi owner" -rg -q 'manageIpc: false' widgets/AudioWidget.qml \ +rg -q 'manageIpc: false' hancore.shibumi.audio/BarWidget.qml \ || fail "audio aliases must use screen-aware host routing, not duplicate IPC handlers" -if rg -q 'Quickshell\.Services\.Pipewire|Pipewire\.' widgets/AudioWidget.qml \ - adapters/AudioPanelBridge.qml; then +if rg -q 'Quickshell\.Services\.Pipewire|Pipewire\.' hancore.shibumi.audio/BarWidget.qml \ + hancore.shibumi.audio/AudioPanelBridge.qml; then fail "Shibumi audio presentation must not create a second PipeWire owner" fi -if rg -q 'Process \{|FileView \{' widgets/AudioWidget.qml \ - adapters/AudioPanelBridge.qml; then +if rg -q 'Process \{|FileView \{' hancore.shibumi.audio/BarWidget.qml \ + hancore.shibumi.audio/AudioPanelBridge.qml; then fail "audio presentation bridge must remain event-driven and worker-free" fi -rg -q 'panelLoader\.active = false' adapters/AudioPanelBridge.qml \ +rg -q 'panelLoader\.active = false' hancore.shibumi.audio/AudioPanelBridge.qml \ || fail "audio backend panel is not unloaded before its host facade" -if rg -q 'panel\.bar = null' adapters/AudioPanelBridge.qml; then +if rg -q 'panel\.bar = null' hancore.shibumi.audio/AudioPanelBridge.qml; then fail "audio teardown invalidates the host facade before panel destruction" fi -[[ $(rg -c 'Timer \{' widgets/AudioWidget.qml) -eq 2 ]] \ +[[ $(rg -c 'Timer \{' hancore.shibumi.audio/BarWidget.qml) -eq 2 ]] \ || fail "audio widget must own only the bounded wheel commit/settle timers" rg -U -q 'Timer \{\n[[:space:]]*id: wheelCommitTimer\n[[:space:]]*interval: 70\n' \ - widgets/AudioWidget.qml \ + hancore.shibumi.audio/BarWidget.qml \ || fail "audio wheel commit timer contract changed" rg -U -q 'Timer \{\n[[:space:]]*id: wheelSettleTimer\n[[:space:]]*interval: 300\n' \ - widgets/AudioWidget.qml \ + hancore.shibumi.audio/BarWidget.qml \ || fail "audio wheel settle timer contract changed" if rg -U -q 'Timer \{([^}]|\n)*(repeat:[[:space:]]*true|running:[[:space:]]*true)' \ - widgets/AudioWidget.qml; then + hancore.shibumi.audio/BarWidget.qml; then fail "audio wheel timers must remain dormant, non-repeating interaction timers" fi -[[ $(rg -c 'PwNodePeakMonitor \{' widgets/AudioPanel.qml) -eq 1 ]] \ +[[ $(rg -c 'PwNodePeakMonitor \{' hancore.shibumi.audio/AudioPanel.qml) -eq 1 ]] \ || fail "audio panel must own exactly one lifecycle-bound microphone meter" -rg -q 'enabled: panel\.open' widgets/AudioPanel.qml \ +rg -q 'enabled: panel\.open' hancore.shibumi.audio/AudioPanel.qml \ || fail "microphone meter is not bounded to the open mixer lifecycle" -if rg -q 'Pipewire\.|Process \{|FileView \{' widgets/AudioPanel.qml; then +if rg -q 'Pipewire\.|Process \{|FileView \{' hancore.shibumi.audio/AudioPanel.qml; then fail "Shibumi audio panel duplicates Quattro audio ownership or shell workers" fi -rg -q 'displaySinks = \[\]' widgets/AudioPanel.qml \ +rg -q 'displaySinks = \[\]' hancore.shibumi.audio/AudioPanel.qml \ || fail "audio panel does not release sink rows on close" -rg -q 'displaySources = \[\]' widgets/AudioPanel.qml \ +rg -q 'displaySources = \[\]' hancore.shibumi.audio/AudioPanel.qml \ || fail "audio panel does not release source rows on close" -rg -q 'displayStreams = \[\]' widgets/AudioPanel.qml \ +rg -q 'displayStreams = \[\]' hancore.shibumi.audio/AudioPanel.qml \ || fail "audio panel does not release stream rows on close" rg -q 'G9: \["hancore.shibumi.media"\]' core/GroupRegistry.js \ || fail "G9 is not owned by the Shibumi media presentation" -rg -q 'hancore\.shibumi\.media' widgets/WidgetRegistry.qml \ +rg -q 'hancore\.shibumi\.media' contracts/plugin-suite-v1.json \ || fail "Shibumi media presentation is not registered" -rg -q 'firstPartyServiceFor\("omarchy\.media"\)' widgets/MediaWidget.qml \ +rg -q 'firstPartyServiceFor\("omarchy\.media"\)' hancore.shibumi.media/BarWidget.qml \ || fail "media presentation does not reuse the official service" if rg -q 'Quickshell\.Services\.(Mpris|Pipewire)|Mpris\.|Pipewire\.' \ - widgets/MediaWidget.qml widgets/MediaPanel.qml widgets/MediaPulse.qml \ - widgets/MediaSpectrum.qml widgets/MediaMuse.qml \ + hancore.shibumi.media/BarWidget.qml hancore.shibumi.media/MediaPanel.qml hancore.shibumi.media/MediaPulse.qml \ + hancore.shibumi.media/MediaSpectrum.qml hancore.shibumi.media/MediaMuse.qml \ hancore.shibumi.media/Service.qml; then fail "Shibumi media presentation must not create a second media owner" fi -if rg -q 'Process \{|Timer \{|FileView \{' widgets/MediaWidget.qml \ - widgets/MediaPulse.qml widgets/MediaSpectrum.qml widgets/MediaMuse.qml; then +if rg -q 'Process \{|Timer \{|FileView \{' hancore.shibumi.media/BarWidget.qml \ + hancore.shibumi.media/MediaPulse.qml hancore.shibumi.media/MediaSpectrum.qml hancore.shibumi.media/MediaMuse.qml; then fail "closed media bar presentation must remain worker-free" fi -rg -q 'serviceFor\("hancore\.shibumi\.media"\)' widgets/MediaWidget.qml \ +rg -q 'serviceFor\("hancore\.shibumi\.media"\)' hancore.shibumi.media/BarWidget.qml \ || fail "media presentation does not resolve the process-wide spectrum service" [[ $(rg -c 'Process \{' hancore.shibumi.media/Service.qml) -eq 2 ]] \ || fail "media spectrum service must own one probe and one Cava process" @@ -526,13 +547,13 @@ rg -q 'target: "shibumi-media-spectrum"' hancore.shibumi.media/Service.qml \ || fail "media spectrum service lacks read-only runtime diagnostics" rg -q 'bars = 24' hancore.shibumi.media/Service.qml \ || fail "media spectrum service does not preserve the V1 band count" -if rg -q 'Process \{|FileView \{' widgets/MediaPanel.qml \ +if rg -q 'Process \{|FileView \{' hancore.shibumi.media/MediaPanel.qml \ hancore.shibumi.media/MediaPanel.qml; then fail "screen-local media panels must not own spectrum workers" fi rg -q 'G10: \["hancore.shibumi.quick-access"\]' core/GroupRegistry.js \ || fail "G10 is not owned by the Shibumi quick-access presentation" -rg -q 'hancore\.shibumi\.quick-access' widgets/WidgetRegistry.qml \ +rg -q 'hancore\.shibumi\.quick-access' contracts/plugin-suite-v1.json \ || fail "Shibumi quick-access presentation is not registered" rg -q 'IdleInhibitor \{' hancore.shibumi.quick-access/BarWidget.qml \ || fail "G10 idle inhibitor is not attached to the output surface" @@ -545,9 +566,9 @@ rg -q 'Hyprland\.focusedMonitor' hancore.shibumi.quick-access/Service.qml \ || fail "G10 IPC picker does not resolve the focused output" rg -q 'function screenForName\(value\)' Bar.qml \ || fail "G10 IPC picker cannot resolve host output objects" -if rg -q 'Commons\.Style\.font\.size\.' widgets/PickerOverlay.qml \ - widgets/PickerImage.qml widgets/TanzakuPickerView.qml \ - widgets/HearthstonePickerView.qml; then +if rg -q 'Commons\.Style\.font\.size\.' hancore.shibumi.quick-access/PickerOverlay.qml \ + hancore.shibumi.quick-access/PickerImage.qml hancore.shibumi.quick-access/TanzakuPickerView.qml \ + hancore.shibumi.quick-access/HearthstonePickerView.qml; then fail "G10 picker uses V1-only font token paths" fi rg -q 'Commons\.Util\.fileUrl\(entry\.thumbnailPath\)' \ @@ -555,14 +576,14 @@ rg -q 'Commons\.Util\.fileUrl\(entry\.thumbnailPath\)' \ || fail "G10 thumbnail URLs do not use the host-safe file URL helper" rg -q 'file -Lb --mime-type' hancore.shibumi.quick-access/Service.qml \ || fail "G10 screenshot copy hard-codes an incorrect image MIME type" -for picker_view in widgets/TanzakuPickerView.qml \ - widgets/HearthstonePickerView.qml; do +for picker_view in hancore.shibumi.quick-access/TanzakuPickerView.qml \ + hancore.shibumi.quick-access/HearthstonePickerView.qml; do if rg -q 'Process \{|Timer \{|FileView \{' "$picker_view"; then fail "picker presentation owns controller work: $picker_view" fi done rg -q 'readonly property bool spectrumRequested: open && active && spectrumEnabled' \ - widgets/MediaPanel.qml \ + hancore.shibumi.media/MediaPanel.qml \ || fail "media spectrum worker is not panel-lifecycle gated" rg -q 'command: \["cava", "-p", "/dev/stdin"\]' \ hancore.shibumi.media/Service.qml \ @@ -574,85 +595,81 @@ if rg -q 'mktemp|/tmp/|<\(printf' hancore.shibumi.media/Service.qml; then fi rg -q 'G11: \["hancore.shibumi.network"\]' core/GroupRegistry.js \ || fail "G11 is not owned by the Shibumi network presentation" -rg -q 'hancore\.shibumi\.network' widgets/WidgetRegistry.qml \ +rg -q 'hancore\.shibumi\.network' contracts/plugin-suite-v1.json \ || fail "Shibumi network presentation is not registered" [[ $(rg -c 'NetworkPanelBridge \{' hancore.shibumi.network/Service.qml) -eq 1 ]] \ || fail "network backend must be process-wide" -rg -q 'Adapters\.NetworkPanelBridge' services/NetworkService.qml \ - || fail "root network service does not preserve the official panel owner" -if rg -q 'Adapters\.NetworkPanelBridge|Quickshell\.Networking|Networking\.' \ - widgets/NetworkWidget.qml widgets/NetworkPanel.qml; then +rg -q 'NetworkPanelBridge' hancore.shibumi.network/Service.qml \ + || fail "process-wide network service does not preserve the official panel owner" +if rg -q 'NetworkPanelBridge|Quickshell\.Networking|Networking\.' \ + hancore.shibumi.network/BarWidget.qml hancore.shibumi.network/NetworkPanel.qml; then fail "screen-local network presentation must not own NetworkManager" fi -if rg -q 'Process \{|FileView \{' widgets/NetworkWidget.qml \ - widgets/NetworkPanel.qml adapters/NetworkPanelBridge.qml; then +if rg -q 'Process \{|FileView \{' hancore.shibumi.network/BarWidget.qml \ + hancore.shibumi.network/NetworkPanel.qml hancore.shibumi.network/NetworkPanelBridge.qml; then fail "screen-local network presentation must not own backend workers" fi -if rg -q 'Timer \{' widgets/NetworkWidget.qml adapters/NetworkPanelBridge.qml; then - fail "network widget and backend bridge must remain event-driven" -fi -[[ $(rg -c 'Adapters\.NetworkPanelBridge \{' services/NetworkService.qml) -eq 1 ]] \ - || fail "root network service must own exactly one official backend" -rg -q 'property var sessionOwners: \[\]' services/NetworkService.qml \ +[[ $(rg -c 'NetworkPanelBridge \{' hancore.shibumi.network/Service.qml) -eq 1 ]] \ + || fail "process-wide network service must own exactly one official backend" +rg -q 'property var sessionOwners: \[\]' hancore.shibumi.network/Service.qml \ || fail "network service lacks multi-output session accounting" -rg -q 'profileList\.running = false' services/NetworkService.qml \ +rg -q 'profileList\.running = false' hancore.shibumi.network/Service.qml \ || fail "network service does not stop saved-profile discovery on final close" -rg -q 'detailsProc\.running = false' services/NetworkService.qml \ +rg -q 'detailsProc\.running = false' hancore.shibumi.network/Service.qml \ || fail "network service does not stop detail sampling on final close" -rg -q 'property var scannerDevice: null' services/NetworkService.qml \ +rg -q 'property var scannerDevice: null' hancore.shibumi.network/Service.qml \ || fail "network service does not track its scanner lease" rg -q 'if \(scannerDevice && scannerDevice !== nextDevice\)' \ - services/NetworkService.qml \ + hancore.shibumi.network/Service.qml \ || fail "network service does not release replaced scanner devices" -rg -q 'releaseWifiScanner\(\)' services/NetworkService.qml \ +rg -q 'releaseWifiScanner\(\)' hancore.shibumi.network/Service.qml \ || fail "network scanner is not released outside panel sessions" -if rg -q 'scannerEnabled' adapters/NetworkPanelBridge.qml; then - fail "network bridge competes with the root scanner owner" +if rg -q 'scannerEnabled' hancore.shibumi.network/NetworkPanelBridge.qml; then + fail "network bridge competes with the process-wide scanner owner" fi rg -q 'command: \["omarchy-network-status", "--verbose"\]' \ - services/NetworkService.qml \ - || fail "network panel details do not use the shared root lifecycle worker" + hancore.shibumi.network/Service.qml \ + || fail "network panel details do not use the shared lifecycle worker" if rg -U -q 'id: detailsPoll[\s\S]{0,240}root\.refresh' \ - services/NetworkService.qml; then + hancore.shibumi.network/Service.qml; then fail "network details poll must not restart DNS/profile refresh work" fi -rg -q 'childPanelWidget\("omarchy\.network"\)' tests/network-widget-smoke.qml \ +rg -q 'childPanelWidget\("omarchy\.network"\)' tests/network-plugin-smoke.qml \ || fail "network alias routing is not regression-tested" rg -q 'G13: \["hancore.shibumi.brightness"\]' core/GroupRegistry.js \ || fail "G13 is not owned by the Shibumi brightness presentation" -rg -q 'hancore\.shibumi\.brightness' widgets/WidgetRegistry.qml \ +rg -q 'hancore\.shibumi\.brightness' contracts/plugin-suite-v1.json \ || fail "Shibumi brightness presentation is not registered" [[ $(rg -c 'MonitorPanelBridge \{' hancore.shibumi.brightness/Service.qml) -eq 1 ]] \ - || fail "brightness/display state must have one root monitor owner" -[[ $(rg -c 'Adapters\.MonitorPanelBridge \{' services/MonitorService.qml) -eq 1 ]] \ - || fail "root monitor service must own exactly one official backend" -rg -q 'bar\.monitorService' widgets/BrightnessWidget.qml \ + || fail "brightness service must own exactly one official backend" +rg -q 'serviceFor\("hancore\.shibumi\.brightness"\)' \ + hancore.shibumi.brightness/BarWidget.qml \ || fail "brightness view does not consume the shared monitor owner" if rg -q 'Quickshell\.Services\.UPower|UPower\.|Quickshell\.Io' \ - widgets/BrightnessWidget.qml widgets/BrightnessPanel.qml \ - adapters/MonitorPanelBridge.qml services/MonitorService.qml; then + hancore.shibumi.brightness/BarWidget.qml hancore.shibumi.brightness/BrightnessPanel.qml \ + hancore.shibumi.brightness/MonitorPanelBridge.qml hancore.shibumi.brightness/Service.qml; then fail "Shibumi brightness presentation must not create a second monitor owner" fi -if rg -q 'Process \{|Timer \{|FileView \{' widgets/BrightnessWidget.qml \ - widgets/BrightnessPanel.qml services/MonitorService.qml \ - adapters/MonitorPanelBridge.qml; then +if rg -q 'Process \{|Timer \{|FileView \{' hancore.shibumi.brightness/BarWidget.qml \ + hancore.shibumi.brightness/BrightnessPanel.qml hancore.shibumi.brightness/Service.qml \ + hancore.shibumi.brightness/MonitorPanelBridge.qml; then fail "brightness presentation and monitor adapter must remain worker-free" fi -if rg -q '^[[:space:]]*selected:' widgets/BrightnessPanel.qml; then +if rg -q '^[[:space:]]*selected:' hancore.shibumi.brightness/BrightnessPanel.qml; then fail "brightness panel uses Button-only selected state on CursorSurface" fi -rg -q 'childPanelWidget\("omarchy\.monitor"\)' tests/brightness-widget-smoke.qml \ +rg -q 'childPanelWidget\("omarchy\.monitor"\)' tests/brightness-plugin-smoke.qml \ || fail "monitor alias routing is not regression-tested" rg -q 'G12: \["hancore.shibumi.battery"\]' core/GroupRegistry.js \ || fail "G12 is not owned by the Shibumi battery presentation" rg -q 'G14: \["hancore.shibumi.power-profile"\]' core/GroupRegistry.js \ || fail "G14 is not owned by the Shibumi power-profile presentation" for power_widget in hancore.shibumi.battery hancore.shibumi.power-profile; do - rg -q "$power_widget" widgets/WidgetRegistry.qml \ + rg -q "$power_widget" contracts/plugin-suite-v1.json \ || fail "Shibumi power presentation is not registered: $power_widget" done rg -q '"service": "Service.qml"' hancore.shibumi.power-state/manifest.json \ - || fail "battery/profile state must have one root power owner" + || fail "battery/profile state must have one process-wide power owner" rg -q 'Quickshell.Services.UPower' services/PowerService.qml \ || fail "power owner does not consume the event-driven UPower singleton" rg -q 'command: \["omarchy-powerprofiles-list", "--active-state"\]' \ @@ -670,47 +687,53 @@ rg -q 'omarchy-battery-status --shell' \ services/PowerService.qml \ || fail "power owner does not use the Quattro battery detail contract" if rg -q 'Quickshell\.Services\.UPower|Quickshell\.Io|Process \{|Timer \{|FileView \{' \ - widgets/BatteryWidget.qml widgets/BatteryPanel.qml \ - widgets/PowerProfileWidget.qml widgets/PowerProfilePanel.qml; then + hancore.shibumi.battery/BarWidget.qml hancore.shibumi.battery/BatteryPanel.qml \ + hancore.shibumi.power-profile/BarWidget.qml hancore.shibumi.power-profile/PowerProfilePanel.qml; then fail "power views must not duplicate UPower, worker, or polling ownership" fi -rg -q 'acquireBatteryDetails' widgets/BatteryWidget.qml \ +rg -q 'acquireBatteryDetails' hancore.shibumi.battery/BarWidget.qml \ || fail "battery detail process is not panel-lifecycle gated" -rg -q 'releaseBatteryDetails' widgets/BatteryWidget.qml \ +rg -q 'releaseBatteryDetails' hancore.shibumi.battery/BarWidget.qml \ || fail "battery detail process lease is not released" -for monitor_widget in Cpu Memory Battery; do +for monitor_plugin in cpu memory battery; do rg -q 'bar\.run\("omarchy-launch-or-focus-tui btop"\)' \ - "widgets/${monitor_widget}Widget.qml" \ - || fail "$monitor_widget does not use the Quattro TUI launcher" + "hancore.shibumi.${monitor_plugin}/BarWidget.qml" \ + || fail "$monitor_plugin does not use the Quattro TUI launcher" + panel_name="$(tr '[:lower:]' '[:upper:]' \ + <<<"${monitor_plugin:0:1}")${monitor_plugin:1}Panel.qml" rg -q 'panel\.ownerWidget\.openSystemMonitor\(\)' \ - "widgets/${monitor_widget}Panel.qml" \ - || fail "$monitor_widget panel bypasses its owner action" + "hancore.shibumi.${monitor_plugin}/${panel_name}" \ + || fail "$monitor_plugin panel bypasses its owner action" done -rg -q 'acquireProfiles' widgets/PowerProfileWidget.qml \ +rg -q 'acquireProfiles' hancore.shibumi.power-profile/BarWidget.qml \ || fail "profile state does not use the shared service lease" -rg -q 'releaseProfiles' widgets/PowerProfileWidget.qml \ +rg -q 'releaseProfiles' hancore.shibumi.power-profile/BarWidget.qml \ || fail "profile state lease is not released" rg -q 'G15: \["hancore.shibumi.bluetooth"\]' core/GroupRegistry.js \ || fail "G15 is not owned by the Shibumi bluetooth presentation" -rg -q 'hancore\.shibumi\.bluetooth' widgets/WidgetRegistry.qml \ - || fail "Shibumi bluetooth presentation is not registered" -[[ $(rg -c 'BluetoothBackendAdapter \{' hancore.shibumi.bluetooth/Service.qml) -eq 1 ]] \ - || fail "Bluetooth state must have one root owner" -[[ $(rg -c 'Adapters\.BluetoothBackendAdapter \{' services/BluetoothService.qml) -eq 1 ]] \ - || fail "root Bluetooth service must own exactly one native backend" -rg -q 'bar\.bluetoothService' widgets/BluetoothWidget.qml \ - || fail "Bluetooth view does not consume the shared owner" -rg -q 'property var sessionOwners: \[\]' services/BluetoothService.qml \ +jq -e ' + .id == "hancore.shibumi.bluetooth" and + .entryPoints.barWidget == "BarWidget.qml" and + .entryPoints.service == "Service.qml" +' hancore.shibumi.bluetooth/manifest.json >/dev/null \ + || fail "Shibumi Bluetooth plugin entry points are not declared" +bluetooth_service=hancore.shibumi.bluetooth/Service.qml +bluetooth_adapter=hancore.shibumi.bluetooth/BluetoothBackendAdapter.qml +bluetooth_widget=hancore.shibumi.bluetooth/BarWidget.qml +bluetooth_panel=hancore.shibumi.bluetooth/BluetoothPanel.qml +[[ $(rg -c 'BluetoothBackendAdapter \{' "$bluetooth_service") -eq 1 ]] \ + || fail "Bluetooth state must have one shipped native owner" +rg -q 'serviceFor\("hancore\.shibumi\.bluetooth"\)' "$bluetooth_widget" \ + || fail "Bluetooth view does not resolve the shipped service" +rg -q 'property var sessionOwners: \[\]' "$bluetooth_service" \ || fail "Bluetooth service lacks multi-output session accounting" -rg -q 'target: "omarchy\.bluetooth"' services/BluetoothService.qml \ +rg -q 'target: "omarchy\.bluetooth"' "$bluetooth_service" \ || fail "Bluetooth service does not own the single legacy IPC target" -rg -q 'bar\.hideBarWidget\("omarchy\.bluetooth"\)' services/BluetoothService.qml \ +rg -q 'bar\.hideBarWidget\("omarchy\.bluetooth"\)' "$bluetooth_service" \ || fail "Bluetooth legacy close/hide is not routed to the local panel" -rg -q 'adapter\.stopDiscovery\(\)' services/BluetoothService.qml \ +rg -q 'adapter\.stopDiscovery\(\)' "$bluetooth_service" \ || fail "Bluetooth discovery is not stopped after the final panel closes" -for bluetooth_adapter in \ - adapters/BluetoothBackendAdapter.qml \ - hancore.shibumi.bluetooth/BluetoothBackendAdapter.qml; do +for bluetooth_adapter in "$bluetooth_adapter"; do rg -q '^import Quickshell\.Bluetooth$' "$bluetooth_adapter" \ || fail "$bluetooth_adapter does not own the native BlueZ model" rg -q '^import Quickshell\.Services\.Pipewire$' "$bluetooth_adapter" \ @@ -740,58 +763,46 @@ for bluetooth_adapter in \ fail "$bluetooth_adapter still owns IPC or loads a foreign UI component" fi done -cmp -s adapters/BluetoothBackendAdapter.qml \ - hancore.shibumi.bluetooth/BluetoothBackendAdapter.qml \ - || fail "root and plugin Bluetooth adapters drifted" -cmp -s adapters/BluetoothModel.js hancore.shibumi.bluetooth/BluetoothModel.js \ - || fail "root and plugin Bluetooth models drifted" -cmp -s adapters/BluetoothDiscoveryGuard.qml \ - hancore.shibumi.bluetooth/BluetoothDiscoveryGuard.qml \ - || fail "root and plugin Bluetooth discovery guards drifted" -for bluetooth_service in \ - services/BluetoothService.qml \ - hancore.shibumi.bluetooth/Service.qml; do - rg -U -q 'id: discoveryRetry[^}]*repeat: true[^}]*running: root\.sessionCount > 0 && root\.adapterAvailable[^}]*root\.radioEnabled && !root\.discovering' \ - "$bluetooth_service" \ - || fail "$bluetooth_service does not bound discovery retries to an open session" -done +rg -U -q 'id: discoveryRetry[^}]*repeat: true[^}]*running: root\.sessionCount > 0 && root\.adapterAvailable[^}]*root\.radioEnabled && !root\.discovering' \ + "$bluetooth_service" \ + || fail "Bluetooth service does not bound discovery retries to an open session" if rg -q 'registeredWidget|registeredSource|registeredComponent|panelSource|panelComponent|Loader \{' \ - services/BluetoothService.qml hancore.shibumi.bluetooth/Service.qml; then + "$bluetooth_service"; then fail "Bluetooth service still resolves or loads the complete Omarchy panel" fi if rg -q 'Quickshell\.Bluetooth|Quickshell\.Services\.Pipewire|Bluetooth\.|Pipewire\.' \ - widgets/BluetoothWidget.qml widgets/BluetoothPanel.qml \ - services/BluetoothService.qml; then + "$bluetooth_widget" "$bluetooth_panel"; then fail "Bluetooth presentation bypasses the process-wide native adapter" fi -if rg -q 'Process \{|Timer \{|FileView \{' widgets/BluetoothWidget.qml \ - widgets/BluetoothPanel.qml; then +if rg -q 'Process \{|Timer \{|FileView \{' \ + "$bluetooth_widget" "$bluetooth_panel"; then fail "Bluetooth presentation must remain worker-free" fi -if rg -q 'Process \{|FileView \{' services/BluetoothService.qml; then - fail "Bluetooth service facade must remain process- and file-worker-free" +if rg -q 'Process \{|FileView \{' "$bluetooth_service" "$bluetooth_adapter"; then + fail "Bluetooth native owner uses a worker instead of native APIs" fi -if rg -q 'Process \{|FileView \{' adapters/BluetoothBackendAdapter.qml \ - hancore.shibumi.bluetooth/BluetoothBackendAdapter.qml; then - fail "Bluetooth native adapter uses a worker instead of native APIs" -fi -rg -q 'childPanelWidget\("omarchy\.bluetooth"\)' tests/bluetooth-widget-smoke.qml \ - || fail "Bluetooth alias routing is not regression-tested" +rg -q 'childPanelWidget\("omarchy\.bluetooth"\)' tests/bluetooth-plugin-smoke.qml \ + || fail "Bluetooth alias routing is not regression-tested against shipped code" [[ $(rg -c 'SystemTelemetry \{' hancore.shibumi.telemetry/Service.qml) -eq 1 ]] \ - || fail "system telemetry must have one root owner" + || fail "system telemetry must have one process-wide owner" if rg -q 'Process \{' services/SystemTelemetry.qml; then fail "system telemetry must read procfs without child processes" fi rg -q '"service": "WorkspaceService.qml"' hancore.shibumi.workspaces/manifest.json \ - || fail "workspace state must have exactly one root owner" + || fail "workspace state must have exactly one shipped owner" +workspace_service=hancore.shibumi.workspaces/WorkspaceService.qml +workspace_actions=hancore.shibumi.workspaces/WorkspaceActions.qml +workspace_widget=hancore.shibumi.workspaces/BarWidget.qml +workspace_panel=hancore.shibumi.workspaces/WorkspacePanel.qml +workspace_panel_content=hancore.shibumi.workspaces/WorkspacePanelContent.qml [[ $(rg -c 'ClockService \{' hancore.shibumi.center/Service.qml) -eq 1 ]] \ - || fail "clock state must have exactly one root owner" -if rg -q 'Process \{|Timer \{|FileView \{' services/ClockService.qml \ - widgets/ClockWidget.qml; then + || fail "clock state must have exactly one process-wide owner" +if rg -q 'Process \{|Timer \{|FileView \{' hancore.shibumi.center/ClockService.qml \ + hancore.shibumi.center/ClockWidget.qml; then fail "clock slice must use the shared event-driven SystemClock" fi -rg -q 'Ui\.WidgetButton \{' widgets/ClockWidget.qml \ +rg -q 'Ui\.WidgetButton \{' hancore.shibumi.center/ClockWidget.qml \ || fail "clock interaction is not registered for overlay click forwarding" [[ $(rg -c 'WorkspaceActions \{' hancore.shibumi.workspaces/WorkspaceService.qml) -eq 1 ]] \ || fail "workspace actions must have exactly one root adapter" @@ -814,42 +825,44 @@ if rg -q 'Process \{|Timer \{|FileView \{' core/LayoutController.qml \ core/DragSession.qml; then fail "layout and drag core must remain event-driven and worker-free" fi -rg -q 'omarchy\.workspaces' widgets/WidgetRegistry.qml \ - || fail "Shibumi workspace widget does not override the host workspace slot" -rg -q '^Ui\.Panel \{' widgets/WorkspaceWidget.qml \ - || fail "workspace widget does not implement the shared panel contract" -rg -q '^ShibumiPanel \{' widgets/WorkspacePanel.qml \ +rg -q 'serviceFor\("hancore\.shibumi\.workspaces"\)' "$workspace_widget" \ + || fail "workspace widget does not resolve the shipped service" +rg -q '^ShibumiPanel \{' "$workspace_panel" \ || fail "workspace panel does not use the screen-local keyboard panel contract" - if rg -q 'Process \{|Timer \{|FileView \{' services/WorkspaceService.qml \ - adapters/WorkspaceActions.qml widgets/WorkspaceWidget.qml widgets/WorkspacePanel.qml \ - widgets/WorkspacePanelContent.qml; then - fail "workspace slice must remain event-driven and worker-free" +if rg -q 'Process \{|Timer \{|FileView \{' \ + "$workspace_service" "$workspace_widget" "$workspace_panel" \ + "$workspace_panel_content"; then + fail "workspace state and views must remain event-driven and worker-free" fi -if rg -q 'Quickshell\.Hyprland|Hyprland\.' widgets/WorkspaceWidget.qml \ - widgets/WorkspacePanel.qml; then +if rg -q 'Quickshell\.Hyprland|Hyprland\.' \ + "$workspace_widget" "$workspace_panel" "$workspace_panel_content"; then fail "workspace views must consume the shared workspace service" fi -rg -q 'Number\.isInteger\(id\)' adapters/WorkspaceActions.qml \ +rg -q 'Number\.isInteger\(id\)' "$workspace_actions" \ || fail "workspace action adapter does not validate ids" -rg -q 'bar\.run\("hyprctl dispatch' adapters/WorkspaceActions.qml \ - || fail "workspace action adapter does not use the host launcher" -rg -q 'hancore\.shibumi\.memory' widgets/WidgetRegistry.qml \ +rg -Fq '"hl.dsp.focus({ workspace = \"" + id + "\" })"' \ + "$workspace_actions" \ + || fail "workspace action adapter does not preserve the typed dispatch" +if rg -q 'bash|-c|bar\.run' "$workspace_actions"; then + fail "workspace action crosses a shell or bar-command boundary" +fi +rg -q 'hancore\.shibumi\.memory' contracts/plugin-suite-v1.json \ || fail "internal memory widget is not registered" -rg -q 'hancore\.shibumi\.cpu' widgets/WidgetRegistry.qml \ +rg -q 'hancore\.shibumi\.cpu' contracts/plugin-suite-v1.json \ || fail "internal CPU widget is not registered" rg -q 'G7: \["hancore\.shibumi\.ai"\]' core/GroupRegistry.js \ || fail "G7 is not owned by the Shibumi AI facade" -rg -q 'hancore\.shibumi\.ai' widgets/WidgetRegistry.qml \ - || fail "Shibumi AI facade is not registered" rg -q '"service": "Service.qml"' hancore.shibumi.ai/manifest.json \ - || fail "AI provider state must have one root owner" -rg -q 'function childPanelWidget\(pluginId\)' widgets/AiUsageWidget.qml \ + || fail "AI provider state must have one shipped owner" +rg -q 'function childPanelWidget\(pluginId\)' hancore.shibumi.ai/BarWidget.qml \ || fail "AI facade does not expose legacy model-usage panel routing" -if rg -q 'CACHE_FILE|stale_last' scripts/opencode-usage; then +if rg -q 'CACHE_FILE|stale_last' \ + hancore.shibumi.ai/scripts/opencode-usage; then fail "OpenCode provider must not persist a V1 usage cache" fi -if rg -q 'Process \{|Timer \{|FileView \{' widgets/AiUsageWidget.qml \ - widgets/AiUsagePanel.qml; then +if rg -q 'Process \{|Timer \{|FileView \{' \ + hancore.shibumi.ai/BarWidget.qml \ + hancore.shibumi.ai/AiUsagePanel.qml; then fail "AI views must not own provider polling or file watchers" fi rg -q 'G1: \["hancore.shibumi.control-center"\]' core/GroupRegistry.js \ @@ -864,13 +877,13 @@ rg -q 'function setBarPosition\(value, ownerValue, screenName\)' Bar.qml \ || fail "G1 cannot persist output-local top/bottom position" rg -q 'function setAllSplits\(value\)' Bar.qml \ || fail "G1 cannot persist split presets" -detail_panel_count=$(rg --files widgets -g '*Panel.qml' \ - | rg -v '(^|/)ShibumiPanel\.qml$' | wc -l) -[[ $(rg -l '^ShibumiPanel \{' widgets/*Panel.qml | wc -l) \ +detail_panel_count=$(find hancore.shibumi.* -maxdepth 1 -type f \ + -name '*Panel.qml' ! -name ShibumiPanel.qml | wc -l) +[[ $(rg -l '^ShibumiPanel \{' hancore.shibumi.*/*Panel.qml | wc -l) \ -eq $detail_panel_count ]] \ - || fail "local detail panels do not share the Shibumi surface contract" -[[ $(rg -l '^Ui\.KeyboardPanel \{' widgets/*Panel.qml | wc -l) -eq 0 ]] \ - || fail "a local detail panel still layers host panel chrome" + || fail "shipped detail panels do not share the Shibumi surface contract" +[[ $(rg -l '^Ui\.KeyboardPanel \{' hancore.shibumi.*/*Panel.qml | wc -l) -eq 0 ]] \ + || fail "a shipped detail panel still layers host panel chrome" rg -q 'default property alias panelContent: shibumiContent\.children' \ widgets/ShibumiPanel.qml \ || fail "Shibumi panel surface does not isolate chrome from panel content" @@ -899,14 +912,14 @@ for token in controlBorderColor controlHoverBorderColor controlFillColor \ || fail "Shibumi panel is missing the shared inner-control token: $token" done for pair in \ - 'widgets/AiUsagePanel.qml:panel.controlBorderColor' \ - 'widgets/AudioPanel.qml:panel.controlHoverBorderColor' \ - 'widgets/BluetoothPanel.qml:panel.controlBorderColor' \ - 'widgets/BrightnessPanel.qml:panel.controlActiveFillColor' \ - 'widgets/BrightnessPanel.qml:panel.controlBorderColor' \ - 'widgets/NetworkPanel.qml:panel.controlBorderColor' \ - 'widgets/PowerProfilePanel.qml:panel.controlBorderColor' \ - 'widgets/WorkspacePanelContent.qml:root.controller.controlFillColor'; do + 'hancore.shibumi.ai/AiUsagePanel.qml:panel.controlBorderColor' \ + 'hancore.shibumi.audio/AudioPanel.qml:panel.controlHoverBorderColor' \ + 'hancore.shibumi.bluetooth/BluetoothPanel.qml:panel.controlBorderColor' \ + 'hancore.shibumi.brightness/BrightnessPanel.qml:panel.controlActiveFillColor' \ + 'hancore.shibumi.brightness/BrightnessPanel.qml:panel.controlBorderColor' \ + 'hancore.shibumi.network/NetworkPanel.qml:panel.controlBorderColor' \ + 'hancore.shibumi.power-profile/PowerProfilePanel.qml:panel.controlBorderColor' \ + 'hancore.shibumi.workspaces/WorkspacePanelContent.qml:root.controller.controlFillColor'; do file=${pair%%:*} token=${pair#*:} rg -Fq "$token" "$file" \ @@ -919,9 +932,9 @@ rg -q 'themeNamePath' hancore.shibumi.state/ThemePalette.qml \ if rg -q 'Process \{|Timer \{' hancore.shibumi.state/ThemePalette.qml; then fail "theme palette bridge must remain event-driven" fi -rg -q 'omarchy\.clock' widgets/WidgetRegistry.qml \ +rg -q 'omarchy\.clock' core/GroupRegistry.js \ || fail "Shibumi clock does not override the host clock slot" -rg -q 'hancore\.shibumi\.center' widgets/WidgetRegistry.qml \ +rg -q 'hancore\.shibumi\.center' contracts/plugin-suite-v1.json \ || fail "Shibumi center composite is not registered" rg -q 'GroupSection' styles/shibumi/BarSurface.qml \ || fail "Shibumi surface does not render persisted groups" @@ -970,13 +983,13 @@ for unsafe_teardown_access in \ fail "group teardown retains an unsafe owner access: $unsafe_teardown_access" fi done -rg -q 'acquire\("memory"\)' widgets/MemoryWidget.qml \ +rg -q 'acquire\("memory"\)' hancore.shibumi.memory/BarWidget.qml \ || fail "memory widget does not activate shared telemetry" -rg -q 'release\("memory"\)' widgets/MemoryWidget.qml \ +rg -q 'release\("memory"\)' hancore.shibumi.memory/BarWidget.qml \ || fail "memory widget does not release shared telemetry" -rg -q 'acquire\("cpu"\)' widgets/CpuWidget.qml \ +rg -q 'acquire\("cpu"\)' hancore.shibumi.cpu/BarWidget.qml \ || fail "CPU widget does not activate shared telemetry" -rg -q 'release\("cpu"\)' widgets/CpuWidget.qml \ +rg -q 'release\("cpu"\)' hancore.shibumi.cpu/BarWidget.qml \ || fail "CPU widget does not release shared telemetry" for facade_name in \ @@ -1057,8 +1070,9 @@ quote_smoke_root=$(mktemp -d) mkdir -p "$quote_smoke_root/services" "$quote_smoke_root/runtime" \ "$quote_smoke_root/home" chmod 700 "$quote_smoke_root/runtime" -cp services/QuoteDefaults.js services/ReactorModel.js \ - services/QuoteService.qml "$quote_smoke_root/services/" +cp hancore.shibumi.reactor/QuoteDefaults.js \ + hancore.shibumi.reactor/ReactorModel.js \ + hancore.shibumi.reactor/QuoteService.qml "$quote_smoke_root/services/" cp tests/quote-service-smoke.qml "$quote_smoke_root/shell.qml" set +e quote_service_output=$(timeout 4 env \ @@ -1252,320 +1266,14 @@ OMARCHY_PATH="$OMARCHY_PATH" "$repo_root/tests/state-service-regression.sh" <<<"$v1_slot_interaction_output" \ || fail "V1 slot interaction smoke did not reach its marker" - cp services/SystemTelemetry.qml "$smoke_root/services/" - cp services/ClockService.qml "$smoke_root/services/" - cp widgets/MemoryWidget.qml "$smoke_root/widgets/" - cp widgets/MemoryRing.qml "$smoke_root/widgets/" - cp widgets/CpuWidget.qml "$smoke_root/widgets/" - cp widgets/CpuWave.qml "$smoke_root/widgets/" - cp widgets/PillSurface.qml widgets/IconText.qml "$smoke_root/widgets/" - cp widgets/ClockWidget.qml "$smoke_root/widgets/" - cp tests/memory-widget-smoke.qml "$smoke_root/shell.qml" - sed -i \ - -e 's#import "../services"#import "services"#' \ - -e 's#import "../widgets"#import "widgets"#' \ - "$smoke_root/shell.qml" - - set +e - smoke_output=$(timeout 5 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - smoke_rc=$? - set -e - printf '%s\n' "$smoke_output" - [[ $smoke_rc -eq 0 ]] || fail "telemetry widget smoke exited $smoke_rc" - grep -q 'telemetry widget smoke passed' <<<"$smoke_output" \ - || fail "telemetry widget smoke did not reach its lifecycle marker" - - cp tests/clock-widget-smoke.qml "$smoke_root/shell.qml" - set +e - clock_smoke_output=$(timeout 5 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - clock_smoke_rc=$? - set -e - printf '%s\n' "$clock_smoke_output" - [[ $clock_smoke_rc -eq 0 ]] || fail "clock widget smoke exited $clock_smoke_rc" - grep -q 'clock widget smoke passed' <<<"$clock_smoke_output" \ - || fail "clock widget smoke did not reach its lifecycle marker" - - cp widgets/CalendarModel.js widgets/CenterLayout.js widgets/CenterWidget.qml \ - widgets/IconText.qml widgets/StatusIndicators.qml widgets/SystemUpdateWidget.qml \ - widgets/WeatherWidget.qml \ - "$smoke_root/widgets/" - cp tests/fixtures/CenterTestCalendar.qml \ - tests/fixtures/WeatherPanelTestView.qml "$smoke_root/" - cp tests/center-widget-smoke.qml "$smoke_root/shell.qml" - set +e - center_smoke_output=$(timeout 6 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - center_smoke_rc=$? - set -e - printf '%s\n' "$center_smoke_output" - [[ $center_smoke_rc -eq 0 ]] || fail "center widget smoke exited $center_smoke_rc" - grep -q 'center widget smoke passed' <<<"$center_smoke_output" \ - || fail "center widget smoke did not reach its lifecycle marker" - ! grep -Eq 'Type .* unavailable|No PanelWindow backend loaded' \ - <<<"$center_smoke_output" \ - || fail "center widget smoke accepted an unavailable panel type" - - cp adapters/AudioPanelBridge.qml "$smoke_root/adapters/" - cp widgets/AudioWidget.qml "$smoke_root/widgets/" - mkdir -p "$smoke_root/fixtures" - cp tests/fixtures/AudioTestPanel.qml tests/fixtures/AudioTestView.qml \ - "$smoke_root/fixtures/" - cp tests/audio-widget-smoke.qml "$smoke_root/shell.qml" - set +e - audio_smoke_output=$(timeout 5 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - audio_smoke_rc=$? - set -e - printf '%s\n' "$audio_smoke_output" - [[ $audio_smoke_rc -eq 0 ]] || fail "audio widget smoke exited $audio_smoke_rc" - grep -q 'audio widget smoke passed' <<<"$audio_smoke_output" \ - || fail "audio widget smoke did not reach its lifecycle marker" - - # Exercise the packaged plugin payload. The predecessor widget tree is not - # part of release archives and may intentionally lag the extracted plugin. - cp hancore.shibumi.network/BarWidget.qml \ - "$smoke_root/widgets/NetworkWidget.qml" - cp hancore.shibumi.network/HostTokens.qml \ - hancore.shibumi.network/IconText.qml \ - hancore.shibumi.network/PillSurface.qml "$smoke_root/widgets/" - cp tests/fixtures/NetworkTestService.qml tests/fixtures/NetworkTestView.qml \ - "$smoke_root/fixtures/" - cp tests/network-widget-smoke.qml "$smoke_root/shell.qml" - set +e - network_smoke_output=$(timeout 5 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - network_smoke_rc=$? - set -e - printf '%s\n' "$network_smoke_output" - [[ $network_smoke_rc -eq 0 ]] || fail "network widget smoke exited $network_smoke_rc" - grep -q 'network widget smoke passed' <<<"$network_smoke_output" \ - || fail "network widget smoke did not reach its lifecycle marker" - - cp adapters/MonitorPanelBridge.qml "$smoke_root/adapters/" - cp services/MonitorService.qml "$smoke_root/services/" - cp widgets/BrightnessWidget.qml widgets/BrightnessPanel.qml \ - "$smoke_root/widgets/" - cp tests/fixtures/MonitorTestPanel.qml tests/fixtures/MonitorTestView.qml \ - "$smoke_root/fixtures/" - cp tests/brightness-widget-smoke.qml "$smoke_root/shell.qml" - set +e - brightness_smoke_output=$(timeout 5 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - brightness_smoke_rc=$? - set -e - printf '%s\n' "$brightness_smoke_output" - [[ $brightness_smoke_rc -eq 0 ]] \ - || fail "brightness widget smoke exited $brightness_smoke_rc" - grep -q 'brightness widget smoke passed' <<<"$brightness_smoke_output" \ - || fail "brightness widget smoke did not reach its lifecycle marker" - - cp services/PowerModel.js "$smoke_root/services/" - cp widgets/BatteryWidget.qml widgets/BatteryPanel.qml \ - widgets/PowerProfileWidget.qml widgets/PowerProfilePanel.qml \ - "$smoke_root/widgets/" - cp tests/fixtures/PowerTestService.qml tests/fixtures/PowerTestPanel.qml \ - "$smoke_root/fixtures/" - cp tests/power-widgets-smoke.qml "$smoke_root/shell.qml" - set +e - power_smoke_output=$(timeout 7 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - power_smoke_rc=$? - set -e - printf '%s\n' "$power_smoke_output" - [[ $power_smoke_rc -eq 0 ]] || fail "power widgets smoke exited $power_smoke_rc" - grep -q 'power widgets smoke passed' <<<"$power_smoke_output" \ - || fail "power widgets smoke did not reach its lifecycle marker" - - cp services/PowerService.qml "$smoke_root/services/" - mkdir -p "$smoke_root/power-bin" - cp tests/fixtures/power-bin/* "$smoke_root/power-bin/" - chmod 700 "$smoke_root/power-bin/"* - printf 'balanced\n' >"$smoke_root/power-state" - cp tests/power-service-runtime-smoke.qml "$smoke_root/shell.qml" - set +e - power_service_output=$(timeout 7 env \ - PATH="$smoke_root/power-bin:$PATH" \ - SHIBUMI_POWER_FIXTURE_STATE="$smoke_root/power-state" \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - power_service_rc=$? - set -e - printf '%s\n' "$power_service_output" - [[ $power_service_rc -eq 0 ]] \ - || fail "power service runtime smoke exited $power_service_rc" - grep -q 'power service runtime smoke passed' <<<"$power_service_output" \ - || fail "power service runtime smoke did not reach its lifecycle marker" - [[ $(<"$smoke_root/power-state") == performance ]] \ - || fail "power service did not execute the exact validated profile action" - - cp adapters/BluetoothBackendAdapter.qml adapters/BluetoothModel.js \ - adapters/BluetoothDiscoveryGuard.qml adapters/qmldir \ - "$smoke_root/adapters/" - cp services/BluetoothService.qml "$smoke_root/services/" - cp widgets/BluetoothWidget.qml widgets/BluetoothPanel.qml "$smoke_root/widgets/" - cp tests/fixtures/BluetoothTestBackend.qml \ - tests/fixtures/BluetoothTestView.qml "$smoke_root/fixtures/" - cp tests/bluetooth-widget-smoke.qml "$smoke_root/shell.qml" - set +e - bluetooth_smoke_output=$(timeout 5 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - bluetooth_smoke_rc=$? - set -e - printf '%s\n' "$bluetooth_smoke_output" - [[ $bluetooth_smoke_rc -eq 0 ]] \ - || fail "bluetooth widget smoke exited $bluetooth_smoke_rc" - grep -q 'bluetooth widget smoke passed' <<<"$bluetooth_smoke_output" \ - || fail "bluetooth widget smoke did not reach its lifecycle marker" - - cp widgets/StatusWidget.qml widgets/TrayStatusView.qml \ - widgets/NotificationStatusView.qml widgets/TrayDrawerPanel.qml \ - "$smoke_root/widgets/" - cp tests/fixtures/StatusTestWidget.qml "$smoke_root/fixtures/" - cp tests/fixtures/TrayDrawerTestPanel.qml "$smoke_root/fixtures/" - cp tests/fixtures/NotificationPanelTestView.qml "$smoke_root/fixtures/" - cp tests/status-widget-smoke.qml "$smoke_root/shell.qml" - set +e - status_smoke_output=$(timeout 5 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - status_smoke_rc=$? - set -e - printf '%s\n' "$status_smoke_output" - [[ $status_smoke_rc -eq 0 ]] \ - || fail "status widget smoke exited $status_smoke_rc" - grep -q 'status widget smoke passed' <<<"$status_smoke_output" \ - || fail "status widget smoke did not reach its lifecycle marker" - - mkdir -p "$smoke_root/widgets/assets" - cp hancore.shibumi.ai/BarWidget.qml \ - "$smoke_root/widgets/AiUsageWidget.qml" - cp hancore.shibumi.ai/HostTokens.qml \ - hancore.shibumi.ai/IconText.qml \ - hancore.shibumi.ai/PillSurface.qml \ - hancore.shibumi.ai/TintedImage.qml "$smoke_root/widgets/" - cp hancore.shibumi.ai/assets/codex.svg \ - hancore.shibumi.ai/assets/opencode-mark.svg \ - "$smoke_root/widgets/assets/" - cp tests/fixtures/AiTestPanel.qml "$smoke_root/" - cp tests/ai-usage-widget-smoke.qml "$smoke_root/shell.qml" - set +e - ai_usage_smoke_output=$(timeout 6 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - ai_usage_smoke_rc=$? - set -e - printf '%s\n' "$ai_usage_smoke_output" - [[ $ai_usage_smoke_rc -eq 0 ]] \ - || fail "AI usage widget smoke exited $ai_usage_smoke_rc" - grep -q 'ai usage widget smoke passed' <<<"$ai_usage_smoke_output" \ - || fail "AI usage widget smoke did not reach its lifecycle marker" - - cp widgets/MediaWidget.qml widgets/MediaPanel.qml widgets/MediaPulse.qml \ - widgets/MediaSpectrum.qml widgets/MediaMuse.qml widgets/CavaThemeModel.js \ - "$smoke_root/widgets/" - cp tests/fixtures/MediaTestPanel.qml "$smoke_root/fixtures/" - cp tests/media-widget-smoke.qml "$smoke_root/shell.qml" - set +e - media_smoke_output=$(timeout 7 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - media_smoke_rc=$? - set -e - printf '%s\n' "$media_smoke_output" - [[ $media_smoke_rc -eq 0 ]] || fail "media widget smoke exited $media_smoke_rc" - grep -q 'media widget smoke passed' <<<"$media_smoke_output" \ - || fail "media widget smoke did not reach its lifecycle marker" - - cp styles/shibumi/Style.qml styles/shibumi/VisualTokens.qml \ - styles/shibumi/BarSurface.qml styles/shibumi/TooltipSurface.qml \ - styles/shibumi/DragGhost.qml styles/shibumi/GroupSection.qml \ - styles/shibumi/RunChrome.qml styles/shibumi/GapEffectsLayer.qml \ - styles/shibumi/ReactorEventLayer.qml \ - "$smoke_root/styles/shibumi/" - cp tests/shibumi-presentation-smoke.qml "$smoke_root/shell.qml" + # Capability smokes above exercise the shipped plugin payloads directly. - set +e - presentation_output=$(timeout 5 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - presentation_rc=$? - set -e - printf '%s\n' "$presentation_output" - [[ $presentation_rc -eq 0 ]] \ - || fail "Shibumi presentation smoke exited $presentation_rc" - grep -q 'shibumi presentation smoke passed' <<<"$presentation_output" \ - || fail "Shibumi presentation smoke did not reach its marker" + # Shared presentation is exercised through shipped capability smokes. - cp adapters/WorkspaceActions.qml "$smoke_root/adapters/" - cp services/WorkspaceModel.js services/WorkspaceService.qml "$smoke_root/services/" - cp widgets/WorkspaceWidget.qml widgets/PacmanWorkspaceMarker.qml \ - widgets/WorkspacePanel.qml \ - widgets/WorkspacePanelContent.qml widgets/ShibumiPanelToolTip.qml \ + cp hancore.shibumi.workspaces/WorkspacePanelContent.qml \ + hancore.shibumi.workspaces/ShibumiPanelToolTip.qml \ + hancore.shibumi.workspaces/IconText.qml \ "$smoke_root/widgets/" - cp tests/fixtures/WorkspaceTestPanel.qml "$smoke_root/" - cp tests/workspace-widget-smoke.qml "$smoke_root/shell.qml" - - set +e - workspace_smoke_output=$(timeout 5 env \ - QT_QPA_PLATFORM=offscreen \ - XDG_RUNTIME_DIR="$smoke_root/runtime" \ - QML_IMPORT_PATH="${OMARCHY_PATH}/shell${QML_IMPORT_PATH:+:${QML_IMPORT_PATH}}" \ - QML2_IMPORT_PATH="${OMARCHY_PATH}/shell${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" \ - /usr/bin/quickshell -p "$smoke_root" 2>&1) - workspace_smoke_rc=$? - set -e - printf '%s\n' "$workspace_smoke_output" - [[ $workspace_smoke_rc -eq 0 ]] || fail "workspace widget smoke exited $workspace_smoke_rc" - grep -q 'workspace widget smoke passed' <<<"$workspace_smoke_output" \ - || fail "workspace widget smoke did not reach its lifecycle marker" - cp tests/workspace-panel-smoke.qml "$smoke_root/shell.qml" set +e workspace_panel_output=$(timeout 5 env \ diff --git a/tests/media-widget-smoke.qml b/tests/media-widget-smoke.qml deleted file mode 100644 index 19b9c89..0000000 --- a/tests/media-widget-smoke.qml +++ /dev/null @@ -1,288 +0,0 @@ -import QtQuick -import Quickshell -import "widgets" as Widgets - -ShellRoot { - id: root - - property int phase: 0 - property int waits: 0 - property real activeWidth: 0 - property var mediaSettings: ({ spectrum: false, compact: false }) - - function fail(message) { - console.error("media-widget-smoke:", message) - Qt.exit(1) - } - - QtObject { - id: playerA - property string key: "player-a" - property string identity: "Vesktop" - property string desktopEntry: "vesktop" - property string trackTitle: "A deliberately long track title for marquee validation" - property string trackArtist: "Artist A" - property string trackAlbum: "Album A" - property string trackArtUrl: "" - property bool isPlaying: true - property bool canGoPrevious: true - property bool canGoNext: true - property bool canTogglePlaying: true - property bool canPlay: true - property bool canPause: true - property real position: 61 - property real length: 245 - } - - QtObject { - id: playerB - property string key: "player-b" - property string identity: "Firefox" - property string desktopEntry: "firefox" - property string trackTitle: "Track B" - property string trackArtist: "Artist B" - property string trackAlbum: "" - property string trackArtUrl: "" - property bool isPlaying: false - property bool canGoPrevious: false - property bool canGoNext: true - property bool canTogglePlaying: true - property bool canPlay: true - property bool canPause: true - property real position: 0 - property real length: 180 - } - - QtObject { - id: mediaState - property var activePlayer: playerA - property var sourcePlayers: [playerA, playerB] - readonly property bool hasMedia: activePlayer !== null - && (activePlayer.trackTitle !== "" || activePlayer.trackArtist !== "") - property string lastAction: "" - property string lastTarget: "" - property string selectedKey: "" - property int actionCount: 0 - - function playerKey(player) { return player ? String(player.key || "") : "" } - - function runAction(action, showFeedback, targetKey) { - lastAction = String(action || "") - lastTarget = String(targetKey || "") - actionCount++ - if (lastAction === "playPause" && activePlayer) - activePlayer.isPlaying = !activePlayer.isPlaying - return activePlayer !== null - } - - function selectPlayer(key) { - selectedKey = String(key || "") - if (selectedKey === playerA.key) activePlayer = playerA - else if (selectedKey === playerB.key) activePlayer = playerB - else return false - return true - } - } - - QtObject { - id: fakeSpectrum - property var clients: [] - property var levels: [ - 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, - 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, - 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, - 0.04, 0.04, 0.04, 0.04, 0.04, 0.04 - ] - property var themeColors: [] - property string state: "running" - property bool workerRunning: false - readonly property int clientCount: clients.length - - function beginSpectrum(owner) { - if (!owner || clients.indexOf(owner) >= 0) return false - clients = clients.concat([owner]) - return true - } - - function endSpectrum(owner) { - if (!owner || clients.indexOf(owner) < 0) return false - clients = clients.filter(function(candidate) { return candidate !== owner }) - return true - } - } - - QtObject { - id: fakeShell - property bool serviceAvailable: true - function firstPartyServiceFor(id) { - return serviceAvailable && String(id || "") === "omarchy.media" - ? mediaState : null - } - function serviceFor(id) { - return String(id || "") === "hancore.shibumi.media" - ? fakeSpectrum : null - } - } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: 28 - property string position: "top" - property string fontFamily: "monospace" - property color foreground: "#e8e8e8" - property color barForeground: foreground - property color background: "#181818" - property color urgent: "#88bbee" - property bool foregroundAnimationEnabled: false - property var shell: fakeShell - property var activePopout: null - property var clickTargets: [] - property var visualTokens: ({ - pillHeight: 24, - pillRadius: 12, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false, - pillPaddingX: 9, - slotHeight: 28, - contentGap: 5, - compactGap: 4, - labelSize: 12, - iconSize: 15, - presentation: ({ radius: "large" }) - }) - - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(_owner, _direction) { return false } - function targetBelongsToWindow(_target, _window) { return true } - function registerClickTarget(target) { - if (clickTargets.indexOf(target) < 0) clickTargets = clickTargets.concat([target]) - } - function unregisterClickTarget(target) { - clickTargets = clickTargets.filter(function(item) { return item !== target }) - } - } - - Loader { - id: mediaLoader - active: true - sourceComponent: Component { - Widgets.MediaWidget { - bar: fakeBar - settings: root.mediaSettings - panelSource: Qt.resolvedUrl("fixtures/MediaTestPanel.qml") - } - } - } - - Widgets.MediaWidget { - id: unavailableMedia - bar: unavailableBar - settings: ({ spectrum: false }) - } - - QtObject { - id: unavailableBar - property var shell: null - property int barSize: 28 - property bool vertical: false - property string fontFamily: "monospace" - property color foreground: "#e8e8e8" - property color barForeground: foreground - property color background: "#181818" - property color urgent: "#88bbee" - property bool foregroundAnimationEnabled: false - property var visualTokens: fakeBar.visualTokens - property var clickTargets: [] - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function registerClickTarget(target) { - if (clickTargets.indexOf(target) < 0) clickTargets = clickTargets.concat([target]) - } - function unregisterClickTarget(target) { - clickTargets = clickTargets.filter(function(item) { return item !== target }) - } - } - - Timer { - interval: 60 - repeat: true - running: true - onTriggered: { - const media = mediaLoader.item - if (root.phase === 0) { - if (!media || !media.mediaService || !media.active || !media.playing - || media.trackLabel.indexOf("Artist A") < 0 || media.panelLoaded - || media.fullMode || media.museBandCount !== 24 - || fakeSpectrum.clientCount !== 0 - || unavailableMedia.visible || fakeBar.clickTargets.length !== 1) - return root.fail("initial media service/presentation contract") - root.activeWidth = media.implicitWidth - media.interactionTarget.triggerPress(Qt.LeftButton) - if (mediaState.lastAction !== "playPause" || playerA.isPlaying) - return root.fail("play/pause action forwarding") - media.interactionTarget.wheelMoved(120) - if (mediaState.lastAction !== "previous") - return root.fail("wheel previous action forwarding") - root.mediaSettings = ({ spectrum: false, compact: true }) - } else if (root.phase === 1) { - if (!media.fullMode || !media.museVisible - || fakeSpectrum.clientCount !== 1) - return root.fail("FULL muse presentation/lease") - media.runMusePrimaryAction() - if (mediaState.lastAction !== "playPause" || !playerA.isPlaying) - return root.fail("FULL muse play/pause forwarding") - media.runMuseWheel(120) - if (mediaState.lastAction !== "next") - return root.fail("FULL muse wheel-next forwarding") - media.runMuseWheel(-120) - if (mediaState.lastAction !== "previous") - return root.fail("FULL muse wheel-previous forwarding") - root.mediaSettings = ({ spectrum: false, compact: false }) - } else if (root.phase === 2) { - if (media.fullMode || media.museVisible - || fakeSpectrum.clientCount !== 0) - return root.fail("FULL muse lease release") - media.interactionTarget.triggerPress(Qt.RightButton) - } else if (root.phase === 3) { - if (!media.opened || !media.panelLoaded || !media.panelItem) return - if (media.panelItem.renderedSourceCount !== 2 - || media.panelItem.spectrumWorkerRunning - || media.panelItem.formatTime(61) !== "1:01" - || media.panelItem.currentLength !== 245) - return root.fail("lazy panel/state contract") - if (!media.panelItem.selectSource(1) - || mediaState.selectedKey !== "player-b" - || mediaState.activePlayer !== playerB) - return root.fail("source selection forwarding") - media.close() - } else if (root.phase === 4) { - if (media.opened || media.panelLoaded || media.activePlayer !== playerB) - return root.fail("lazy panel close") - mediaState.activePlayer = null - } else if (root.phase === 5) { - if (media.active || media.implicitWidth >= root.activeWidth) - return root.fail("idle V1 presentation") - media.interactionTarget.triggerPress(Qt.LeftButton) - } else if (root.phase === 6) { - if (!media.opened || !media.panelLoaded) return - media.close() - mediaLoader.active = false - } else { - if (fakeBar.clickTargets.length !== 0 || fakeBar.activePopout !== null - || fakeSpectrum.clientCount !== 0) - return root.fail("media teardown") - stop() - console.log("media widget smoke passed") - Qt.quit() - } - root.phase++ - } - } -} diff --git a/tests/memory-widget-smoke.qml b/tests/memory-widget-smoke.qml deleted file mode 100644 index 44523d8..0000000 --- a/tests/memory-widget-smoke.qml +++ /dev/null @@ -1,128 +0,0 @@ -import QtQuick -import Quickshell -import "../services" as Services -import "../widgets" as Widgets - -ShellRoot { - id: root - - property bool failed: false - - function fail(message) { - failed = true - console.error("memory-widget-smoke:", message) - Qt.quit() - } - - Services.SystemTelemetry { - id: telemetry - } - - QtObject { - id: actions - function openSystemMonitor() { return true } - } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: 26 - property string position: "top" - property string fontFamily: "monospace" - property color foreground: "#e8e8e8" - property color barForeground: foreground - property color background: "#181818" - property color urgent: "#88bbee" - property var visualTokens: ({ - pillPaddingX: 9, - slotHeight: 28, - pillHeight: 24, - pillRadius: 12, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false, - contentGap: 5, - compactGap: 4, - labelSize: 12, - iconSize: 15 - }) - property bool foregroundAnimationEnabled: false - property var activePopout: null - property var systemTelemetry: telemetry - property var gpuTelemetry: null - property var systemActions: actions - - function showTooltip(target, text) {} - function hideTooltip(target) {} - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(owner, direction) { return false } - } - - Loader { - id: memoryLoader - active: true - sourceComponent: Component { - Widgets.MemoryWidget { - bar: fakeBar - settings: ({ compact: false }) - } - } - } - - Loader { - id: cpuLoader - active: true - sourceComponent: Component { - Widgets.CpuWidget { - bar: fakeBar - settings: ({ compact: false }) - } - } - } - - Timer { - interval: 800 - running: true - onTriggered: { - if (!memoryLoader.item) { - root.fail("widget did not instantiate") - return - } - if (telemetry.memoryConsumers !== 1) { - root.fail("widget did not acquire exactly one telemetry consumer") - return - } - if (telemetry.cpuConsumers !== 1 || telemetry.previousCpuTotal <= 0) { - root.fail("CPU widget did not acquire or prime shared telemetry") - return - } - if (telemetry.memTotalMiB <= 0 || telemetry.memAvailableMiB <= 0) { - root.fail("procfs memory data did not load") - return - } - memoryLoader.active = false - cpuLoader.active = false - releaseCheck.restart() - } - } - - Timer { - id: releaseCheck - interval: 0 - onTriggered: { - if (telemetry.memoryConsumers !== 0) { - root.fail("widget did not release telemetry on destruction") - return - } - if (telemetry.cpuConsumers !== 0) { - root.fail("CPU widget did not release telemetry on destruction") - return - } - console.log("telemetry widget smoke passed") - Qt.quit() - } - } -} diff --git a/tests/network-plugin-regression.sh b/tests/network-plugin-regression.sh index 9e71d9a..d654084 100755 --- a/tests/network-plugin-regression.sh +++ b/tests/network-plugin-regression.sh @@ -23,8 +23,6 @@ chmod 700 "$tmpdir/runtime" cp -a -- "$repo_root/hancore.shibumi.network" "$tmpdir/network" cp -a -- "$repo_root/hancore.shibumi.network" \ "$tmpdir/hancore.shibumi.network" -cp -a -- "$repo_root/services" "$tmpdir/services" -cp -a -- "$repo_root/adapters" "$tmpdir/adapters" cp -a -- "$omarchy_path/shell/Commons" "$tmpdir/Commons" cp -a -- "$omarchy_path/shell/Ui" "$tmpdir/Ui" install -m 0644 "$repo_root/tests/network-plugin-smoke.qml" "$tmpdir/shell.qml" @@ -107,36 +105,30 @@ fi install -m 0644 "$repo_root/tests/network-scanner-lifecycle-smoke.qml" \ "$tmpdir/shell.qml" -for scanner_mode in plugin canonical; do - mkdir -p "$tmpdir/scanner-$scanner_mode-runtime" \ - "$tmpdir/scanner-$scanner_mode-home" - chmod 700 "$tmpdir/scanner-$scanner_mode-runtime" - canonical_flag=0 - [[ $scanner_mode == canonical ]] && canonical_flag=1 - set +e - scanner_output=$(timeout 12 env \ - HOME="$tmpdir/scanner-$scanner_mode-home" \ - PATH="$tmpdir/bin:$PATH" \ - SHIBUMI_TEST_CANONICAL_NETWORK="$canonical_flag" \ - QT_QPA_PLATFORM=offscreen \ - WAYLAND_DISPLAY= \ - XDG_RUNTIME_DIR="$tmpdir/scanner-$scanner_mode-runtime" \ - QML_IMPORT_PATH="$omarchy_path/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \ - QML2_IMPORT_PATH="$omarchy_path/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \ - "$quickshell_bin" -p "$tmpdir" 2>&1) - scanner_rc=$? - set -e - printf '%s\n' "$scanner_output" - [[ $scanner_rc -eq 0 ]] \ - || fail "$scanner_mode scanner lifecycle smoke exited $scanner_rc" - grep -F 'network scanner lifecycle smoke passed' \ - <<<"$scanner_output" >/dev/null \ - || fail "$scanner_mode scanner lifecycle success marker missing" - if grep -Eq 'TypeError|ReferenceError|Binding loop|Unable to assign' \ - <<<"$scanner_output"; then - fail "$scanner_mode scanner lifecycle produced a QML runtime error" - fi -done +mkdir -p "$tmpdir/scanner-runtime" "$tmpdir/scanner-home" +chmod 700 "$tmpdir/scanner-runtime" +set +e +scanner_output=$(timeout 12 env \ + HOME="$tmpdir/scanner-home" \ + PATH="$tmpdir/bin:$PATH" \ + QT_QPA_PLATFORM=offscreen \ + WAYLAND_DISPLAY= \ + XDG_RUNTIME_DIR="$tmpdir/scanner-runtime" \ + QML_IMPORT_PATH="$omarchy_path/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \ + QML2_IMPORT_PATH="$omarchy_path/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \ + "$quickshell_bin" -p "$tmpdir" 2>&1) +scanner_rc=$? +set -e +printf '%s\n' "$scanner_output" +[[ $scanner_rc -eq 0 ]] \ + || fail "scanner lifecycle smoke exited $scanner_rc" +grep -F 'network scanner lifecycle smoke passed' \ + <<<"$scanner_output" >/dev/null \ + || fail "scanner lifecycle success marker missing" +if grep -Eq 'TypeError|ReferenceError|Binding loop|Unable to assign' \ + <<<"$scanner_output"; then + fail "scanner lifecycle produced a QML runtime error" +fi normal_pid_log="$tmpdir/bounded-parent-pid" normal_child_pid_log="$tmpdir/bounded-child-pid" diff --git a/tests/network-scanner-lifecycle-smoke.qml b/tests/network-scanner-lifecycle-smoke.qml index d0e8006..ce61869 100644 --- a/tests/network-scanner-lifecycle-smoke.qml +++ b/tests/network-scanner-lifecycle-smoke.qml @@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import "network" as Network -import "services" as Services import "fixtures" as Fixtures ShellRoot { @@ -14,8 +13,6 @@ ShellRoot { property int activationBaseline: 0 property int legacyActivationBaseline: 0 property var clickTargets: [] - readonly property bool canonicalService: - Quickshell.env("SHIBUMI_TEST_CANONICAL_NETWORK") === "1" readonly property var networkService: networkServiceLoader.item readonly property var legacyService: legacyServiceLoader.item readonly property var destructionService: destructionServiceLoader.item @@ -101,46 +98,25 @@ ShellRoot { Network.Service { bar: fakeBar; panelComponent: legacyScannerPanel } } - Component { - id: canonicalScannerService - Services.NetworkService { bar: fakeBar; panelComponent: scannerGatePanel } - } - - Component { - id: canonicalLegacyService - Services.NetworkService { bar: fakeBar; panelComponent: legacyScannerPanel } - } - Component { id: pluginDestructionService Network.Service { bar: fakeBar; panelComponent: destructionScannerPanel } } - Component { - id: canonicalDestructionService - Services.NetworkService { - bar: fakeBar - panelComponent: destructionScannerPanel - } - } - Loader { id: networkServiceLoader - sourceComponent: root.canonicalService - ? canonicalScannerService : pluginScannerService + sourceComponent: pluginScannerService } Loader { id: legacyServiceLoader - sourceComponent: root.canonicalService - ? canonicalLegacyService : pluginLegacyService + sourceComponent: pluginLegacyService } Loader { id: destructionServiceLoader active: false - sourceComponent: root.canonicalService - ? canonicalDestructionService : pluginDestructionService + sourceComponent: pluginDestructionService } Item { id: firstOwner } diff --git a/tests/network-widget-smoke.qml b/tests/network-widget-smoke.qml deleted file mode 100644 index 04c435e..0000000 --- a/tests/network-widget-smoke.qml +++ /dev/null @@ -1,196 +0,0 @@ -import QtQuick -import Quickshell -import "widgets" as Widgets -import "fixtures" as Fixtures - -ShellRoot { - id: root - - property int phase: 0 - property int phaseTicks: 0 - property real fullWidth: 0 - property var clickTargets: [] - - function fail(message) { - console.error("network-widget-smoke:", message) - Qt.exit(1) - } - - Fixtures.NetworkTestService { id: sharedNetworkService } - Fixtures.NetworkTestService { id: unavailableService; ready: false } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: 35 - property int sizeHorizontal: 35 - property string position: "top" - property string fontFamily: "monospace" - property color foreground: "#eeeeee" - property color barForeground: foreground - property color urgent: "#88bbee" - property bool foregroundAnimationEnabled: false - property var activePopout: null - property var clickTargets: root.clickTargets - property var shell: null - property var networkService: sharedNetworkService - property var visualTokens: ({ - pillHeight: 24, - pillRadius: 12, - pillPaddingX: 9, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false, - slotHeight: 28, - contentGap: 5, - compactGap: 4, - labelSize: 12, - iconSize: 15 - }) - - function registerClickTarget(target) { - if (root.clickTargets.indexOf(target) < 0) - root.clickTargets = root.clickTargets.concat([target]) - } - function unregisterClickTarget(target) { - root.clickTargets = root.clickTargets.filter(item => item !== target) - } - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(_owner, _direction) { return false } - function targetBelongsToWindow(_target, _window) { return true } - } - - Loader { - id: firstLoader - active: true - sourceComponent: Component { - Widgets.NetworkWidget { - bar: fakeBar - settings: ({ compact: false }) - networkServiceOverride: sharedNetworkService - popupSource: Qt.resolvedUrl("fixtures/NetworkTestView.qml") - } - } - } - - Loader { - id: secondLoader - active: true - sourceComponent: Component { - Widgets.NetworkWidget { - bar: fakeBar - settings: ({ compact: false }) - networkServiceOverride: sharedNetworkService - popupSource: Qt.resolvedUrl("fixtures/NetworkTestView.qml") - } - } - } - - Widgets.NetworkWidget { - id: unavailableNetwork - bar: fakeBar - networkServiceOverride: unavailableService - } - - Timer { - interval: 80 - repeat: true - running: true - onTriggered: { - root.phaseTicks++ - const first = firstLoader.item - const second = secondLoader.item - if (root.phase === 0) { - if (!first || !second || !first.networkReady || !second.networkReady - || root.phaseTicks < 3) return - if (first.networkService !== second.networkService - || first.networkService !== sharedNetworkService - || first.mode !== "wifi" || first.label !== "Test Network" - || first.signal !== 73 || first.implicitHeight !== 35 - || unavailableNetwork.visible) - return root.fail("shared backend readiness/state/geometry") - if (first.childPanelWidget("omarchy.network") !== first - || !first.ownsPanelWidget(first) - || second.childPanelWidget("omarchy.network") !== second) - return root.fail("screen-local alias routing") - if (root.clickTargets.length !== 2 || sharedNetworkService.sessionCount !== 0) - return root.fail("visible click targets or closed lifecycle") - - root.fullWidth = first.implicitWidth - first.settings = ({ compact: true }) - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 1) { - if (root.phaseTicks < 3) return - if (!first.compact || first.implicitWidth >= root.fullWidth) - return root.fail("compact presentation width") - - first.interactionTarget.triggerPress(Qt.LeftButton) - second.open() - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 2) { - if (root.phaseTicks < 3) return - if (!first.opened || !second.opened || !first.panelLoaded - || !second.panelLoaded || sharedNetworkService.sessionCount !== 2 - || sharedNetworkService.beginCount !== 2 - || sharedNetworkService.viewLoadCount !== 2) - return root.fail("two-output local panel sessions") - - first.interactionTarget.triggerPress(Qt.RightButton) - if (!first.opened || !sharedNetworkService.lastScanWifi - || sharedNetworkService.refreshCount !== 1) - return root.fail("right-click scan forwarding") - - sharedNetworkService.kind = "ethernet" - sharedNetworkService.label = "enp1s0" - sharedNetworkService.downloadRate = 1536 - sharedNetworkService.uploadRate = 2 * 1024 * 1024 - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 3) { - if (root.phaseTicks < 2) return - if (first.mode !== "ethernet" || second.label !== "enp1s0" - || second.displayLabel !== "Ethernet" - || !second.v1TrafficPresentation || second.v2TrafficPresentation - || second.compactRate(second.downloadRate) !== "1.5K" - || second.compactRate(second.uploadRate) !== "2.0M" - || first.tooltipText.indexOf("Ethernet") !== 0) - return root.fail("shared reactive ethernet state") - const v2Tokens = ({}) - for (const key in fakeBar.visualTokens) - v2Tokens[key] = fakeBar.visualTokens[key] - v2Tokens.v2Shell = true - fakeBar.visualTokens = v2Tokens - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 4) { - if (root.phaseTicks < 2) return - if (second.v1TrafficPresentation || !second.v2TrafficPresentation) - return root.fail("V2 ethernet traffic presentation") - first.close() - secondLoader.active = false - root.phase++ - root.phaseTicks = 0 - } else { - if (sharedNetworkService.sessionCount !== 0 - || sharedNetworkService.endCount !== 2 - || first.panelLoaded || root.clickTargets.length !== 1) - return root.fail("session and loader teardown") - firstLoader.active = false - Qt.callLater(function() { - if (root.clickTargets.length !== 0) - return root.fail("click target destruction cleanup") - console.log("network widget smoke passed") - Qt.quit() - }) - stop() - } - } - } -} diff --git a/tests/opencode-usage-regression.py b/tests/opencode-usage-regression.py index 1be86b2..4766194 100755 --- a/tests/opencode-usage-regression.py +++ b/tests/opencode-usage-regression.py @@ -13,10 +13,7 @@ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -SCRIPTS = [ - REPO_ROOT / "scripts" / "opencode-usage", - REPO_ROOT / "hancore.shibumi.ai" / "scripts" / "opencode-usage", -] +SCRIPT = REPO_ROOT / "hancore.shibumi.ai" / "scripts" / "opencode-usage" class OpenCodeUsageRegressionTests(unittest.TestCase): @@ -72,24 +69,15 @@ def add_message( def run_scripts(self) -> dict: environment = os.environ.copy() environment["OPENCODE_DATA_DIR"] = str(self.data_dir) - outputs = [] - for script in SCRIPTS: - result = subprocess.run( - [str(script)], - check=True, - capture_output=True, - text=True, - env=environment, - timeout=10, - ) - outputs.append(json.loads(result.stdout)) - stable_outputs = [] - for output in outputs: - stable = dict(output) - stable.pop("updated_at", None) - stable_outputs.append(stable) - self.assertEqual(stable_outputs[0], stable_outputs[1]) - return outputs[0] + result = subprocess.run( + [str(SCRIPT)], + check=True, + capture_output=True, + text=True, + env=environment, + timeout=10, + ) + return json.loads(result.stdout) def test_models_and_latest_model_use_positive_today_scope(self) -> None: now = datetime.now() @@ -173,7 +161,7 @@ def test_local_day_bounds_follow_dst_midnights(self) -> None: os.environ["TZ"] = "Europe/Berlin" time.tzset() loader = importlib.machinery.SourceFileLoader( - "shibumi_opencode_usage", str(SCRIPTS[0]) + "shibumi_opencode_usage", str(SCRIPT) ) spec = importlib.util.spec_from_loader(loader.name, loader) self.assertIsNotNone(spec) diff --git a/tests/power-plugins-regression.sh b/tests/power-plugins-regression.sh index 4e74e1c..5a92984 100755 --- a/tests/power-plugins-regression.sh +++ b/tests/power-plugins-regression.sh @@ -22,6 +22,7 @@ mkdir -p "$tmpdir/runtime" "$tmpdir/fixtures" chmod 700 "$tmpdir/runtime" cp -a -- "$repo_root/hancore.shibumi.battery" "$tmpdir/battery" cp -a -- "$repo_root/hancore.shibumi.power-profile" "$tmpdir/powerProfile" +cp -a -- "$repo_root/hancore.shibumi.power-state" "$tmpdir/powerState" cp -a -- "$omarchy_path/shell/Commons" "$tmpdir/Commons" cp -a -- "$omarchy_path/shell/Ui" "$tmpdir/Ui" install -m 0644 "$repo_root/tests/fixtures/ShibumiPanelTest.qml" \ @@ -46,6 +47,31 @@ printf '%s\n' "$output" grep -F 'power plugins smoke passed' <<<"$output" >/dev/null \ || fail "success marker missing" +mkdir -p "$tmpdir/power-runtime" "$tmpdir/power-bin" +chmod 700 "$tmpdir/power-runtime" +install -m 0755 "$repo_root"/tests/fixtures/power-bin/* "$tmpdir/power-bin/" +printf 'balanced\n' >"$tmpdir/power-state" +install -m 0644 "$repo_root/tests/power-service-runtime-smoke.qml" \ + "$tmpdir/shell.qml" +set +e +service_output=$(timeout 8 env \ + PATH="$tmpdir/power-bin:$PATH" \ + SHIBUMI_POWER_FIXTURE_STATE="$tmpdir/power-state" \ + QT_QPA_PLATFORM=offscreen \ + WAYLAND_DISPLAY= \ + XDG_RUNTIME_DIR="$tmpdir/power-runtime" \ + QML_IMPORT_PATH="$omarchy_path/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \ + QML2_IMPORT_PATH="$omarchy_path/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \ + "$quickshell_bin" -p "$tmpdir" 2>&1) +service_rc=$? +set -e +printf '%s\n' "$service_output" +[[ $service_rc -eq 0 ]] || fail "power-state service smoke exited $service_rc" +grep -F 'power service runtime smoke passed' <<<"$service_output" >/dev/null \ + || fail "power-state service success marker missing" +[[ $(<"$tmpdir/power-state") == performance ]] \ + || fail "power-state service did not execute the validated profile action" + for plugin in battery power-profile; do widget="$repo_root/hancore.shibumi.$plugin/BarWidget.qml" rg -q 'serviceFor\("hancore\.shibumi\.power-state"\)' "$widget" \ diff --git a/tests/power-service-runtime-smoke.qml b/tests/power-service-runtime-smoke.qml index 860895f..5687941 100644 --- a/tests/power-service-runtime-smoke.qml +++ b/tests/power-service-runtime-smoke.qml @@ -2,7 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import "services" as Services +import "powerState" as PowerState ShellRoot { id: root @@ -10,7 +10,7 @@ ShellRoot { property int phase: 0 property int attempts: 0 - Services.PowerService { id: power } + PowerState.Service { id: power } function fail(message) { console.error("power-service-runtime-smoke:", message) diff --git a/tests/power-widgets-smoke.qml b/tests/power-widgets-smoke.qml deleted file mode 100644 index f03f2ca..0000000 --- a/tests/power-widgets-smoke.qml +++ /dev/null @@ -1,203 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import "widgets" as Widgets -import "fixtures" as Fixtures -import "services/PowerModel.js" as PowerModel - -ShellRoot { - id: root - - property int phase: 0 - property int ticks: 0 - property real batteryFullWidth: 0 - property real profileFullWidth: 0 - - function fail(message) { - console.error("power-widgets-smoke:", message) - Qt.exit(1) - } - - Fixtures.PowerTestService { id: power } - - QtObject { - id: actions - property int btopCalls: 0 - function openSystemMonitor() { btopCalls++; return true } - } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: 35 - property int sizeHorizontal: 35 - property string position: "top" - property string fontFamily: "monospace" - property color foreground: "#eeeeee" - property color barForeground: foreground - property color background: "#191919" - property color urgent: "#88bbee" - property bool foregroundAnimationEnabled: false - property var activePopout: null - property var powerService: power - property var systemActions: actions - property var visualTokens: ({ - pillHeight: 24, - pillRadius: 12, - pillPaddingX: 9, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false, - slotHeight: 28, - contentGap: 5, - compactGap: 4, - labelSize: 12, - iconSize: 15 - }) - - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function requestPopout(owner) { - var previous = activePopout - activePopout = owner - if (previous && previous !== owner && typeof previous.close === "function") - previous.close() - } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(_owner, _direction) { return false } - function targetBelongsToWindow(_target, _window) { return true } - } - - Loader { - id: batteryA - active: true - sourceComponent: Component { - Widgets.BatteryWidget { - bar: fakeBar - settings: ({ compact: false }) - panelSource: Qt.resolvedUrl("fixtures/PowerTestPanel.qml") - } - } - } - Loader { - id: batteryB - active: true - sourceComponent: Component { - Widgets.BatteryWidget { - bar: fakeBar - settings: ({ compact: true }) - panelSource: Qt.resolvedUrl("fixtures/PowerTestPanel.qml") - } - } - } - Loader { - id: profileA - active: true - sourceComponent: Component { - Widgets.PowerProfileWidget { - bar: fakeBar - settings: ({ compact: false }) - panelSource: Qt.resolvedUrl("fixtures/PowerTestPanel.qml") - } - } - } - Loader { - id: profileB - active: true - sourceComponent: Component { - Widgets.PowerProfileWidget { - bar: fakeBar - settings: ({ compact: true }) - panelSource: Qt.resolvedUrl("fixtures/PowerTestPanel.qml") - } - } - } - - Timer { - interval: 80 - repeat: true - running: true - onTriggered: { - root.ticks++ - var batA = batteryA.item - var batB = batteryB.item - var pwrA = profileA.item - var pwrB = profileB.item - if (!batA || !batB || !pwrA || !pwrB) return - - if (root.phase === 0) { - if (root.ticks < 3) return - var parsed = PowerModel.parseProfiles( - "power-saver\t0\nbalanced\t1\nperformance\t0\ninvalid name\t1\n") - if (parsed.profiles.length !== 3 || parsed.activeProfile !== "balanced" - || PowerModel.clampPercent(0.2) !== 20 - || PowerModel.duration(4260) !== "1h 11m") - return root.fail("power model parsing") - if (batA.visible || batA.implicitWidth !== 0 - || !pwrA.visible || pwrA.implicitWidth <= 0 - || power.profileConsumers !== 2) - return root.fail("desktop without battery must retain both profile views") - root.profileFullWidth = pwrA.implicitWidth - if (pwrB.implicitWidth >= root.profileFullWidth) - return root.fail("power-profile compact width") - power.hasBattery = true - root.phase++ - root.ticks = 0 - } else if (root.phase === 1) { - if (root.ticks < 3) return - if (!batA.visible || batA.implicitWidth <= 0 || batA.percent !== 20) - return root.fail("laptop battery visibility/state") - root.batteryFullWidth = batA.implicitWidth - if (batB.implicitWidth >= root.batteryFullWidth) - return root.fail("battery compact width") - if (batA.powerService !== power || batB.powerService !== power - || pwrA.powerService !== power || pwrB.powerService !== power) - return root.fail("multi-output views do not share one service owner") - if (typeof pwrA.childPanelWidget !== "function" - || pwrA.childPanelWidget("omarchy.power") !== pwrA - || typeof batA.childPanelWidget === "function" - && batA.childPanelWidget("omarchy.power") !== null) - return root.fail("canonical omarchy.power routing owner") - batA.activate() - root.phase++ - root.ticks = 0 - } else if (root.phase === 2) { - if (root.ticks < 3) return - if (!batA.opened || !batA.panelItem || power.detailConsumers !== 1 - || fakeBar.activePopout !== batA) - return root.fail("battery panel/detail lifecycle: opened=" + batA.opened - + " panel=" + batA.panelItem + " details=" + power.detailConsumers - + " active=" + fakeBar.activePopout + " expected=" + batA) - pwrA.activate(Qt.LeftButton) - root.phase++ - root.ticks = 0 - } else if (root.phase === 3) { - if (root.ticks < 3) return - if (batA.opened || power.detailConsumers !== 0 || !pwrA.opened - || !pwrA.panelItem || fakeBar.activePopout !== pwrA) - return root.fail("cross-group popout ownership") - pwrA.close() - var before = power.setProfileCalls - if (!pwrA.activate(Qt.RightButton) - || power.setProfileCalls !== before + 1 - || power.activeProfile !== "performance") - return root.fail("right-click profile cycle") - batB.activate() - power.hasBattery = false - root.phase++ - root.ticks = 0 - } else if (root.phase === 4) { - if (root.ticks < 3) return - if (batB.opened || power.detailConsumers !== 0 - || batA.visible || batB.visible || !pwrA.visible || !pwrB.visible) - return root.fail("battery removal must not remove power profiles") - stop() - console.log("power widgets smoke passed") - Qt.quit() - } - } - } -} diff --git a/tests/production-boundary-regression.py b/tests/production-boundary-regression.py new file mode 100755 index 0000000..ef8ae71 --- /dev/null +++ b/tests/production-boundary-regression.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +LINTER = ROOT / "scripts/check-production-boundary" + + +class ProductionBoundaryRegressionTests(unittest.TestCase): + def run_linter(self, root: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [str(LINTER), "--root", str(root)], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + + def fixture_result( + self, source: str, name: str = "Service.qml" + ) -> subprocess.CompletedProcess[str]: + temporary = tempfile.TemporaryDirectory(prefix="shibumi-boundary-test.") + self.addCleanup(temporary.cleanup) + root = Path(temporary.name) + plugin = root / "hancore.shibumi.fixture" + plugin.mkdir() + (plugin / name).write_text(source, encoding="utf-8") + return self.run_linter(root) + + def test_shipped_source_passes(self) -> None: + result = self.run_linter(ROOT) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("production boundary check passed", result.stdout) + + def test_direct_forbidden_boundaries_fail_closed(self) -> None: + cases = { + "absolute-component": ( + 'property string source: "/usr/share/omarchy/shell/plugins/' + 'services/example/Service.qml"\n', + "private component path", + ), + "relative-component": ( + 'property string source: "shell/plugins/services/example.qml"\n', + "private component path", + ), + "registry-provider": ( + 'property var backend: bar.registeredWidgetSource(' + '"omarchy.example")\n', + "private provider QML", + ), + "first-party-service": ( + 'property var backend: shell.firstPartyServiceFor(' + '"omarchy.example")\n', + "undeclared first-party service", + ), + "helper-state": ( + 'property var command: ["omarchy-example-status"]\n', + "helper-backed state polling", + ), + "root-escape": ( + 'import "../services" as Services\n', + "package escape", + ), + } + for name, (source, expected) in cases.items(): + with self.subTest(name=name): + result = self.fixture_result(source) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn(expected, result.stderr) + + def test_all_executables_are_scanned_and_invalid_utf8_fails(self) -> None: + with tempfile.TemporaryDirectory( + prefix="shibumi-boundary-test." + ) as temporary: + root = Path(temporary) + plugin = root / "hancore.shibumi.fixture" + plugin.mkdir() + executable = plugin / "probe.bin" + executable.write_text("omarchy-example-status\n", encoding="utf-8") + executable.chmod(0o755) + result = self.run_linter(root) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("helper-backed state polling", result.stderr) + + executable.write_text("#!/bin/sh\npgrep example\n", encoding="utf-8") + result = self.run_linter(root) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("helper-backed state polling", result.stderr) + + executable.write_bytes(b"#!/bin/sh\n\xff\n") + result = self.run_linter(root) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("undecodable production source", result.stderr) + + def test_production_symlinks_fail_closed(self) -> None: + with tempfile.TemporaryDirectory( + prefix="shibumi-boundary-test." + ) as temporary: + root = Path(temporary) + payload = root / "payload" + payload.mkdir() + (payload / "Service.qml").write_text( + "property bool safe: true\n", encoding="utf-8" + ) + plugin = root / "hancore.shibumi.fixture" + plugin.symlink_to(payload.name, target_is_directory=True) + result = self.run_linter(root) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("production symlink", result.stderr) + + with tempfile.TemporaryDirectory( + prefix="shibumi-boundary-test." + ) as temporary: + root = Path(temporary) + plugin = root / "hancore.shibumi.fixture" + plugin.mkdir() + target = plugin / "payload.bin" + target.write_text("property bool safe: true\n", encoding="utf-8") + (plugin / "Service.qml").symlink_to(target.name) + result = self.run_linter(root) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("production symlink", result.stderr) + + def test_removed_debt_requires_allowance_cleanup(self) -> None: + with tempfile.TemporaryDirectory( + prefix="shibumi-boundary-test." + ) as temporary: + root = Path(temporary) + plugin = root / "hancore.shibumi.status" + plugin.mkdir() + source = ( + ROOT / "hancore.shibumi.status/Service.qml" + ).read_text(encoding="utf-8") + (plugin / "Service.qml").write_text( + source.replace("omarchy-voxtype-status", "voxtype-status"), + encoding="utf-8", + ) + result = self.run_linter(root) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("approved omarchy-voxtype-status", result.stderr) + + def test_missing_allowlisted_file_fails_when_plugin_is_present(self) -> None: + with tempfile.TemporaryDirectory( + prefix="shibumi-boundary-test." + ) as temporary: + root = Path(temporary) + plugin = root / "hancore.shibumi.media" + plugin.mkdir() + (plugin / "manifest.json").write_text("{}\n", encoding="utf-8") + result = self.run_linter(root) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn( + "hancore.shibumi.media/Service.qml: stale boundary allowance", + result.stderr, + ) + + def test_complete_suite_rejects_allowance_for_missing_plugin(self) -> None: + with tempfile.TemporaryDirectory( + prefix="shibumi-boundary-test." + ) as temporary: + root = Path(temporary) + contracts = root / "contracts" + contracts.mkdir() + (contracts / "plugin-suite-v1.json").write_text( + "{}\n", encoding="utf-8" + ) + result = self.run_linter(root) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn( + "hancore.shibumi.audio/BarWidget.qml: stale boundary allowance", + result.stderr, + ) + + def test_package_job_runs_boundary_regression_directly(self) -> None: + workflow = ( + ROOT / ".github/workflows/package-release.yml" + ).read_text(encoding="utf-8") + self.assertIn( + "python3 tests/production-boundary-regression.py", + workflow, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/quote-service-smoke.qml b/tests/quote-service-smoke.qml index 59138d6..ae8cea4 100644 --- a/tests/quote-service-smoke.qml +++ b/tests/quote-service-smoke.qml @@ -6,14 +6,9 @@ ShellRoot { id: root property var receivedEvent: null - QtObject { - id: fakeBar - property int reactorMode: 8 - } - Services.QuoteService { id: quoteService - bar: fakeBar + backendEnabled: true } Connections { diff --git a/tests/shibumi-presentation-smoke.qml b/tests/shibumi-presentation-smoke.qml deleted file mode 100644 index d682ecd..0000000 --- a/tests/shibumi-presentation-smoke.qml +++ /dev/null @@ -1,288 +0,0 @@ -import QtQuick -import Quickshell -import "services" as Services -import "styles/shibumi" as ShibumiStyle -import "widgets" as Widgets - -ShellRoot { - id: root - - Services.SystemTelemetry { id: telemetry } - - QtObject { - id: actions - function openSystemMonitor() { return true } - } - - QtObject { - id: fakeStateService - property var config: ({ - presentation: { - border: true, - shadow: false, - frost: false, - radius: "large", - shellStyle: "shibumi" - } - }) - property color selectedColor: "#88aaff" - - function paletteColor(id) { - return id === "color05" ? selectedColor : "#ffffff" - } - - function paletteContrastColor(id) { - return id === "color05" ? "#111111" : "#ffffff" - } - } - - QtObject { - id: dynamicTooltipTarget - property string tooltipText: "Audio 90%" - } - - QtObject { - id: fakeShell - function serviceFor(pluginId) { - return pluginId === "hancore.shibumi.state" ? fakeStateService : null - } - } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: style.sizeHorizontal - property string position: "top" - property string fontFamily: style.fontFamily - property color foreground: style.foreground - property color barForeground: style.barForeground - property color background: style.background - property color urgent: style.urgent - property var visualTokens: style.visualTokens - property var shell: fakeShell - property var activePopout: null - property var tooltipTarget: dynamicTooltipTarget - property string tooltipText: "stale snapshot" - property var systemTelemetry: telemetry - property var gpuTelemetry: null - property var systemActions: actions - - function showTooltip(target, text) {} - function hideTooltip(target) {} - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(owner, direction) { return false } - } - - ShibumiStyle.Style { - id: style - bar: fakeBar - } - - Loader { - id: tooltipSurface - sourceComponent: style.tooltipSurfaceComponent - } - - Widgets.MemoryWidget { - id: memoryFull - bar: fakeBar - settings: ({ compact: false }) - } - - Widgets.MemoryWidget { - id: memoryCompact - bar: fakeBar - settings: ({ compact: true }) - } - - Widgets.CpuWidget { - id: cpuFull - bar: fakeBar - settings: ({ compact: false }) - } - - Widgets.PillSurface { - id: pillProbe - width: 100 - height: 24 - bar: fakeBar - settings: ({ color: "color05", colorMode: "fill" }) - } - - Widgets.PillSurface { - id: v1FillProbe - width: 100 - height: 24 - bar: fakeBar - v1AppearanceEnabled: true - settings: ({ - color: "color05", - colorMode: "fill", - tone: "background", - surfaceOpacity: 0.4 - }) - } - - Widgets.CpuWidget { - id: cpuCompact - bar: fakeBar - settings: ({ compact: true }) - } - - function fail(message) { - console.error("shibumi-presentation-smoke:", message) - Qt.exit(1) - } - - Timer { - interval: 1000 - running: true - onTriggered: { - if (style.sizeHorizontal !== 35 || style.exclusiveSizeHorizontal !== 38) - return root.fail("V1 bar geometry contract changed") - if (style.visualTokens.islandHeight !== 32 - || style.visualTokens.pillHeight !== 24 - || style.visualTokens.pillRadius !== 12 - || style.visualTokens.panelRadius !== 12 - || style.visualTokens.panelBorderWidth !== 1 - || style.visualTokens.panelBackground.a < 0.93 - || style.visualTokens.panelBackground.a > 0.95 - || style.visualTokens.islandInsetX !== 5 - || style.visualTokens.islandContentInsetX !== 4 - || style.visualTokens.tooltipRadius !== 6 - || style.visualTokens.tooltipPaddingX !== 10 - || style.visualTokens.tooltipPaddingY !== 4 - || style.tooltipGap !== 6) - return root.fail("V1 surface token contract changed") - const outlineSettings = { - colorMode: "border", - color: "color01", - widgetBorder: true, - widgetBorderColor: "color05" - } - if (style.visualTokens.widgetBorderColor(outlineSettings) - !== fakeStateService.selectedColor) - return root.fail("widget outline ignored its selected palette color") - if (style.visualTokens.widgetBorderColor({ - colorMode: "border", - color: "color05", - widgetBorder: true - }) !== style.visualTokens.panelBorder) - return root.fail("widget outline used the surface color without consent") - if (style.visualTokens.widgetBorderColor({ - colorMode: "border", - color: "inherit", - widgetBorder: true, - widgetBorderColor: "inherit" - }) !== style.visualTokens.panelBorder) - return root.fail("inherited widget outline lost the panel border token") - if (style.visualTokens.widgetBorderColor({ - colorMode: "border", - color: "color05", - widgetBorder: true, - widgetBorderUsesSurfaceColor: true - }) !== fakeStateService.selectedColor) - return root.fail("legacy coupled widget outline lost compatibility") - if (style.visualTokens.widgetBorderWidth({ - widgetBorderWidth: 1.5 - }) !== 1.5) - return root.fail("half-pixel widget outline width was rounded away") - if (style.visualTokens.widgetBorderWidth({ - widgetBorderWidth: 0.5 - }) !== 0.5) - return root.fail("minimum widget outline width was clamped away") - if (style.visualTokens.widgetSurfaceOpacity({ - surfaceOpacity: 0.4 - }) !== 0.4) - return root.fail("40 percent widget opacity was clamped away") - if (memoryCompact.implicitWidth >= memoryFull.implicitWidth) - return root.fail("memory compact presentation did not reduce width") - if (cpuCompact.implicitWidth >= cpuFull.implicitWidth) - return root.fail("CPU compact presentation did not reduce width") - if (memoryFull.implicitHeight !== 35 || cpuFull.implicitHeight !== 35) - return root.fail("widgets do not follow Shibumi bar height") - if (pillProbe.renderedSurfaceCount !== 1 - || pillProbe.renderedShadowCount !== 0 - || !pillProbe.shellPillVisible - || pillProbe.v1CustomFill - || pillProbe.renderedFillColor !== style.visualTokens.pill) - return root.fail("non-opted V1 pill did not retain its surface") - if (!v1FillProbe.v1CustomFill - || v1FillProbe.renderedSurfaceCount !== 1 - || Math.abs(v1FillProbe.v1FillOpacity - 0.4) > 0.001 - || Math.abs(v1FillProbe.renderedFillColor.r - - fakeStateService.selectedColor.r) > 0.001 - || Math.abs(v1FillProbe.renderedFillColor.g - - fakeStateService.selectedColor.g) > 0.001 - || Math.abs(v1FillProbe.renderedFillColor.b - - fakeStateService.selectedColor.b) > 0.001 - || Math.abs(v1FillProbe.renderedFillColor.a - 0.4) > 0.001) - return root.fail("V1 fill did not reach the native pill surface") - const staleV2SurfaceSettings = { - color: "color05", - colorMode: "border", - tone: "background" - } - if (!style.visualTokens.widgetHasFill(staleV2SurfaceSettings) - || style.visualTokens.widgetContentColor( - staleV2SurfaceSettings, "#ffffff") !== style.visualTokens.paper) - return root.fail("hidden V2 surface state disabled V1 appearance") - fakeStateService.config = ({ - presentation: { - border: true, - shadow: true, - frost: true, - radius: "large", - shellStyle: "shibumi" - } - }) - if (pillProbe.renderedShadowCount !== 1 - || style.visualTokens.barBackground.a < 0.67 - || style.visualTokens.barBackground.a > 0.69 - || style.visualTokens.pill.a < 0.17 - || style.visualTokens.pill.a > 0.19 - || style.visualTokens.panelBackground.a < 0.93 - || style.visualTokens.panelBackground.a > 0.95) - return root.fail("V1 Shadow or reference Frost alpha did not apply") - fakeStateService.config = ({ - presentation: { - border: true, - shadow: true, - frost: true, - radius: "large", - shellStyle: "full" - } - }) - if (pillProbe.renderedSurfaceCount !== 0 - || pillProbe.renderedShadowCount !== 0 - || pillProbe.shellPillVisible - || v1FillProbe.v1CustomFill - || style.visualTokens.widgetHasFill(staleV2SurfaceSettings)) - return root.fail("V1 widget appearance leaked into V2 shell") - if (style.sizeHorizontal !== 33 - || style.exclusiveSizeHorizontal !== 36 - || style.visualTokens.panelRadius !== 6 - || style.visualTokens.tileRadius !== 10 - || style.visualTokens.shellFitRadius !== 6 - || style.visualTokens.shellDockRadius !== 8) - return root.fail("V2 shell geometry contract changed") - fakeStateService.config = ({ - presentation: { - border: true, - shadow: false, - frost: false, - radius: "large", - shellStyle: "shibumi" - } - }) - if (!tooltipSurface.item || tooltipSurface.item.resolvedText !== "Audio 90%") - return root.fail("dynamic tooltip target was not resolved") - dynamicTooltipTarget.tooltipText = "Audio 100%" - if (tooltipSurface.item.resolvedText !== "Audio 100%") - return root.fail("visible tooltip did not update reactively") - console.log("shibumi presentation smoke passed") - Qt.exit(0) - } - } -} diff --git a/tests/status-widget-smoke.qml b/tests/status-widget-smoke.qml deleted file mode 100644 index 01791a7..0000000 --- a/tests/status-widget-smoke.qml +++ /dev/null @@ -1,270 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import "widgets" as Widgets -import "fixtures" as Fixtures - -ShellRoot { - id: root - - property int phase: 0 - property int phaseTicks: 0 - property real initialWidth: 0 - property var clickTargets: [] - - function fail(message) { - console.error("status-widget-smoke:", message) - Qt.exit(1) - } - - ListModel { - id: pendingNotifications - ListElement { summary: "First" } - ListElement { summary: "Second" } - ListElement { summary: "Third" } - } - - QtObject { - id: notificationService - property var pendingModel: pendingNotifications - property bool doNotDisturb: false - function setDoNotDisturb(value) { doNotDisturb = value } - function markAllSeen() {} - function dismissPending(_index) {} - } - - QtObject { - id: fakeState - property var writes: [] - - function setWidgetSetting(groupId, moduleId, key, value) { - writes = writes.concat([{ - groupId: String(groupId || ""), - moduleId: String(moduleId || ""), - key: String(key || ""), - value: JSON.parse(JSON.stringify(value)) - }]) - return true - } - } - - QtObject { - id: fakeShell - function serviceFor(pluginId) { - return pluginId === "hancore.shibumi.state" ? fakeState : null - } - function firstPartyServiceFor(pluginId) { - return pluginId === "omarchy.notifications" ? notificationService : null - } - } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: 35 - property string fontFamily: "monospace" - property color foreground: "#eeeeee" - property color barForeground: foreground - property color background: "#111111" - property color urgent: "#88bbee" - property bool foregroundAnimationEnabled: false - property var activePopout: null - property var shell: fakeShell - property var layoutConfig: ({ left: [], center: [], right: [] }) - property var clickTargets: root.clickTargets - property var barWidgetRegistry: null - property var visualTokens: ({ - v2Shell: false, - pillHeight: 24, - pillRadius: 12, - pillPaddingX: 9, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false - }) - - function widgetSettings(group, module) { - return group === "G3" ? ({ - marker: module, - layoutRevision: Number(layoutConfig.revision || 0) - }) : ({}) - } - function registerClickTarget(target) { - if (root.clickTargets.indexOf(target) < 0) - root.clickTargets = root.clickTargets.concat([target]) - } - function unregisterClickTarget(target) { - root.clickTargets = root.clickTargets.filter(item => item !== target) - } - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(_owner, _direction) { return true } - function targetBelongsToWindow(_target, _window) { return true } - } - - Component { - id: childComponent - Fixtures.StatusTestWidget {} - } - - Loader { - id: statusLoader - active: true - sourceComponent: Component { - Widgets.StatusWidget { - bar: fakeBar - updateComponent: childComponent - trayComponent: childComponent - trayDrawerSource: Qt.resolvedUrl("fixtures/TrayDrawerTestPanel.qml") - notificationPanelSource: Qt.resolvedUrl( - "fixtures/NotificationPanelTestView.qml") - } - } - } - - Timer { - interval: 80 - repeat: true - running: true - onTriggered: { - root.phaseTicks++ - const status = statusLoader.item - if (root.phase === 0) { - if (!status || !status.ready || root.phaseTicks < 3) return - if (!status.visible - || !status.updateWidget || !status.trayWidget - || !status.notificationService || root.clickTargets.length !== 4) - return root.fail("child lifecycle/readiness: visible=" + status.visible - + ", update=" + !!status.updateWidget - + ", tray=" + !!status.trayWidget - + ", notifications=" + !!status.notificationService - + ", trayModule=" + String(status.trayWidget - ? status.trayWidget.moduleName : "") - + ", trayPinned=" + Number(status.trayWidget - ? status.trayWidget.pinnedItems.length : -1) - + ", trayDrawer=" + Number(status.trayWidget - ? status.trayWidget.drawerCount : -1) - + ", clickTargets=" + root.clickTargets.length) - if (status.updateWidget.moduleName !== "hancore.shibumi.update-center" - || status.trayWidget.moduleName !== "omarchy.tray" - || status.updateWidget.settings.marker !== "hancore.shibumi.update-center" - || status.trayWidget.settings.marker !== "omarchy.tray") - return root.fail("child identity/settings injection") - if (status.childPanelWidget("hancore.shibumi.update-center") - !== status.updateWidget - || status.childPanelWidget("omarchy.notifications") !== status - || !status.ownsPanelWidget(status.updateWidget) - || status.childPanelWidget("omarchy.tray") !== null) - return root.fail("nested panel routing") - if (!status.updatePresented || !status.trayPresented - || !status.notificationPresented - || status.notificationService.pendingModel.count !== 3 - || status.pendingCount !== 3 || status.recentCount !== 0 - || status.notificationCount !== 3) - return root.fail("V1 tray/notification facade state") - if (status.v2Mode - || status.horizontalInset !== 9 - || status.childGap !== 4 - || status.updateSlotWidth !== 22 - || status.traySlotWidth !== 42 - || status.notificationSlotWidth !== 22 - || status.trayPinnedIconOffset !== 1 - || status.notificationIconOffset !== 1 - || status.implicitWidth !== 112) - return root.fail("V1 status geometry is not symmetric: inset=" - + Number(status.horizontalInset) + ", gap=" - + Number(status.childGap) + ", slots=" - + JSON.stringify([status.updateSlotWidth, - status.traySlotWidth, status.notificationSlotWidth]) - + ", trayOffset=" + Number(status.trayPinnedIconOffset) - + ", bellOffset=" + Number(status.notificationIconOffset) - + ", width=" + Number(status.implicitWidth)) - status.updateWidget.open() - if (!status.toggleTrayDrawer() || !status.trayDrawerOpen - || status.updateWidget.popupOpen) - return root.fail("tray drawer did not open") - - root.initialWidth = status.implicitWidth - fakeBar.layoutConfig = ({ - left: [], center: [], right: [], revision: 1 - }) - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 1) { - if (!status.trayDrawerLoaded || root.phaseTicks < 3) return - if (status.trayWidget.settings.layoutRevision !== 1) - return root.fail("tray settings did not react to host layout changes") - const drawer = status.trayDrawerItem - if (drawer.ownerWidget !== status - || drawer.trayBackend !== status.trayWidget - || drawer.anchorItem === null - || drawer.bar !== fakeBar) - return root.fail("tray drawer injection contract") - status.trayWidget.togglePin("fixture-drawer") - if (status.trayWidget.pinToggleCount !== 1 - || fakeState.writes.length !== 2 - || fakeState.writes[0].groupId !== "G3" - || fakeState.writes[0].moduleId !== "omarchy.tray" - || fakeState.writes[0].key !== "pinned" - || fakeState.writes[0].value[0] !== "fixture-drawer" - || fakeState.writes[1].key !== "hidden") - return root.fail("embedded tray pin did not persist into G3: " - + JSON.stringify(fakeState.writes)) - status.closeTrayDrawer() - status.trayWidget.pinnedItems = [] - status.trayWidget.drawerItems = [] - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 2) { - if (root.phaseTicks < 3) return - if (status.trayDrawerLoaded) - return root.fail("closed tray drawer remained loaded") - if (status.implicitWidth >= root.initialWidth) - return root.fail("hidden tray did not release width: initial=" - + root.initialWidth + ", current=" + status.implicitWidth - + ", anyPresented=" + status.hasVisibleChild - + ", rowWidth=" + status.contentWidth) - if (!status.open() || !status.opened - || !status.notificationPanelOpen) - return root.fail("local notification panel did not open") - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 3) { - if (!status.notificationPanelLoaded || root.phaseTicks < 3) return - const notificationPanel = status.notificationPanelItem - if (notificationPanel.ownerWidget !== status - || notificationPanel.notificationService - !== status.notificationService - || notificationPanel.anchorItem === null - || notificationPanel.bar !== fakeBar - || notificationPanel.pendingCount !== 3 - || fakeBar.activePopout !== status) - return root.fail("notification panel injection/popout ownership") - status.trayWidget.managePopupOpen = true - status.close() - root.phase++ - root.phaseTicks = 0 - } else if (root.phase === 4) { - if (root.phaseTicks < 3) return - if (status.opened || status.notificationPanelLoaded - || fakeBar.activePopout !== null) - return root.fail("nested close cleanup") - statusLoader.active = false - root.phase++ - root.phaseTicks = 0 - } else { - if (root.phaseTicks < 3) return - if (root.clickTargets.length !== 0 || fakeBar.activePopout !== null) - return root.fail("destruction cleanup") - stop() - console.log("status widget smoke passed") - Qt.quit() - } - } - } -} diff --git a/tests/test_package_release.py b/tests/test_package_release.py index 08f1d20..bcd82a3 100644 --- a/tests/test_package_release.py +++ b/tests/test_package_release.py @@ -317,6 +317,7 @@ def test_release_archive_is_reproducible_and_complete(self) -> None: 255, "gzip OS header must be independent of the Python host", ) + extracted = output / "archive-root" with tarfile.open(archive, "r:gz") as payload: names = payload.getnames() private_home = b"/home/" + b"hancore/" @@ -330,6 +331,7 @@ def test_release_archive_is_reproducible_and_complete(self) -> None: stream.read(), f"release payload exposes a private path: {member.name}", ) + payload.extractall(extracted, filter="data") roots = {name.split("/", 1)[0] for name in names} self.assertEqual(roots, {f"shibumi-shell-{inventory['version']}"}) self.assertFalse( @@ -344,6 +346,18 @@ def test_release_archive_is_reproducible_and_complete(self) -> None: ) manifests = [name for name in names if name.endswith("/manifest.json")] self.assertEqual(len(manifests), 24) + boundary = subprocess.run( + [ + str(ROOT / "scripts/check-production-boundary"), + "--root", + str(extracted / next(iter(roots))), + ], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(boundary.returncode, 0, boundary.stderr) if __name__ == "__main__": diff --git a/tests/v2-feature-evidence-regression.sh b/tests/v2-feature-evidence-regression.sh index 78ebfd6..1fb435f 100755 --- a/tests/v2-feature-evidence-regression.sh +++ b/tests/v2-feature-evidence-regression.sh @@ -18,7 +18,8 @@ jq -e ' and (.features | length) == 36 and (.features | all( (.implementation | type) == "array" and (.implementation | length) > 0 - and (.tests | type) == "array" and (.tests | length) > 0)) + and (.tests | type) == "array" and (.tests | length) > 0 + and (.tests | length) == (.tests | unique | length))) ' "$evidence_contract" >/dev/null || fail "evidence schema is incomplete" port_baseline=$(jq -r '.referenceBaseline' "$port_contract") diff --git a/tests/workspace-model-regression.qml b/tests/workspace-model-regression.qml index 9ce6098..dcf35e9 100644 --- a/tests/workspace-model-regression.qml +++ b/tests/workspace-model-regression.qml @@ -1,6 +1,6 @@ import QtQuick import QtTest -import "../services/WorkspaceModel.js" as WorkspaceModel +import "../hancore.shibumi.workspaces/WorkspaceModel.js" as WorkspaceModel TestCase { name: "WorkspaceModel" diff --git a/tests/workspace-widget-smoke.qml b/tests/workspace-widget-smoke.qml deleted file mode 100644 index 310e4e1..0000000 --- a/tests/workspace-widget-smoke.qml +++ /dev/null @@ -1,173 +0,0 @@ -import QtQuick -import Quickshell -import "adapters" as Adapters -import "services" as Services -import "widgets" as Widgets - -ShellRoot { - id: root - - property int phase: 0 - property int geometryWaits: 0 - - function fail(message) { - console.error("workspace-widget-smoke:", message) - Qt.exit(1) - } - - QtObject { - id: fakeBar - property bool vertical: false - property int barSize: 28 - property string position: "top" - property string fontFamily: "monospace" - property color foreground: "#e8e8e8" - property color barForeground: foreground - property color background: "#181818" - property color urgent: "#88bbee" - property var visualTokens: ({ - pillHeight: 24, - pillRadius: 12, - pill: "#332f2f", - pillBorder: "#555050", - pillBorderWidth: 1, - pillShadow: "#000000", - shadowEnabled: false, - contentGap: 5, - labelSize: 12, - presentation: ({ radius: "large" }), - workspacePillPadding: function(style) { return style === "numbers" ? 2 : 4 } - }) - property bool foregroundAnimationEnabled: false - property var activePopout: null - property var workspaceConfig: ({ version: 1, mode: "5", style: "numbers" }) - property var workspaceService: workspaceState - property var workspaceActions: actions - property string lastCommand: "" - - function run(command) { lastCommand = String(command || "") } - function mutateShibumiConfig(mutator) { - const next = { - version: 1, - workspace: JSON.parse(JSON.stringify(workspaceConfig)) - } - mutator(next) - workspaceConfig = next.workspace - return true - } - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - function requestPopout(owner) { activePopout = owner } - function releasePopout(owner) { if (activePopout === owner) activePopout = null } - function switchPanelFrom(_owner, _direction) { return false } - } - - Adapters.WorkspaceActions { - id: actions - bar: fakeBar - } - - Services.WorkspaceService { - id: workspaceState - bar: fakeBar - actionAdapter: actions - config: fakeBar.workspaceConfig - focusedWorkspaceSource: ({ id: 8 }) - workspaceSource: [ - { id: 2, toplevels: { values: [{}] } }, - { id: 8, toplevels: { values: [{}, {}] } }, - { id: -1, toplevels: { values: [{}] } } - ] - } - - Services.WorkspaceService { - id: occupiedOutsidePersistState - actionAdapter: actions - config: ({ version: 1, mode: "5", style: "numbers" }) - focusedWorkspaceSource: ({ id: 2 }) - workspaceSource: [ - { id: 2, toplevels: { values: [{}] } }, - { id: 7, toplevels: { values: [{}] } } - ] - } - - Widgets.WorkspaceWidget { - id: widget - bar: fakeBar - settings: ({}) - panelSource: Qt.resolvedUrl("WorkspaceTestPanel.qml") - } - - Timer { - interval: 60 - repeat: true - running: true - onTriggered: { - if (root.phase === 0) { - if (workspaceState.entries.length !== 2 - || workspaceState.visibleWorkspaceIds.join(",") !== "1,2,3,4,5,8" - || occupiedOutsidePersistState.visibleWorkspaceIds.join(",") - !== "1,2,3,4,5,7" - || widget.renderedWorkspaceCount !== 6 || widget.panelLoaded) - return root.fail("initial service/widget contract") - if (widget.workspacePadding !== 2 || Math.round(widget.implicitWidth) !== 161) - return root.fail("V1 numbers presentation geometry") - if (!widget.activateWorkspace(8) - || fakeBar.lastCommand !== "hyprctl dispatch 'hl.dsp.focus({ workspace = \"8\" })'") - return root.fail("validated workspace dispatch") - if (widget.workspaceTooltip(8) - !== "Workspace 8 · 2 windows · Right-click for workspace panel") - return root.fail("workspace tooltip contract") - if (!actions.focusWorkspace(9999) - || fakeBar.lastCommand - !== "hyprctl dispatch 'hl.dsp.focus({ workspace = \"9999\" })'") - return root.fail("workspace dispatch upper bound") - fakeBar.lastCommand = "" - if (actions.focusWorkspace(-1) || actions.focusWorkspace(1.5) - || actions.focusWorkspace(10000) - || actions.focusWorkspace("1; reboot") - || fakeBar.lastCommand !== "") - return root.fail("invalid workspace dispatch was not rejected") - if (!workspaceState.setPreference("style", "magic") - || !workspaceState.setPreference("mode", "active") - || fakeBar.workspaceConfig.style !== "magic" - || fakeBar.workspaceConfig.mode !== "active" - || workspaceState.setPreference("mode", "unsafe")) - return root.fail("workspace preference persistence") - widget.open() - } else if (root.phase === 1) { - if (widget.workspaceStyle !== "magic" - || workspaceState.visibleWorkspaceIds.join(",") !== "2,8" - || widget.renderedWorkspaceCount !== 2) - return root.fail("reactive workspace presentation") - if (widget.workspacePadding !== 4 || Math.round(widget.implicitWidth) !== 51) { - root.geometryWaits++ - if (root.geometryWaits < 10) return - return root.fail("V1 magic presentation geometry: " + widget.implicitWidth) - } - root.geometryWaits = 0 - if (!widget.opened || !widget.panelLoaded || !widget.panelLoaderReady) - return root.fail("lazy workspace panel open") - widget.close() - if (!workspaceState.setPreference("style", "default") - || !workspaceState.setPreference("mode", "10")) - return root.fail("workspace presentation reset") - } else { - if (widget.opened || widget.panelLoaded - || widget.workspaceStyle !== "default" - || workspaceState.visibleWorkspaceIds.length !== 10 - || widget.renderedWorkspaceCount !== 10) - return root.fail("lazy workspace panel close") - if (widget.workspacePadding !== 4 || Math.round(widget.implicitWidth) !== 229) { - root.geometryWaits++ - if (root.geometryWaits < 10) return - return root.fail("V1 default presentation geometry: " + widget.implicitWidth) - } - stop() - console.log("workspace widget smoke passed") - Qt.quit() - } - root.phase++ - } - } -} diff --git a/tests/workspaces-plugin-regression.sh b/tests/workspaces-plugin-regression.sh index fcbc151..6afa9f2 100755 --- a/tests/workspaces-plugin-regression.sh +++ b/tests/workspaces-plugin-regression.sh @@ -17,10 +17,6 @@ fail() { [[ -d $omarchy_path/shell ]] || fail "Omarchy shell not found: $omarchy_path/shell" [[ -x $quickshell_bin ]] || fail "Quickshell not found: $quickshell_bin" -cmp -s "$repo_root/services/WorkspaceModel.js" \ - "$repo_root/hancore.shibumi.workspaces/WorkspaceModel.js" \ - || fail "standalone and plugin workspace models drifted" - mkdir -p "$tmpdir/runtime" "$tmpdir/fixtures" chmod 700 "$tmpdir/runtime" cp -a -- "$repo_root/hancore.shibumi.workspaces" "$tmpdir/workspaces" diff --git a/widgets/AiUsagePanel.qml b/widgets/AiUsagePanel.qml deleted file mode 100644 index 0525919..0000000 --- a/widgets/AiUsagePanel.qml +++ /dev/null @@ -1,545 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var aiService - readonly property var provider: aiService ? aiService.selectedProvider : null - readonly property int renderedProviderCount: providerTabs.count - readonly property bool primaryUsageVisible: primaryUsage.visible - readonly property bool secondaryUsageVisible: secondaryUsage.visible - readonly property var providerModels: provider - && Array.isArray(provider.models) ? provider.models : [] - readonly property int renderedModelCount: modelRepeater.count - readonly property bool providerReady: provider && provider.ready !== undefined - ? provider.ready === true : true - readonly property bool providerHasUsage: provider - && (Number(provider.rateLimitPercent) >= 0 - || Number(provider.secondaryRateLimitPercent) >= 0) - readonly property bool providerHasCurrentData: provider && aiService - && typeof aiService.providerHasCurrentData === "function" - ? aiService.providerHasCurrentData(provider) - : provider && (providerHasUsage - || Number(provider.todayTotalTokens) > 0 - || Number(provider.todayPrompts) > 0 - || Number(provider.todaySessions) > 0 - || providerModels.length > 0) - readonly property string providerEmptyStateText: !provider - ? "No supported AI usage data was found." - : providerHasCurrentData ? "" - : aiService && typeof aiService.providerCurrentDataMessage === "function" - ? aiService.providerCurrentDataMessage(provider) - : String(provider.authHelpText || "") - || "No current usage or limit data." - - owner: ownerWidget - open: ownerWidget.opened - focusTarget: keyCatcher - contentWidth: fittedContentWidth(Commons.Style.space(360)) - contentHeight: fittedContentHeight(contentColumn.implicitHeight, - Commons.Style.space(560)) - - function moveProvider(direction) { - if (aiService) aiService.cycleTool(direction) - } - - function resetText(timestamp) { - return provider && aiService - ? aiService.resetText(provider, timestamp) : "" - } - - function providerTabLabel(providerValue) { - if (!providerValue) return "AI" - const id = String(providerValue.providerId || "") - if (id === "claude") return "Claude" - if (id === "codex") return "Codex" - if (id === "opencode") return "OpenCode" - return String(providerValue.providerName || id || "AI") - } - - function providerHeading(providerValue) { - if (!providerValue) return "AI" - const id = String(providerValue.providerId || "") - if (id === "claude") return "Claude Code" - if (id === "codex") return "OpenAI Codex" - if (id === "opencode") return "OpenCode" - return String(providerValue.providerName || id || "AI") - } - - function tierLabel(providerValue) { - if (!providerValue) return "" - return aiService && typeof aiService.displayTierLabel === "function" - ? aiService.displayTierLabel(providerValue.tierLabel) - : String(providerValue.tierLabel || "") - } - - function displayPercent(providerValue, value) { - return aiService && typeof aiService.displayPercent === "function" - ? aiService.displayPercent(providerValue, value) : Number(value) - } - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.ownerWidget.close() - onTabRequested: function(direction) { panel.ownerWidget.switchPanel(direction) } - onMoveRequested: function(dx, _dy) { if (dx !== 0) panel.moveProvider(dx) } - - Flickable { - id: scroller - anchors.fill: parent - contentWidth: width - contentHeight: contentColumn.implicitHeight - clip: true - boundsBehavior: Flickable.StopAtBounds - interactive: contentHeight > height - - Column { - id: contentColumn - width: scroller.width - spacing: Commons.Style.space(8) - - Item { - id: header - width: parent.width - height: Commons.Style.space(28) - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "AI USAGE" - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.subtitle - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Row { - id: actionRow - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: Commons.Style.space(2) - - IconAction { - icon: panel.aiService && panel.aiService.refreshing - ? "sync" : "refresh" - tooltip: panel.aiService && panel.aiService.refreshing - ? "Refreshing usage" : "Refresh usage" - action: function() { panel.aiService.refreshAll(true) } - } - - IconAction { - icon: "close" - tooltip: "Close" - action: function() { panel.ownerWidget.close() } - } - } - } - - Row { - id: providerRow - width: parent.width - height: Commons.Style.space(28) - spacing: Commons.Style.space(6) - - Repeater { - id: providerTabs - model: panel.aiService ? panel.aiService.providers : [] - - Rectangle { - id: providerTab - required property var modelData - readonly property bool selected: - panel.aiService.selectedTool === modelData.providerId - readonly property bool hovered: tabMouse.containsMouse - width: (providerRow.width - Math.max(0, - (panel.aiService.providers.length - 1) * providerRow.spacing)) - / Math.max(1, panel.aiService.providers.length) - height: parent.height - radius: panel.controlRadius - color: selected ? panel.controlActiveFillColor - : hovered ? panel.controlHoverFillColor : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: selected || hovered ? panel.controlAccent - : panel.controlBorderColor - - Behavior on color { ColorAnimation { duration: 100 } } - Behavior on border.color { ColorAnimation { duration: 100 } } - - Text { - anchors.centerIn: parent - text: panel.providerTabLabel(providerTab.modelData) - color: providerTab.selected || providerTab.hovered - ? panel.controlAccent : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - font.weight: providerTab.selected ? Font.Medium : Font.Normal - renderType: Text.NativeRendering - } - - MouseArea { - id: tabMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.aiService.selectTool(providerTab.modelData.providerId) - } - } - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.dividerColor - } - - Item { - visible: panel.provider !== null - width: parent.width - height: visible ? Commons.Style.space(16) : 0 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: panel.provider ? panel.providerHeading(panel.provider) - + (panel.tierLabel(panel.provider) !== "" - ? " · " + panel.tierLabel(panel.provider) : "") : "" - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: panel.providerReady ? "live" : "stale" - color: panel.providerReady ? panel.controlMuted - : panel.controlAccent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - - Text { - visible: panel.providerEmptyStateText !== "" - width: parent.width - text: panel.providerEmptyStateText - wrapMode: Text.WordWrap - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - renderType: Text.NativeRendering - } - - UsageRow { - id: primaryUsage - visible: panel.provider && Number(panel.provider.rateLimitPercent) >= 0 - label: panel.provider ? String(panel.provider.rateLimitLabel || "Primary") : "" - value: panel.provider ? panel.displayPercent(panel.provider, - panel.provider.rateLimitPercent) : 0 - dimmed: !panel.providerReady - } - - UsageRow { - id: secondaryUsage - visible: panel.provider && Number(panel.provider.secondaryRateLimitPercent) >= 0 - label: panel.provider - ? String(panel.provider.secondaryRateLimitLabel || "Secondary") : "" - value: panel.provider ? panel.displayPercent(panel.provider, - panel.provider.secondaryRateLimitPercent) : 0 - dimmed: !panel.providerReady - } - - DetailRow { - visible: primaryUsage.visible && panel.resetText( - panel.provider.rateLimitResetAt) !== "" - label: primaryUsage.label + " resets in" - value: visible ? panel.resetText(panel.provider.rateLimitResetAt) : "" - } - DetailRow { - visible: secondaryUsage.visible && panel.resetText( - panel.provider.secondaryRateLimitResetAt) !== "" - label: secondaryUsage.label + " resets in" - value: visible ? panel.resetText( - panel.provider.secondaryRateLimitResetAt) : "" - } - DetailRow { - visible: panel.provider && String(panel.provider.usageStatusText || "") !== "" - label: panel.provider && String(panel.provider.providerId || "") === "codex" - ? "General limit" : "Status" - value: panel.provider ? String(panel.provider.usageStatusText || "") : "" - } - DetailRow { - visible: panel.provider && Number(panel.provider.windowTokens) > 0 - label: panel.provider && String(panel.provider.providerId || "") - === "opencode" ? "5h tokens" : "Tokens" - value: visible && panel.aiService - ? panel.aiService.formatTokens(panel.provider.windowTokens) : "" - } - DetailRow { - visible: panel.provider && Number(panel.provider.hourlyTokens) > 0 - label: panel.provider && String(panel.provider.providerId || "") - === "opencode" ? "1h rate" : "Rate" - value: visible && panel.aiService - ? panel.aiService.formatTokens(panel.provider.hourlyTokens) + "/h" : "" - } - DetailRow { - visible: panel.provider && Number(panel.provider.todayTotalTokens) > 0 - label: "Today" - value: visible && panel.aiService - ? panel.aiService.formatTokens(panel.provider.todayTotalTokens) + " tokens" : "" - } - DetailRow { - visible: panel.provider && Number(panel.provider.todayPrompts) > 0 - label: "Today prompts" - value: visible ? String(Math.round( - Number(panel.provider.todayPrompts) || 0)) : "" - } - DetailRow { - visible: panel.provider && Number(panel.provider.todaySessions) > 0 - label: "Today sessions" - value: visible ? String(Math.round( - Number(panel.provider.todaySessions) || 0)) : "" - } - DetailRow { - visible: panel.provider && String(panel.provider.latestModel || "") !== "" - label: panel.provider && String(panel.provider.providerId || "") - === "opencode" ? "Latest today" : "Latest" - value: panel.provider ? String(panel.provider.latestModel || "") : "" - } - - Item { - visible: panel.providerModels.length > 0 - width: parent.width - height: visible ? Commons.Style.space(16) : 0 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "MODELS" - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.letterSpacing: 1 - renderType: Text.NativeRendering - } - Text { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: panel.provider && String(panel.provider.providerId || "") - === "opencode" ? "today" : "recent" - color: panel.controlMuted - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - - Repeater { - id: modelRepeater - model: panel.providerModels - delegate: ModelUsageRow { - required property var modelData - width: contentColumn.width - entry: modelData - } - } - } - } - } - - component IconAction: Ui.CursorSurface { - id: iconAction - required property string icon - required property string tooltip - required property var action - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: iconAction.icon - color: iconAction.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: iconAction.hasCursor = containsMouse - onClicked: iconAction.action() - } - - ShibumiPanelToolTip { - panel: panel - visible: iconAction.tooltip !== "" && actionMouse.containsMouse - text: iconAction.tooltip - } - } - - component UsageRow: Item { - id: usageRow - required property string label - required property real value - property bool dimmed: false - width: parent.width - height: Commons.Style.space(16) - - Text { - id: usageLabel - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: usageRow.label - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - font.letterSpacing: 1 - renderType: Text.NativeRendering - } - - Text { - id: usageValue - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: Math.round(usageRow.value) + "%" - color: usageRow.dimmed ? panel.controlMuted : panel.controlAccent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Rectangle { - anchors.left: usageLabel.right - anchors.leftMargin: Commons.Style.space(8) - anchors.right: usageValue.left - anchors.rightMargin: Commons.Style.space(8) - anchors.verticalCenter: parent.verticalCenter - height: Commons.Style.space(8) - radius: height / 2 - color: Commons.Util.alpha(panel.controlAccent, 0.15) - Rectangle { - width: parent.width * Math.max(0, Math.min(100, usageRow.value)) / 100 - height: parent.height - radius: height / 2 - color: panel.controlAccent - Behavior on width { NumberAnimation { duration: 300; easing.type: Easing.OutCubic } } - } - } - } - - component DetailRow: Row { - required property string label - required property string value - width: parent.width - height: Commons.Style.space(16) - - Text { - width: parent.width * 0.45 - text: parent.label - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - renderType: Text.NativeRendering - } - Text { - width: parent.width * 0.55 - horizontalAlignment: Text.AlignRight - elide: Text.ElideLeft - text: parent.value - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - renderType: Text.NativeRendering - } - } - - component ModelUsageRow: Item { - id: modelRow - required property var entry - height: Commons.Style.space(42) - readonly property real percent: Math.max(0, Math.min(100, - Number(entry && entry.pct) || 0)) - - Text { - id: modelName - anchors.left: parent.left - anchors.top: parent.top - width: parent.width * 0.68 - text: String(modelRow.entry && modelRow.entry.name || "") - elide: Text.ElideRight - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - anchors.right: parent.right - anchors.top: parent.top - text: String(modelRow.entry && modelRow.entry.totalLabel || "") - color: panel.controlAccent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: modelName.bottom - anchors.topMargin: Commons.Style.space(5) - height: Commons.Style.space(6) - radius: height / 2 - color: Commons.Util.alpha(panel.controlAccent, 0.14) - - Rectangle { - width: parent.width * modelRow.percent / 100 - height: parent.height - radius: height / 2 - color: panel.controlAccent - Behavior on width { NumberAnimation { duration: 300 } } - } - } - - Text { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - text: "I " + String(modelRow.entry && modelRow.entry.inputLabel || "0") - + " O " + String(modelRow.entry && modelRow.entry.outputLabel || "0") - + (String(modelRow.entry && modelRow.entry.reasoningLabel || "0") !== "0" - ? " R " + String(modelRow.entry.reasoningLabel) : "") - + (String(modelRow.entry && modelRow.entry.cacheReadLabel || "0") !== "0" - ? " CR " + String(modelRow.entry.cacheReadLabel) : "") - + (String(modelRow.entry && modelRow.entry.cacheWriteLabel || "0") !== "0" - ? " CW " + String(modelRow.entry.cacheWriteLabel) : "") - + (String(modelRow.entry && modelRow.entry.todayLabel || "0") !== "0" - ? " today " + String(modelRow.entry.todayLabel) : "") - elide: Text.ElideRight - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.space(9) - renderType: Text.NativeRendering - } - } -} diff --git a/widgets/AiUsageWidget.qml b/widgets/AiUsageWidget.qml deleted file mode 100644 index 16f9f8e..0000000 --- a/widgets/AiUsageWidget.qml +++ /dev/null @@ -1,211 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui -import "../menu" as Menu - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.ai" - manageIpc: false - property url panelSource: Qt.resolvedUrl("AiUsagePanel.qml") - readonly property var aiService: bar ? bar.aiUsageService : null - readonly property var provider: aiService ? aiService.selectedProvider : null - readonly property string providerId: provider - ? String(provider.providerId || "") : "" - readonly property int usagePercent: aiService - ? aiService.usagePercent(provider) : -1 - readonly property int steppedPercent: usagePercent < 0 - ? 0 : Math.round(usagePercent / 5) * 5 - readonly property var tokens: bar ? bar.visualTokens : null - readonly property var interactionTarget: actionButton - readonly property bool panelLoaded: panelLoader.item !== null - readonly property var panelItem: panelLoader.item - readonly property string tooltipText: aiService - ? aiService.tooltipText() : "No AI usage providers detected" - - visible: aiService && aiService.providers.length > 0 - implicitWidth: visible ? (bar && bar.vertical - ? bar.barSize : aiSurface.implicitWidth) : 0 - implicitHeight: visible ? (bar ? bar.barSize : Commons.Style.space(35)) : 0 - - function syncPanelLoader() { - if (!opened) { - panelLoader.source = "" - return - } - panelLoader.setSource(panelSource, { - anchorItem: aiSurface, - bar: root.bar, - ownerWidget: root, - aiService: root.aiService - }) - } - - function childPanelWidget(pluginId) { - const id = String(pluginId || "") - return id === moduleName || id === "omarchy.agents" - || id === "omarchy.model-usage" ? root : null - } - - onOpenedChanged: syncPanelLoader() - - Item { - id: aiSurface - anchors.centerIn: parent - implicitWidth: root.bar && root.bar.vertical - ? root.bar.barSize : contentRow.implicitWidth + 2 * (root.tokens ? root.tokens.pillPaddingX : 9) - implicitHeight: root.tokens ? root.tokens.slotHeight : Commons.Style.space(28) - width: implicitWidth - height: implicitHeight - - PillSurface { - anchors.fill: parent - anchors.topMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - anchors.bottomMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - bar: root.bar - } - - Ui.WidgetButton { - id: actionButton - anchors.fill: parent - bar: root.bar - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - fixedWidth: aiSurface.width - fixedHeight: aiSurface.height - tooltipText: root.tooltipText - onPressed: function(button) { - if (!root.aiService) return - if (button === Qt.RightButton) root.aiService.refreshAll(true) - else if (button === Qt.MiddleButton) root.aiService.cycleTool(1) - else root.toggle() - } - onWheelMoved: function(delta) { - if (root.aiService) root.aiService.cycleTool(delta > 0 ? -1 : 1) - } - } - - Row { - id: contentRow - anchors.centerIn: parent - spacing: root.tokens ? root.tokens.compactGap : Commons.Style.space(5) - - Item { - id: providerIcon - anchors.verticalCenter: parent.verticalCenter - width: root.providerId === "opencode" ? 20 - : root.providerId === "codex" ? 14 : 15 - height: root.providerId === "opencode" ? 12 - : root.providerId === "codex" ? 14 : 15 - - Item { - anchors.centerIn: parent - width: 15 - height: 15 - visible: root.providerId === "claude" - - Text { - anchors.centerIn: parent - text: "\udb85\ude7a" - color: root.bar ? Qt.rgba(root.bar.foreground.r, - root.bar.foreground.g, root.bar.foreground.b, 0.25) - : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 14 - renderType: Text.QtRendering - } - - Item { - width: parent.width - height: root.steppedPercent > 0 - ? Math.min(parent.height, Math.max(parent.height * root.steppedPercent / 100, - parent.height * 0.25)) : 0 - anchors.bottom: parent.bottom - clip: true - Behavior on height { NumberAnimation { duration: 600; easing.type: Easing.OutCubic } } - - Text { - width: providerIcon.width - height: providerIcon.height - anchors.bottom: parent.bottom - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: "\udb85\ude7a" - color: root.bar ? root.bar.urgent : Commons.Color.urgent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 14 - renderType: Text.QtRendering - } - } - } - - Item { - anchors.fill: parent - visible: root.providerId === "codex" || root.providerId === "opencode" - - Menu.TintedImage { - anchors.fill: parent - source: root.providerId === "opencode" - ? Qt.resolvedUrl("../assets/opencode-mark.svg") - : Qt.resolvedUrl("../assets/codex.svg") - sourceSize: root.providerId === "opencode" - ? Qt.size(20, 12) : Qt.size(56, 56) - smooth: root.providerId !== "opencode" - mipmap: root.providerId !== "opencode" - tint: root.bar ? Qt.rgba(root.bar.foreground.r, - root.bar.foreground.g, root.bar.foreground.b, - root.providerId === "codex" ? 0.65 : 0.5) - : Commons.Color.foreground - } - - Item { - width: parent.width - height: root.steppedPercent > 0 - ? Math.min(parent.height, Math.max(parent.height * root.steppedPercent / 100, - parent.height * 0.22)) : 0 - anchors.bottom: parent.bottom - clip: true - Behavior on height { NumberAnimation { duration: 600; easing.type: Easing.OutCubic } } - - Menu.TintedImage { - width: providerIcon.width - height: providerIcon.height - anchors.bottom: parent.bottom - source: root.providerId === "opencode" - ? Qt.resolvedUrl("../assets/opencode-mark.svg") - : Qt.resolvedUrl("../assets/codex.svg") - sourceSize: root.providerId === "opencode" - ? Qt.size(20, 12) : Qt.size(56, 56) - smooth: root.providerId !== "opencode" - mipmap: root.providerId !== "opencode" - tint: root.bar ? root.bar.urgent : Commons.Color.urgent - } - } - } - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: root.usagePercent >= 0 - ? String(root.usagePercent).padStart(2, "0") + "%" : "··" - color: root.bar ? Qt.rgba(root.bar.foreground.r, - root.bar.foreground.g, root.bar.foreground.b, 0.88) - : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 12 - renderType: Text.NativeRendering - } - } - } - - Loader { - id: panelLoader - } -} diff --git a/widgets/AudioPanel.qml b/widgets/AudioPanel.qml deleted file mode 100644 index 741f568..0000000 --- a/widgets/AudioPanel.qml +++ /dev/null @@ -1,630 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell.Services.Pipewire -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var audioBackend - - property var displaySinks: [] - property var displaySources: [] - property var displayStreams: [] - readonly property real outputVolume: audioBackend - ? Number(audioBackend.outputVolume || 0) : 0 - readonly property bool outputMuted: audioBackend - ? audioBackend.outputMuted === true : false - readonly property real inputVolume: audioBackend - ? Number(audioBackend.inputVolume || 0) : 0 - readonly property bool inputMuted: audioBackend - ? audioBackend.inputMuted === true : false - readonly property real microphoneLevel: inputMuted || !audioBackend - ? 0 : peakToMeter(inputPeakMonitor.peak) - readonly property int renderedSinkCount: displaySinks.length - readonly property int renderedSourceCount: displaySources.length - readonly property int renderedStreamCount: displayStreams.length - - owner: ownerWidget - open: ownerWidget.opened && audioBackend && audioBackend.ready - focusTarget: keyCatcher - gap: 8 - padding: 12 - contentWidth: fittedContentWidth(280) - contentHeight: fittedContentHeight(contentColumn.implicitHeight, - Commons.Style.space(580)) - - function listSnapshot(values) { - const result = [] - if (!values) return result - for (let i = 0; i < values.length; i++) { - if (values[i]) result.push(values[i]) - } - return result - } - - function refreshModels() { - if (!open || !audioBackend) return - displaySinks = listSnapshot(audioBackend.audioSinks) - displaySources = listSnapshot(audioBackend.audioSources) - displayStreams = listSnapshot(audioBackend.audioStreams) - } - - function scheduleRefresh() { - if (open) modelRefresh.restart() - } - - function clearModels() { - modelRefresh.stop() - displaySinks = [] - displaySources = [] - displayStreams = [] - } - - function sameNode(left, right) { - if (!left || !right) return false - if (left === right) return true - return left.id !== undefined && right.id !== undefined - && String(left.id) === String(right.id) - } - - function peakToMeter(peak) { - const value = Number(peak || 0) - if (!isFinite(value) || value <= 0) return 0 - const amplitude = value * value * value - const db = 20 * Math.log(amplitude) / Math.LN10 - const noiseFloor = -55 - if (db <= noiseFloor) return 0 - return Math.max(0, Math.min(1, (db - noiseFloor) / -noiseFloor)) - } - - function adjustOutput(delta) { - if (audioBackend) audioBackend.setOutputVolume(outputVolume + delta) - } - - function sliderKnobColor(muted, sliderEnabled) { - const stateOpacity = (muted ? 0.55 : 1) - * (sliderEnabled ? 1 : 0.5) - return Commons.Util.alpha(controlAccent, controlAccent.a * stateOpacity) - } - - onOpenChanged: { - if (open) { - refreshModels() - Qt.callLater(refreshModels) - } else { - clearModels() - } - } - - Item { - width: 0 - height: 0 - visible: false - - Connections { - target: panel.audioBackend - function onAudioSinksChanged() { panel.scheduleRefresh() } - function onAudioSourcesChanged() { panel.scheduleRefresh() } - function onAudioStreamsChanged() { panel.scheduleRefresh() } - } - - Timer { - id: modelRefresh - interval: 75 - repeat: false - onTriggered: panel.refreshModels() - } - - PwNodePeakMonitor { - id: inputPeakMonitor - node: panel.audioBackend ? panel.audioBackend.source : null - enabled: panel.open && panel.audioBackend && panel.audioBackend.source !== null - } - } - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.ownerWidget.close() - onTabRequested: function(direction) { panel.ownerWidget.switchPanel(direction) } - onMoveRequested: function(dx, _dy) { - if (dx !== 0) panel.adjustOutput(dx * 0.05) - } - - Flickable { - id: scroller - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: contentColumn.implicitHeight - boundsBehavior: Flickable.StopAtBounds - interactive: contentHeight > height - - Column { - id: contentColumn - width: scroller.width - spacing: 8 - - Item { - width: parent.width - height: 24 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Volume" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 13 - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: "\u2715" - color: closeMouse.containsMouse - ? panel.controlAccent : panel.controlMuted - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 12 - renderType: Text.NativeRendering - - Behavior on color { ColorAnimation { duration: 120 } } - - MouseArea { - id: closeMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.ownerWidget.close() - } - } - } - - PanelDivider {} - - Column { - id: outputControl - width: parent.width - spacing: Commons.Style.space(2) - - SectionLabel { text: "OUTPUT" } - - Item { - width: parent.width - height: Commons.Style.space(30) - - Text { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - text: panel.outputMuted ? "Muted" - : Math.round(panel.outputVolume * 100) + "%" - color: panel.outputMuted - ? Commons.Util.alpha(panel.controlAccent, 0.4) - : panel.controlAccent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Ui.PanelSlider { - id: outputSlider - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - bar: panel.bar - maximum: 1 - value: panel.outputVolume - trackColor: panel.controlActiveFillColor - fillColor: panel.outputMuted - ? Commons.Util.alpha(panel.controlAccent, 0.4) - : panel.controlAccent - knobColor: panel.sliderKnobColor(panel.outputMuted, - outputSlider.enabled) - enabled: panel.audioBackend && panel.audioBackend.sink !== null - onMoved: function(value) { - panel.audioBackend.setOutputVolume(value) - } - } - } - } - - Text { - width: parent.width - visible: panel.displaySinks.length > 0 - text: "OUTPUT DEVICE" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.52) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 10 - font.letterSpacing: 1 - renderType: Text.NativeRendering - } - - Column { - width: parent.width - spacing: 4 - visible: panel.displaySinks.length > 0 - Repeater { - model: panel.displaySinks - - DeviceRow { - required property var modelData - required property int index - width: contentColumn.width - node: modelData - sourceDevice: false - rowIndex: index - } - } - } - - PanelDivider {} - - ActionButton { - label: panel.outputMuted ? "Unmute volume" : "Mute volume" - active: panel.outputMuted - action: function() { panel.audioBackend.toggleOutputMute() } - } - - PanelDivider { visible: panel.displayStreams.length > 0 } - - Column { - width: parent.width - spacing: Commons.Style.space(8) - visible: panel.displayStreams.length > 0 - - SectionLabel { text: "APPS" } - - Repeater { - model: panel.displayStreams - - StreamRow { - required property var modelData - required property int index - width: contentColumn.width - node: modelData - rowIndex: index - } - } - } - - PanelDivider { visible: panel.audioBackend && panel.audioBackend.source !== null } - - Column { - width: parent.width - spacing: 8 - visible: panel.audioBackend && panel.audioBackend.source !== null - - Column { - id: inputControl - width: parent.width - spacing: Commons.Style.space(2) - - SectionLabel { text: "INPUT" } - - Item { - width: parent.width - height: inputState.implicitHeight + Commons.Style.space(4) - + inputSlider.implicitHeight + Commons.Style.space(8) - - Row { - id: inputState - width: parent.width - - Text { - width: parent.width / 2 - text: "Microphone" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.68) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - - Text { - width: parent.width / 2 - text: panel.inputMuted ? "Muted" - : Math.round(panel.inputVolume * 100) + "%" - color: panel.inputMuted - ? Commons.Util.alpha(panel.controlAccent, 0.4) - : panel.controlAccent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - font.weight: Font.Medium - horizontalAlignment: Text.AlignRight - renderType: Text.NativeRendering - } - } - - Ui.PanelSlider { - id: inputSlider - anchors.left: parent.left - anchors.right: parent.right - anchors.top: inputState.bottom - anchors.topMargin: Commons.Style.space(4) - bar: panel.bar - maximum: 1 - value: panel.inputVolume - trackColor: panel.controlActiveFillColor - fillColor: panel.inputMuted - ? Commons.Util.alpha(panel.controlAccent, 0.4) - : panel.controlAccent - knobColor: panel.sliderKnobColor(panel.inputMuted, - inputSlider.enabled) - enabled: panel.audioBackend && panel.audioBackend.source !== null - onMoved: function(value) { - panel.audioBackend.setInputVolume(value) - } - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: inputSlider.bottom - anchors.topMargin: Commons.Style.space(4) - height: Commons.Style.space(4) - radius: height / 2 - color: panel.controlFillColor - border.color: panel.controlBorderColor - border.width: 1 - - Rectangle { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - width: parent.width * panel.microphoneLevel - radius: parent.radius - color: panel.inputMuted - ? Commons.Util.alpha(panel.controlAccent, 0.25) - : panel.controlAccent - Behavior on width { - NumberAnimation { duration: 70; easing.type: Easing.OutCubic } - } - } - } - } - } - - Column { - width: parent.width - spacing: Commons.Style.space(4) - visible: panel.displaySources.length > 1 - - Text { - text: "INPUT DEVICE" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.52) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.letterSpacing: 1 - renderType: Text.NativeRendering - } - - Repeater { - model: panel.displaySources - - DeviceRow { - required property var modelData - required property int index - width: contentColumn.width - node: modelData - sourceDevice: true - rowIndex: index - } - } - } - - ActionButton { - label: panel.inputMuted ? "Unmute mic" : "Mute mic" - active: panel.inputMuted - action: function() { panel.audioBackend.toggleInputMute() } - } - } - } - } - } - - component SectionLabel: Text { - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 10 - font.letterSpacing: 1 - renderType: Text.NativeRendering - } - - component PanelDivider: Rectangle { - width: contentColumn.width - height: 1 - color: panel.dividerColor - } - - component ActionButton: Rectangle { - id: actionButton - required property string label - required property var action - property bool active: false - width: contentColumn.width - height: 28 - radius: panel.controlRadius - color: active - ? panel.controlActiveFillColor - : actionMouse.containsMouse ? panel.controlHoverFillColor - : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: active - ? panel.bar ? panel.bar.urgent : Commons.Color.accent - : actionMouse.containsMouse ? panel.controlHoverBorderColor - : panel.controlBorderColor - - Text { - anchors.centerIn: parent - text: actionButton.label - color: parent.active || actionMouse.containsMouse - ? panel.bar ? panel.bar.urgent : Commons.Color.accent - : panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: actionButton.action() - } - } - - component DeviceRow: Rectangle { - id: deviceRow - required property var node - required property bool sourceDevice - required property int rowIndex - readonly property var activeNode: sourceDevice - ? panel.audioBackend.source : panel.audioBackend.sink - readonly property bool current: panel.sameNode(node, activeNode) - width: contentColumn.width - height: 26 - radius: panel.controlRadius - color: current - ? panel.controlActiveFillColor - : deviceMouse.containsMouse ? panel.controlHoverFillColor - : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: current - ? panel.bar ? panel.bar.urgent : Commons.Color.accent - : deviceMouse.containsMouse ? panel.controlHoverBorderColor - : panel.controlBorderColor - - Row { - anchors.fill: parent - anchors.leftMargin: 8 - anchors.rightMargin: 8 - spacing: 6 - - Text { - anchors.verticalCenter: parent.verticalCenter - text: deviceRow.current ? "●" : "○" - color: deviceRow.current - ? panel.controlAccent : panel.controlMuted - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 10 - renderType: Text.NativeRendering - } - - Text { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 22 - text: panel.audioBackend.nodeLabel(deviceRow.node) - color: deviceRow.current || deviceMouse.containsMouse - ? panel.controlAccent : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - font.weight: deviceRow.current ? Font.Medium : Font.Normal - elide: Text.ElideRight - renderType: Text.NativeRendering - } - } - - MouseArea { - id: deviceMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (deviceRow.sourceDevice) - panel.audioBackend.setDefaultSource(deviceRow.node) - else panel.audioBackend.setDefaultSink(deviceRow.node) - } - } - } - - component StreamRow: Item { - id: streamRow - required property var node - required property int rowIndex - readonly property var audio: node ? node.audio : null - width: contentColumn.width - height: Commons.Style.space(32) - - IconText { - id: muteIcon - anchors.left: parent.left - anchors.top: parent.top - width: Commons.Style.space(20) - text: streamRow.audio && streamRow.audio.muted ? "volume_off" : "volume_up" - color: streamRow.audio && streamRow.audio.muted && panel.bar - ? Qt.rgba(panel.bar.foreground.r, panel.bar.foreground.g, - panel.bar.foreground.b, 0.4) - : panel.bar ? panel.bar.urgent : Commons.Color.accent - font.pixelSize: Commons.Style.font.body - fill: 1 - - MouseArea { - anchors.fill: parent - anchors.margins: -Commons.Style.space(3) - cursorShape: Qt.PointingHandCursor - onClicked: panel.audioBackend.toggleStreamMute(streamRow.node) - } - } - - Text { - anchors.left: muteIcon.right - anchors.leftMargin: Commons.Style.space(5) - anchors.right: streamPercent.left - anchors.rightMargin: Commons.Style.space(5) - anchors.verticalCenter: muteIcon.verticalCenter - text: panel.audioBackend.streamLabel(streamRow.node) - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - opacity: streamRow.audio && streamRow.audio.muted ? 0.46 : 1 - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - id: streamPercent - anchors.right: parent.right - anchors.verticalCenter: muteIcon.verticalCenter - text: Math.round(Number(streamRow.audio ? streamRow.audio.volume : 0) * 100) + "%" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Ui.PanelSlider { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - bar: panel.bar - maximum: 1 - value: streamRow.audio ? Number(streamRow.audio.volume || 0) : 0 - trackColor: panel.controlActiveFillColor - fillColor: streamRow.audio && streamRow.audio.muted - ? Commons.Util.alpha(panel.controlAccent, 0.4) - : panel.controlAccent - knobColor: streamRow.audio && streamRow.audio.muted - ? Commons.Util.alpha(panel.controlAccent, 0.55) - : panel.controlAccent - onMoved: function(value) { - panel.audioBackend.setStreamVolume(streamRow.node, value) - } - } - } -} diff --git a/widgets/AudioWidget.qml b/widgets/AudioWidget.qml deleted file mode 100644 index cae4123..0000000 --- a/widgets/AudioWidget.qml +++ /dev/null @@ -1,316 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui -import "../adapters" as Adapters - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.audio" - manageIpc: false - property url popupSource: Qt.resolvedUrl("AudioPanel.qml") - readonly property url backendPanelSource: registeredSource("omarchy.audio") - property Component panelComponent: String(backendPanelSource) ? null - : registeredComponent("omarchy.audio") - - readonly property var tokens: bar ? bar.visualTokens : null - readonly property bool compact: setting("compact", false) === true - readonly property var audioPanel: audioBridge.panel - readonly property var panelItem: popupLoader.item - readonly property bool panelLoaded: panelItem !== null - readonly property bool audioReady: audioBridge.ready - property bool wheelAdjustmentPending: false - property real wheelTargetVolume: 0 - readonly property real displayedOutputVolume: wheelAdjustmentPending - ? wheelTargetVolume : audioBridge.outputVolume - readonly property int volume: Math.round(displayedOutputVolume * 100) - readonly property bool muted: audioBridge.outputMuted - readonly property string tooltipText: !audioReady ? "Audio unavailable" - : muted ? "Muted · " + volume + "%" : "Audio " + volume + "%" - readonly property var interactionTarget: actionButton - - visible: audioReady - implicitWidth: visible ? (bar && bar.vertical ? bar.barSize : audioSurface.implicitWidth) : 0 - implicitHeight: visible - ? (bar && bar.vertical ? audioSurface.implicitHeight : bar ? bar.barSize : 28) : 0 - - function setting(name, fallback) { - const value = settings ? settings[name] : undefined - return value === undefined || value === null ? fallback : value - } - - function registeredComponent(id) { - if (bar && typeof bar.registeredWidgetComponent === "function") - return bar.registeredWidgetComponent(id) - const registry = bar ? bar.barWidgetRegistry : null - if (registry) void(registry.revision) - const widgets = registry && registry.widgets ? registry.widgets : ({}) - const entry = widgets[id] - return entry ? entry.component : null - } - - function registeredSource(id) { - return bar && typeof bar.registeredWidgetSource === "function" - ? bar.registeredWidgetSource(id) : "" - } - - function officialSettings() { - return bar && typeof bar.widgetSettings === "function" - ? bar.widgetSettings("G6", "omarchy.audio") : ({}) - } - - function childPanelWidget(pluginId) { - return String(pluginId || "") === "omarchy.audio" ? root : null - } - - function ownsPanelWidget(owner) { - return !!owner && (owner === root || owner === audioPanel) - } - - function toggleOutputMute() { return audioBridge.toggleOutputMute() } - function setOutputVolume(value) { return audioBridge.setOutputVolume(value) } - function adjustVolume(delta) { - if (!audioReady || Number(delta || 0) === 0) return false - const base = wheelAdjustmentPending - ? wheelTargetVolume : audioBridge.outputVolume - wheelTargetVolume = Math.max(0, Math.min(1, - Math.round((base + Number(delta)) * 100) / 100)) - wheelAdjustmentPending = true - wheelCommitTimer.restart() - return true - } - - function syncPanelLoader() { - popupLoader.source = "" - if (!opened || !audioReady || !String(popupSource)) return - popupLoader.setSource(popupSource, { - anchorItem: audioSurface, - bar: root.bar, - ownerWidget: root, - audioBackend: audioBridge - }) - } - - onOpenedChanged: syncPanelLoader() - onAudioReadyChanged: { - if (opened) syncPanelLoader() - if (!audioReady) { - wheelCommitTimer.stop() - wheelSettleTimer.stop() - wheelAdjustmentPending = false - } - } - onPopupSourceChanged: if (opened) syncPanelLoader() - Component.onDestruction: close() - - Adapters.AudioPanelBridge { - id: audioBridge - anchors.fill: audioSurface - bar: root.bar - ownerWidget: root - panelComponent: root.panelComponent - panelSource: root.backendPanelSource - panelSettings: root.officialSettings() - } - - Loader { id: popupLoader } - - Timer { - id: wheelCommitTimer - interval: 70 - onTriggered: { - if (!root.setOutputVolume(root.wheelTargetVolume)) { - root.wheelAdjustmentPending = false - return - } - wheelSettleTimer.restart() - } - } - - Timer { - id: wheelSettleTimer - interval: 300 - onTriggered: root.wheelAdjustmentPending = false - } - - Item { - id: audioSurface - anchors.centerIn: parent - implicitWidth: !root.bar || !root.tokens ? 0 - : root.bar.vertical - ? root.bar.barSize : content.implicitWidth + 2 * root.tokens.pillPaddingX - implicitHeight: !root.bar || !root.tokens ? 0 - : root.bar.vertical - ? content.implicitHeight + Commons.Style.space(10) - : root.tokens.slotHeight - width: implicitWidth - height: implicitHeight - - Loader { - anchors.fill: parent - anchors.topMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - anchors.bottomMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - active: root.bar !== null && root.tokens !== null - sourceComponent: Component { - PillSurface { - anchors.fill: parent - bar: root.bar - } - } - } - - Loader { - id: content - anchors.centerIn: parent - sourceComponent: !root.bar || !root.tokens ? null - : root.bar.vertical ? verticalContent - : root.compact ? compactHorizontalContent : fullHorizontalContent - } - - Ui.WidgetButton { - id: actionButton - anchors.fill: parent - bar: root.audioReady ? root.bar : null - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - fixedWidth: audioSurface.width - fixedHeight: audioSurface.height - tooltipText: root.tooltipText - onPressed: function(button) { - if (button === Qt.RightButton) root.toggleOutputMute() - else root.toggle() - } - onWheelMoved: function(delta) { - if (delta !== 0) root.adjustVolume(delta > 0 ? 0.05 : -0.05) - } - } - } - - Component { - id: fullHorizontalContent - - Row { - spacing: root.tokens.contentGap - - Text { - anchors.verticalCenter: parent.verticalCenter - text: "VOL" - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, root.muted ? 0.25 : 0.6) : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - font.letterSpacing: 0.5 - renderType: Text.NativeRendering - } - - Item { - anchors.verticalCenter: parent.verticalCenter - width: Commons.Style.space(34) - height: Commons.Style.space(14) - - readonly property real ratio: root.muted ? 0 : Math.min(root.volume / 100, 1) - - Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - height: Commons.Style.space(8) - radius: height / 2 - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.18) : Commons.Color.background - } - - Rectangle { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: Math.max(parent.ratio > 0 ? Commons.Style.space(8) : 0, - parent.width * parent.ratio) - height: Commons.Style.space(8) - radius: height / 2 - color: root.bar ? root.bar.urgent : Commons.Color.accent - - Behavior on width { - NumberAnimation { duration: 150; easing.type: Easing.OutCubic } - } - } - } - - Text { - anchors.verticalCenter: parent.verticalCenter - width: Commons.Style.space(30) - text: String(root.volume).padStart(2, "0") + "%" - color: root.bar ? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, - root.bar.urgent.b, root.muted ? 0.35 : 1) : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - horizontalAlignment: Text.AlignRight - renderType: Text.NativeRendering - - Behavior on color { ColorAnimation { duration: 160 } } - } - } - } - - Component { - id: compactHorizontalContent - - Row { - spacing: root.tokens.compactGap - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: "graphic_eq" - color: root.bar ? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, - root.bar.urgent.b, root.muted ? 0.35 : 1) : Commons.Color.accent - font.pixelSize: root.tokens.iconSize - font.weight: Font.Medium - fill: 1 - } - - Text { - anchors.verticalCenter: parent.verticalCenter - width: Commons.Style.space(30) - text: String(root.volume).padStart(2, "0") + "%" - color: root.bar ? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, - root.bar.urgent.b, root.muted ? 0.35 : 1) : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - horizontalAlignment: Text.AlignLeft - renderType: Text.NativeRendering - - Behavior on color { ColorAnimation { duration: 160 } } - } - } - } - - Component { - id: verticalContent - - Column { - spacing: Commons.Style.space(2) - - IconText { - anchors.horizontalCenter: parent.horizontalCenter - text: "graphic_eq" - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.pixelSize: root.tokens.iconSize - font.weight: Font.Medium - fill: 1 - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: String(root.volume).padStart(2, "0") + "%" - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - } -} diff --git a/widgets/BatteryPanel.qml b/widgets/BatteryPanel.qml deleted file mode 100644 index bd17995..0000000 --- a/widgets/BatteryPanel.qml +++ /dev/null @@ -1,208 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var powerService - - owner: ownerWidget - open: ownerWidget.opened && powerService.hasBattery - focusTarget: keyCatcher - contentWidth: fittedContentWidth(Commons.Style.space(300)) - contentHeight: fittedContentHeight(column.implicitHeight) - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.ownerWidget.close() - onTabRequested: function(direction) { panel.ownerWidget.switchPanel(direction) } - - Column { - id: column - width: parent.width - spacing: Commons.Style.space(8) - - Row { - width: parent.width - spacing: Commons.Style.space(4) - Text { - width: parent.width - closeAction.width - parent.spacing - anchors.verticalCenter: parent.verticalCenter - text: "Battery" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.heading - font.weight: Font.Medium - } - IconAction { - id: closeAction - anchors.verticalCenter: parent.verticalCenter - icon: "close" - tooltip: "Close" - onClicked: panel.ownerWidget.close() - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, panel.bar.foreground.g, - panel.bar.foreground.b, 0.18) : Commons.Color.popups.border - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: panel.powerService.percent + "%" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.heading - font.weight: Font.Medium - } - - Rectangle { - width: parent.width - height: Commons.Style.space(8) - radius: height / 2 - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, panel.bar.foreground.g, - panel.bar.foreground.b, 0.12) : Commons.Color.background - Rectangle { - width: parent.width * panel.powerService.percent / 100 - height: parent.height - radius: height / 2 - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - Behavior on width { NumberAnimation { duration: 300 } } - } - } - - Column { - width: parent.width - spacing: Commons.Style.space(5) - BatteryInfoRow { label: "Status"; value: panel.powerService.batteryStatus } - BatteryInfoRow { - visible: panel.powerService.timeText !== "" - label: panel.powerService.charging ? "Time to full" : "Time left" - value: panel.powerService.timeText - } - BatteryInfoRow { - visible: panel.powerService.healthSupported - label: "Health" - value: panel.powerService.healthPercent + "%" - } - BatteryInfoRow { - visible: panel.powerService.changeRate > 0 - label: panel.powerService.charging ? "Charge rate" : "Power draw" - value: panel.powerService.changeRate.toFixed(1) + " W" - } - BatteryInfoRow { - visible: panel.powerService.batteryInfo.size !== undefined - label: "Battery size" - value: String(panel.powerService.batteryInfo.size || "") - } - BatteryInfoRow { - visible: panel.powerService.batteryInfo.cycles !== undefined - label: "Charge cycles" - value: String(panel.powerService.batteryInfo.cycles || "") - } - BatteryInfoRow { - visible: panel.powerService.batteryInfo.threshold !== undefined - label: "Charge threshold" - value: String(panel.powerService.batteryInfo.threshold || "") - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, panel.bar.foreground.g, - panel.bar.foreground.b, 0.18) : Commons.Color.popups.border - } - - Rectangle { - width: parent.width - height: Commons.Style.space(30) - radius: panel.controlRadius - color: btopMouse.containsMouse && panel.bar - ? Qt.lighter(panel.bar.urgent, 1.08) - : panel.bar ? panel.bar.urgent : Commons.Color.accent - Text { - anchors.centerIn: parent - text: "Open btop" - color: panel.bar ? panel.bar.background : Commons.Color.background - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - } - MouseArea { - id: btopMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - panel.ownerWidget.close() - panel.ownerWidget.openSystemMonitor() - } - } - } - } - } - - component BatteryInfoRow: Row { - required property string label - required property string value - height: Commons.Style.space(16) - Text { - width: parent.width * 0.42 - text: parent.label - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, panel.bar.foreground.g, - panel.bar.foreground.b, 0.65) : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - } - Text { - width: parent.width * 0.58 - text: parent.value - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - elide: Text.ElideRight - } - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: action.icon - color: action.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } -} diff --git a/widgets/BatteryWidget.qml b/widgets/BatteryWidget.qml deleted file mode 100644 index f1293b6..0000000 --- a/widgets/BatteryWidget.qml +++ /dev/null @@ -1,302 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.battery" - manageIpc: false - property url panelSource: Qt.resolvedUrl("BatteryPanel.qml") - - readonly property var powerService: bar ? bar.powerService : null - readonly property var tokens: bar ? bar.visualTokens : null - readonly property bool compact: setting("compact", false) === true - readonly property bool hasBattery: !!(powerService && powerService.hasBattery) - readonly property int percent: powerService ? powerService.percent : 0 - readonly property bool charging: !!(powerService && powerService.charging) - readonly property bool full: !!(powerService && powerService.fullyCharged) - readonly property bool low: hasBattery && !charging && !full && percent <= 20 - readonly property string tooltipText: powerService - ? powerService.batteryStatus + " · " + percent + "%" - + (powerService.timeText ? " · " + powerService.timeText : "") - : "Battery unavailable" - property var detailOwner: null - readonly property var interactionTarget: interaction - readonly property var panelItem: panelLoader.item - - visible: hasBattery - implicitWidth: visible ? (bar && bar.vertical ? bar.barSize : surface.implicitWidth) : 0 - implicitHeight: visible - ? (bar && bar.vertical ? surface.implicitHeight : bar ? bar.barSize : 28) : 0 - - function syncDetailLease() { - var wanted = opened && hasBattery ? powerService : null - if (wanted === detailOwner) return - if (detailOwner) detailOwner.releaseBatteryDetails() - detailOwner = wanted - if (detailOwner) detailOwner.acquireBatteryDetails() - } - - function syncPanelLoader() { - if (!opened || !hasBattery) { - panelLoader.source = "" - return - } - panelLoader.setSource(panelSource, { - anchorItem: surface, - bar: root.bar, - ownerWidget: root, - powerService: root.powerService - }) - } - - function activate() { toggle() } - - function openSystemMonitor() { - if (!bar || typeof bar.run !== "function") return false - bar.run("omarchy-launch-or-focus-tui btop") - return true - } - - onOpenedChanged: { - syncDetailLease() - syncPanelLoader() - } - onHasBatteryChanged: { - if (!hasBattery && opened) close() - syncDetailLease() - } - onPowerServiceChanged: syncDetailLease() - Component.onDestruction: { - if (detailOwner) detailOwner.releaseBatteryDetails() - detailOwner = null - } - - Item { - id: surface - anchors.centerIn: parent - implicitWidth: !root.bar || !root.tokens ? 0 - : root.bar.vertical ? root.bar.barSize - : content.implicitWidth + 2 * root.tokens.pillPaddingX - implicitHeight: !root.bar || !root.tokens ? 0 - : root.bar.vertical ? content.implicitHeight + Commons.Style.space(10) - : root.tokens.slotHeight - width: implicitWidth - height: implicitHeight - - PillSurface { - anchors.fill: parent - anchors.topMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - anchors.bottomMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - bar: root.bar - } - - Loader { - id: content - anchors.centerIn: parent - sourceComponent: !root.bar || !root.tokens ? null - : root.compact || root.bar.vertical ? compactContent : fullContent - } - - MouseArea { - id: interaction - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) root.bar.showTooltip(surface, root.tooltipText) - onExited: if (root.bar) root.bar.hideTooltip(surface) - onClicked: { - if (root.bar) root.bar.hideTooltip(surface) - root.activate() - } - } - } - - Loader { id: panelLoader } - - Component { - id: fullContent - Row { - spacing: root.tokens.contentGap - Text { - anchors.verticalCenter: parent.verticalCenter - text: "BAT" - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.6) : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - font.letterSpacing: 0.5 - renderType: Text.NativeRendering - } - BatteryGauge { - anchors.verticalCenter: parent.verticalCenter - ratio: root.percent / 100 - charging: root.charging - full: root.full - low: root.low - color: root.bar ? root.bar.urgent : Commons.Color.accent - paper: root.bar ? root.bar.background : Commons.Color.background - } - Text { - anchors.verticalCenter: parent.verticalCenter - text: root.percent + "%" - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - renderType: Text.NativeRendering - } - } - } - - Component { - id: compactContent - Row { - spacing: root.tokens.compactGap - BatteryGauge { - anchors.verticalCenter: parent.verticalCenter - ratio: root.percent / 100 - charging: root.charging - full: root.full - low: root.low - color: root.bar ? root.bar.urgent : Commons.Color.accent - paper: root.bar ? root.bar.background : Commons.Color.background - } - Text { - anchors.verticalCenter: parent.verticalCenter - visible: !root.bar.vertical - text: root.percent + "%" - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - renderType: Text.NativeRendering - } - } - } - - component BatteryGauge: Item { - id: gauge - required property real ratio - required property bool charging - required property bool full - required property bool low - required property color color - required property color paper - - width: Commons.Style.space(19) - height: Commons.Style.space(10) - opacity: low ? pulse : 1 - property real pulse: 1 - - SequentialAnimation on pulse { - running: gauge.visible && gauge.low - loops: Animation.Infinite - NumberAnimation { from: 1; to: 0.35; duration: 1100; easing.type: Easing.InOutSine } - NumberAnimation { from: 0.35; to: 1; duration: 1100; easing.type: Easing.InOutSine } - } - - Item { - id: batteryVisual - anchors.centerIn: parent - width: Commons.Style.space(19) - height: Commons.Style.space(10) - - Rectangle { - id: batteryBody - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: Commons.Style.space(16) - height: Commons.Style.space(9) - radius: Commons.Style.space(2.5) - color: "transparent" - border.width: Commons.Style.space(1.2) - border.color: gauge.color - - Rectangle { - visible: gauge.charging - anchors.fill: parent - anchors.margins: Commons.Style.space(1.8) - radius: Commons.Style.space(1.2) - color: Qt.rgba(gauge.color.r, gauge.color.g, gauge.color.b, 0.28) - } - - Rectangle { - id: batteryFill - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: Commons.Style.space(1.8) - width: Math.max(gauge.ratio > 0 ? Commons.Style.space(1.5) : 0, - (parent.width - Commons.Style.space(3.6)) * Math.max(0, Math.min(1, gauge.ratio))) - radius: Commons.Style.space(1.2) - clip: true - color: gauge.color - Behavior on width { NumberAnimation { duration: 320; easing.type: Easing.OutCubic } } - - Rectangle { - visible: gauge.charging && !gauge.full - anchors.top: parent.top - anchors.bottom: parent.bottom - width: Commons.Style.space(6) - radius: parent.radius - color: Qt.rgba(1, 1, 1, 0.18) - property real pos: 0 - x: (parent.width + width) * pos - width - - NumberAnimation on pos { - running: gauge.visible && gauge.charging && !gauge.full - from: 0 - to: 1 - duration: 1100 - easing.type: Easing.InOutSine - } - } - } - - Canvas { - id: chargingBolt - visible: gauge.charging || gauge.full - anchors.centerIn: parent - width: Commons.Style.space(6) - height: Commons.Style.space(8) - - onPaint: { - var ctx = getContext("2d") - ctx.clearRect(0, 0, width, height) - ctx.beginPath() - ctx.moveTo(width * 0.55, 0) - ctx.lineTo(width * 0.12, height * 0.55) - ctx.lineTo(width * 0.45, height * 0.55) - ctx.lineTo(width * 0.38, height) - ctx.lineTo(width * 0.88, height * 0.45) - ctx.lineTo(width * 0.55, height * 0.45) - ctx.closePath() - ctx.fillStyle = gauge.paper - ctx.fill() - } - - Component.onCompleted: requestPaint() - Connections { - target: gauge - function onPaperChanged() { chargingBolt.requestPaint() } - } - } - } - - Rectangle { - anchors.left: batteryBody.right - anchors.leftMargin: -Commons.Style.space(0.5) - anchors.verticalCenter: parent.verticalCenter - width: Commons.Style.space(2.5) - height: Commons.Style.space(5) - radius: Commons.Style.space(1.2) - color: gauge.color - } - } - } -} diff --git a/widgets/BluetoothPanel.qml b/widgets/BluetoothPanel.qml deleted file mode 100644 index 6c57cb7..0000000 --- a/widgets/BluetoothPanel.qml +++ /dev/null @@ -1,551 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var bluetoothService - property int selectedIndex: 0 - property bool cursorActive: false - property bool actionFocused: false - property string selectedAddress: "" - - readonly property var rows: { - const result = [] - const append = function(section, values) { - const list = Array.isArray(values) ? values : [] - for (let i = 0; i < list.length; i++) - if (list[i]) result.push({ section: section, device: list[i] }) - } - append("connected", bluetoothService.connectedDevices) - append("paired", bluetoothService.knownDevices) - if (bluetoothService.discovering) - append("available", bluetoothService.discoveredDevices) - return result - } - - owner: ownerWidget - open: ownerWidget.opened && bluetoothService && bluetoothService.ready - focusTarget: keyCatcher - contentWidth: fittedContentWidth(Commons.Style.space(380)) - contentHeight: fittedContentHeight(contentColumn.implicitHeight, - Commons.Style.space(620)) - - function sectionLabel(section) { - if (section === "connected") return "CONNECTED" - if (section === "paired") return "PAIRED" - return "AVAILABLE" - } - - function sectionStarts(index) { - const current = rowAt(index) - if (!current) return false - const previous = rowAt(index - 1) - return !previous || previous.section !== current.section - } - - function rowAt(index) { - return index >= 0 && index < rows.length ? rows[index] : null - } - - function deviceAddress(device) { - return device ? String(device.address || "") : "" - } - - function canForget(row) { - return row && (row.section === "connected" || row.section === "paired") - } - - function primaryAction(row) { - if (!row || !row.device) return false - return row.device.connected - ? bluetoothService.disconnectDevice(row.device) - : bluetoothService.connectDevice(row.device) - } - - function forget(row) { - return canForget(row) && bluetoothService.forgetDevice(row.device) - } - - function batteryStatusText(device) { - if (!device || !device.batteryAvailable) return "" - const icon = String(device.icon || "").toLowerCase() - // BlueZ may expose coarse or stale phone charge without provenance, and - // Quickshell does not expose Battery1.Source or an update timestamp. - if (icon === "phone" || icon === "smartphone") return "" - const battery = Number(device.battery) - if (!Number.isFinite(battery) || battery < 0 || battery > 1) return "" - return Math.round(battery * 100) + "%" - } - - function statusText(row) { - if (!row || !row.device) return "" - void(bluetoothService.pendingActions) - const device = row.device - const pending = bluetoothService.pendingAction(deviceAddress(device)) - if (pending === "forgetting") return "Forgetting..." - if (pending === "disconnecting" || Number(device.state) === 2) - return "Disconnecting..." - if (pending === "connecting" || Number(device.state) === 3 - || device.pairing === true) return "Connecting..." - if (device.connected) { - const batteryText = batteryStatusText(device) - if (batteryText !== "") return "Connected · " + batteryText - return "Connected" - } - return row.section === "paired" ? "Paired" : "Available" - } - - function resetCursor() { - selectedIndex = 0 - selectedAddress = rows.length > 0 ? deviceAddress(rows[0].device) : "" - cursorActive = false - actionFocused = false - } - - function moveCursor(delta) { - if (rows.length === 0) return - selectedIndex = Math.max(0, Math.min(rows.length - 1, selectedIndex + delta)) - selectedAddress = deviceAddress(rows[selectedIndex].device) - actionFocused = false - ensureVisibleForIndex(selectedIndex) - } - - function moveCursorHorizontal(delta) { - const row = rowAt(selectedIndex) - if (!canForget(row)) return - actionFocused = delta > 0 - } - - function activateCursor() { - const row = rowAt(selectedIndex) - if (!row) return - if (actionFocused) forget(row) - else primaryAction(row) - } - - function reselectAddress() { - if (rows.length === 0) { - selectedIndex = 0 - selectedAddress = "" - actionFocused = false - return - } - for (let i = 0; i < rows.length; i++) { - if (deviceAddress(rows[i].device) === selectedAddress) { - selectedIndex = i - if (!canForget(rows[i])) actionFocused = false - return - } - } - selectedIndex = Math.max(0, Math.min(rows.length - 1, selectedIndex)) - selectedAddress = deviceAddress(rows[selectedIndex].device) - actionFocused = false - } - - function ensureVisibleForIndex(index) { - if (!deviceRepeater || index < 0 || index >= deviceRepeater.count) return - const item = deviceRepeater.itemAt(index) - if (!item || !deviceFlick) return - const point = item.mapToItem(deviceFlick.contentItem, 0, 0) - const top = point.y - const bottom = top + item.height - if (top < deviceFlick.contentY) - deviceFlick.contentY = Math.max(0, top - Commons.Style.space(4)) - else if (bottom > deviceFlick.contentY + deviceFlick.height) - deviceFlick.contentY = Math.max(0, - bottom - deviceFlick.height + Commons.Style.space(4)) - } - - function heroStatus() { - if (!bluetoothService.adapterAvailable) return "NO ADAPTER" - if (!bluetoothService.radioEnabled) return "TURNED OFF" - if (bluetoothService.discovering) return "SCANNING FOR DEVICES" - if (bluetoothService.connectedCount > 0) - return bluetoothService.connectedCount + " CONNECTED" - return "READY" - } - - onOpenChanged: if (open) resetCursor() - onRowsChanged: reselectAddress() - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.ownerWidget.close() - onTabRequested: function(direction) { panel.ownerWidget.switchPanel(direction) } - onMoveRequested: function(dx, dy) { - if (!panel.cursorActive) { - panel.cursorActive = true - return - } - if (dy !== 0) panel.moveCursor(dy) - else if (dx !== 0) panel.moveCursorHorizontal(dx) - } - onActivateRequested: if (panel.cursorActive) panel.activateCursor() - onDeleteRequested: if (panel.cursorActive) - panel.forget(panel.rowAt(panel.selectedIndex)) - - Column { - id: contentColumn - width: parent.width - spacing: Commons.Style.space(10) - - Row { - width: parent.width - spacing: Commons.Style.space(4) - - Text { - width: parent.width - headerActions.width - parent.spacing - anchors.verticalCenter: parent.verticalCenter - text: "Bluetooth" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.heading - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Row { - id: headerActions - spacing: Commons.Style.space(4) - - IconAction { - icon: panel.bluetoothService.discovering ? "sync" : "refresh" - tooltip: "Scan for devices" - enabled: panel.bluetoothService.radioEnabled - onClicked: panel.bluetoothService.restartDiscovery() - } - - IconAction { - icon: "close" - tooltip: "Close" - onClicked: panel.ownerWidget.close() - } - } - } - - Ui.PanelSeparator { width: parent.width } - - Row { - width: parent.width - spacing: Commons.Style.space(10) - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: !panel.bluetoothService.radioEnabled ? "\uE1A9" - : panel.bluetoothService.connectedCount > 0 - ? "\uE1A8" : "\uE1A7" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - opacity: panel.bluetoothService.radioEnabled ? 1 : 0.45 - font.pixelSize: Commons.Style.font.display - fill: 1 - } - - Column { - width: parent.width - x - heroPowerToggle.width - parent.spacing - anchors.verticalCenter: parent.verticalCenter - spacing: Commons.Style.space(2) - - Text { - width: parent.width - text: "Bluetooth" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.title - font.weight: Font.DemiBold - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: parent.width - text: panel.heroStatus() - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.56) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.letterSpacing: 1.1 - elide: Text.ElideRight - renderType: Text.NativeRendering - } - } - - PowerToggle { - id: heroPowerToggle - anchors.verticalCenter: parent.verticalCenter - } - } - - Ui.PanelSeparator { width: parent.width } - - Text { - width: parent.width - visible: !panel.bluetoothService.radioEnabled - text: "Bluetooth is off" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.48) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - horizontalAlignment: Text.AlignHCenter - renderType: Text.NativeRendering - } - - Flickable { - id: deviceFlick - width: parent.width - height: panel.bluetoothService.radioEnabled - ? Math.min(deviceColumn.implicitHeight, Commons.Style.space(400)) : 0 - visible: panel.bluetoothService.radioEnabled - contentWidth: width - contentHeight: deviceColumn.implicitHeight - clip: true - boundsBehavior: Flickable.StopAtBounds - interactive: contentHeight > height - - Column { - id: deviceColumn - width: deviceFlick.width - spacing: Commons.Style.space(6) - - Repeater { - id: deviceRepeater - model: panel.rows - - DeviceEntry { - required property var modelData - required property int index - width: deviceColumn.width - entry: modelData - rowIndex: index - sectionStart: panel.sectionStarts(index) - } - } - - Text { - width: parent.width - visible: panel.rows.length === 0 - text: panel.bluetoothService.discovering - ? "Scanning for devices..." : "No Bluetooth devices found" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.48) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - renderType: Text.NativeRendering - } - } - } - - } - } - - component SectionLabel: Text { - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.58) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.letterSpacing: 1.2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - opacity: enabled ? 1 : 0.35 - - IconText { - anchors.centerIn: parent - text: action.icon - color: action.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - enabled: action.enabled - hoverEnabled: true - cursorShape: action.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } - - component PowerToggle: Item { - id: powerToggle - implicitWidth: Commons.Style.space(46) - implicitHeight: Commons.Style.space(28) - - Rectangle { - anchors.centerIn: parent - width: Commons.Style.space(42) - height: Commons.Style.space(20) - radius: height / 2 - color: panel.bluetoothService.radioEnabled && panel.bar - ? Qt.rgba(panel.bar.urgent.r, panel.bar.urgent.g, - panel.bar.urgent.b, 0.18) - : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: panel.bluetoothService.radioEnabled && panel.bar - ? panel.bar.urgent : panel.controlBorderColor - - Rectangle { - anchors.verticalCenter: parent.verticalCenter - x: panel.bluetoothService.radioEnabled - ? parent.width - width - Commons.Style.space(3) : Commons.Style.space(3) - width: Commons.Style.space(14) - height: width - radius: width / 2 - color: panel.bluetoothService.radioEnabled && panel.bar - ? panel.bar.urgent : panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.55) - : Commons.Color.foreground - Behavior on x { - NumberAnimation { duration: 140; easing.type: Easing.OutCubic } - } - } - } - - MouseArea { - id: powerMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.bluetoothService.toggleBluetooth() - } - - ShibumiPanelToolTip { - panel: panel - visible: powerMouse.containsMouse - text: panel.bluetoothService.radioEnabled - ? "Turn Bluetooth off" : "Turn Bluetooth on" - } - } - - component DeviceEntry: Column { - id: entryItem - required property var entry - required property int rowIndex - required property bool sectionStart - readonly property var device: entry ? entry.device : null - readonly property bool connected: device && device.connected === true - readonly property bool forgettable: panel.canForget(entry) - readonly property bool selected: panel.cursorActive - && panel.selectedIndex === rowIndex - spacing: Commons.Style.space(4) - - SectionLabel { - visible: entryItem.sectionStart - width: parent.width - text: panel.sectionLabel(entryItem.entry.section) - } - - Ui.CursorSurface { - id: deviceSurface - width: parent.width - implicitHeight: Commons.Style.space(42) - radius: panel.controlRadius - bordered: true - current: entryItem.connected - hasCursor: entryItem.selected && !panel.actionFocused - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - Row { - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Commons.Style.space(8) - anchors.rightMargin: Commons.Style.space(8) - anchors.verticalCenter: parent.verticalCenter - spacing: Commons.Style.space(8) - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: entryItem.connected ? "\uE1A8" : "\uE1A7" - color: entryItem.connected && panel.bar - ? panel.bar.urgent : deviceSurface.foreground - font.pixelSize: Commons.Style.font.heading - fill: 1 - } - - Column { - width: parent.width - x - forgetAction.width - parent.spacing - anchors.verticalCenter: parent.verticalCenter - spacing: 0 - - Text { - width: parent.width - text: panel.bluetoothService.deviceLabel(entryItem.device) || "Device" - color: deviceSurface.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: entryItem.connected ? Font.DemiBold : Font.Normal - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: parent.width - text: panel.statusText(entryItem.entry) - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.56) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - elide: Text.ElideRight - renderType: Text.NativeRendering - } - } - - IconAction { - id: forgetAction - visible: entryItem.forgettable - && (deviceMouse.containsMouse || entryItem.selected) - icon: "delete" - tooltip: "Forget device" - onClicked: panel.forget(entryItem.entry) - } - } - - MouseArea { - id: deviceMouse - anchors.fill: parent - anchors.rightMargin: forgetAction.visible ? forgetAction.width : 0 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: if (containsMouse) { - panel.cursorActive = true - panel.selectedIndex = entryItem.rowIndex - panel.selectedAddress = panel.deviceAddress(entryItem.device) - panel.actionFocused = false - } - onClicked: panel.primaryAction(entryItem.entry) - } - } - } -} diff --git a/widgets/BluetoothWidget.qml b/widgets/BluetoothWidget.qml deleted file mode 100644 index c777d32..0000000 --- a/widgets/BluetoothWidget.qml +++ /dev/null @@ -1,209 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.bluetooth" - manageIpc: false - property url popupSource: Qt.resolvedUrl("BluetoothPanel.qml") - property var bluetoothServiceOverride: null - property var sessionService: null - - readonly property var tokens: bar ? bar.visualTokens : null - readonly property bool compact: setting("compact", false) === true - readonly property var bluetoothService: bluetoothServiceOverride - || (bar ? bar.bluetoothService : null) - readonly property bool bluetoothReady: bluetoothService && bluetoothService.ready - readonly property bool adapterAvailable: bluetoothReady - && bluetoothService.adapterAvailable - readonly property bool radioEnabled: adapterAvailable - && bluetoothService.radioEnabled - readonly property int connectedCount: bluetoothReady - ? bluetoothService.connectedCount : 0 - readonly property bool connected: connectedCount > 0 - readonly property bool showConnectedCount: connected - && bar && !bar.vertical - readonly property string stateIcon: !radioEnabled ? "\uE1A9" - : connected ? "\uE1A8" : "\uE1A7" - readonly property string tooltipText: !bluetoothReady ? "Bluetooth unavailable" - : !adapterAvailable ? "No Bluetooth adapter" - : connected ? "Bluetooth · " + connectedCount + " connected" - : radioEnabled ? "Bluetooth on" : "Bluetooth off" - readonly property var panelItem: popupLoader.item - readonly property bool panelLoaded: panelItem !== null - readonly property var interactionTarget: actionButton - - visible: bluetoothReady && adapterAvailable - implicitWidth: visible ? (bar && bar.vertical ? bar.barSize : surface.implicitWidth) : 0 - implicitHeight: visible - ? (bar && bar.vertical ? surface.implicitHeight : bar ? bar.barSize : 28) : 0 - - function childPanelWidget(pluginId) { - const id = String(pluginId || "") - return id === moduleName || id === "omarchy.bluetooth" ? root : null - } - - function ownsPanelWidget(owner) { return owner === root } - - function releaseSession() { - if (sessionService && typeof sessionService.endSession === "function") - sessionService.endSession(root) - sessionService = null - } - - function syncPanelLoader() { - popupLoader.source = "" - if (!opened || !bluetoothReady || !adapterAvailable || !String(popupSource)) { - releaseSession() - return - } - if (sessionService !== bluetoothService) { - releaseSession() - sessionService = bluetoothService - sessionService.beginSession(root) - } - popupLoader.setSource(popupSource, { - anchorItem: surface, - bar: root.bar, - ownerWidget: root, - bluetoothService: bluetoothService - }) - } - - function toggleBluetooth() { - return bluetoothService && bluetoothService.toggleBluetooth() - } - - onOpenedChanged: syncPanelLoader() - onBluetoothReadyChanged: syncPanelLoader() - onAdapterAvailableChanged: syncPanelLoader() - onPopupSourceChanged: syncPanelLoader() - Component.onDestruction: { - close() - releaseSession() - } - - Loader { id: popupLoader } - - Item { - id: surface - anchors.centerIn: parent - implicitWidth: !root.bar || !root.tokens ? 0 - : root.bar.vertical ? root.bar.barSize - : content.implicitWidth + 2 * root.tokens.pillPaddingX - implicitHeight: !root.bar || !root.tokens ? 0 - : root.bar.vertical ? content.implicitHeight + Commons.Style.space(10) - : root.tokens.slotHeight - width: implicitWidth - height: implicitHeight - - Loader { - anchors.fill: parent - anchors.topMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - anchors.bottomMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - active: root.bar !== null && root.tokens !== null - sourceComponent: Component { - PillSurface { anchors.fill: parent; bar: root.bar } - } - } - - Loader { - id: content - anchors.centerIn: parent - sourceComponent: !root.bar || !root.tokens ? null - : root.compact || root.bar.vertical ? compactContent : fullContent - } - - Ui.WidgetButton { - id: actionButton - anchors.fill: parent - bar: root.visible ? root.bar : null - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - fixedWidth: surface.width - fixedHeight: surface.height - tooltipText: root.tooltipText - onPressed: function(button) { - if (button === Qt.RightButton) root.toggleBluetooth() - else root.toggle() - } - } - } - - Component { - id: fullContent - - Row { - spacing: root.tokens.contentGap - - Text { - anchors.verticalCenter: parent.verticalCenter - text: "BT" - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.6) : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - font.letterSpacing: 0.5 - renderType: Text.NativeRendering - } - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: root.stateIcon - color: root.bar ? (root.connected ? root.bar.urgent - : root.bar.foreground) : Commons.Color.accent - opacity: root.radioEnabled ? (root.connected ? 1 : 0.7) : 0.35 - font.pixelSize: 14 - font.weight: Font.Medium - fill: 1 - } - - Text { - anchors.verticalCenter: parent.verticalCenter - visible: root.showConnectedCount - text: String(root.connectedCount) - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - renderType: Text.NativeRendering - } - } - } - - Component { - id: compactContent - - Row { - spacing: root.tokens.compactGap - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: root.stateIcon - color: root.bar ? (root.connected ? root.bar.urgent - : root.bar.foreground) : Commons.Color.accent - opacity: root.radioEnabled ? (root.connected ? 1 : 0.7) : 0.35 - font.pixelSize: 14 - font.weight: Font.Medium - fill: 1 - } - - Text { - anchors.verticalCenter: parent.verticalCenter - visible: root.showConnectedCount - text: String(root.connectedCount) - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - renderType: Text.NativeRendering - } - } - } -} diff --git a/widgets/BrightnessPanel.qml b/widgets/BrightnessPanel.qml deleted file mode 100644 index ef607b6..0000000 --- a/widgets/BrightnessPanel.qml +++ /dev/null @@ -1,720 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var monitorService - - property string cursorSection: "" - property int cursorIndex: 0 - property bool cursorActive: false - readonly property var visibleSections: { - const result = [] - if (monitorService.brightnessAvailable) result.push("brightness") - if (monitorService.textSizeAvailable) result.push("textsize") - result.push("scale") - if (monitorService.displays.length > 1) result.push("displays") - return result - } - - owner: ownerWidget - open: ownerWidget.opened && monitorService && monitorService.ready - focusTarget: keyCatcher - contentWidth: fittedContentWidth(Commons.Style.space(380)) - contentHeight: fittedContentHeight(contentColumn.implicitHeight, - Commons.Style.space(620)) - - function resetCursor() { - cursorSection = monitorService.brightnessAvailable ? "brightness" : "scale" - cursorIndex = cursorSection === "brightness" ? -1 : 0 - cursorActive = false - } - - function refreshDisplayState() { - return monitorService && typeof monitorService.refresh === "function" - ? monitorService.refresh() : false - } - - function sectionIndex() { return visibleSections.indexOf(cursorSection) } - - function moveVertical(delta) { - const sections = visibleSections - if (sections.length === 0) return - let current = sectionIndex() - if (current < 0) { - resetCursor() - return - } - - if (cursorSection === "displays") { - const count = monitorService.displays.length - const next = cursorIndex + delta - if (next >= 0 && next < count) { - cursorIndex = next - return - } - } - - current = Math.max(0, Math.min(sections.length - 1, current + delta)) - cursorSection = sections[current] - cursorIndex = cursorSection === "brightness" - || cursorSection === "textsize" ? -1 : 0 - } - - function moveHorizontal(delta) { - if (cursorSection === "brightness") { - monitorService.setBrightness(monitorService.brightnessPercent + delta * 5) - return - } - if (cursorSection === "textsize") { - monitorService.adjustTextSize(delta) - return - } - if (cursorSection !== "scale") return - cursorIndex = Math.max(0, Math.min(monitorService.scaleValues.length - 1, - cursorIndex + delta)) - } - - function activateCursor() { - if (cursorSection === "scale" && cursorIndex >= 0 - && cursorIndex < monitorService.scaleValues.length) { - monitorService.setScale(monitorService.scaleValues[cursorIndex]) - return - } - if (cursorSection !== "displays" || cursorIndex < 0 - || cursorIndex >= monitorService.displays.length) return - const display = monitorService.displays[cursorIndex] - if (!display || (display.enabled && monitorService.enabledDisplayCount <= 1)) return - monitorService.toggleDisplay(display.name, display.enabled) - } - - function ensureVisible(item) { - if (!item || !scroller) return - const point = item.mapToItem(contentColumn, 0, 0) - const top = point.y - const bottom = top + item.height - if (top < scroller.contentY) scroller.contentY = Math.max(0, top - 6) - else if (bottom > scroller.contentY + scroller.height) - scroller.contentY = Math.max(0, bottom - scroller.height + 6) - } - - onOpenChanged: { - if (!open) return - resetCursor() - } - - onVisibleSectionsChanged: { - if (visibleSections.indexOf(cursorSection) < 0) resetCursor() - } - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.ownerWidget.close() - onTabRequested: function(direction) { panel.ownerWidget.switchPanel(direction) } - onMoveRequested: function(dx, dy) { - if (!panel.cursorActive) { - panel.cursorActive = true - return - } - if (dy !== 0) panel.moveVertical(dy) - else if (dx !== 0) panel.moveHorizontal(dx) - } - onActivateRequested: if (panel.cursorActive) panel.activateCursor() - - Flickable { - id: scroller - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: contentColumn.implicitHeight - boundsBehavior: Flickable.StopAtBounds - interactive: contentHeight > height - - Column { - id: contentColumn - width: scroller.width - spacing: Commons.Style.space(10) - - Row { - width: parent.width - spacing: Commons.Style.space(4) - - Text { - width: parent.width - headerActions.width - parent.spacing - anchors.verticalCenter: parent.verticalCenter - text: "Display" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.heading - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Row { - id: headerActions - spacing: Commons.Style.space(2) - - IconAction { - icon: "refresh" - tooltip: "Refresh display state" - onClicked: panel.refreshDisplayState() - } - - IconAction { - icon: "close" - tooltip: "Close" - onClicked: panel.ownerWidget.close() - } - } - } - - Ui.PanelSeparator { width: parent.width } - - Row { - width: parent.width - spacing: Commons.Style.space(10) - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: panel.monitorService.displays.length > 1 - ? "desktop_windows" : "monitor" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.pixelSize: Commons.Style.font.display - fill: 1 - } - - Column { - width: parent.width - x - anchors.verticalCenter: parent.verticalCenter - spacing: Commons.Style.space(2) - - Text { - width: parent.width - text: panel.monitorService.focusedMonitor || "Display controls" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: Font.Medium - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: parent.width - text: panel.monitorService.brightnessAvailable - ? panel.monitorService.brightnessName( - panel.monitorService.brightnessPercent).toUpperCase() - + " · " + panel.monitorService.brightnessPercent + "%" - : "NO CONTROLLABLE BACKLIGHT" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.58) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.letterSpacing: 1 - elide: Text.ElideRight - renderType: Text.NativeRendering - } - } - } - - Ui.PanelSeparator { - width: parent.width - visible: panel.monitorService.brightnessAvailable - } - - Column { - width: parent.width - visible: panel.monitorService.brightnessAvailable - spacing: Commons.Style.space(5) - - SectionLabel { text: "BRIGHTNESS" } - - Item { - id: brightnessRow - width: parent.width - height: Commons.Style.space(30) - - Text { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - text: panel.monitorService.brightnessPercent + "%" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Ui.PanelSlider { - id: brightnessSlider - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.leftMargin: Commons.Style.space(6) - anchors.rightMargin: Commons.Style.space(6) - bar: panel.bar - minimum: 1 - maximum: 100 - step: 1 - integer: true - trackColor: panel.controlActiveFillColor - fillColor: panel.controlAccent - knobColor: panel.controlAccent - value: panel.monitorService.brightnessPercent - onMoved: function(value) { - panel.monitorService.previewBrightness(value) - } - onReleased: function(value) { - panel.monitorService.setBrightness(value) - } - } - - HoverHandler { - onHoveredChanged: if (hovered) { - panel.cursorActive = true - panel.cursorSection = "brightness" - panel.cursorIndex = -1 - panel.ensureVisible(brightnessRow) - } - } - } - - Row { - width: parent.width - spacing: Commons.Style.space(6) - - PanelButton { - width: (parent.width - parent.spacing) / 2 - label: "− 5%" - onClicked: panel.monitorService.setBrightness( - panel.monitorService.brightnessPercent - 5) - } - PanelButton { - width: (parent.width - parent.spacing) / 2 - label: "+ 5%" - onClicked: panel.monitorService.setBrightness( - panel.monitorService.brightnessPercent + 5) - } - } - } - - Ui.PanelSeparator { - width: parent.width - visible: panel.monitorService.textSizeAvailable - } - - Column { - width: parent.width - visible: panel.monitorService.textSizeAvailable - spacing: Commons.Style.space(5) - - Item { - width: parent.width - implicitHeight: Math.max(textSizeLabel.implicitHeight, - textSizeValue.implicitHeight) - - SectionLabel { - id: textSizeLabel - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "TEXT SIZE" - } - - Text { - id: textSizeValue - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: (textSizeSlider.dragging - ? panel.monitorService.textSizeStops[ - Math.round(textSizeSlider.liveValue)] - : panel.monitorService.textSizePx) + "px" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.58) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.weight: Font.Medium - renderType: Text.NativeRendering - } - } - - Ui.CursorSurface { - id: textSizeRow - width: parent.width - height: textSizeSlider.implicitHeight - + Commons.Style.spacing.controlGap - hasCursor: panel.cursorActive - && panel.cursorSection === "textsize" && panel.cursorIndex === -1 - foreground: panel.controlForeground - accent: panel.controlAccent - outline: true - onHasCursorChanged: if (hasCursor) panel.ensureVisible(textSizeRow) - - Row { - id: v2TextSizeSegments - visible: panel.shellStyle !== "shibumi" - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Commons.Style.space(6) - anchors.rightMargin: Commons.Style.space(6) - anchors.verticalCenter: textSizeSlider.verticalCenter - height: textSizeSlider.trackHeight - spacing: Commons.Style.space(2) - readonly property int segmentCount: Math.max(0, - panel.monitorService.textSizeStops.length - 1) - - Repeater { - model: Math.max(0, - panel.monitorService.textSizeStops.length - 1) - - Rectangle { - required property int index - width: (v2TextSizeSegments.width - - v2TextSizeSegments.spacing - * Math.max(0, v2TextSizeSegments.segmentCount - 1)) - / Math.max(1, v2TextSizeSegments.segmentCount) - height: v2TextSizeSegments.height - radius: height / 2 - color: panel.controlActiveFillColor - - Rectangle { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - width: parent.width * Math.max(0, Math.min(1, - textSizeSlider.progress - * v2TextSizeSegments.segmentCount - parent.index)) - radius: parent.radius - color: panel.controlAccent - } - } - } - } - - Ui.PanelSlider { - id: textSizeSlider - anchors.fill: parent - anchors.leftMargin: Commons.Style.space(6) - anchors.rightMargin: Commons.Style.space(6) - bar: panel.bar - minimum: 0 - maximum: Math.max(0, - panel.monitorService.textSizeStops.length - 1) - step: 1 - integer: true - tickCount: panel.shellStyle === "shibumi" - ? panel.monitorService.textSizeStops.length : 0 - trackColor: panel.shellStyle === "shibumi" - ? panel.controlActiveFillColor : "transparent" - fillColor: panel.shellStyle === "shibumi" - ? panel.controlAccent : "transparent" - knobColor: panel.controlAccent - tickColor: panel.renderedSurfaceColor - value: panel.monitorService.textSizeIndex - onReleased: function(value) { - const index = Math.max(0, Math.min( - panel.monitorService.textSizeStops.length - 1, - Math.round(value))) - panel.monitorService.setTextSize( - panel.monitorService.textSizeStops[index]) - } - } - - HoverHandler { - onHoveredChanged: if (hovered) { - panel.cursorActive = true - panel.cursorSection = "textsize" - panel.cursorIndex = -1 - } - } - } - } - - Ui.PanelSeparator { width: parent.width } - - Column { - width: parent.width - spacing: Commons.Style.space(6) - - SectionLabel { text: "SCALE" } - - Grid { - id: scaleGrid - width: parent.width - columns: panel.monitorService.scaleValues.length - spacing: Commons.Style.space(3) - readonly property real cellWidth: columns > 0 - ? (width - spacing * (columns - 1)) / columns : 0 - - Repeater { - model: panel.monitorService.scaleValues - delegate: ScaleButton { - required property string modelData - required property int index - width: scaleGrid.cellWidth - scaleValue: modelData - scaleIndex: index - } - } - } - } - - Ui.PanelSeparator { - width: parent.width - visible: panel.monitorService.displays.length > 1 - } - - Column { - width: parent.width - spacing: Commons.Style.space(5) - visible: panel.monitorService.displays.length > 1 - - SectionLabel { text: "DISPLAYS" } - - Repeater { - model: panel.monitorService.displays - delegate: DisplayRow { - required property var modelData - required property int index - width: contentColumn.width - display: modelData - rowIndex: index - } - } - } - } - } - } - - component SectionLabel: Text { - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.58) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.letterSpacing: 1.2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - component PanelButton: Rectangle { - id: button - property string label: "" - signal clicked() - readonly property bool hovered: buttonMouse.containsMouse - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - color: hovered ? panel.controlHoverFillColor : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: hovered ? panel.controlHoverBorderColor - : panel.controlBorderColor - - Behavior on color { ColorAnimation { duration: 100 } } - Behavior on border.color { ColorAnimation { duration: 100 } } - - Text { - anchors.centerIn: parent - text: button.label - color: button.hovered ? panel.controlAccent : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - - MouseArea { - id: buttonMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: button.clicked() - } - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: action.icon - color: action.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } - - component ScaleButton: Rectangle { - id: scaleButton - required property string scaleValue - required property int scaleIndex - readonly property bool activeScale: panel.monitorService.normalizeScale( - panel.monitorService.monitorScale) === panel.monitorService.normalizeScale(scaleValue) - readonly property bool keyboardFocused: panel.cursorActive - && panel.cursorSection === "scale" && panel.cursorIndex === scaleIndex - readonly property bool hovered: scaleMouse.containsMouse - readonly property bool highlighted: keyboardFocused || hovered - implicitHeight: Commons.Style.space(30) - radius: panel.controlRadius - color: activeScale ? panel.controlActiveFillColor - : highlighted ? panel.controlHoverFillColor : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: activeScale || highlighted ? panel.controlAccent - : panel.controlBorderColor - - Behavior on color { ColorAnimation { duration: 100 } } - Behavior on border.color { ColorAnimation { duration: 100 } } - - Text { - anchors.centerIn: parent - text: scaleButton.scaleValue + "x" - color: scaleButton.activeScale || scaleButton.highlighted - ? panel.controlAccent : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.weight: scaleButton.activeScale ? Font.DemiBold : Font.Normal - renderType: Text.NativeRendering - } - - MouseArea { - id: scaleMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: if (containsMouse) { - panel.cursorActive = true - panel.cursorSection = "scale" - panel.cursorIndex = scaleButton.scaleIndex - } - onClicked: panel.monitorService.setScale(scaleButton.scaleValue) - } - } - - component DisplayRow: Rectangle { - id: displayRow - required property var display - required property int rowIndex - readonly property bool enabledDisplay: display && display.enabled === true - readonly property bool focusedDisplay: display && display.focused === true - readonly property bool canToggle: display - && (!enabledDisplay || panel.monitorService.enabledDisplayCount > 1) - readonly property bool keyboardFocused: panel.cursorActive - && panel.cursorSection === "displays" && panel.cursorIndex === rowIndex - readonly property bool hovered: displayMouse.containsMouse && canToggle - readonly property bool highlighted: keyboardFocused || hovered - implicitHeight: Commons.Style.space(36) - radius: panel.controlRadius - color: focusedDisplay ? panel.controlActiveFillColor - : highlighted ? panel.controlHoverFillColor : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: focusedDisplay || highlighted ? panel.controlAccent - : panel.controlBorderColor - opacity: canToggle ? 1 : 0.45 - - Behavior on color { ColorAnimation { duration: 100 } } - Behavior on border.color { ColorAnimation { duration: 100 } } - onKeyboardFocusedChanged: if (keyboardFocused) panel.ensureVisible(displayRow) - - Row { - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Commons.Style.space(8) - anchors.rightMargin: Commons.Style.space(8) - anchors.verticalCenter: parent.verticalCenter - spacing: Commons.Style.space(7) - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: "monitor" - color: displayRow.focusedDisplay || displayRow.highlighted - ? panel.controlAccent : panel.controlForeground - font.pixelSize: Commons.Style.font.body - fill: 1 - } - - Column { - width: parent.width - x - stateIcon.width - parent.spacing - anchors.verticalCenter: parent.verticalCenter - spacing: 0 - - Text { - width: parent.width - text: String(displayRow.display.name || "Display") - color: displayRow.focusedDisplay || displayRow.highlighted - ? panel.controlAccent : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: displayRow.focusedDisplay ? Font.DemiBold : Font.Normal - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: parent.width - text: displayRow.focusedDisplay ? "Focused" - : displayRow.enabledDisplay ? "Enabled" : "Disabled" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.55) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - - IconText { - id: stateIcon - anchors.verticalCenter: parent.verticalCenter - text: displayRow.enabledDisplay ? "check_circle" : "radio_button_unchecked" - color: displayRow.enabledDisplay - ? panel.controlAccent : panel.controlForeground - font.pixelSize: Commons.Style.font.body - fill: displayRow.enabledDisplay ? 1 : 0 - } - } - - MouseArea { - id: displayMouse - anchors.fill: parent - hoverEnabled: true - enabled: displayRow.canToggle - cursorShape: displayRow.canToggle ? Qt.PointingHandCursor : Qt.ArrowCursor - onContainsMouseChanged: if (containsMouse) { - panel.cursorActive = true - panel.cursorSection = "displays" - panel.cursorIndex = displayRow.rowIndex - } - onClicked: panel.monitorService.toggleDisplay( - displayRow.display.name, displayRow.enabledDisplay) - } - } -} diff --git a/widgets/BrightnessWidget.qml b/widgets/BrightnessWidget.qml deleted file mode 100644 index 3ad5df2..0000000 --- a/widgets/BrightnessWidget.qml +++ /dev/null @@ -1,250 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.brightness" - manageIpc: false - property url popupSource: Qt.resolvedUrl("BrightnessPanel.qml") - property var monitorServiceOverride: null - - readonly property var tokens: bar ? bar.visualTokens : null - readonly property bool compact: setting("compact", false) === true - readonly property var monitorService: monitorServiceOverride - || (bar ? bar.monitorService : null) - readonly property bool monitorReady: monitorService && monitorService.ready - readonly property bool brightnessAvailable: monitorReady - && monitorService.brightnessAvailable - readonly property bool internalDisplay: monitorReady - && hasInternalDisplay(monitorService.displays) - readonly property int percent: monitorReady - ? monitorService.brightnessPercent : 0 - readonly property int displayCount: monitorReady - && Array.isArray(monitorService.displays) ? monitorService.displays.length : 0 - readonly property string displayGlyph: Quickshell.screens.length > 1 - ? "󰍺" : "󰍹" - readonly property string tooltipText: !monitorReady ? "Display unavailable" - : brightnessAvailable ? "Brightness · " + percent + "%" - : "Display controls" - readonly property var panelItem: popupLoader.item - readonly property bool panelLoaded: panelItem !== null - readonly property var interactionTarget: actionButton - - visible: monitorReady - implicitWidth: visible ? (bar && bar.vertical ? bar.barSize : surface.implicitWidth) : 0 - implicitHeight: visible - ? (bar && bar.vertical ? surface.implicitHeight : bar ? bar.barSize : 28) : 0 - - function setting(name, fallback) { - const value = settings ? settings[name] : undefined - return value === undefined || value === null ? fallback : value - } - - function hasInternalDisplay(displays) { - const rows = Array.isArray(displays) ? displays : [] - for (let i = 0; i < rows.length; i++) { - if (/^(eDP|LVDS|DSI)-/.test(String(rows[i] && rows[i].name || ""))) - return true - } - return false - } - - function childPanelWidget(pluginId) { - const id = String(pluginId || "") - return id === moduleName || id === "omarchy.monitor" ? root : null - } - - function ownsPanelWidget(owner) { return owner === root } - - function adjustBrightness(delta) { - return monitorService && monitorService.setBrightness(percent + Number(delta || 0)) - } - - function syncPanelLoader() { - popupLoader.source = "" - if (!opened || !monitorReady || !String(popupSource)) return - monitorService.refresh() - popupLoader.setSource(popupSource, { - anchorItem: surface, - bar: root.bar, - ownerWidget: root, - monitorService: monitorService - }) - } - - onOpenedChanged: syncPanelLoader() - onMonitorReadyChanged: syncPanelLoader() - onPopupSourceChanged: syncPanelLoader() - Component.onDestruction: close() - - Loader { id: popupLoader } - - Item { - id: surface - anchors.centerIn: parent - implicitWidth: !root.bar || !root.tokens ? 0 - : root.bar.vertical ? root.bar.barSize - : content.implicitWidth + 2 * root.tokens.pillPaddingX - implicitHeight: !root.bar || !root.tokens ? 0 - : root.bar.vertical ? content.implicitHeight + Commons.Style.space(10) - : root.tokens.slotHeight - width: implicitWidth - height: implicitHeight - - Loader { - anchors.fill: parent - anchors.topMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - anchors.bottomMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - active: root.bar !== null && root.tokens !== null - sourceComponent: Component { - PillSurface { anchors.fill: parent; bar: root.bar } - } - } - - Loader { - id: content - anchors.centerIn: parent - sourceComponent: !root.bar || !root.tokens ? null - : root.internalDisplay - ? (root.compact ? compactBrightnessContent : fullBrightnessContent) - : displayContent - } - - Ui.WidgetButton { - id: actionButton - anchors.fill: parent - bar: root.monitorReady ? root.bar : null - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - fixedWidth: surface.width - fixedHeight: surface.height - tooltipText: root.tooltipText - onPressed: function(_button) { root.toggle() } - onWheelMoved: function(delta) { - if (root.brightnessAvailable) - root.adjustBrightness(delta > 0 ? 5 : -5) - } - } - } - - Component { - id: fullBrightnessContent - - Row { - spacing: root.tokens.contentGap - - Text { - anchors.verticalCenter: parent.verticalCenter - text: "BRI" - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.6) : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - font.letterSpacing: 0.5 - renderType: Text.NativeRendering - } - - SunIndicator { - anchors.verticalCenter: parent.verticalCenter - ratio: root.percent / 100 - color: root.bar ? (root.percent >= 100 ? root.bar.urgent - : root.bar.foreground) : Commons.Color.accent - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: root.percent + "%" - color: root.bar ? (root.percent >= 100 ? root.bar.urgent - : root.bar.foreground) : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - renderType: Text.NativeRendering - } - } - } - - Component { - id: compactBrightnessContent - - Row { - spacing: root.tokens.compactGap - - SunIndicator { - anchors.verticalCenter: parent.verticalCenter - ratio: root.percent / 100 - color: root.bar ? (root.percent >= 100 ? root.bar.urgent - : root.bar.foreground) : Commons.Color.accent - } - - Text { - anchors.verticalCenter: parent.verticalCenter - visible: !root.bar.vertical - text: root.percent + "%" - color: root.bar ? (root.percent >= 100 ? root.bar.urgent - : root.bar.foreground) : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - renderType: Text.NativeRendering - } - } - } - - Component { - id: displayContent - - Text { - text: root.displayGlyph - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.iconSize - renderType: Text.NativeRendering - } - } - - component SunIndicator: Item { - id: sun - - required property real ratio - required property color color - - width: Commons.Style.space(13) - height: Commons.Style.space(13) - - Rectangle { - anchors.centerIn: parent - width: Commons.Style.space(6.5) - height: width - radius: width / 2 - color: sun.color - Behavior on color { ColorAnimation { duration: 200 } } - } - - Repeater { - model: 8 - delegate: Item { - required property int index - anchors.fill: parent - rotation: index * 45 - - Rectangle { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - width: Commons.Style.space(1.5) - height: Commons.Style.space(2 + 1.4 * sun.ratio) - radius: width / 2 - color: sun.color - opacity: 0.35 + 0.65 * sun.ratio - } - } - } - } -} diff --git a/widgets/CalendarModel.js b/widgets/CalendarModel.js deleted file mode 100644 index d764f1c..0000000 --- a/widgets/CalendarModel.js +++ /dev/null @@ -1,52 +0,0 @@ -.pragma library - -var MonthNames = [ - "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", - "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER" -] - -function validDate(value) { - return value instanceof Date && !isNaN(value.getTime()) -} - -function normalizedDate(value) { - return validDate(value) ? value : new Date() -} - -function monthDate(value, offset) { - var date = normalizedDate(value) - return new Date(date.getFullYear(), date.getMonth() + Number(offset || 0), 1) -} - -function monthName(value, offset) { - return MonthNames[monthDate(value, offset).getMonth()] -} - -function year(value, offset) { - return String(monthDate(value, offset).getFullYear()) -} - -function dateLabel(value) { - var date = normalizedDate(value) - var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] - return days[date.getDay()] + " " + date.getDate() -} - -function cells(value, offset) { - var today = normalizedDate(value) - var first = monthDate(today, offset) - var currentYear = first.getFullYear() - var currentMonth = first.getMonth() - var lastDay = new Date(currentYear, currentMonth + 1, 0).getDate() - var startDay = (first.getDay() + 6) % 7 - var isCurrentMonth = currentYear === today.getFullYear() - && currentMonth === today.getMonth() - var result = [] - - for (var i = 0; i < startDay; i++) - result.push({ day: 0, today: false }) - for (var day = 1; day <= lastDay; day++) - result.push({ day: day, today: isCurrentMonth && day === today.getDate() }) - while (result.length < 42) result.push({ day: 0, today: false }) - return result -} diff --git a/widgets/CalendarPanel.qml b/widgets/CalendarPanel.qml deleted file mode 100644 index 218b6f6..0000000 --- a/widgets/CalendarPanel.qml +++ /dev/null @@ -1,241 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui -import "CalendarModel.js" as CalendarModel - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var clockService - property int monthOffset: 0 - property int selectedDay: 0 - readonly property date displayDate: clockService ? clockService.date : new Date() - readonly property var calendarCells: CalendarModel.cells(displayDate, monthOffset) - readonly property string monthName: CalendarModel.monthName(displayDate, monthOffset) - readonly property string calendarYear: CalendarModel.year(displayDate, monthOffset) - readonly property int renderedDayCount: dayRepeater.count - readonly property var stateService: bar && bar.shell - && typeof bar.shell.serviceFor === "function" - ? bar.shell.serviceFor("hancore.shibumi.state") : null - readonly property color deepInk: stateService - && typeof stateService.paletteColor === "function" - ? stateService.paletteColor("color07") : controlForeground - - owner: ownerWidget - open: ownerWidget.opened - focusTarget: keyCatcher - centerOnBar: true - gap: 8 - padding: 12 - contentWidth: fittedContentWidth(280) - contentHeight: fittedContentHeight(content.implicitHeight) - - function previousMonth() { monthOffset-- } - function nextMonth() { monthOffset++ } - function resetToToday() { - monthOffset = 0 - selectedDay = displayDate.getDate() - } - - Component.onCompleted: resetToToday() - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: ownerWidget.close() - onMoveRequested: function(dx, _dy) { - if (dx < 0) panel.previousMonth() - else if (dx > 0) panel.nextMonth() - } - onTabRequested: function(direction) { - panel.ownerWidget.switchPanel(direction) - } - - Column { - id: content - width: parent.width - spacing: 10 - - Item { - width: parent.width - height: 24 - - CalendarAction { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - iconText: "‹" - tooltipText: "Previous month" - onClicked: panel.previousMonth() - } - - Text { - anchors.centerIn: parent - text: panel.monthName + " " + panel.calendarYear - color: monthMouse.containsMouse && panel.monthOffset !== 0 - ? panel.controlAccent : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 12 - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - - MouseArea { - id: monthMouse - anchors.fill: parent - anchors.margins: -6 - hoverEnabled: true - enabled: panel.monthOffset !== 0 - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: panel.resetToToday() - } - } - - CalendarAction { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - iconText: "›" - tooltipText: "Next month" - onClicked: panel.nextMonth() - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.dividerColor - } - - Row { - width: parent.width - - Repeater { - model: ["MO", "TU", "WE", "TH", "FR", "SA", "SU"] - - delegate: Item { - required property string modelData - required property int index - width: content.width / 7 - height: 20 - - Text { - anchors.centerIn: parent - text: modelData - color: index >= 5 ? panel.controlAccent : panel.deepInk - opacity: index >= 5 ? 0.85 : 0.7 - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 10 - font.letterSpacing: 2 - renderType: Text.NativeRendering - } - } - } - } - - Grid { - columns: 7 - rowSpacing: 2 - columnSpacing: 0 - width: parent.width - - Repeater { - id: dayRepeater - model: panel.calendarCells - - delegate: Item { - id: dayCell - required property var modelData - required property int index - readonly property bool hasDay: modelData.day > 0 - readonly property bool today: modelData.today === true - readonly property bool selected: hasDay && !today - && panel.selectedDay === modelData.day && panel.monthOffset === 0 - readonly property bool weekend: index % 7 >= 5 - width: content.width / 7 - height: 28 - - Rectangle { - anchors.centerIn: parent - width: 24 - height: width - radius: 12 - visible: dayCell.today - color: panel.controlAccent - } - - Rectangle { - anchors.centerIn: parent - width: 24 - height: width - radius: 12 - visible: dayCell.selected - color: "transparent" - border.width: 1 - border.color: panel.controlAccent - } - - Text { - anchors.centerIn: parent - text: dayCell.hasDay ? dayCell.modelData.day : "" - color: dayCell.today - ? panel.controlAccent.hsvValue < 0.5 - ? panel.controlForeground : Commons.Color.background - : dayCell.weekend ? panel.controlAccent : panel.controlForeground - opacity: dayCell.hasDay ? 1 : 0.35 - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 12 - font.weight: dayCell.today ? Font.Medium : Font.Light - renderType: Text.NativeRendering - } - - MouseArea { - anchors.fill: parent - enabled: dayCell.hasDay - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: panel.selectedDay = dayCell.modelData.day - } - } - } - } - } - } - - component CalendarAction: Rectangle { - id: action - property string iconText: "" - property string tooltipText: "" - signal clicked() - readonly property bool hovered: actionMouse.containsMouse - width: 24 - height: 24 - radius: panel.controlRadius - color: "transparent" - - Text { - anchors.centerIn: parent - text: action.iconText - color: action.hovered ? panel.controlAccent : panel.controlMuted - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 16 - renderType: Text.NativeRendering - Behavior on color { ColorAnimation { duration: 120 } } - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: panel.shellStyle !== "shibumi" - && action.tooltipText !== "" && actionMouse.containsMouse - text: action.tooltipText - } - } -} diff --git a/widgets/CavaThemeModel.js b/widgets/CavaThemeModel.js deleted file mode 100644 index 46360b3..0000000 --- a/widgets/CavaThemeModel.js +++ /dev/null @@ -1,50 +0,0 @@ -.pragma library - -var ColorPattern = /^#(?:[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/ -var MaxInputLength = 65536 -var MaxLines = 1024 -var MaxGradientColors = 8 - -function parse(raw) { - var gradientEnabled = false - var gradientCount = 0 - var foreground = "" - var indexedColors = {} - var lines = String(raw || "").slice(0, MaxInputLength).split("\n") - - for (var i = 0; i < lines.length && i < MaxLines; i++) { - var assignment = lines[i].match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/) - if (!assignment) continue - var key = String(assignment[1]).toLowerCase() - var value = String(assignment[2]).replace(/^\s*["']?|["']?\s*$/g, "") - - if (key === "gradient") { - gradientEnabled = value === "1" || value.toLowerCase() === "true" - } else if (key === "gradient_count") { - var count = parseInt(value) - gradientCount = isNaN(count) ? 0 - : Math.max(0, Math.min(MaxGradientColors, count)) - } else if (key === "foreground" && ColorPattern.test(value)) { - foreground = value - } else { - var colorMatch = key.match(/^gradient_color_([1-8])$/) - if (colorMatch && ColorPattern.test(value)) - indexedColors[parseInt(colorMatch[1])] = value - } - } - - if (!gradientEnabled) return foreground ? [foreground] : [] - - var count = gradientCount - if (count === 0) { - while (count < MaxGradientColors && indexedColors[count + 1]) count++ - } - if (count < 2) return foreground ? [foreground] : [] - - var colors = [] - for (var index = 1; index <= count; index++) { - if (!indexedColors[index]) return foreground ? [foreground] : [] - colors.push(indexedColors[index]) - } - return colors -} diff --git a/widgets/CenterLayout.js b/widgets/CenterLayout.js deleted file mode 100644 index 726c14b..0000000 --- a/widgets/CenterLayout.js +++ /dev/null @@ -1,17 +0,0 @@ -.pragma library - -function nextStage(currentStage, availableWidth, needNormal, needCompact, - centered) { - if (!centered || Number(availableWidth) <= 0) return 0 - - var stage = Math.max(0, Math.min(2, Number(currentStage) || 0)) - var available = Math.max(0, Number(availableWidth) || 0) - var normal = Math.max(0, Number(needNormal) || 0) - var compact = Math.max(0, Number(needCompact) || 0) - - if (stage === 0 && normal + 24 > available) stage = 1 - if (stage === 1 && compact + 24 > available) stage = 2 - if (stage === 2 && compact + 48 <= available) stage = 1 - if (stage === 1 && normal + 48 <= available) stage = 0 - return stage -} diff --git a/widgets/CenterWidget.qml b/widgets/CenterWidget.qml deleted file mode 100644 index d980242..0000000 --- a/widgets/CenterWidget.qml +++ /dev/null @@ -1,279 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui -import "CenterLayout.js" as CenterLayout -import "CalendarModel.js" as CalendarModel - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.center" - manageIpc: false - property url calendarSource: Qt.resolvedUrl("CalendarPanel.qml") - property real availableWidth: 0 - property int stage: 0 - readonly property var tokens: bar ? bar.visualTokens : null - readonly property var clock: bar ? bar.clockService : null - readonly property date displayDate: clock ? clock.date : new Date() - readonly property string dateText: CalendarModel.dateLabel(displayDate) - readonly property bool centered: availableWidth > 0 - readonly property bool showWeather: stage <= 1 - readonly property bool showDate: stage === 0 - readonly property real weatherNaturalWidth: weatherView.implicitWidth - readonly property real indicatorNaturalWidth: statusIndicators.implicitWidth - readonly property real updateNaturalWidth: updateView.implicitWidth - readonly property bool showIndicators: indicatorNaturalWidth > 0.5 - || updateNaturalWidth > 0.5 - readonly property real statusNaturalWidth: indicatorNaturalWidth - + updateNaturalWidth - + (indicatorNaturalWidth > 0.5 && updateNaturalWidth > 0.5 ? Commons.Style.space(8) : 0) - readonly property real centerPadding: tokens ? tokens.pillPaddingX : 9 - readonly property real centerSpacing: Commons.Style.space(8) - readonly property real needMinimal: 2 * centerPadding + clockWidget.implicitWidth - + (showIndicators ? centerSpacing + statusNaturalWidth : 0) - readonly property real minimumResponsiveWidth: needMinimal - readonly property real needCompact: needMinimal - + (weatherNaturalWidth > 0.5 ? centerSpacing + weatherNaturalWidth : 0) - readonly property real needNormal: needCompact - + (dateButton.implicitWidth > 0.5 ? centerSpacing + dateButton.implicitWidth : 0) - readonly property var weatherWidget: weatherView - readonly property var indicatorWidget: statusIndicators - readonly property var updateWidget: updateView - readonly property var updateBackend: updateLoader.item - readonly property bool calendarLoaded: calendarLoader.item !== null - readonly property bool calendarLoaderReady: calendarLoader.item - ? calendarLoader.item.ready === true : false - readonly property var weatherSettings: childSettings("omarchy.weather") - readonly property var clockSettings: childSettings("omarchy.clock") - readonly property var updateSettings: childSettings("omarchy.system-update") - readonly property url updateSource: registeredSource("omarchy.system-update") - property Component updateComponent: String(updateSource) ? null - : registeredComponent("omarchy.system-update") - - implicitWidth: bar && bar.vertical ? bar.barSize - : Math.round(centerRow.implicitWidth) + 2 * centerPadding - implicitHeight: bar ? bar.barSize : Commons.Style.space(35) - - function childSettings(id) { - if (bar && typeof bar.widgetSettings === "function") - return bar.widgetSettings("G8", id) - return settings && settings[id] && typeof settings[id] === "object" - ? settings[id] : ({}) - } - - function registeredComponent(id) { - if (bar && typeof bar.registeredWidgetComponent === "function") - return bar.registeredWidgetComponent(id) - const registry = bar ? bar.barWidgetRegistry : null - if (registry) void(registry.revision) - const widgets = registry && registry.widgets ? registry.widgets : ({}) - const entry = widgets[id] - return entry ? entry.component : null - } - - function registeredSource(id) { - return bar && typeof bar.registeredWidgetSource === "function" - ? bar.registeredWidgetSource(id) : "" - } - - function syncOfficialLoader(loader, component, source, id, childSettingsValue) { - loader.sourceComponent = null - loader.source = "" - if (component !== null) { - loader.sourceComponent = component - } else if (String(source)) { - loader.setSource(source, { - bar: root.bar, - moduleName: id, - settings: childSettingsValue - }) - } - } - - function syncUpdateLoader() { - syncOfficialLoader(updateLoader, updateComponent, updateSource, - "omarchy.system-update", updateSettings) - } - - function injectChild(item, id, childSettingsValue) { - if (!item) return - if ("bar" in item) item.bar = bar - if ("moduleName" in item) item.moduleName = id - if ("settings" in item) item.settings = childSettingsValue - } - - function injectChildren() { - injectChild(updateLoader.item, "omarchy.system-update", updateSettings) - } - - function childPanelWidget(pluginId) { - const id = String(pluginId || "") - if (id === "omarchy.clock") return root - if (id === "omarchy.weather") return weatherView - return null - } - - function updateStage() { - stage = CenterLayout.nextStage(stage, availableWidth, - needNormal, needCompact, centered) - } - - function syncCalendarLoader() { - if (!opened) { - calendarLoader.source = "" - return - } - calendarLoader.setSource(calendarSource, { - anchorItem: dateButton, - bar: root.bar, - ownerWidget: root, - clockService: root.clock - }) - } - - onOpenedChanged: syncCalendarLoader() - onBarChanged: Qt.callLater(injectChildren) - onUpdateSettingsChanged: injectChildren() - onUpdateComponentChanged: Qt.callLater(syncUpdateLoader) - onUpdateSourceChanged: Qt.callLater(syncUpdateLoader) - onAvailableWidthChanged: restageTimer.restart() - onNeedNormalChanged: restageTimer.restart() - onNeedCompactChanged: restageTimer.restart() - Component.onCompleted: { - restageTimer.restart() - Qt.callLater(syncUpdateLoader) - } - - Timer { - id: restageTimer - interval: 80 - onTriggered: root.updateStage() - } - - PillSurface { - bar: root.bar - anchors.fill: parent - anchors.topMargin: Math.round((parent.height - root.tokens.pillHeight) / 2) - anchors.bottomMargin: Math.round((parent.height - root.tokens.pillHeight) / 2) - } - - Row { - id: centerRow - anchors.centerIn: parent - spacing: root.centerSpacing - - Item { - id: weatherHolder - visible: width > 0.5 - width: root.showWeather ? root.weatherNaturalWidth : 0 - height: root.implicitHeight - clip: true - opacity: root.showWeather ? 1 : 0 - - Behavior on width { - NumberAnimation { duration: 160; easing.type: Easing.OutCubic } - } - Behavior on opacity { - NumberAnimation { duration: 140; easing.type: Easing.OutCubic } - } - - WeatherWidget { - id: weatherView - anchors.centerIn: parent - bar: root.bar - settings: root.weatherSettings - weatherService: root.bar ? root.bar.weatherService : null - } - } - - ClockWidget { - id: clockWidget - bar: root.bar - settings: root.clockSettings - } - - Item { - id: dateHolder - visible: width > 0.5 - width: root.showDate ? dateButton.implicitWidth : 0 - height: root.implicitHeight - clip: true - opacity: root.showDate ? 1 : 0 - - Behavior on width { - NumberAnimation { duration: 160; easing.type: Easing.OutCubic } - } - Behavior on opacity { - NumberAnimation { duration: 140; easing.type: Easing.OutCubic } - } - - Ui.WidgetButton { - id: dateButton - anchors.centerIn: parent - bar: root.bar - text: root.dateText - fontFamily: root.bar ? root.bar.fontFamily : Commons.Style.font.family - fontSize: root.tokens ? root.tokens.captionSize : 10 - foreground: root.tokens - ? Commons.Util.alpha(root.tokens.ink, 0.5) - : Commons.Util.alpha(Commons.Color.foreground, 0.5) - horizontalMargin: 0 - verticalPadding: 0 - fixedHeight: root.implicitHeight - tooltipText: Qt.formatDate(root.displayDate, "dddd, d MMMM yyyy") - onPressed: function(button) { - if (button === Qt.RightButton && root.bar) root.bar.run("omarchy-menu-timezone") - else root.toggle() - } - } - } - - Item { - id: indicatorHolder - visible: root.showIndicators || width > 0.5 - width: root.showIndicators ? root.statusNaturalWidth : 0 - height: root.implicitHeight - clip: true - opacity: root.showIndicators ? 1 : 0 - - Behavior on width { - NumberAnimation { duration: 160; easing.type: Easing.OutCubic } - } - Behavior on opacity { - NumberAnimation { duration: 140; easing.type: Easing.OutCubic } - } - - Row { - anchors.centerIn: parent - spacing: Commons.Style.space(8) - - StatusIndicators { - id: statusIndicators - width: implicitWidth - height: root.implicitHeight - bar: root.bar - statusService: root.bar ? root.bar.statusService : null - } - - SystemUpdateWidget { - id: updateView - bar: root.bar - settings: root.updateSettings - backendWidget: updateLoader.item - } - } - } - } - - Loader { - id: updateLoader - visible: false - onLoaded: { - root.injectChild(item, "omarchy.system-update", root.updateSettings) - } - } - - Loader { id: calendarLoader } -} diff --git a/widgets/ClockWidget.qml b/widgets/ClockWidget.qml deleted file mode 100644 index c321de1..0000000 --- a/widgets/ClockWidget.qml +++ /dev/null @@ -1,76 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.BarWidget { - id: root - - moduleName: "omarchy.clock" - - readonly property var clock: bar ? bar.clockService : null - readonly property date displayDate: clock ? clock.date : new Date() - readonly property bool clock12h: setting("clock12h", false) === true - readonly property string timeText: formatTime(displayDate) - readonly property string tooltipText: Qt.formatDate(displayDate, "ddd, d MMMM yyyy") - - implicitWidth: bar && bar.vertical ? bar.barSize : label.implicitWidth - implicitHeight: bar ? bar.barSize : 35 - - function pad(value) { - return Number(value) < 10 ? "0" + Number(value) : String(Number(value)) - } - - function formatTime(value) { - if (!(value instanceof Date) || isNaN(value.getTime())) return "--:--" - const hours = value.getHours() - if (clock12h) { - let displayHours = hours % 12 - if (displayHours === 0) displayHours = 12 - return displayHours + ":" + pad(value.getMinutes()) - + (hours < 12 ? " AM" : " PM") - } - return pad(hours) + ":" + pad(value.getMinutes()) - } - - function toggleFormat() { - return bar && typeof bar.setWidgetSetting === "function" - ? bar.setWidgetSetting("G8", moduleName, "clock12h", !clock12h) - : false - } - - function openTimezoneMenu() { - if (!bar || typeof bar.run !== "function") return false - bar.run("omarchy-menu-timezone") - return true - } - - Text { - id: label - anchors.centerIn: parent - text: root.bar && root.bar.vertical - ? Qt.formatTime(root.displayDate, "HH\nmm") : root.timeText - horizontalAlignment: Text.AlignHCenter - color: root.bar ? root.bar.foreground : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.bar && root.bar.visualTokens - ? root.bar.visualTokens.labelSize : 12 - font.letterSpacing: root.bar && root.bar.vertical ? 0 : 1 - renderType: Text.NativeRendering - } - - Ui.WidgetButton { - anchors.fill: parent - bar: root.bar - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - tooltipText: root.tooltipText - onPressed: function(button) { - if (button === Qt.RightButton) root.openTimezoneMenu() - else root.toggleFormat() - } - } -} diff --git a/widgets/CpuPanel.qml b/widgets/CpuPanel.qml deleted file mode 100644 index b57c080..0000000 --- a/widgets/CpuPanel.qml +++ /dev/null @@ -1,238 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var systemTelemetry - required property var gpuTelemetry - - owner: ownerWidget - open: ownerWidget.opened - focusTarget: keyCatcher - gap: 8 - padding: 12 - contentWidth: fittedContentWidth(320) - contentHeight: fittedContentHeight(panelColumn.implicitHeight) - - Component.onCompleted: if (gpuTelemetry) gpuTelemetry.acquire() - Component.onDestruction: if (gpuTelemetry) gpuTelemetry.release() - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.ownerWidget.close() - onTabRequested: function(direction) { panel.ownerWidget.switchPanel(direction) } - - Column { - id: panelColumn - width: parent.width - spacing: 8 - - Item { - width: parent.width - height: 24 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "CPU · GPU" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 13 - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: "\u2715" - color: closeMouse.containsMouse - ? panel.controlAccent : panel.controlMuted - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 12 - renderType: Text.NativeRendering - - Behavior on color { ColorAnimation { duration: 120 } } - - MouseArea { - id: closeMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.ownerWidget.close() - } - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, panel.bar.foreground.g, - panel.bar.foreground.b, 0.18) : Commons.Color.popups.border - } - - UsageRow { - width: parent.width - label: "CPU" - value: panel.systemTelemetry.cpuPercent - bar: panel.bar - } - - UsageRow { - width: parent.width - visible: panel.gpuTelemetry && panel.gpuTelemetry.available - label: "GPU" - value: panel.gpuTelemetry ? panel.gpuTelemetry.utilization : 0 - bar: panel.bar - } - - Row { - width: parent.width - visible: panel.gpuTelemetry && panel.gpuTelemetry.available - && panel.gpuTelemetry.temperatureC > 0 - - Text { - width: parent.width * 0.4 - text: "Temperature" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, panel.bar.foreground.g, - panel.bar.foreground.b, 0.65) : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - Text { - width: parent.width * 0.3 - text: panel.gpuTelemetry ? panel.gpuTelemetry.temperatureC + "°C" : "" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - } - - Row { - width: parent.width - visible: panel.gpuTelemetry && panel.gpuTelemetry.memoryTotalMiB > 0 - - Text { - width: parent.width * 0.4 - text: "VRAM" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, panel.bar.foreground.g, - panel.bar.foreground.b, 0.65) : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - Text { - width: parent.width * 0.3 - text: panel.gpuTelemetry - ? panel.gpuTelemetry.memoryUsedMiB + " / " + panel.gpuTelemetry.memoryTotalMiB + " MiB" - : "" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.dividerColor - } - - Rectangle { - width: parent.width - height: 28 - radius: panel.controlRadius - color: monitorMouse.containsMouse - ? panel.controlPrimaryHoverColor - : panel.bar ? panel.bar.urgent : Commons.Color.accent - - Behavior on color { ColorAnimation { duration: 120 } } - - Text { - anchors.centerIn: parent - text: "Open btop" - color: panel.bar ? panel.bar.background : Commons.Color.background - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - - MouseArea { - id: monitorMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - panel.ownerWidget.close() - panel.ownerWidget.openSystemMonitor() - } - } - } - } - } - - component UsageRow: Item { - id: usageRow - - required property string label - required property int value - required property var bar - - height: 16 - - Text { - id: usageLabel - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: parent.label - color: parent.bar ? Qt.rgba(parent.bar.foreground.r, parent.bar.foreground.g, - parent.bar.foreground.b, 0.65) : Commons.Color.foreground - font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - font.letterSpacing: 1 - renderType: Text.NativeRendering - } - - Text { - id: usageValue - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: parent.value + "%" - color: parent.bar ? parent.bar.urgent : Commons.Color.accent - font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Rectangle { - anchors.left: usageLabel.right - anchors.leftMargin: 8 - anchors.right: usageValue.left - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - height: 8 - radius: height / 2 - color: panel.controlActiveFillColor - - Rectangle { - width: parent.width * Math.max(0, Math.min(100, usageRow.value)) / 100 - height: parent.height - radius: height / 2 - color: usageRow.bar ? usageRow.bar.urgent : Commons.Color.accent - Behavior on width { NumberAnimation { duration: 300 } } - } - } - } -} diff --git a/widgets/CpuWave.qml b/widgets/CpuWave.qml deleted file mode 100644 index e652bac..0000000 --- a/widgets/CpuWave.qml +++ /dev/null @@ -1,62 +0,0 @@ -import QtQuick -import qs.Commons as Commons - -Canvas { - id: root - - required property var history - required property color accent - property int maxSamples: 30 - - width: Commons.Style.space(36) - height: Commons.Style.space(14) - - onHistoryChanged: requestPaint() - onAccentChanged: requestPaint() - Component.onCompleted: requestPaint() - - onPaint: { - const context = getContext("2d") - context.clearRect(0, 0, width, height) - const values = root.history || [] - if (values.length < 2) return - - let maximum = 0.25 - for (let i = 0; i < values.length; i++) maximum = Math.max(maximum, values[i]) - maximum = Math.min(1, Math.max(0.25, maximum * 1.15)) - - const points = [] - for (let j = 0; j < values.length; j++) { - points.push({ - x: (j / Math.max(1, maxSamples - 1)) * width, - y: height - (values[j] / maximum) * height - }) - } - - context.beginPath() - context.moveTo(points[0].x, height) - context.lineTo(points[0].x, points[0].y) - for (let k = 1; k < points.length; k++) { - const controlX = (points[k - 1].x + points[k].x) / 2 - context.bezierCurveTo(controlX, points[k - 1].y, controlX, points[k].y, - points[k].x, points[k].y) - } - context.lineTo(points[points.length - 1].x, height) - context.closePath() - context.fillStyle = Qt.rgba(accent.r, accent.g, accent.b, 0.12) - context.fill() - - context.beginPath() - context.moveTo(points[0].x, points[0].y) - for (let n = 1; n < points.length; n++) { - const controlX = (points[n - 1].x + points[n].x) / 2 - context.bezierCurveTo(controlX, points[n - 1].y, controlX, points[n].y, - points[n].x, points[n].y) - } - context.strokeStyle = accent - context.lineWidth = 1.5 - context.lineCap = "round" - context.lineJoin = "round" - context.stroke() - } -} diff --git a/widgets/CpuWidget.qml b/widgets/CpuWidget.qml deleted file mode 100644 index 9b4b31c..0000000 --- a/widgets/CpuWidget.qml +++ /dev/null @@ -1,172 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.cpu" - manageIpc: false - - readonly property var telemetry: bar ? bar.systemTelemetry : null - readonly property var tokens: bar ? bar.visualTokens : null - readonly property var gpuTelemetry: bar ? bar.gpuTelemetry : null - readonly property bool compact: setting("compact", false) === true - readonly property int percent: telemetry ? telemetry.cpuPercent : 0 - readonly property var history: telemetry ? telemetry.cpuHistory : [] - property var acquiredTelemetry: null - - implicitWidth: bar && bar.vertical ? bar.barSize : cpuSurface.implicitWidth - implicitHeight: bar && bar.vertical ? cpuSurface.implicitHeight : bar ? bar.barSize : 28 - - function syncTelemetryOwner() { - if (acquiredTelemetry === telemetry) return - if (acquiredTelemetry) acquiredTelemetry.release("cpu") - acquiredTelemetry = telemetry - if (acquiredTelemetry) acquiredTelemetry.acquire("cpu") - } - - function syncPanelLoader() { - if (!opened) { - panelLoader.source = "" - return - } - panelLoader.setSource(Qt.resolvedUrl("CpuPanel.qml"), { - anchorItem: cpuSurface, - bar: root.bar, - ownerWidget: root, - systemTelemetry: root.telemetry, - gpuTelemetry: root.gpuTelemetry - }) - } - - function openSystemMonitor() { - if (!bar || typeof bar.run !== "function") return false - bar.run("omarchy-launch-or-focus-tui btop") - return true - } - - onTelemetryChanged: syncTelemetryOwner() - onOpenedChanged: syncPanelLoader() - Component.onCompleted: syncTelemetryOwner() - Component.onDestruction: if (acquiredTelemetry) acquiredTelemetry.release("cpu") - - Item { - id: cpuSurface - - anchors.centerIn: parent - implicitWidth: root.bar && root.bar.vertical - ? root.bar.barSize - : content.implicitWidth + 2 * root.tokens.pillPaddingX - implicitHeight: root.bar && root.bar.vertical - ? content.implicitHeight + Commons.Style.space(10) - : root.tokens ? root.tokens.slotHeight : 28 - width: implicitWidth - height: implicitHeight - - PillSurface { - bar: root.bar - anchors.fill: parent - anchors.topMargin: Math.round((parent.height - root.tokens.pillHeight) / 2) - anchors.bottomMargin: Math.round((parent.height - root.tokens.pillHeight) / 2) - } - - Loader { - id: content - anchors.centerIn: parent - sourceComponent: root.bar && root.bar.vertical ? verticalContent : horizontalContent - } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) root.bar.showTooltip(cpuSurface, root.percent + "%") - onExited: if (root.bar) root.bar.hideTooltip(cpuSurface) - onClicked: { - if (root.bar) root.bar.hideTooltip(cpuSurface) - root.toggle() - } - } - } - - Loader { - id: panelLoader - } - - Component { - id: horizontalContent - - Row { - spacing: root.compact ? root.tokens.compactGap : root.tokens.contentGap - - Text { - visible: !root.compact - anchors.verticalCenter: parent.verticalCenter - text: "CPU" - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.6) : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - font.letterSpacing: 0.5 - renderType: Text.NativeRendering - } - - CpuWave { - visible: !root.compact - anchors.verticalCenter: parent.verticalCenter - history: root.history - accent: root.bar ? root.bar.urgent : Commons.Color.accent - } - - IconText { - visible: root.compact - anchors.verticalCenter: parent.verticalCenter - text: "planner_review" - color: root.bar.urgent - font.pixelSize: root.tokens.iconSize - font.weight: Font.DemiBold - fill: 1 - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: String(root.percent).padStart(2, "0") + "%" - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - renderType: Text.NativeRendering - } - } - } - - Component { - id: verticalContent - - Column { - spacing: Commons.Style.space(2) - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "CPU" - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.62) : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: String(root.percent).padStart(2, "0") + "%" - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - } -} diff --git a/widgets/HearthstonePickerView.qml b/widgets/HearthstonePickerView.qml deleted file mode 100644 index 46719c4..0000000 --- a/widgets/HearthstonePickerView.qml +++ /dev/null @@ -1,255 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Shapes -import qs.Commons as Commons -import "PickerModel.js" as PickerModel - -Item { - id: root - required property var bar - required property var controller - - readonly property real cardWidth: Commons.Style.space(232) - readonly property real cardHeight: Commons.Style.space(330) - readonly property real stepX: Commons.Style.space(128) - readonly property real focusLift: Commons.Style.space(54) - readonly property real focusScale: 1.24 - readonly property real spreadDegrees: 6 - readonly property int maxVisible: 5 - readonly property color frameColor: "#16161c" - readonly property color textColor: "#ececee" - readonly property color dimTextColor: Qt.rgba(0.92, 0.92, 0.94, 0.55) - property real dealProgress: 0 - property bool dealStarted: false - readonly property bool dealSettled: dealProgress >= 0.999 - - function startDeal() { - if (dealStarted || controller.filteredEntries.length === 0) return - dealStarted = true - dealProgress = 0 - Qt.callLater(function() { root.dealProgress = 1 }) - } - - Component.onCompleted: startDeal() - - Connections { - target: root.controller - function onFilteredEntriesChanged() { root.startDeal() } - } - - Behavior on dealProgress { - NumberAnimation { - duration: root.controller.mode === "theme" ? 260 : 180 - easing.type: Easing.OutCubic - } - } - - Text { - visible: root.controller.filteredEntries.length > 0 - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: Commons.Style.space(40) - text: root.controller.mode.toUpperCase() + " " - + (root.controller.selectedIndex + 1) + " / " - + root.controller.filteredEntries.length - color: root.dimTextColor - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - font.letterSpacing: 2 - renderType: Text.NativeRendering - } - - Item { - id: hand - anchors.centerIn: parent - anchors.verticalCenterOffset: -Commons.Style.space(16) - width: parent.width - height: root.cardHeight + root.focusLift + Commons.Style.space(40) - - Repeater { - model: root.controller.filteredEntries - - delegate: Item { - id: card - required property int index - required property var modelData - readonly property int relative: index - root.controller.selectedIndex - readonly property bool focused: relative === 0 - readonly property bool nearby: Math.abs(relative) <= root.maxVisible - readonly property real shade: focused ? 0 - : Math.min(0.62, 0.30 + Math.abs(relative) * 0.05) - readonly property bool current: root.controller.mode === "theme" - ? String(modelData.label || "") === String(root.controller.currentSelection || "") - : String(modelData.sourcePath || "") === String(root.controller.currentSelection || "") - - visible: nearby - width: root.cardWidth - height: root.cardHeight - transformOrigin: Item.Bottom - x: (hand.width - width) / 2 - + relative * root.stepX * root.dealProgress - y: hand.height - height - - (focused ? root.focusLift * root.dealProgress : 0) - z: focused ? 1000 : 500 - Math.min(Math.abs(relative), 40) - rotation: relative * root.spreadDegrees * root.dealProgress - scale: focused - ? 1 + (root.focusScale - 1) * root.dealProgress : 1 - opacity: root.dealProgress - - Behavior on x { - enabled: root.dealSettled - NumberAnimation { duration: 180; easing.type: Easing.OutCubic } - } - Behavior on y { - enabled: root.dealSettled - NumberAnimation { duration: 180; easing.type: Easing.OutCubic } - } - Behavior on rotation { - enabled: root.dealSettled - NumberAnimation { duration: 180; easing.type: Easing.OutCubic } - } - Behavior on scale { - enabled: root.dealSettled - NumberAnimation { duration: 180; easing.type: Easing.OutCubic } - } - - Item { - anchors.fill: parent - anchors.margins: Commons.Style.space(6) - clip: true - - Image { - anchors.fill: parent - source: root.controller.thumbnailUrl(card.modelData) - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - retainWhileLoading: true - sourceSize.width: Math.round(root.cardWidth * 2) - sourceSize.height: Math.round(root.cardHeight * 2) - } - - IconText { - anchors.centerIn: parent - visible: !card.modelData || !card.modelData.thumbnailReady - text: root.controller.videoMode ? "movie" : "image" - color: Qt.rgba(root.textColor.r, root.textColor.g, - root.textColor.b, 0.34) - font.pixelSize: Commons.Style.font.displayLarge - fill: 0 - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: Commons.Style.space(70) - gradient: Gradient { - GradientStop { position: 0; color: "transparent" } - GradientStop { position: 1; color: Qt.rgba(0, 0, 0, 0.72) } - } - } - - Text { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Commons.Style.space(12) - text: !card.modelData ? "" - : root.controller.mediaMode - ? PickerModel.mediaLabel(card.modelData.sourcePath) - : String(card.modelData.label || "") - color: root.textColor - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.space(13) - font.weight: Font.DemiBold - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - maximumLineCount: 1 - renderType: Text.NativeRendering - } - - Rectangle { - anchors.fill: parent - color: "black" - opacity: card.shade - Behavior on opacity { NumberAnimation { duration: 180 } } - } - } - - Shape { - id: frame - anchors.fill: parent - preferredRendererType: Shape.CurveRenderer - readonly property real outerRadius: Commons.Style.space(18) - readonly property real matWidth: Commons.Style.space(8) - readonly property real innerRadius: Commons.Style.space(10) - - ShapePath { - fillRule: ShapePath.OddEvenFill - fillColor: root.frameColor - strokeColor: "transparent" - strokeWidth: 0 - startX: frame.outerRadius - startY: 0 - PathLine { x: frame.width - frame.outerRadius; y: 0 } - PathArc { x: frame.width; y: frame.outerRadius; radiusX: frame.outerRadius; radiusY: frame.outerRadius } - PathLine { x: frame.width; y: frame.height - frame.outerRadius } - PathArc { x: frame.width - frame.outerRadius; y: frame.height; radiusX: frame.outerRadius; radiusY: frame.outerRadius } - PathLine { x: frame.outerRadius; y: frame.height } - PathArc { x: 0; y: frame.height - frame.outerRadius; radiusX: frame.outerRadius; radiusY: frame.outerRadius } - PathLine { x: 0; y: frame.outerRadius } - PathArc { x: frame.outerRadius; y: 0; radiusX: frame.outerRadius; radiusY: frame.outerRadius } - PathMove { x: frame.matWidth + frame.innerRadius; y: frame.matWidth } - PathLine { x: frame.width - frame.matWidth - frame.innerRadius; y: frame.matWidth } - PathArc { x: frame.width - frame.matWidth; y: frame.matWidth + frame.innerRadius; radiusX: frame.innerRadius; radiusY: frame.innerRadius } - PathLine { x: frame.width - frame.matWidth; y: frame.height - frame.matWidth - frame.innerRadius } - PathArc { x: frame.width - frame.matWidth - frame.innerRadius; y: frame.height - frame.matWidth; radiusX: frame.innerRadius; radiusY: frame.innerRadius } - PathLine { x: frame.matWidth + frame.innerRadius; y: frame.height - frame.matWidth } - PathArc { x: frame.matWidth; y: frame.height - frame.matWidth - frame.innerRadius; radiusX: frame.innerRadius; radiusY: frame.innerRadius } - PathLine { x: frame.matWidth; y: frame.matWidth + frame.innerRadius } - PathArc { x: frame.matWidth + frame.innerRadius; y: frame.matWidth; radiusX: frame.innerRadius; radiusY: frame.innerRadius } - } - - ShapePath { - fillColor: "transparent" - strokeColor: card.focused ? root.bar.urgent : "transparent" - strokeWidth: card.focused ? Commons.Style.space(2) : 0 - startX: frame.outerRadius - startY: 0 - PathLine { x: frame.width - frame.outerRadius; y: 0 } - PathArc { x: frame.width; y: frame.outerRadius; radiusX: frame.outerRadius; radiusY: frame.outerRadius } - PathLine { x: frame.width; y: frame.height - frame.outerRadius } - PathArc { x: frame.width - frame.outerRadius; y: frame.height; radiusX: frame.outerRadius; radiusY: frame.outerRadius } - PathLine { x: frame.outerRadius; y: frame.height } - PathArc { x: 0; y: frame.height - frame.outerRadius; radiusX: frame.outerRadius; radiusY: frame.outerRadius } - PathLine { x: 0; y: frame.outerRadius } - PathArc { x: frame.outerRadius; y: 0; radiusX: frame.outerRadius; radiusY: frame.outerRadius } - } - } - - Rectangle { - visible: card.current - width: Commons.Style.space(9) - height: width - radius: width / 2 - x: Commons.Style.space(14) - y: Commons.Style.space(14) - z: 5 - color: root.bar.urgent - border.color: Qt.rgba(0, 0, 0, 0.35) - border.width: 1 - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: card.focused - ? root.controller.activateSelected() - : root.controller.selectIndex(card.index) - } - } - } - } -} diff --git a/widgets/MediaMuse.qml b/widgets/MediaMuse.qml deleted file mode 100644 index fbd0922..0000000 --- a/widgets/MediaMuse.qml +++ /dev/null @@ -1,142 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick - -Item { - id: root - - property var levels: [] - property color tint: "white" - property bool playing: false - readonly property int bandCount: 24 - readonly property int renderedBandCount: bandCount - - implicitWidth: 144 - implicitHeight: 28 - width: implicitWidth - height: implicitHeight - - Row { - anchors.centerIn: parent - spacing: 7 - - Item { - id: vinylMark - width: 18 - height: 18 - anchors.verticalCenter: parent.verticalCenter - transformOrigin: Item.Center - - Canvas { - anchors.fill: parent - antialiasing: true - property color canvasTint: root.tint - onCanvasTintChanged: requestPaint() - onPaint: { - const ctx = getContext("2d") - ctx.clearRect(0, 0, width, height) - ctx.strokeStyle = canvasTint - ctx.fillStyle = canvasTint - ctx.lineWidth = 1.4 - - ctx.beginPath() - ctx.arc(width / 2, height / 2, 7.2, 0, Math.PI * 2) - ctx.stroke() - ctx.globalAlpha = 0.55 - ctx.beginPath() - ctx.arc(width / 2, height / 2, 4.6, -0.35, 2.35) - ctx.stroke() - ctx.beginPath() - ctx.arc(width / 2, height / 2, 4.6, 2.8, 5.5) - ctx.stroke() - ctx.globalAlpha = 1 - ctx.beginPath() - ctx.arc(width / 2, height / 2, 1.45, 0, Math.PI * 2) - ctx.fill() - } - Component.onCompleted: requestPaint() - } - - NumberAnimation on rotation { - from: 0 - to: 360 - duration: 3200 - loops: Animation.Infinite - running: root.visible && root.playing - } - } - - Item { - width: 96 - height: 22 - anchors.verticalCenter: parent.verticalCenter - - Repeater { - model: root.bandCount - - Rectangle { - required property int index - readonly property real level: { - const values = root.levels || [] - return values[index] === undefined - ? 0.04 : Math.max(0, Math.min(1, Number(values[index]) || 0)) - } - readonly property real halfHeight: 1 + level * 9 - - x: index * ((96 - 2) / (root.bandCount - 1)) - anchors.verticalCenter: parent.verticalCenter - width: 2 - height: halfHeight * 2 - radius: 1 - color: root.tint - antialiasing: true - } - } - } - - Item { - width: 16 - height: 18 - anchors.verticalCenter: parent.verticalCenter - - Row { - anchors.centerIn: parent - spacing: 3 - visible: root.playing - - Repeater { - model: 2 - Rectangle { - required property int index - width: 3 - height: 10 - radius: 1 - color: root.tint - } - } - } - - Canvas { - anchors.centerIn: parent - width: 11 - height: 12 - visible: !root.playing - antialiasing: true - property color canvasTint: root.tint - onCanvasTintChanged: requestPaint() - onPaint: { - const ctx = getContext("2d") - ctx.clearRect(0, 0, width, height) - ctx.fillStyle = canvasTint - ctx.beginPath() - ctx.moveTo(2, 1) - ctx.lineTo(width - 1, height / 2) - ctx.lineTo(2, height - 1) - ctx.closePath() - ctx.fill() - } - Component.onCompleted: requestPaint() - } - } - } -} diff --git a/widgets/MediaPanel.qml b/widgets/MediaPanel.qml deleted file mode 100644 index 8e53fb0..0000000 --- a/widgets/MediaPanel.qml +++ /dev/null @@ -1,585 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var mediaService - property var spectrumService: null - property bool spectrumEnabled: true - property string focusSection: "controls" - property int cursorIndex: 1 - property real currentPosition: 0 - property real currentLength: 0 - property real lastReportedPosition: -1 - property var leasedSpectrumService: null - - readonly property var player: mediaService ? mediaService.activePlayer : null - readonly property var sourcePlayers: mediaService && mediaService.sourcePlayers - ? mediaService.sourcePlayers : [] - readonly property bool active: mediaService - ? mediaService.hasMedia === true && player !== null : false - readonly property bool playing: active && player.isPlaying === true - readonly property bool spectrumRequested: open && active && spectrumEnabled - readonly property var levels: spectrumService && spectrumService.levels - ? spectrumService.levels : flatLevels(active ? 0.04 : 0.02) - readonly property var spectrumThemeColors: spectrumService - && spectrumService.themeColors ? spectrumService.themeColors : [] - readonly property int renderedSourceCount: sourceRepeater.count - readonly property bool spectrumWorkerRunning: spectrumService - ? spectrumService.workerRunning === true : false - readonly property string spectrumState: spectrumService - ? String(spectrumService.state || "inactive") : "unavailable" - readonly property string playerName: player - ? String(player.identity || player.desktopEntry || "") : "" - - owner: ownerWidget - open: ownerWidget.opened - focusTarget: keyCatcher - contentWidth: fittedContentWidth(Commons.Style.space(320)) - contentHeight: fittedContentHeight(content.implicitHeight) - - function flatLevels(value) { - const result = [] - for (let i = 0; i < 24; i++) result.push(Number(value) || 0) - return result - } - - function playerKey(target) { - return mediaService && typeof mediaService.playerKey === "function" - ? String(mediaService.playerKey(target) || "") : "" - } - - function runAction(action) { - return mediaService && typeof mediaService.runAction === "function" - ? mediaService.runAction(String(action || ""), false, - playerKey(player)) === true : false - } - - function selectSource(index) { - if (index < 0 || index >= sourcePlayers.length || !mediaService - || typeof mediaService.selectPlayer !== "function") return false - return mediaService.selectPlayer(playerKey(sourcePlayers[index])) === true - } - - function activateCursor() { - if (focusSection === "sources") return selectSource(cursorIndex) - if (cursorIndex === 0) return runAction("previous") - if (cursorIndex === 2) return runAction("next") - return runAction("playPause") - } - - function moveCursor(dx, dy) { - if (focusSection === "controls") { - if (dy > 0 && sourcePlayers.length > 1) { - focusSection = "sources" - cursorIndex = 0 - } else if (dx !== 0) { - cursorIndex = Math.max(0, Math.min(2, cursorIndex + dx)) - } - return - } - - if (dy < 0 && cursorIndex === 0) { - focusSection = "controls" - cursorIndex = 1 - } else if (dy !== 0 && sourcePlayers.length > 0) { - cursorIndex = (cursorIndex + dy + sourcePlayers.length) % sourcePlayers.length - } - } - - function syncPosition() { - if (!player) return - const reported = Number(player.position || 0) - currentLength = Math.max(0, Number(player.length || 0)) - if (Math.abs(reported - lastReportedPosition) > 0.05) { - currentPosition = reported - lastReportedPosition = reported - } else if (playing) { - const cap = currentLength > 0 ? currentLength : reported + 1e9 - currentPosition = Math.min(cap, currentPosition + 0.5) - } - } - - function resetPosition() { - currentPosition = player ? Number(player.position || 0) : 0 - currentLength = player ? Math.max(0, Number(player.length || 0)) : 0 - lastReportedPosition = -1 - } - - function formatTime(seconds) { - const value = Math.max(0, Math.floor(Number(seconds) || 0)) - const minutes = Math.floor(value / 60) - const rest = value % 60 - return minutes + ":" + String(rest).padStart(2, "0") - } - - function syncSpectrumLease() { - const next = spectrumRequested ? spectrumService : null - if (leasedSpectrumService === next) return - if (leasedSpectrumService - && typeof leasedSpectrumService.endSpectrum === "function") - leasedSpectrumService.endSpectrum(panel) - leasedSpectrumService = next - if (leasedSpectrumService - && typeof leasedSpectrumService.beginSpectrum === "function") - leasedSpectrumService.beginSpectrum(panel) - } - - function releaseSpectrumLease() { - if (leasedSpectrumService - && typeof leasedSpectrumService.endSpectrum === "function") - leasedSpectrumService.endSpectrum(panel) - leasedSpectrumService = null - } - - onPlayerChanged: resetPosition() - onPlayingChanged: { - lastReportedPosition = -1 - } - onSpectrumRequestedChanged: syncSpectrumLease() - onSpectrumServiceChanged: syncSpectrumLease() - onSourcePlayersChanged: { - if (focusSection === "sources" && sourcePlayers.length <= 1) { - focusSection = "controls" - cursorIndex = 1 - } else if (focusSection === "sources") { - cursorIndex = Math.max(0, Math.min(cursorIndex, sourcePlayers.length - 1)) - } - } - Component.onCompleted: { - resetPosition() - syncSpectrumLease() - } - Component.onDestruction: releaseSpectrumLease() - - property Timer positionTimer: Timer { - interval: 500 - repeat: true - running: panel.open && panel.active - triggeredOnStart: true - onTriggered: panel.syncPosition() - } - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.ownerWidget.close() - onTabRequested: function(direction) { panel.ownerWidget.switchPanel(direction) } - onMoveRequested: function(dx, dy) { panel.moveCursor(dx, dy) } - onActivateRequested: panel.activateCursor() - - Column { - id: content - width: parent.width - spacing: Commons.Style.space(9) - - Item { - width: parent.width - height: Commons.Style.space(24) - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "NOW PLAYING" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Row { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: Commons.Style.space(8) - - Text { - visible: panel.active && panel.playerName !== "" - anchors.verticalCenter: parent.verticalCenter - text: panel.playerName - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.45) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - - IconAction { - anchors.verticalCenter: parent.verticalCenter - icon: "close" - tooltip: "Close" - onClicked: panel.ownerWidget.close() - } - } - } - - Ui.PanelSeparator { - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - } - - Row { - width: parent.width - spacing: Commons.Style.space(10) - visible: panel.active - - Rectangle { - width: Commons.Style.space(56) - height: width - radius: Commons.Style.space(5) - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.10) - : Commons.Color.background - clip: true - - Image { - anchors.fill: parent - source: panel.player ? String(panel.player.trackArtUrl || "") : "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - retainWhileLoading: true - } - - IconText { - anchors.centerIn: parent - visible: !panel.player || !panel.player.trackArtUrl - text: "music_note" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.pixelSize: Commons.Style.font.displayLarge - fill: 1 - } - } - - Column { - width: parent.width - Commons.Style.space(66) - anchors.verticalCenter: parent.verticalCenter - spacing: Commons.Style.space(3) - - Text { - width: parent.width - text: panel.player ? String(panel.player.trackTitle || "Unknown") : "" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: Font.Medium - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: parent.width - visible: text !== "" - text: panel.player ? String(panel.player.trackArtist || "") : "" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.68) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: parent.width - visible: text !== "" - text: panel.player ? String(panel.player.trackAlbum || "") : "" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.45) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - elide: Text.ElideRight - renderType: Text.NativeRendering - } - } - } - - Item { - width: parent.width - height: Commons.Style.space(18) - visible: panel.active && panel.currentLength > 0 - - Rectangle { - id: progressTrack - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - height: Commons.Style.space(4) - radius: height / 2 - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.12) - : Commons.Color.background - - Rectangle { - height: parent.height - width: parent.width * Math.min(1, - panel.currentPosition / Math.max(1, panel.currentLength)) - radius: height / 2 - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - Behavior on width { NumberAnimation { duration: 450 } } - } - } - - Text { - anchors.left: parent.left - anchors.top: progressTrack.bottom - anchors.topMargin: 2 - text: panel.formatTime(panel.currentPosition) - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.58) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - - Text { - anchors.right: parent.right - anchors.top: progressTrack.bottom - anchors.topMargin: 2 - text: panel.formatTime(panel.currentLength) - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.58) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - - Item { - width: parent.width - height: Commons.Style.space(40) - - MediaSpectrum { - anchors.fill: parent - visible: panel.active - levels: panel.levels - tint: panel.bar ? panel.bar.urgent : Commons.Color.accent - themeColors: panel.spectrumThemeColors - opacity: panel.playing ? 1 : 0.5 - } - - Column { - anchors.centerIn: parent - visible: !panel.active - spacing: 1 - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "No song playing" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.58) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "no active player" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.32) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - } - - Row { - visible: panel.active - anchors.horizontalCenter: parent.horizontalCenter - spacing: Commons.Style.space(12) - - MediaPanelButton { - icon: "skip_previous" - action: "previous" - controlIndex: 0 - enabled: panel.player && panel.player.canGoPrevious === true - } - MediaPanelButton { - icon: panel.playing ? "pause" : "play_arrow" - action: "playPause" - controlIndex: 1 - accent: true - enabled: panel.player && (panel.player.canTogglePlaying === true - || panel.player.canPlay === true || panel.player.canPause === true) - } - MediaPanelButton { - icon: "skip_next" - action: "next" - controlIndex: 2 - enabled: panel.player && panel.player.canGoNext === true - } - } - - Ui.PanelSeparator { - visible: panel.sourcePlayers.length > 1 - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - } - - Column { - width: parent.width - visible: panel.sourcePlayers.length > 1 - spacing: Commons.Style.space(2) - - Repeater { - id: sourceRepeater - model: panel.sourcePlayers - - delegate: Rectangle { - id: sourceRow - required property var modelData - required property int index - readonly property bool selected: panel.playerKey(panel.player) - === panel.playerKey(modelData) - readonly property bool cursor: panel.focusSection === "sources" - && panel.cursorIndex === index - - width: parent.width - height: Commons.Style.space(34) - radius: panel.controlRadius - color: cursor || sourceMouse.containsMouse - ? panel.bar ? Qt.rgba(panel.bar.urgent.r, panel.bar.urgent.g, - panel.bar.urgent.b, 0.14) : Commons.Color.background - : "transparent" - - Row { - anchors.fill: parent - anchors.leftMargin: Commons.Style.space(6) - anchors.rightMargin: Commons.Style.space(6) - spacing: Commons.Style.space(7) - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: sourceRow.modelData && sourceRow.modelData.isPlaying - ? "pause" : "play_arrow" - color: sourceRow.selected && panel.bar - ? panel.bar.urgent : panel.bar - ? panel.bar.foreground : Commons.Color.foreground - font.pixelSize: Commons.Style.font.icon - fill: sourceRow.selected ? 1 : 0 - } - - Column { - width: parent.width - Commons.Style.space(30) - anchors.verticalCenter: parent.verticalCenter - spacing: 1 - - Text { - width: parent.width - text: sourceRow.modelData - ? String(sourceRow.modelData.trackTitle - || sourceRow.modelData.identity || "Media source") - : "Media source" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - font.weight: sourceRow.selected ? Font.Medium : Font.Normal - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: parent.width - visible: text !== "" - text: sourceRow.modelData - ? String(sourceRow.modelData.trackArtist - || sourceRow.modelData.identity || "") : "" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.52) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - elide: Text.ElideRight - renderType: Text.NativeRendering - } - } - } - - MouseArea { - id: sourceMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: { - panel.focusSection = "sources" - panel.cursorIndex = sourceRow.index - } - onClicked: panel.selectSource(sourceRow.index) - } - } - } - } - } - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: action.icon - color: action.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } - - component MediaPanelButton: Ui.PanelActionButton { - required property string icon - required property string action - required property int controlIndex - property bool accent: false - - iconText: icon - foreground: accent && panel.bar - ? panel.bar.urgent : panel.bar ? panel.bar.foreground : Commons.Color.foreground - hoverColor: panel.bar ? panel.bar.urgent : Commons.Color.accent - fontFamily: "Material Symbols Rounded" - fontSize: accent ? Commons.Style.font.iconLarge : Commons.Style.font.icon - size: accent ? Commons.Style.space(34) : Commons.Style.space(28) - radius: panel.controlRadius - hasCursor: panel.focusSection === "controls" && panel.cursorIndex === controlIndex - onHovered: function(isHovered) { - if (isHovered) { - panel.focusSection = "controls" - panel.cursorIndex = controlIndex - } - } - onClicked: panel.runAction(action) - } -} diff --git a/widgets/MediaPulse.qml b/widgets/MediaPulse.qml deleted file mode 100644 index f913500..0000000 --- a/widgets/MediaPulse.qml +++ /dev/null @@ -1,80 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick - -Item { - id: root - - required property bool playing - property color tint: "white" - property real barH1: 0.08 - property real barH2: 0.08 - property real barH3: 0.08 - - implicitWidth: 16 - implicitHeight: 14 - - function reset() { restAnimation.restart() } - - onPlayingChanged: if (!playing) reset() - - SequentialAnimation { - running: root.visible && root.playing - loops: Animation.Infinite - NumberAnimation { target: root; property: "barH1"; to: 0.85; duration: 220; easing.type: Easing.InOutSine } - NumberAnimation { target: root; property: "barH1"; to: 0.18; duration: 300; easing.type: Easing.InOutSine } - NumberAnimation { target: root; property: "barH1"; to: 0.70; duration: 260; easing.type: Easing.InOutSine } - NumberAnimation { target: root; property: "barH1"; to: 0.10; duration: 280; easing.type: Easing.InOutSine } - } - - SequentialAnimation { - running: root.visible && root.playing - loops: Animation.Infinite - NumberAnimation { target: root; property: "barH2"; to: 0.45; duration: 310; easing.type: Easing.InOutSine } - NumberAnimation { target: root; property: "barH2"; to: 0.92; duration: 280; easing.type: Easing.InOutSine } - NumberAnimation { target: root; property: "barH2"; to: 0.28; duration: 340; easing.type: Easing.InOutSine } - NumberAnimation { target: root; property: "barH2"; to: 0.65; duration: 290; easing.type: Easing.InOutSine } - } - - SequentialAnimation { - running: root.visible && root.playing - loops: Animation.Infinite - NumberAnimation { target: root; property: "barH3"; to: 0.60; duration: 380; easing.type: Easing.InOutSine } - NumberAnimation { target: root; property: "barH3"; to: 0.12; duration: 320; easing.type: Easing.InOutSine } - NumberAnimation { target: root; property: "barH3"; to: 0.95; duration: 350; easing.type: Easing.InOutSine } - NumberAnimation { target: root; property: "barH3"; to: 0.32; duration: 400; easing.type: Easing.InOutSine } - } - - ParallelAnimation { - id: restAnimation - NumberAnimation { target: root; property: "barH1"; to: 0.08; duration: 380; easing.type: Easing.OutCubic } - NumberAnimation { target: root; property: "barH2"; to: 0.08; duration: 430; easing.type: Easing.OutCubic } - NumberAnimation { target: root; property: "barH3"; to: 0.08; duration: 480; easing.type: Easing.OutCubic } - } - - Row { - anchors.fill: parent - spacing: 2 - - Repeater { - model: 3 - - delegate: Item { - required property int index - readonly property real level: index === 0 ? root.barH1 - : index === 1 ? root.barH2 : root.barH3 - width: 4 - height: parent.height - - Rectangle { - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - width: 3 - height: Math.max(width, parent.height * parent.level) - radius: width / 2 - color: root.tint - } - } - } - } -} diff --git a/widgets/MediaSpectrum.qml b/widgets/MediaSpectrum.qml deleted file mode 100644 index 329b958..0000000 --- a/widgets/MediaSpectrum.qml +++ /dev/null @@ -1,57 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick - -Canvas { - id: root - - property var levels: [] - property color tint: "white" - property var themeColors: [] - - onLevelsChanged: requestPaint() - onTintChanged: requestPaint() - onThemeColorsChanged: requestPaint() - onWidthChanged: requestPaint() - onHeightChanged: requestPaint() - Component.onCompleted: requestPaint() - - onPaint: { - const ctx = getContext("2d") - ctx.clearRect(0, 0, width, height) - const values = levels || [] - if (values.length === 0) return - - const barWidth = 4 - const gap = Math.max(1, (width - values.length * barWidth) / (values.length + 1)) - const maxHeight = Math.max(barWidth, height - 2) - const palette = themeColors || [] - if (palette.length > 1) { - const gradient = ctx.createLinearGradient(0, height, 0, 0) - for (let index = 0; index < palette.length; index++) - gradient.addColorStop(index / (palette.length - 1), palette[index]) - ctx.fillStyle = gradient - } else { - ctx.fillStyle = palette.length === 1 ? palette[0] : tint - } - - for (let i = 0; i < values.length; i++) { - const barHeight = Math.max(barWidth, - Math.min(1, Number(values[i]) || 0) * maxHeight) - const x = gap + i * (barWidth + gap) - const y = height - barHeight - const radius = barWidth / 2 - - ctx.beginPath() - ctx.moveTo(x + radius, y) - ctx.lineTo(x + barWidth - radius, y) - ctx.arcTo(x + barWidth, y, x + barWidth, y + radius, radius) - ctx.lineTo(x + barWidth, y + barHeight) - ctx.lineTo(x, y + barHeight) - ctx.lineTo(x, y + radius) - ctx.arcTo(x, y, x + radius, y, radius) - ctx.closePath() - ctx.fill() - } - } -} diff --git a/widgets/MediaWidget.qml b/widgets/MediaWidget.qml deleted file mode 100644 index d1358f0..0000000 --- a/widgets/MediaWidget.qml +++ /dev/null @@ -1,349 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Effects -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.media" - manageIpc: false - property url panelSource: Qt.resolvedUrl("MediaPanel.qml") - readonly property var mediaService: bar && bar.shell - && typeof bar.shell.firstPartyServiceFor === "function" - ? bar.shell.firstPartyServiceFor("omarchy.media") : null - readonly property var spectrumService: bar && bar.shell - && typeof bar.shell.serviceFor === "function" - ? bar.shell.serviceFor("hancore.shibumi.media") : null - readonly property var activePlayer: mediaService ? mediaService.activePlayer : null - readonly property bool active: mediaService - ? mediaService.hasMedia === true && activePlayer !== null : false - readonly property bool playing: active && activePlayer.isPlaying === true - readonly property string title: activePlayer ? String(activePlayer.trackTitle || "") : "" - readonly property string artist: activePlayer ? String(activePlayer.trackArtist || "") : "" - readonly property string trackLabel: title && artist - ? title + " · " + artist : title || artist - readonly property string tooltipText: active - ? (artist ? artist + " — " + title : title) : "No active media player" - readonly property var tokens: bar ? bar.visualTokens : null - readonly property bool fullMode: setting("compact", false) === true - readonly property bool spectrumEnabled: setting("spectrum", true) !== false - readonly property bool spectrumRequested: visible && active && fullMode - && (!bar || !bar.vertical) - readonly property var museLevels: spectrumService && spectrumService.levels - ? spectrumService.levels : [] - readonly property string spectrumState: spectrumService - ? String(spectrumService.state || "inactive") : "unavailable" - property var leasedSpectrumService: null - readonly property bool panelLoaded: panelLoader.item !== null - readonly property var panelItem: panelLoader.item - readonly property var interactionTarget: actionButton - readonly property var museInteractionTarget: museMouse - readonly property bool museVisible: muse.visible - readonly property int museBandCount: muse.renderedBandCount - - visible: mediaService !== null - implicitWidth: visible ? (bar && bar.vertical - ? bar.barSize : mediaSurface.implicitWidth) : 0 - implicitHeight: visible ? (bar && bar.vertical - ? mediaSurface.implicitHeight : bar ? bar.barSize : 28) : 0 - - function runAction(action) { - return mediaService && typeof mediaService.runAction === "function" - ? mediaService.runAction(String(action || ""), false) === true : false - } - - function runMusePrimaryAction() { - return runAction("playPause") - } - - function runMuseWheel(delta) { - if (Number(delta) === 0) return false - return runAction(Number(delta) > 0 ? "next" : "previous") - } - - function syncSpectrumLease() { - const next = spectrumRequested ? spectrumService : null - if (leasedSpectrumService === next) return - if (leasedSpectrumService - && typeof leasedSpectrumService.endSpectrum === "function") - leasedSpectrumService.endSpectrum(root) - leasedSpectrumService = next - if (leasedSpectrumService - && typeof leasedSpectrumService.beginSpectrum === "function") - leasedSpectrumService.beginSpectrum(root) - } - - function releaseSpectrumLease() { - if (leasedSpectrumService - && typeof leasedSpectrumService.endSpectrum === "function") - leasedSpectrumService.endSpectrum(root) - leasedSpectrumService = null - } - - function syncPanelLoader() { - if (!opened) { - panelLoader.source = "" - return - } - panelLoader.setSource(panelSource, { - anchorItem: mediaSurface, - bar: root.bar, - ownerWidget: root, - mediaService: root.mediaService, - spectrumService: root.spectrumService, - spectrumEnabled: root.spectrumEnabled - }) - } - - onOpenedChanged: syncPanelLoader() - onSpectrumRequestedChanged: syncSpectrumLease() - onSpectrumServiceChanged: syncSpectrumLease() - Component.onCompleted: syncSpectrumLease() - Component.onDestruction: releaseSpectrumLease() - - Item { - id: mediaSurface - anchors.centerIn: parent - implicitWidth: !root.tokens ? 0 : root.bar && root.bar.vertical - ? root.bar.barSize - : (root.active - ? (root.fullMode ? muse.implicitWidth : activeRow.implicitWidth) - : idleIcon.implicitWidth) - + 2 * root.tokens.pillPaddingX - implicitHeight: root.bar && root.bar.vertical - ? Commons.Style.space(34) : root.tokens ? root.tokens.slotHeight : 28 - width: implicitWidth - height: implicitHeight - - PillSurface { - anchors.fill: parent - anchors.topMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - anchors.bottomMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - bar: root.bar - } - - // This is the one registered surface used by KeyboardPanel's bar-click - // forwarding. Transport controls above it retain their dedicated actions. - Ui.WidgetButton { - id: actionButton - anchors.fill: parent - bar: root.mediaService ? root.bar : null - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - fixedWidth: mediaSurface.width - fixedHeight: mediaSurface.height - tooltipText: root.tooltipText - onPressed: function(button) { - if (button === Qt.RightButton || !root.active) root.toggle() - else if (button === Qt.MiddleButton) root.runAction("next") - else root.runAction("playPause") - } - onWheelMoved: function(delta) { - root.runAction(delta > 0 ? "previous" : "next") - } - } - - IconText { - id: idleIcon - anchors.centerIn: parent - visible: !root.active - text: "music_note" - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.45) : Commons.Color.foreground - font.pixelSize: 15 - fill: 0 - } - - Row { - id: activeRow - visible: root.active && !root.fullMode - && (!root.bar || !root.bar.vertical) - anchors.centerIn: parent - spacing: root.tokens ? root.tokens.compactGap : Commons.Style.space(4) - - MediaControl { - icon: "skip_previous" - enabled: root.activePlayer && root.activePlayer.canGoPrevious === true - action: "previous" - } - - MediaControl { - icon: root.playing ? "pause" : "play_arrow" - enabled: root.activePlayer && (root.activePlayer.canTogglePlaying === true - || root.activePlayer.canPlay === true || root.activePlayer.canPause === true) - action: "playPause" - accent: true - } - - MediaControl { - icon: "skip_next" - enabled: root.activePlayer && root.activePlayer.canGoNext === true - action: "next" - } - - Item { - id: marqueeMask - width: Commons.Style.space(88) - height: mediaSurface.height - visible: false - layer.enabled: true - - Rectangle { - anchors.fill: parent - gradient: Gradient { - orientation: Gradient.Horizontal - GradientStop { position: 0.0; color: "white" } - GradientStop { position: 0.92; color: "white" } - GradientStop { position: 1.0; color: "transparent" } - } - } - } - - Item { - id: marqueeClip - width: Commons.Style.space(88) - height: mediaSurface.height - anchors.verticalCenter: parent.verticalCenter - layer.enabled: true - layer.effect: MultiEffect { - maskEnabled: true - maskSource: marqueeMask - maskThresholdMin: 0.5 - maskSpreadAtMin: 0.5 - } - - function reset() { - marqueeAnimation.stop() - marqueeText.x = 0 - Qt.callLater(function() { - if (root.visible && root.playing && !root.opened - && marqueeText.implicitWidth > marqueeClip.width) - marqueeAnimation.start() - }) - } - - Text { - id: marqueeText - anchors.verticalCenter: parent.verticalCenter - text: root.trackLabel - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, root.playing ? 0.85 : 0.42) - : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens ? root.tokens.labelSize : Commons.Style.font.body - renderType: Text.NativeRendering - onTextChanged: marqueeClip.reset() - } - - Connections { - target: root - function onPlayingChanged() { marqueeClip.reset() } - function onOpenedChanged() { marqueeClip.reset() } - function onVisibleChanged() { marqueeClip.reset() } - } - - SequentialAnimation { - id: marqueeAnimation - loops: Animation.Infinite - PauseAnimation { duration: 2000 } - NumberAnimation { - target: marqueeText - property: "x" - to: -(marqueeText.implicitWidth - marqueeClip.width + 4) - duration: Math.max(100, - marqueeText.implicitWidth - marqueeClip.width + 4) * 20 - easing.type: Easing.Linear - } - PauseAnimation { duration: 900 } - PropertyAction { target: marqueeText; property: "x"; value: 0 } - } - } - - MediaPulse { - anchors.verticalCenter: parent.verticalCenter - playing: root.playing - tint: root.bar ? root.bar.urgent : Commons.Color.accent - } - } - - MediaMuse { - id: muse - visible: root.active && root.fullMode - && (!root.bar || !root.bar.vertical) - anchors.centerIn: parent - levels: root.museLevels - playing: root.playing - tint: root.bar ? root.bar.urgent : Commons.Color.accent - - MouseArea { - id: museMouse - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton - cursorShape: Qt.PointingHandCursor - onClicked: root.runMusePrimaryAction() - onWheel: function(wheel) { - if (wheel.angleDelta.y === 0) return - root.runMuseWheel(wheel.angleDelta.y) - wheel.accepted = true - } - } - } - - Column { - visible: root.active && root.bar && root.bar.vertical - anchors.centerIn: parent - spacing: Commons.Style.space(2) - - IconText { - anchors.horizontalCenter: parent.horizontalCenter - text: root.playing ? "pause" : "play_arrow" - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.pixelSize: root.tokens ? root.tokens.iconSize : Commons.Style.font.body - fill: 1 - } - - MediaPulse { - anchors.horizontalCenter: parent.horizontalCenter - playing: root.playing - tint: root.bar ? root.bar.urgent : Commons.Color.accent - scale: 0.75 - } - } - } - - Loader { id: panelLoader } - - component MediaControl: Item { - required property string icon - required property string action - property bool accent: false - - implicitWidth: Commons.Style.space(18) - implicitHeight: mediaSurface.height - opacity: enabled ? 1 : 0.28 - - IconText { - anchors.centerIn: parent - text: parent.icon - color: parent.accent && root.bar - ? root.bar.urgent : root.bar ? root.bar.foreground : Commons.Color.foreground - font.pixelSize: 13 - fill: parent.accent ? 1 : 0 - } - - MouseArea { - anchors.fill: parent - enabled: parent.enabled - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: root.runAction(parent.action) - } - } -} diff --git a/widgets/MemoryPanel.qml b/widgets/MemoryPanel.qml deleted file mode 100644 index de50505..0000000 --- a/widgets/MemoryPanel.qml +++ /dev/null @@ -1,213 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var telemetry - - owner: ownerWidget - open: ownerWidget.opened - focusTarget: keyCatcher - gap: 8 - padding: 12 - contentWidth: fittedContentWidth(320) - contentHeight: fittedContentHeight(panelColumn.implicitHeight) - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.ownerWidget.close() - onTabRequested: function(direction) { panel.ownerWidget.switchPanel(direction) } - - Column { - id: panelColumn - width: parent.width - spacing: 8 - - Item { - width: parent.width - height: 24 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Memory" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 13 - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: "\u2715" - color: closeMouse.containsMouse - ? panel.controlAccent : panel.controlMuted - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 12 - renderType: Text.NativeRendering - - Behavior on color { ColorAnimation { duration: 120 } } - - MouseArea { - id: closeMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.ownerWidget.close() - } - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, panel.bar.foreground.g, - panel.bar.foreground.b, 0.18) : Commons.Color.popups.border - } - - Item { - width: parent.width - height: 30 - - Text { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - text: panel.telemetry.memPercent + "%" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Rectangle { - anchors.bottom: parent.bottom - width: parent.width - height: 8 - radius: height / 2 - color: panel.controlActiveFillColor - - Rectangle { - width: parent.width * panel.telemetry.memPercent / 100 - height: parent.height - radius: height / 2 - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - Behavior on width { NumberAnimation { duration: 300 } } - } - } - } - - Column { - width: parent.width - spacing: 4 - - MemoryStatRow { - width: parent.width - label: "Used" - value: panel.telemetry.memUsedGiB.toFixed(1) + " GiB" - detail: panel.telemetry.memUsedMiB + " MiB" - bar: panel.bar - } - MemoryStatRow { - width: parent.width - label: "Available" - value: (panel.telemetry.memAvailableMiB / 1024).toFixed(1) + " GiB" - detail: panel.telemetry.memAvailableMiB + " MiB" - bar: panel.bar - } - MemoryStatRow { - width: parent.width - label: "Total" - value: panel.telemetry.memTotalGiB.toFixed(1) + " GiB" - detail: panel.telemetry.memTotalMiB + " MiB" - bar: panel.bar - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.dividerColor - } - - Rectangle { - width: parent.width - height: 28 - radius: panel.controlRadius - color: monitorMouse.containsMouse - ? panel.controlPrimaryHoverColor - : panel.bar ? panel.bar.urgent : Commons.Color.accent - - Behavior on color { ColorAnimation { duration: 120 } } - - Text { - anchors.centerIn: parent - text: "Open btop" - color: panel.bar ? panel.bar.background : Commons.Color.background - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - - MouseArea { - id: monitorMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - panel.ownerWidget.close() - panel.ownerWidget.openSystemMonitor() - } - } - } - } - } - - component MemoryStatRow: Row { - required property string label - required property string value - required property string detail - required property var bar - - Text { - id: labelText - width: parent.width * 0.4 - text: parent.label - color: parent.bar ? Qt.rgba(parent.bar.foreground.r, parent.bar.foreground.g, - parent.bar.foreground.b, 0.65) : Commons.Color.foreground - font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - Text { - id: valueText - width: parent.width * 0.3 - text: parent.value - color: parent.bar ? parent.bar.foreground : Commons.Color.foreground - font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - Text { - id: detailText - width: parent.width * 0.3 - text: parent.detail - color: parent.bar ? Qt.rgba(parent.bar.foreground.r, parent.bar.foreground.g, - parent.bar.foreground.b, 0.58) : Commons.Color.foreground - font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - } -} diff --git a/widgets/MemoryRing.qml b/widgets/MemoryRing.qml deleted file mode 100644 index c3567db..0000000 --- a/widgets/MemoryRing.qml +++ /dev/null @@ -1,41 +0,0 @@ -import QtQuick -import qs.Commons as Commons - -Canvas { - id: root - - required property int percent - required property color foreground - required property color accent - - width: Commons.Style.space(16) - height: width - - onPercentChanged: requestPaint() - onForegroundChanged: requestPaint() - onAccentChanged: requestPaint() - Component.onCompleted: requestPaint() - - onPaint: { - const context = getContext("2d") - context.clearRect(0, 0, width, height) - const center = width / 2 - const radius = width / 2 - 1.5 - const ratio = Math.max(0, Math.min(1, percent / 100)) - const start = -Math.PI / 2 - - context.lineWidth = 1.8 - context.lineCap = "round" - context.beginPath() - context.arc(center, center, radius, 0, Math.PI * 2) - context.strokeStyle = Qt.rgba(foreground.r, foreground.g, foreground.b, 0.2) - context.stroke() - - if (ratio > 0) { - context.beginPath() - context.arc(center, center, radius, start, start + Math.PI * 2 * ratio) - context.strokeStyle = accent - context.stroke() - } - } -} diff --git a/widgets/MemoryWidget.qml b/widgets/MemoryWidget.qml deleted file mode 100644 index d211880..0000000 --- a/widgets/MemoryWidget.qml +++ /dev/null @@ -1,160 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.memory" - manageIpc: false - - readonly property var telemetry: bar ? bar.systemTelemetry : null - readonly property var tokens: bar ? bar.visualTokens : null - readonly property bool compact: setting("compact", false) === true - readonly property int percent: telemetry ? telemetry.memPercent : 0 - readonly property real usedGiB: telemetry ? telemetry.memUsedGiB : 0 - readonly property real totalGiB: telemetry ? telemetry.memTotalGiB : 0 - readonly property string usedLabel: String(Math.round(usedGiB)).padStart(2, "0") + "G" - readonly property string usageLabel: usedGiB.toFixed(1) + "/" + totalGiB.toFixed(1) + " GiB" - property var acquiredTelemetry: null - - implicitWidth: bar && bar.vertical ? bar.barSize : memorySurface.implicitWidth - implicitHeight: bar && bar.vertical ? memorySurface.implicitHeight : bar ? bar.barSize : 28 - - function syncTelemetryOwner() { - if (acquiredTelemetry === telemetry) return - if (acquiredTelemetry) acquiredTelemetry.release("memory") - acquiredTelemetry = telemetry - if (acquiredTelemetry) acquiredTelemetry.acquire("memory") - } - - function syncPanelLoader() { - if (!opened) { - panelLoader.source = "" - return - } - panelLoader.setSource(Qt.resolvedUrl("MemoryPanel.qml"), { - anchorItem: memorySurface, - bar: root.bar, - ownerWidget: root, - telemetry: root.telemetry - }) - } - - function openSystemMonitor() { - if (!bar || typeof bar.run !== "function") return false - bar.run("omarchy-launch-or-focus-tui btop") - return true - } - - onTelemetryChanged: syncTelemetryOwner() - onOpenedChanged: syncPanelLoader() - Component.onCompleted: syncTelemetryOwner() - Component.onDestruction: if (acquiredTelemetry) acquiredTelemetry.release("memory") - - Item { - id: memorySurface - - anchors.centerIn: parent - implicitWidth: root.bar && root.bar.vertical - ? root.bar.barSize - : content.implicitWidth + 2 * root.tokens.pillPaddingX - implicitHeight: root.bar && root.bar.vertical - ? content.implicitHeight + Commons.Style.space(10) - : root.tokens ? root.tokens.slotHeight : 28 - width: implicitWidth - height: implicitHeight - - PillSurface { - bar: root.bar - anchors.fill: parent - anchors.topMargin: Math.round((parent.height - root.tokens.pillHeight) / 2) - anchors.bottomMargin: Math.round((parent.height - root.tokens.pillHeight) / 2) - } - - Loader { - id: content - anchors.centerIn: parent - sourceComponent: root.bar && root.bar.vertical ? verticalContent : horizontalContent - } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) root.bar.showTooltip(memorySurface, root.usageLabel) - onExited: if (root.bar) root.bar.hideTooltip(memorySurface) - onClicked: { - if (root.bar) root.bar.hideTooltip(memorySurface) - root.toggle() - } - } - } - - Loader { - id: panelLoader - } - - Component { - id: horizontalContent - - Row { - spacing: root.compact ? root.tokens.compactGap : root.tokens.contentGap - - Text { - visible: !root.compact - anchors.verticalCenter: parent.verticalCenter - text: "MEM" - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.6) : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - font.letterSpacing: 0.5 - renderType: Text.NativeRendering - } - - MemoryRing { - anchors.verticalCenter: parent.verticalCenter - percent: root.percent - foreground: root.bar ? root.bar.foreground : Commons.Color.foreground - accent: root.bar ? root.bar.urgent : Commons.Color.accent - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: root.usedLabel - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - renderType: Text.NativeRendering - } - } - } - - Component { - id: verticalContent - - Column { - spacing: Commons.Style.space(2) - - MemoryRing { - anchors.horizontalCenter: parent.horizontalCenter - percent: root.percent - foreground: root.bar ? root.bar.foreground : Commons.Color.foreground - accent: root.bar ? root.bar.urgent : Commons.Color.accent - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: root.usedLabel - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - } -} diff --git a/widgets/NetworkPanel.qml b/widgets/NetworkPanel.qml deleted file mode 100644 index 4bebb09..0000000 --- a/widgets/NetworkPanel.qml +++ /dev/null @@ -1,1137 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var networkService - - property bool savedOnly: false - property bool speedDetailsVisible: false - property string expandedKey: "" - property string passwordKey: "" - property string passwordText: "" - property string identityText: "" - property var credentialDisplayNetworks: [] - property string credentialSsid: "" - property var credentialSecurity: null - property string credentialError: "" - property Item credentialEditor: null - property string pendingForgetKey: "" - property int cursorIndex: -1 - readonly property bool wifiControlsVisible: networkService - && networkService.wifiAvailable === true - readonly property var displayNetworks: filteredNetworks() - readonly property var presentedNetworks: passwordKey !== "" - ? credentialDisplayNetworks : displayNetworks - readonly property bool credentialEditorFocused: !!(credentialEditor - && credentialEditor.activeFocus) - readonly property bool panelKeyboardFocusActive: keyCatcher.activeFocus - readonly property int savedCount: { - let count = 0 - const rows = networkService ? networkService.networks : [] - for (let i = 0; i < rows.length; i++) { - if (rows[i] && rows[i].known === true) count++ - } - return count - } - - owner: ownerWidget - open: ownerWidget.opened && networkService && networkService.ready - focusTarget: keyCatcher - contentWidth: fittedContentWidth(Commons.Style.space(380)) - contentHeight: fittedContentHeight(contentColumn.implicitHeight, - Commons.Style.space(680)) - - function filteredNetworks() { - const result = [] - if (!wifiControlsVisible) return result - const rows = networkService ? networkService.networks : [] - for (let i = 0; i < rows.length; i++) { - const row = rows[i] - if (!row) continue - if (savedOnly ? row.known === true : row.visible !== false) result.push(row) - } - return result - } - - function entryKey(entry) { return entry ? String(entry.entryKey || "") : "" } - - function signalIcon(value) { - const signal = Number(value || 0) - if (signal <= 0) return "\uE1DA" - if (signal < 22) return "\uEBE4" - if (signal < 44) return "\uEBD6" - if (signal < 66) return "\uEBE1" - return "\uF065" - } - - function statusText() { - if (!networkService.backendAvailable) return "NetworkManager unavailable" - if (wifiControlsVisible && !networkService.wifiEnabled - && networkService.kind === "disconnected") - return "Wi-Fi disabled" - if (networkService.kind === "wifi") return networkService.label || "Wi-Fi connected" - if (networkService.kind === "ethernet") return ethernetAddress() - return "Offline" - } - - function ethernetAddress() { - const info = networkService.info || ({}) - if (!info.ip) return networkService.label || "Ethernet connected" - return String(info.ip) + (info.prefix ? "/" + info.prefix : "") - } - - function statusMeta() { - const info = networkService.info || ({}) - const fields = [] - if (networkService.kind === "wifi") fields.push("Wi-Fi") - else if (networkService.kind === "ethernet") fields.push("Ethernet") - if (info.iface) fields.push(String(info.iface)) - if (networkService.kind !== "ethernet" && info.ip) - fields.push(String(info.ip) + (info.prefix ? "/" + info.prefix : "")) - if (networkService.kind === "wifi" && networkService.signalStrength >= 0) - fields.push(networkService.signalStrength + "% signal") - return fields.join(" · ") - } - - function frequencyText() { - const raw = String((networkService.info || ({})).freq || "").trim() - if (!raw) return "" - return /[a-z]/i.test(raw) ? raw : raw + " MHz" - } - - function linkSpeedText() { - const info = networkService.info || ({}) - if (networkService.kind === "wifi") return String(info.bitrate || "").trim() - const raw = String(info.speed || "").trim() - if (!raw) return "" - const speed = Number(raw) - if (!isFinite(speed) || speed <= 0) return raw - return speed >= 1000 - ? (speed / 1000).toFixed(speed % 1000 === 0 ? 0 : 1) + " Gbit/s" - : speed + " Mbit/s" - } - - function metadata(entry) { - if (!entry) return "" - const fields = [String(entry.securityLabel || "Unknown")] - if (entry.visible !== false) fields.push("Signal " + Math.round(entry.signal || 0) + "%") - if (entry.known) fields.push("Saved") - if (entry.visible === false) fields.push("Not currently visible") - return fields.join(" · ") - } - - function toggleExpanded(entry) { - const key = entryKey(entry) - expandedKey = expandedKey === key ? "" : key - if (expandedKey !== key || passwordKey !== key) clearPassword() - } - - function clearPassword() { - const restorePanelFocus = passwordKey !== "" && open - passwordKey = "" - passwordText = "" - identityText = "" - credentialDisplayNetworks = [] - credentialSsid = "" - credentialSecurity = null - credentialError = "" - credentialEditor = null - if (restorePanelFocus) requestPanelKeyboardFocus(keyCatcher) - } - - function openPassword(entry) { - credentialDisplayNetworks = displayNetworks.slice() - credentialSsid = String(entry && entry.ssid || "") - credentialSecurity = entry ? entry.security : null - credentialError = "" - expandedKey = entryKey(entry) - passwordKey = expandedKey - passwordText = "" - identityText = "" - } - - function currentCredentialRow() { - let result = null - let count = 0 - const rows = displayNetworks - for (let i = 0; i < rows.length; i++) { - const row = rows[i] - if (!row || row.visible === false - || String(row.ssid || "") !== credentialSsid - || row.security !== credentialSecurity) continue - result = row - count++ - } - return count === 1 ? result : null - } - - function evaluateCredentialCompletion() { - if (passwordKey === "") return - const current = currentCredentialRow() - if (current && current.connected === true) clearPassword() - } - - function requestPanelKeyboardFocus(target) { - if (!open || !target) return - if (typeof requestKeyboardFocus === "function") { - requestKeyboardFocus(target) - return - } - Qt.callLater(function() { - if (panel.open && target) target.forceActiveFocus() - }) - } - - function registerCredentialEditor(editor) { - if (!editor) return - credentialEditor = editor - requestPanelKeyboardFocus(editor) - } - - function credentialEditorFocusChanged(editor, active) { - if (!editor) return - if (active) { - credentialEditor = editor - return - } - Qt.callLater(function() { - if (panel.open && panel.passwordKey !== "" - && panel.credentialEditor === editor && editor.visible - && !editor.activeFocus && keyCatcher.activeFocus) - panel.requestPanelKeyboardFocus(editor) - }) - } - - function openNetworkSettings() { - ownerWidget.close() - if (bar && typeof bar.run === "function") - bar.run("omarchy-launch-or-focus-tui nmtui") - } - - function needsCredentials(entry) { - return !!entry && !entry.known - && (entry.securityKind === "psk" - || entry.securityKind === "enterprise") - } - - function needsNetworkSettings(entry) { - return !!entry && !entry.known && !needsCredentials(entry) - && entry.securityKind !== "open" - } - - function runPrimary(entry) { - if (!entry || networkService.busy) return - if (entry.connected) { - networkService.disconnect(entry) - return - } - if (needsCredentials(entry)) { - openPassword(entry) - return - } - if (needsNetworkSettings(entry)) { - openNetworkSettings() - return - } - networkService.connect(entry) - } - - function submitPassword(entry) { - if (!entry || !passwordText || networkService.busy) return false - let accepted = false - if (entry.securityKind === "enterprise") { - if (!identityText) return false - accepted = networkService.connectEnterprise( - entry, identityText, passwordText) - } else { - accepted = networkService.connectWithPassphrase(entry, passwordText) - } - if (!accepted) - credentialError = "Network changed. Select it again." - return accepted - } - - function requestForget(entry) { - const key = entryKey(entry) - if (!key || !entry || !entry.known || entry.connected) return - if (pendingForgetKey === key) { - pendingForgetKey = "" - forgetTimer.stop() - networkService.forget(entry) - return - } - pendingForgetKey = key - forgetTimer.restart() - } - - function moveCursor(delta) { - if (displayNetworks.length === 0) { - cursorIndex = -1 - return - } - cursorIndex = Math.max(0, Math.min(displayNetworks.length - 1, - cursorIndex < 0 ? (delta > 0 ? 0 : displayNetworks.length - 1) - : cursorIndex + delta)) - } - - function activateCursor() { - if (cursorIndex < 0 || cursorIndex >= displayNetworks.length) return - toggleExpanded(displayNetworks[cursorIndex]) - } - - onSavedOnlyChanged: { - expandedKey = "" - clearPassword() - cursorIndex = displayNetworks.length > 0 ? 0 : -1 - } - - onDisplayNetworksChanged: { - if (cursorIndex >= displayNetworks.length) cursorIndex = displayNetworks.length - 1 - if (displayNetworks.length === 0) cursorIndex = -1 - if (passwordKey !== "") - Qt.callLater(function() { panel.evaluateCredentialCompletion() }) - } - - onOpenChanged: { - if (open) { - speedDetailsVisible = networkService.speedTestRunning - cursorIndex = displayNetworks.length > 0 ? 0 : -1 - } else { - savedOnly = false - expandedKey = "" - pendingForgetKey = "" - speedDetailsVisible = false - clearPassword() - forgetTimer.stop() - } - } - - property Connections networkConnections: Connections { - target: panel.networkService - - function onActionKindChanged() { - if (!panel.networkService || panel.networkService.actionKind !== "" - || panel.passwordKey === "") return - Qt.callLater(function() { panel.evaluateCredentialCompletion() }) - } - } - - property Timer forgetTimer: Timer { - id: forgetTimer - interval: 5000 - onTriggered: panel.pendingForgetKey = "" - } - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - blocked: panel.passwordKey !== "" - onActiveFocusChanged: { - if (activeFocus && panel.passwordKey !== "" - && panel.credentialEditor) Qt.callLater(function() { - if (keyCatcher.activeFocus && panel.open - && panel.passwordKey !== "" && panel.credentialEditor) - panel.requestPanelKeyboardFocus(panel.credentialEditor) - }) - } - onCloseRequested: panel.ownerWidget.close() - onTabRequested: function(direction) { panel.ownerWidget.switchPanel(direction) } - onMoveRequested: function(_dx, dy) { - if (dy !== 0) panel.moveCursor(dy) - } - onActivateRequested: panel.activateCursor() - - Flickable { - id: scroller - anchors.fill: parent - clip: true - contentWidth: width - contentHeight: contentColumn.implicitHeight - boundsBehavior: Flickable.StopAtBounds - interactive: contentHeight > height - - Column { - id: contentColumn - width: scroller.width - spacing: Commons.Style.space(8) - - Row { - width: parent.width - spacing: Commons.Style.space(4) - - Text { - width: parent.width - headerActions.width - parent.spacing - anchors.verticalCenter: parent.verticalCenter - text: "Network" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.heading - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Row { - id: headerActions - spacing: Commons.Style.space(2) - - IconAction { - visible: panel.wifiControlsVisible - icon: panel.networkService.scanning ? "sync" : "refresh" - tooltip: "Rescan Wi-Fi" - enabled: panel.networkService.wifiAvailable - && panel.networkService.wifiEnabled - onClicked: panel.networkService.refresh(true) - } - - IconAction { - icon: "close" - tooltip: "Close" - onClicked: panel.ownerWidget.close() - } - } - } - - Ui.PanelSeparator { width: parent.width } - - Column { - width: parent.width - spacing: Commons.Style.space(3) - - Row { - width: parent.width - spacing: Commons.Style.space(8) - - Item { - visible: panel.networkService.kind === "ethernet" - anchors.verticalCenter: parent.verticalCenter - width: 20 - height: 20 - - Image { - id: ethernetIconSource - anchors.fill: parent - visible: false - source: Qt.resolvedUrl("lan.svg") - sourceSize: Qt.size(20, 20) - fillMode: Image.PreserveAspectFit - smooth: false - mipmap: false - } - - MultiEffect { - anchors.fill: parent - source: ethernetIconSource - colorization: 1 - colorizationColor: panel.bar - ? panel.bar.urgent : Commons.Color.accent - } - } - - IconText { - visible: panel.networkService.kind !== "ethernet" - anchors.verticalCenter: parent.verticalCenter - text: panel.networkService.kind === "wifi" - ? panel.signalIcon(panel.networkService.signalStrength) - : "\uE2C1" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.pixelSize: Commons.Style.font.heading - font.hintingPreference: Font.PreferFullHinting - renderType: Text.NativeRendering - fill: 1 - } - - Text { - width: parent.width - x - anchors.verticalCenter: parent.verticalCenter - text: panel.statusText() - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: Font.Medium - elide: Text.ElideRight - renderType: Text.NativeRendering - } - } - - Text { - width: parent.width - visible: text !== "" - text: panel.statusMeta() - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.58) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - elide: Text.ElideRight - renderType: Text.NativeRendering - } - } - - Grid { - width: parent.width - columns: 2 - columnSpacing: Commons.Style.space(6) - rowSpacing: Commons.Style.space(4) - - DetailCell { - width: (contentColumn.width - parent.columnSpacing) / 2 - label: "PING" - value: panel.networkService.internetPingLatency >= 0 - ? panel.networkService.formatPing(panel.networkService.internetPingLatency) - : "--" - } - DetailCell { - width: (contentColumn.width - parent.columnSpacing) / 2 - label: "GATEWAY" - value: String(panel.networkService.info.gateway || "--") - } - DetailCell { - width: (contentColumn.width - parent.columnSpacing) / 2 - label: "DOWNLOAD" - value: panel.networkService.formatRate(panel.networkService.downloadRate) - } - DetailCell { - width: (contentColumn.width - parent.columnSpacing) / 2 - label: "UPLOAD" - value: panel.networkService.formatRate(panel.networkService.uploadRate) - } - DetailCell { - width: (contentColumn.width - parent.columnSpacing) / 2 - visible: panel.frequencyText() !== "" - label: "FREQUENCY" - value: panel.frequencyText() - } - DetailCell { - width: (contentColumn.width - parent.columnSpacing) / 2 - visible: panel.linkSpeedText() !== "" - label: "LINK SPEED" - value: panel.linkSpeedText() - } - } - - Ui.PanelSeparator { width: parent.width } - - Column { - width: parent.width - spacing: Commons.Style.space(6) - - Row { - width: parent.width - - SectionLabel { - width: parent.width - speedButton.width - anchors.verticalCenter: parent.verticalCenter - text: "SPEED TEST" - } - - PanelButton { - id: speedButton - width: Commons.Style.space(86) - label: panel.networkService.speedTestRunning ? "Running..." : "Run test" - enabled: !panel.networkService.speedTestRunning - && panel.networkService.kind !== "disconnected" - onClicked: { - panel.speedDetailsVisible = true - panel.networkService.runSpeedTest() - } - } - } - - Row { - width: parent.width - spacing: Commons.Style.space(6) - visible: panel.speedDetailsVisible - - DetailCell { - width: (parent.width - 2 * parent.spacing) / 3 - label: "PING" - value: panel.networkService.speedTestRunning - && panel.networkService.speedTestPhase === "down" ? "Testing..." - : panel.networkService.formatPing(panel.networkService.internetPingLatency) - } - DetailCell { - width: (parent.width - 2 * parent.spacing) / 3 - label: "DOWNLOAD" - value: panel.networkService.speedTestRunning - && panel.networkService.speedTestPhase === "down" ? "Testing..." - : panel.networkService.formatSpeed(panel.networkService.speedTestDownloadMbps) - } - DetailCell { - width: (parent.width - 2 * parent.spacing) / 3 - label: "UPLOAD" - value: panel.networkService.speedTestRunning - && panel.networkService.speedTestPhase === "up" ? "Testing..." - : panel.networkService.formatSpeed(panel.networkService.speedTestUploadMbps) - } - } - - Text { - width: parent.width - visible: panel.speedDetailsVisible - && panel.networkService.speedTestError !== "" - text: panel.networkService.speedTestError - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - wrapMode: Text.Wrap - } - } - - Ui.PanelSeparator { width: parent.width } - - Column { - width: parent.width - spacing: Commons.Style.space(5) - - SectionLabel { text: "DNS" } - - Row { - width: parent.width - spacing: Commons.Style.space(4) - - Repeater { - model: panel.networkService.dnsProviders - - PanelButton { - required property var modelData - width: (contentColumn.width - 3 * Commons.Style.space(4)) / 4 - label: String(modelData) - current: panel.networkService.dnsProvider === String(modelData) - onClicked: { - panel.networkService.setDns(String(modelData)) - if (String(modelData) === "Custom") panel.ownerWidget.close() - } - } - } - } - } - - Ui.PanelSeparator { - width: parent.width - visible: panel.wifiControlsVisible - } - - Row { - width: parent.width - height: Commons.Style.space(24) - visible: panel.wifiControlsVisible - - Text { - width: parent.width - wifiToggle.width - anchors.verticalCenter: parent.verticalCenter - text: "Wi-Fi" - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - renderType: Text.NativeRendering - } - - PanelButton { - id: wifiToggle - width: Commons.Style.space(50) - height: Commons.Style.space(22) - label: panel.networkService.wifiEnabled ? "ON" : "OFF" - current: panel.networkService.wifiEnabled - enabled: panel.networkService.backendAvailable - onClicked: panel.networkService.toggleWifi() - } - } - - Row { - width: parent.width - spacing: Commons.Style.space(4) - visible: panel.wifiControlsVisible - - PanelButton { - width: (parent.width - parent.spacing) / 2 - label: "Available" - current: !panel.savedOnly - onClicked: { - panel.savedOnly = false - panel.networkService.refresh(true) - } - } - - PanelButton { - width: (parent.width - parent.spacing) / 2 - label: "Saved" + (panel.savedCount > 0 ? " (" + panel.savedCount + ")" : "") - current: panel.savedOnly - onClicked: panel.savedOnly = true - } - } - - Row { - width: parent.width - visible: panel.wifiControlsVisible - - SectionLabel { - width: parent.width - scanState.width - text: panel.savedOnly ? "SAVED NETWORKS" : "AVAILABLE NETWORKS" - } - - Text { - id: scanState - text: panel.networkService.scanning ? "Scanning..." : "" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - } - } - - Column { - width: parent.width - spacing: Commons.Style.space(3) - visible: panel.wifiControlsVisible - - Repeater { - // Scanner/signal updates replace the service's JS array. Keep the - // delegate containing an active credential editor alive until the - // editor closes, while actions still revalidate against live rows. - model: panel.presentedNetworks - - Ui.CursorSurface { - id: networkRow - required property var modelData - required property int index - readonly property string key: panel.entryKey(modelData) - readonly property bool expanded: panel.expandedKey === key - readonly property bool passwordOpen: panel.passwordKey === key - readonly property bool enterpriseCredentials: - modelData.securityKind === "enterprise" - readonly property bool actionRunning: - panel.networkService.actionSsid === String(modelData.ssid || "") - || (modelData.profileUuid && panel.networkService.busy) - width: contentColumn.width - height: rowColumn.implicitHeight + Commons.Style.space(12) - radius: panel.controlRadius - hasCursor: panel.cursorIndex === index - current: modelData.connected === true - bordered: expanded - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - Column { - id: rowColumn - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Commons.Style.space(8) - anchors.rightMargin: Commons.Style.space(8) - anchors.verticalCenter: parent.verticalCenter - spacing: Commons.Style.space(5) - - Row { - width: parent.width - spacing: Commons.Style.space(7) - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: networkRow.modelData.visible === false ? "bookmark" - : panel.signalIcon(networkRow.modelData.signal) - color: panel.bar ? (networkRow.modelData.connected - ? panel.bar.urgent : panel.bar.foreground) : Commons.Color.accent - opacity: networkRow.modelData.visible === false ? 0.58 : 1 - font.pixelSize: Commons.Style.font.body - fill: 1 - } - - Text { - width: parent.width - x - expandIcon.width - parent.spacing - anchors.verticalCenter: parent.verticalCenter - text: String(networkRow.modelData.ssid || "Hidden network") - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: networkRow.modelData.connected ? Font.Medium : Font.Normal - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - IconText { - id: expandIcon - anchors.verticalCenter: parent.verticalCenter - text: networkRow.expanded ? "expand_less" : "expand_more" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - opacity: 0.55 - font.pixelSize: Commons.Style.font.body - fill: 1 - } - } - - Text { - width: parent.width - visible: networkRow.expanded - text: panel.metadata(networkRow.modelData) - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.58) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - wrapMode: Text.Wrap - renderType: Text.NativeRendering - } - - Text { - width: parent.width - visible: networkRow.expanded - && panel.networkService.failureSsid === networkRow.modelData.ssid - && panel.networkService.failureReason !== "" - text: panel.networkService.failureReason - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - wrapMode: Text.Wrap - } - - Text { - width: parent.width - visible: networkRow.expanded - && panel.networkService.profileError !== "" - && networkRow.modelData.profileUuid !== "" - text: panel.networkService.profileError - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - wrapMode: Text.Wrap - } - - Row { - width: parent.width - spacing: Commons.Style.space(5) - visible: networkRow.expanded && !networkRow.passwordOpen - - PanelButton { - width: parent.width - (forgetButton.visible - ? forgetButton.width + parent.spacing : 0) - label: networkRow.actionRunning - ? (networkRow.modelData.connected ? "Disconnecting..." : "Connecting...") - : networkRow.modelData.connected ? "Disconnect" - : panel.needsNetworkSettings(networkRow.modelData) - ? "Open network settings" : "Connect" - enabled: !panel.networkService.busy - onClicked: panel.runPrimary(networkRow.modelData) - } - - PanelButton { - id: forgetButton - visible: networkRow.modelData.known === true - && networkRow.modelData.connected !== true - width: Commons.Style.space(92) - label: panel.pendingForgetKey === networkRow.key - ? "Confirm" : "Forget" - urgent: true - onClicked: panel.requestForget(networkRow.modelData) - } - } - - Column { - width: parent.width - spacing: Commons.Style.space(5) - visible: networkRow.passwordOpen - - TextField { - id: identityField - width: parent.width - height: Commons.Style.space(30) - visible: networkRow.passwordOpen - && networkRow.enterpriseCredentials - text: panel.identityText - placeholderText: "Identity (user@domain)" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - placeholderTextColor: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.42) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - selectByMouse: true - onTextChanged: panel.identityText = text - onAccepted: passwordField.forceActiveFocus() - Keys.onEscapePressed: panel.clearPassword() - onActiveFocusChanged: - panel.credentialEditorFocusChanged(identityField, - activeFocus) - onVisibleChanged: { - if (visible) panel.registerCredentialEditor(identityField) - else if (panel.credentialEditor === identityField) - panel.credentialEditor = null - } - Component.onCompleted: if (visible) - panel.registerCredentialEditor(identityField) - Component.onDestruction: if (panel.credentialEditor === identityField) - panel.credentialEditor = null - background: Rectangle { - radius: panel.controlRadius - color: identityField.activeFocus - ? panel.controlHoverFillColor : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: identityField.activeFocus - ? panel.controlHoverBorderColor - : panel.controlBorderColor - } - } - - Text { - width: parent.width - visible: panel.credentialError !== "" - text: panel.credentialError - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar - ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - wrapMode: Text.Wrap - } - - Row { - width: parent.width - spacing: Commons.Style.space(5) - - TextField { - id: passwordField - width: parent.width - connectPassword.width - cancelPassword.width - - 2 * parent.spacing - height: Commons.Style.space(30) - visible: networkRow.passwordOpen - text: panel.passwordText - placeholderText: networkRow.enterpriseCredentials - ? "Enterprise password" : "Wi-Fi password" - echoMode: TextInput.Password - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - placeholderTextColor: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.42) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - selectByMouse: true - onTextChanged: panel.passwordText = text - onAccepted: panel.submitPassword(networkRow.modelData) - Keys.onEscapePressed: panel.clearPassword() - onActiveFocusChanged: - panel.credentialEditorFocusChanged(passwordField, - activeFocus) - onVisibleChanged: { - if (visible && !networkRow.enterpriseCredentials) - panel.registerCredentialEditor(passwordField) - else if (!visible - && panel.credentialEditor === passwordField) - panel.credentialEditor = null - } - Component.onCompleted: if (visible - && !networkRow.enterpriseCredentials) - panel.registerCredentialEditor(passwordField) - Component.onDestruction: if (panel.credentialEditor === passwordField) - panel.credentialEditor = null - background: Rectangle { - radius: panel.controlRadius - color: passwordField.activeFocus - ? panel.controlHoverFillColor : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: passwordField.activeFocus - ? panel.controlHoverBorderColor - : panel.controlBorderColor - } - } - - PanelButton { - id: connectPassword - width: Commons.Style.space(66) - label: panel.networkService.busy ? "Wait" : "Connect" - enabled: panel.passwordText !== "" - && (!networkRow.enterpriseCredentials - || panel.identityText !== "") - && panel.credentialError === "" - && !panel.networkService.busy - onClicked: panel.submitPassword(networkRow.modelData) - } - - PanelButton { - id: cancelPassword - width: Commons.Style.space(54) - label: "Cancel" - onClicked: panel.clearPassword() - } - } - } - } - - MouseArea { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - height: Commons.Style.space(34) - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: panel.cursorIndex = networkRow.index - onClicked: panel.toggleExpanded(networkRow.modelData) - } - } - } - - Text { - width: parent.width - visible: panel.wifiControlsVisible - && panel.displayNetworks.length === 0 - text: panel.savedOnly - ? (panel.networkService.profilesLoaded - ? "No saved networks" : "Loading saved networks...") - : panel.networkService.wifiEnabled - ? "No networks found" : "Wi-Fi is disabled" - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.58) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - horizontalAlignment: Text.AlignHCenter - topPadding: Commons.Style.space(10) - bottomPadding: Commons.Style.space(10) - } - } - - PanelButton { - width: parent.width - label: "Network settings" - primary: true - onClicked: panel.openNetworkSettings() - } - } - } - } - - component SectionLabel: Text { - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.52) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.letterSpacing: 1 - renderType: Text.NativeRendering - } - - component DetailCell: Column { - property string label: "" - property string value: "" - spacing: Commons.Style.space(1) - - Text { - width: parent.width - text: parent.label - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.45) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.letterSpacing: 0.6 - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: parent.width - text: parent.value - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - elide: Text.ElideRight - renderType: Text.NativeRendering - } - } - - component PanelButton: Rectangle { - id: button - property string label: "" - property bool current: false - property bool primary: false - property bool urgent: false - signal clicked() - readonly property bool hovered: buttonMouse.containsMouse && enabled - readonly property color actionColor: panel.bar - ? panel.bar.urgent : Commons.Color.accent - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - opacity: enabled ? 1 : 0.42 - color: primary - ? (hovered ? panel.controlPrimaryHoverColor : actionColor) - : current ? panel.controlActiveFillColor - : hovered ? panel.controlHoverFillColor : panel.controlFillColor - border.width: primary ? 0 : panel.controlBorderWidth - border.color: current || hovered - ? actionColor : panel.controlBorderColor - - Behavior on color { ColorAnimation { duration: 100 } } - Behavior on border.color { ColorAnimation { duration: 100 } } - - Text { - anchors.centerIn: parent - width: parent.width - Commons.Style.space(8) - text: button.label - color: !button.enabled - ? Qt.rgba(panel.controlForeground.r, panel.controlForeground.g, - panel.controlForeground.b, 0.35) - : button.primary ? (panel.shibumiTokens - ? panel.shibumiTokens.paper : Commons.Color.background) - : button.current || button.hovered || button.urgent && button.hovered - ? button.actionColor : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - font.weight: button.current ? Font.Medium : Font.Normal - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - MouseArea { - id: buttonMouse - anchors.fill: parent - hoverEnabled: true - enabled: button.enabled - cursorShape: button.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: button.clicked() - } - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - property bool accentAction: false - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: action.icon - color: !action.enabled - ? Qt.rgba(action.foreground.r, action.foreground.g, action.foreground.b, 0.3) - : action.accentAction && panel.bar ? panel.bar.urgent : action.foreground - font.pixelSize: Commons.Style.font.body - fill: action.accentAction ? 1 : 0 - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - enabled: action.enabled - cursorShape: action.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } -} diff --git a/widgets/NetworkWidget.qml b/widgets/NetworkWidget.qml deleted file mode 100644 index 863d5ee..0000000 --- a/widgets/NetworkWidget.qml +++ /dev/null @@ -1,214 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.network" - manageIpc: false - property url popupSource: Qt.resolvedUrl("NetworkPanel.qml") - property var networkServiceOverride: null - property var sessionService: null - - readonly property var tokens: bar ? bar.visualTokens : null - readonly property bool compact: setting("compact", false) === true - readonly property var networkService: networkServiceOverride - || (bar ? bar.networkService : null) - readonly property bool networkReady: networkService && networkService.ready - readonly property bool backendAvailable: networkReady - && networkService.backendAvailable - readonly property string mode: !networkReady ? "none" - : networkService.kind === "wifi" ? "wifi" - : networkService.kind === "ethernet" ? "ethernet" : "none" - readonly property string label: networkReady ? networkService.label : "" - readonly property int signal: networkReady ? networkService.signalStrength : 0 - readonly property string wifiIcon: signal <= 0 ? "signal_wifi_off" - : signal < 22 ? "network_wifi_1_bar" - : signal < 44 ? "network_wifi_2_bar" - : signal < 66 ? "network_wifi_3_bar" : "signal_wifi_4_bar" - readonly property string stateIcon: mode === "wifi" ? wifiIcon - : mode === "ethernet" ? "lan" : "signal_wifi_off" - readonly property string tooltipText: !networkReady ? "Network unavailable" - : mode === "wifi" ? (label || "Wi-Fi") + " · " + signal + "%" - : mode === "ethernet" ? "Ethernet · " + (label || "connected") - : backendAvailable ? "Offline" : "NetworkManager unavailable" - readonly property var panelItem: popupLoader.item - readonly property bool panelLoaded: panelItem !== null - readonly property var interactionTarget: actionButton - - visible: networkReady - implicitWidth: visible ? (bar && bar.vertical ? bar.barSize : surface.implicitWidth) : 0 - implicitHeight: visible - ? (bar && bar.vertical ? surface.implicitHeight : bar ? bar.barSize : 28) : 0 - - function childPanelWidget(pluginId) { - const id = String(pluginId || "") - return id === moduleName || id === "omarchy.network" ? root : null - } - - function ownsPanelWidget(owner) { return owner === root } - - function releaseSession() { - if (sessionService && typeof sessionService.endSession === "function") - sessionService.endSession(root) - sessionService = null - } - - function syncPanelLoader() { - popupLoader.source = "" - if (!opened || !networkReady || !String(popupSource)) { - releaseSession() - return - } - if (sessionService !== networkService) { - releaseSession() - sessionService = networkService - sessionService.beginSession(root) - } - popupLoader.setSource(popupSource, { - anchorItem: surface, - bar: root.bar, - ownerWidget: root, - networkService: networkService - }) - } - - function openAndScan() { - const alreadyOpen = opened - open() - return networkService && (alreadyOpen ? networkService.refresh(true) : true) - } - - onOpenedChanged: syncPanelLoader() - onNetworkReadyChanged: syncPanelLoader() - onPopupSourceChanged: syncPanelLoader() - Component.onDestruction: { - close() - releaseSession() - } - - Loader { id: popupLoader } - - Item { - id: surface - anchors.centerIn: parent - implicitWidth: !root.bar || !root.tokens ? 0 - : root.bar.vertical ? root.bar.barSize - : content.implicitWidth + 2 * root.tokens.pillPaddingX - implicitHeight: !root.bar || !root.tokens ? 0 - : root.bar.vertical ? content.implicitHeight + Commons.Style.space(10) - : root.tokens.slotHeight - width: implicitWidth - height: implicitHeight - - Loader { - anchors.fill: parent - anchors.topMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - anchors.bottomMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - active: root.bar !== null && root.tokens !== null - sourceComponent: Component { - PillSurface { anchors.fill: parent; bar: root.bar } - } - } - - Loader { - id: content - anchors.centerIn: parent - sourceComponent: !root.bar || !root.tokens ? null - : root.bar.vertical ? verticalContent - : root.compact ? compactContent : fullContent - } - - Ui.WidgetButton { - id: actionButton - anchors.fill: parent - bar: root.networkReady ? root.bar : null - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - fixedWidth: surface.width - fixedHeight: surface.height - tooltipText: root.tooltipText - onPressed: function(button) { - if (button === Qt.RightButton) root.openAndScan() - else root.toggle() - } - } - } - - Component { - id: fullContent - - Row { - spacing: root.tokens.contentGap - - Text { - anchors.verticalCenter: parent.verticalCenter - text: "NET" - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, root.mode === "none" ? 0.38 : 0.6) - : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - font.letterSpacing: 0.5 - renderType: Text.NativeRendering - } - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: root.stateIcon - color: root.bar ? root.bar.urgent : Commons.Color.accent - opacity: root.mode === "none" ? 0.58 : 1 - font.pixelSize: root.mode === "none" ? 15 : 14 - font.weight: Font.Medium - fill: 1 - } - - Text { - anchors.verticalCenter: parent.verticalCenter - visible: root.mode !== "none" - text: root.label - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - font.letterSpacing: root.mode === "wifi" ? 1 : 0 - elide: Text.ElideRight - maximumLineCount: 1 - width: Math.min(implicitWidth, Commons.Style.space(128)) - renderType: Text.NativeRendering - } - } - } - - Component { - id: compactContent - - IconText { - text: root.stateIcon - color: root.bar ? root.bar.urgent : Commons.Color.accent - opacity: root.mode === "none" ? 0.58 : 1 - font.pixelSize: root.mode === "none" ? 15 : root.compact ? 15 : 14 - font.weight: Font.Medium - fill: 1 - } - } - - Component { - id: verticalContent - - IconText { - text: root.stateIcon - color: root.bar ? root.bar.urgent : Commons.Color.accent - opacity: root.mode === "none" ? 0.58 : 1 - font.pixelSize: root.mode === "none" ? 15 : 14 - font.weight: Font.Medium - fill: 1 - } - } -} diff --git a/widgets/NotificationPanel.qml b/widgets/NotificationPanel.qml deleted file mode 100644 index e7412a4..0000000 --- a/widgets/NotificationPanel.qml +++ /dev/null @@ -1,396 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var notificationService - readonly property int pendingCount: notificationService - && notificationService.pendingModel - ? notificationService.pendingModel.count : 0 - readonly property int recentCount: notificationService - && notificationService.pastModel - ? notificationService.pastModel.count : 0 - readonly property int activeCount: pendingCount + recentCount - readonly property var activeRows: { - const rows = [] - function append(model, bucket) { - if (!model) return - for (let index = 0; index < model.count; index++) { - const entry = model.get(index) - if (!entry) continue - rows.push({ - bucket: bucket, - sourceIndex: index, - app: String(entry.app || ""), - appIcon: String(entry.appIcon || ""), - summary: String(entry.summary || ""), - body: String(entry.body || "") - }) - } - } - append(notificationService ? notificationService.pendingModel : null, - "pending") - append(notificationService ? notificationService.pastModel : null, - "past") - return rows - } - - owner: ownerWidget - open: ownerWidget.notificationPanelOpen && notificationService !== null - focusTarget: keyCatcher - gap: 8 - padding: 12 - contentWidth: fittedContentWidth(320) - contentHeight: fittedContentHeight(contentColumn.implicitHeight, - 540) - - function closePanel() { - ownerWidget.closeNotificationPanel() - } - - function setDnd(value) { - if (notificationService - && typeof notificationService.setDoNotDisturb === "function") - notificationService.setDoNotDisturb(value === true) - } - - function dismiss(bucket, index) { - if (!notificationService) return - if (bucket === "past" - && typeof notificationService.dismissPast === "function") - notificationService.dismissPast(index) - else if (bucket === "pending" - && typeof notificationService.dismissPending === "function") - notificationService.dismissPending(index) - } - - function clearActive() { - if (!notificationService) return - if (typeof notificationService.clearPending === "function") - notificationService.clearPending() - if (typeof notificationService.clearPast === "function") - notificationService.clearPast() - } - - function openNotification(bucket, index) { - if (!notificationService) { - closePanel() - return - } - const model = bucket === "past" ? notificationService.pastModel - : notificationService.pendingModel - if (!model || index < 0 || index >= model.count) { - closePanel() - return - } - const entry = model.get(index) - if (entry && typeof notificationService.focusApp === "function") - notificationService.focusApp(entry) - closePanel() - } - - function sanitizedBody(body, app, appIcon) { - let text = String(body || "").replace(/]*>/gi, "") - const source = (String(app || "") + "\n" - + String(appIcon || "")).toLowerCase() - const chromium = source.indexOf("chrom") >= 0 - || source.indexOf("brave") >= 0 - || source.indexOf("vivaldi") >= 0 - || source.indexOf("microsoft-edge") >= 0 - || source.indexOf("opera") >= 0 - if (!chromium) return text - return text - .replace(/^\s*]*>\s*(?:https?:\/\/|www\.)?(?:[a-z0-9-]+\.)+[a-z]{2,}(?::\d+)?(?:\/[^<\s]*)?\s*<\/a>\s*/i, "") - .replace(/^\s*(?:https?:\/\/|www\.)?(?:[a-z0-9-]+\.)+[a-z]{2,}(?::\d+)?(?:\/\S*)?\s+/i, "") - } - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.closePanel() - onTabRequested: function(direction) { - if (panel.ownerWidget - && typeof panel.ownerWidget.switchPanel === "function") - panel.ownerWidget.switchPanel(direction) - } - - Column { - id: contentColumn - width: parent.width - spacing: 8 - - Item { - width: parent.width - height: 24 - - Text { - anchors.left: parent.left - anchors.right: panel.shellStyle === "shibumi" - ? closeText.left : closeAction.left - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - text: panel.activeCount > 0 - ? "Notifications · " + panel.activeCount : "Notifications" - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 13 - font.letterSpacing: 2 - font.weight: Font.Medium - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - id: closeText - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - visible: panel.shellStyle === "shibumi" - text: "\u2715" - color: closeMouse.containsMouse - ? panel.controlAccent : panel.controlMuted - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 12 - renderType: Text.NativeRendering - Behavior on color { ColorAnimation { duration: 120 } } - - MouseArea { - id: closeMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.closePanel() - } - } - - IconAction { - id: closeAction - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - visible: panel.shellStyle !== "shibumi" - icon: "close" - tooltip: "Close" - onClicked: panel.closePanel() - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.dividerColor - } - - Item { - id: listViewport - width: parent.width - height: notificationList.count > 0 - ? Math.min(notificationList.contentHeight, 420) - : emptyLabel.implicitHeight - - ListView { - id: notificationList - anchors.fill: parent - visible: count > 0 - clip: true - spacing: 6 - boundsBehavior: Flickable.StopAtBounds - model: panel.activeRows - - delegate: Rectangle { - id: notificationRow - required property int index - required property string bucket - required property int sourceIndex - required property string app - required property string appIcon - required property string summary - required property string body - - readonly property string cleanBody: panel.sanitizedBody( - body, app, appIcon) - - width: ListView.view ? ListView.view.width : 0 - height: rowContent.implicitHeight + 16 - radius: panel.controlRadius - color: rowHover.containsMouse - ? panel.controlHoverFillColor : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: rowHover.containsMouse - ? panel.controlHoverBorderColor : panel.controlBorderColor - - MouseArea { - id: rowHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.openNotification(notificationRow.bucket, - notificationRow.sourceIndex) - } - - Column { - id: rowContent - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - anchors.rightMargin: 26 - spacing: 3 - - Text { - width: parent.width - text: notificationRow.app || "App" - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 10 - font.letterSpacing: 0.5 - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: parent.width - visible: text.length > 0 - text: notificationRow.summary - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: parent.width - visible: text.length > 0 - text: notificationRow.cleanBody - color: Commons.Util.alpha(panel.controlForeground, 0.6) - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 10 - wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight - textFormat: Text.PlainText - renderType: Text.NativeRendering - } - } - - Item { - width: 18 - height: 18 - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: 4 - z: 2 - - Text { - anchors.centerIn: parent - text: "✕" - color: dismissMouse.containsMouse - ? panel.controlAccent - : Commons.Util.alpha(panel.controlForeground, 0.45) - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 10 - renderType: Text.NativeRendering - } - - MouseArea { - id: dismissMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.dismiss(notificationRow.bucket, - notificationRow.sourceIndex) - } - } - } - } - - Text { - id: emptyLabel - anchors.centerIn: parent - visible: notificationList.count === 0 - text: "No notifications" - color: Commons.Util.alpha(panel.controlForeground, 0.3) - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - } - - Rectangle { - width: parent.width - height: 28 - visible: panel.activeCount > 0 - radius: panel.controlRadius - color: clearMouse.containsMouse - ? panel.controlHoverFillColor : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: clearMouse.containsMouse - ? panel.controlHoverBorderColor : panel.controlBorderColor - - Text { - anchors.centerIn: parent - text: "Clear all" - color: clearMouse.containsMouse - ? panel.controlAccent : panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - - MouseArea { - id: clearMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.clearActive() - } - } - } - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: action.icon - color: action.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } -} diff --git a/widgets/NotificationStatusView.qml b/widgets/NotificationStatusView.qml deleted file mode 100644 index 10c4516..0000000 --- a/widgets/NotificationStatusView.qml +++ /dev/null @@ -1,103 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons - -Item { - id: root - - required property var bar - property real slotWidth: Commons.Style.space(26) - readonly property real iconHorizontalOffset: Commons.Style.space(1) - property var notificationService: null - readonly property int pendingCount: notificationService - && notificationService.pendingModel - ? Math.max(0, Number(notificationService.pendingModel.count) || 0) : 0 - readonly property int recentCount: notificationService - && notificationService.pastModel - ? Math.max(0, Number(notificationService.pastModel.count) || 0) : 0 - readonly property int notificationCount: pendingCount + recentCount - readonly property bool presented: notificationService !== null - signal toggleRequested() - signal dndRequested() - property bool registered: false - - visible: presented - implicitWidth: presented ? slotWidth : 0 - implicitHeight: bar ? bar.barSize : Commons.Style.space(35) - width: implicitWidth - height: implicitHeight - - function syncRegistration() { - if (!bar) return - if (visible && !registered) { - bar.registerClickTarget(root) - registered = true - } else if (!visible && registered) { - bar.unregisterClickTarget(root) - registered = false - } - } - - onVisibleChanged: syncRegistration() - Component.onCompleted: syncRegistration() - Component.onDestruction: if (bar && registered) bar.unregisterClickTarget(root) - - IconText { - id: bellIcon - anchors.centerIn: parent - anchors.horizontalCenterOffset: root.iconHorizontalOffset - text: "\uE7F4" - font.pixelSize: 15 - color: root.notificationCount > 0 && root.bar - ? root.bar.urgent - : root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.4) : Commons.Color.foreground - - Behavior on color { ColorAnimation { duration: 150 } } - } - - Rectangle { - visible: root.notificationCount > 0 - width: Math.max(Commons.Style.space(12), badgeText.implicitWidth + 6) - height: Commons.Style.space(12) - radius: height / 2 - color: root.bar ? root.bar.urgent : Commons.Color.accent - anchors.verticalCenter: bellIcon.verticalCenter - anchors.verticalCenterOffset: -6 - anchors.horizontalCenter: bellIcon.horizontalCenter - anchors.horizontalCenterOffset: 7 - - Text { - id: badgeText - anchors.centerIn: parent - text: root.notificationCount > 99 ? "99" - : String(root.notificationCount) - color: root.bar ? root.bar.background : Commons.Color.background - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 7 - font.weight: Font.Bold - } - } - - MouseArea { - id: notificationMouse - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) root.bar.showTooltip(root, - root.notificationCount > 0 - ? root.notificationCount + (root.notificationCount === 1 - ? " notification" : " notifications") - : "No notifications") - onExited: if (root.bar) root.bar.hideTooltip(root) - onClicked: function(mouse) { - if (root.bar) root.bar.hideTooltip(root) - if (mouse.button === Qt.RightButton) root.dndRequested() - else root.toggleRequested() - } - } - - readonly property bool tooltipHovered: visible && notificationMouse.containsMouse -} diff --git a/widgets/PickerImage.qml b/widgets/PickerImage.qml deleted file mode 100644 index 3aa0b8f..0000000 --- a/widgets/PickerImage.qml +++ /dev/null @@ -1,85 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons - -Item { - id: root - - required property var bar - required property var controller - required property var entry - property real imageRadius: Commons.Style.cornerRadius - property real imageInset: 0 - property real washOpacity: 0 - property real decodeWidth: 0 - property real decodeHeight: 0 - property bool selected: false - property bool current: false - property bool interactive: true - signal activated() - - Rectangle { - anchors.fill: parent - radius: root.imageRadius - color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.055) - border.color: root.selected ? root.bar.urgent - : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.2) - border.width: root.selected ? Commons.Style.space(2) : 1 - clip: true - - Image { - anchors.fill: parent - anchors.margins: Math.max(parent.border.width, root.imageInset) - source: root.controller.thumbnailUrl(root.entry) - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - retainWhileLoading: true - sourceSize.width: Math.max(1, Math.round(root.decodeWidth > 0 - ? root.decodeWidth : width * 1.25)) - sourceSize.height: Math.max(1, Math.round(root.decodeHeight > 0 - ? root.decodeHeight : height * 1.25)) - } - - Rectangle { - anchors.fill: parent - anchors.margins: Math.max(parent.border.width, root.imageInset) - radius: Math.max(0, root.imageRadius - anchors.margins) - color: root.bar.visualTokens ? root.bar.visualTokens.paper : root.bar.background - opacity: root.washOpacity - } - - IconText { - anchors.centerIn: parent - visible: !root.entry || !root.entry.thumbnailReady - text: root.controller.videoMode ? "movie" : "image" - color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.34) - font.pixelSize: Commons.Style.font.displayLarge - fill: 0 - } - - Rectangle { - visible: root.current - width: Commons.Style.space(8) - height: width - radius: width / 2 - x: Commons.Style.space(9) - y: Commons.Style.space(9) - color: root.bar.urgent - border.color: Qt.rgba(0, 0, 0, 0.35) - border.width: 1 - } - } - - MouseArea { - anchors.fill: parent - enabled: root.interactive - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.activated() - } -} diff --git a/widgets/PickerOverlay.qml b/widgets/PickerOverlay.qml deleted file mode 100644 index 2cbba28..0000000 --- a/widgets/PickerOverlay.qml +++ /dev/null @@ -1,452 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Wayland -import qs.Commons as Commons -import "PickerModel.js" as PickerModel - -PanelWindow { - id: root - - required property var bar - required property var controller - readonly property bool validScreen: controller.activeScreen !== null - && String(controller.activeScreen.name || "") !== "" - readonly property bool tanzakuActive: controller.pickerStyle === "tanzaku" - readonly property bool selectedIsCurrent: controller.selectedEntry !== null - && (controller.mode === "theme" - ? String(controller.selectedEntry.label || "") - === String(controller.currentSelection || "") - : String(controller.selectedEntry.sourcePath || "") - === String(controller.currentSelection || "")) - - function selectedLabel() { - const entry = controller.selectedEntry - if (!entry) return controller.emptyText - return controller.mediaMode - ? PickerModel.mediaLabel(entry.sourcePath) - : String(entry.label || "") - } - - function selectedHeadline() { - const label = selectedLabel() - return controller.mediaMode - ? controller.title + " · " + label : label - } - - screen: controller.activeScreen - visible: controller.opened && validScreen - anchors { top: true; bottom: true; left: true; right: true } - color: "transparent" - exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "shibumi-picker" - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: visible - ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None - - onVisibleChanged: if (visible) Qt.callLater(stage.forceActiveFocus) - - Rectangle { - anchors.fill: parent - color: root.tanzakuActive - ? Qt.rgba(root.bar.background.r, root.bar.background.g, - root.bar.background.b, 0.8) - : Qt.rgba(0.035, 0.035, 0.05, 0.975) - - Behavior on opacity { NumberAnimation { duration: 170 } } - } - - MouseArea { - anchors.fill: parent - onClicked: root.controller.close() - } - - FocusScope { - id: stage - anchors.fill: parent - focus: root.visible - - Keys.onPressed: function(event) { - if (event.key === Qt.Key_Escape) { - root.controller.close() - } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Up) { - root.controller.moveSelection(-1) - } else if (event.key === Qt.Key_Right || event.key === Qt.Key_Down) { - root.controller.moveSelection(1) - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - root.controller.activateSelected() - } else if (event.key === Qt.Key_Backspace) { - root.controller.updateFilter(root.controller.filterText.slice(0, -1)) - } else if (event.key === Qt.Key_Delete && root.controller.mediaMode) { - root.controller.requestDeleteSelected() - } else if ((event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_C - && root.controller.mediaMode) { - root.controller.copySelected() - } else if (event.key === Qt.Key_Tab) { - root.controller.cycleStyle(event.modifiers & Qt.ShiftModifier ? -1 : 1) - } else if (!(event.modifiers & (Qt.ControlModifier | Qt.AltModifier - | Qt.MetaModifier)) && event.text && event.text.length === 1 - && event.text.charCodeAt(0) >= 32) { - root.controller.updateFilter(root.controller.filterText + event.text) - } else { - return - } - event.accepted = true - } - - Loader { - id: viewLoader - anchors.fill: parent - sourceComponent: root.controller.pickerStyle === "hearthstone" ? hearthstoneView - : tanzakuView - } - - Component { - id: tanzakuView - TanzakuPickerView { controller: root.controller; bar: root.bar } - } - - Component { - id: hearthstoneView - HearthstonePickerView { controller: root.controller; bar: root.bar } - } - - Column { - id: tanzakuFooter - - readonly property real focusedWidth: Math.min(Commons.Style.space(460), - parent.width * 0.48) - readonly property real focusedHeight: focusedWidth * 9 / 16 - - visible: root.tanzakuActive && root.controller.selectedEntry !== null - anchors.horizontalCenter: parent.horizontalCenter - y: parent.height / 2 - Commons.Style.space(10) + focusedHeight / 2 - + Commons.Style.space(16) - width: Math.min(parent.width - Commons.Style.space(48), - focusedWidth + Commons.Style.space(120)) - spacing: Commons.Style.space(12) - - Rectangle { - anchors.horizontalCenter: parent.horizontalCenter - width: tanzakuFooter.focusedWidth * 0.42 - height: Commons.Style.space(3) - radius: height / 2 - gradient: Gradient { - orientation: Gradient.Horizontal - GradientStop { position: 0; color: "transparent" } - GradientStop { position: 0.5; color: root.bar.urgent } - GradientStop { position: 1; color: "transparent" } - } - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - horizontalAlignment: Text.AlignHCenter - text: root.selectedHeadline() - color: root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.space(22) - font.weight: Font.DemiBold - elide: Text.ElideRight - maximumLineCount: 1 - renderType: Text.NativeRendering - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.controller.mode === "theme" - && root.controller.selectedThemePalette.length > 0 - spacing: Commons.Style.space(6) - - Repeater { - model: root.controller.selectedThemePalette - delegate: Rectangle { - required property var modelData - width: Commons.Style.space(13) - height: width - radius: width / 2 - color: String(modelData || "transparent") - border.color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.15) - border.width: 1 - } - } - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.selectedIsCurrent - || (root.controller.mode === "theme" - && root.controller.selectedThemeAuthor !== "") - spacing: Commons.Style.space(12) - - Text { - anchors.verticalCenter: parent.verticalCenter - visible: root.selectedIsCurrent - text: "● current" - color: root.bar.urgent - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - - Text { - id: tanzakuAuthor - anchors.verticalCenter: parent.verticalCenter - visible: root.controller.mode === "theme" - && root.controller.selectedThemeAuthor !== "" - text: "by " + root.controller.selectedThemeAuthor + " ↗" - color: tanzakuAuthorMouse.containsMouse ? root.bar.urgent - : root.bar.visualTokens ? root.bar.visualTokens.mutedInk - : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.45) - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - - MouseArea { - id: tanzakuAuthorMouse - anchors.fill: parent - enabled: root.controller.selectedThemeRepo !== "" - hoverEnabled: true - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: root.controller.openSelectedThemeRepo() - } - } - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.controller.mediaMode - spacing: Commons.Style.space(7) - - OverlayAction { - icon: "open_in_new" - label: "Open" - onTriggered: root.controller.activateSelected() - } - OverlayAction { - icon: "content_copy" - label: "Copy" - onTriggered: root.controller.copySelected() - } - OverlayAction { - icon: root.controller.confirmDelete ? "delete_forever" : "delete" - label: root.controller.confirmDelete ? "Confirm" : "Trash" - urgent: root.controller.confirmDelete - onTriggered: root.controller.requestDeleteSelected() - } - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.controller.filterText !== "" - text: root.controller.filterText - color: root.bar.urgent - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.body - renderType: Text.NativeRendering - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "← → navigate Enter " - + (root.controller.mediaMode ? "open" : "apply") - + " Esc cancel type to filter Tab style" - color: root.bar.visualTokens ? root.bar.visualTokens.mutedInk - : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.45) - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - horizontalAlignment: Text.AlignHCenter - renderType: Text.NativeRendering - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.controller.statusText !== "" - text: root.controller.statusText - color: root.bar.urgent - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - - Column { - visible: !root.tanzakuActive - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: Math.max(Commons.Style.space(32), parent.height * 0.06) - spacing: Commons.Style.space(5) - width: Math.min(parent.width - Commons.Style.space(48), Commons.Style.space(760)) - - Text { - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - horizontalAlignment: Text.AlignHCenter - text: root.selectedHeadline() - color: root.controller.selectedEntry ? "#ececee" - : Qt.rgba(0.92, 0.92, 0.94, 0.55) - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.space(26) - font.weight: Font.DemiBold - elide: Text.ElideMiddle - maximumLineCount: 1 - renderType: Text.NativeRendering - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.controller.mode === "theme" - && (root.controller.selectedThemeAuthor !== "" - || root.controller.selectedThemePalette.length > 0) - spacing: Commons.Style.space(6) - - Repeater { - model: root.controller.selectedThemePalette - delegate: Rectangle { - required property var modelData - width: Commons.Style.space(12) - height: width - radius: width / 2 - color: String(modelData || "transparent") - border.color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.28) - border.width: 1 - } - } - - Text { - anchors.verticalCenter: parent.verticalCenter - visible: root.controller.selectedThemeAuthor !== "" - text: "by " + root.controller.selectedThemeAuthor - color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.62) - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - - MouseArea { - anchors.fill: parent - enabled: root.controller.selectedThemeRepo !== "" - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: root.controller.openSelectedThemeRepo() - } - } - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.controller.mediaMode && root.controller.selectedEntry !== null - spacing: Commons.Style.space(7) - - OverlayAction { - icon: "open_in_new" - label: "Open" - onTriggered: root.controller.activateSelected() - } - OverlayAction { - icon: "content_copy" - label: "Copy" - onTriggered: root.controller.copySelected() - } - OverlayAction { - icon: root.controller.confirmDelete ? "delete_forever" : "delete" - label: root.controller.confirmDelete ? "Confirm" : "Trash" - urgent: root.controller.confirmDelete - onTriggered: root.controller.requestDeleteSelected() - } - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "← → scroll navigate Enter " - + (root.controller.mediaMode ? "open" : "apply") + " Esc cancel" - + " type to filter Tab switch style" - color: Qt.rgba(0.92, 0.92, 0.94, 0.55) - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.controller.filterText !== "" - || root.controller.confirmDelete - text: root.controller.filterText !== "" - ? "Filter: " + root.controller.filterText - : "Press Delete again to move this file to Trash" - color: root.controller.confirmDelete ? root.bar.urgent - : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.62) - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - visible: root.controller.statusText !== "" - text: root.controller.statusText - color: root.bar.urgent - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - } - - component OverlayAction: Rectangle { - id: action - required property string icon - required property string label - property bool urgent: false - signal triggered() - - implicitWidth: actionContent.implicitWidth + Commons.Style.space(18) - implicitHeight: Commons.Style.space(28) - radius: root.bar.visualTokens ? root.bar.visualTokens.tileRadius - : Commons.Style.cornerRadius - color: action.urgent - ? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, root.bar.urgent.b, 0.18) - : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, actionMouse.containsMouse ? 0.14 : 0.08) - border.color: action.urgent ? root.bar.urgent - : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.22) - border.width: 1 - - Row { - id: actionContent - anchors.centerIn: parent - spacing: Commons.Style.space(5) - - IconText { - anchors.verticalCenter: parent.verticalCenter - text: action.icon - color: action.urgent ? root.bar.urgent : root.bar.foreground - font.pixelSize: Commons.Style.font.body - fill: 0 - } - Text { - anchors.verticalCenter: parent.verticalCenter - text: action.label - color: action.urgent ? root.bar.urgent : root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: action.triggered() - } - } -} diff --git a/widgets/PowerProfilePanel.qml b/widgets/PowerProfilePanel.qml deleted file mode 100644 index cb86568..0000000 --- a/widgets/PowerProfilePanel.qml +++ /dev/null @@ -1,206 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui -import "../services/PowerModel.js" as PowerModel - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var powerService - property int selectedIndex: 0 - - owner: ownerWidget - open: ownerWidget.opened && powerService.profileAvailable - focusTarget: keyCatcher - contentWidth: fittedContentWidth(Commons.Style.space(220)) - contentHeight: fittedContentHeight(column.implicitHeight) - - function syncSelection() { - var index = powerService.profiles.indexOf(powerService.activeProfile) - selectedIndex = index >= 0 ? index : 0 - } - - function moveSelection(delta) { - var count = powerService.profiles.length - if (count <= 0) return - selectedIndex = (selectedIndex + delta + count) % count - } - - function activateSelected() { - if (selectedIndex < 0 || selectedIndex >= powerService.profiles.length) return - if (powerService.setProfile(powerService.profiles[selectedIndex])) - ownerWidget.close() - } - - onOpenChanged: if (open) { - powerService.refreshProfiles() - syncSelection() - } - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - - Connections { - target: panel.powerService - function onProfilesChanged() { panel.syncSelection() } - function onActiveProfileChanged() { panel.syncSelection() } - } - - onMoveRequested: function(dx, dy) { - panel.moveSelection(dx !== 0 ? dx : dy) - } - onActivateRequested: panel.activateSelected() - onCloseRequested: panel.ownerWidget.close() - onTabRequested: function(direction) { panel.ownerWidget.switchPanel(direction) } - - Column { - id: column - width: parent.width - spacing: Commons.Style.space(5) - - Row { - width: parent.width - spacing: Commons.Style.space(4) - Text { - width: parent.width - closeAction.width - parent.spacing - anchors.verticalCenter: parent.verticalCenter - text: "Power Profile" - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.heading - font.weight: Font.Medium - } - IconAction { - id: closeAction - anchors.verticalCenter: parent.verticalCenter - icon: "close" - tooltip: "Close" - onClicked: panel.ownerWidget.close() - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, panel.bar.foreground.g, - panel.bar.foreground.b, 0.18) : Commons.Color.popups.border - } - - Repeater { - model: panel.powerService.profiles - delegate: Item { - id: profileRow - required property string modelData - required property int index - readonly property bool active: panel.powerService.activeProfile === modelData - readonly property bool selected: panel.selectedIndex === index - width: column.width - height: Commons.Style.space(32) - - Rectangle { - anchors.fill: parent - radius: panel.controlRadius - color: profileMouse.containsMouse || profileRow.selected - ? panel.bar ? Qt.rgba(panel.bar.urgent.r, panel.bar.urgent.g, - panel.bar.urgent.b, 0.18) : Commons.Color.background - : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: profileRow.active && panel.bar - ? panel.bar.urgent : profileMouse.containsMouse || profileRow.selected - ? panel.controlHoverBorderColor : panel.controlBorderColor - } - - Row { - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Commons.Style.space(8) - anchors.rightMargin: Commons.Style.space(8) - anchors.verticalCenter: parent.verticalCenter - spacing: Commons.Style.space(8) - Text { - anchors.verticalCenter: parent.verticalCenter - text: profileRow.modelData === "power-saver" ? "\uF06C" - : profileRow.modelData === "performance" ? "\uF0E7" : "\uF24E" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.heading - renderType: Text.QtRendering - } - Text { - anchors.verticalCenter: parent.verticalCenter - text: PowerModel.profileLabel(profileRow.modelData) - color: panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: profileRow.active ? Font.DemiBold : Font.Normal - } - } - - MouseArea { - id: profileMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: !panel.powerService.profileActionRunning - onEntered: panel.selectedIndex = profileRow.index - onClicked: { - if (panel.powerService.setProfile(profileRow.modelData)) - panel.ownerWidget.close() - } - } - } - } - - Text { - visible: panel.powerService.profileActionRunning - || panel.powerService.profileError !== "" - width: parent.width - text: panel.powerService.profileError !== "" - ? panel.powerService.profileError : "Applying profile…" - color: panel.powerService.profileError !== "" && panel.bar - ? panel.bar.urgent : panel.bar ? panel.bar.foreground : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - horizontalAlignment: Text.AlignHCenter - } - } - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: action.icon - color: action.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } -} diff --git a/widgets/PowerProfileWidget.qml b/widgets/PowerProfileWidget.qml deleted file mode 100644 index 76bf0e8..0000000 --- a/widgets/PowerProfileWidget.qml +++ /dev/null @@ -1,154 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.Panel { - id: root - - moduleName: "hancore.shibumi.power-profile" - manageIpc: false - property url panelSource: Qt.resolvedUrl("PowerProfilePanel.qml") - - readonly property var powerService: bar ? bar.powerService : null - readonly property var tokens: bar ? bar.visualTokens : null - readonly property bool compact: setting("compact", false) === true - readonly property bool profileAvailable: !!(powerService && powerService.profileAvailable) - readonly property string profile: powerService ? powerService.activeProfile : "" - readonly property string shortName: powerService - ? powerService.activeProfileShortName : "---" - readonly property string profileLabel: powerService - ? powerService.activeProfileLabel : "Power profiles unavailable" - readonly property string profileIcon: profile === "power-saver" ? "\uF06C" - : profile === "performance" ? "\uF0E7" : "\uF24E" - readonly property var interactionTarget: interaction - readonly property var panelItem: panelLoader.item - property var profileOwner: null - - visible: profileAvailable - implicitWidth: visible ? (bar && bar.vertical ? bar.barSize : surface.implicitWidth) : 0 - implicitHeight: visible - ? (bar && bar.vertical ? surface.implicitHeight : bar ? bar.barSize : 28) : 0 - - function childPanelWidget(pluginId) { - return String(pluginId || "") === "omarchy.power" ? root : null - } - - function syncProfileLease() { - if (powerService === profileOwner) return - if (profileOwner) profileOwner.releaseProfiles() - profileOwner = powerService - if (profileOwner) profileOwner.acquireProfiles() - } - - function syncPanelLoader() { - if (!opened || !profileAvailable) { - panelLoader.source = "" - return - } - panelLoader.setSource(panelSource, { - anchorItem: surface, - bar: root.bar, - ownerWidget: root, - powerService: root.powerService - }) - } - - function activate(button) { - if (!powerService) return false - if (button === Qt.RightButton) return powerService.cycleProfile() - toggle() - return true - } - - onOpenedChanged: syncPanelLoader() - onProfileAvailableChanged: if (!profileAvailable && opened) close() - onPowerServiceChanged: syncProfileLease() - Component.onCompleted: syncProfileLease() - Component.onDestruction: { - if (profileOwner) profileOwner.releaseProfiles() - profileOwner = null - } - - Item { - id: surface - anchors.centerIn: parent - implicitWidth: !root.bar || !root.tokens ? 0 - : root.bar.vertical ? root.bar.barSize - : content.implicitWidth + 2 * root.tokens.pillPaddingX - implicitHeight: !root.bar || !root.tokens ? 0 - : root.bar.vertical ? content.implicitHeight + Commons.Style.space(10) - : root.tokens.slotHeight - width: implicitWidth - height: implicitHeight - - PillSurface { - anchors.fill: parent - anchors.topMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - anchors.bottomMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - bar: root.bar - } - - Loader { - id: content - anchors.centerIn: parent - sourceComponent: !root.bar || !root.tokens ? null - : root.compact || root.bar.vertical ? compactContent : fullContent - } - - MouseArea { - id: interaction - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) root.bar.showTooltip(surface, root.profileLabel) - onExited: if (root.bar) root.bar.hideTooltip(surface) - onClicked: function(event) { - if (root.bar) root.bar.hideTooltip(surface) - root.activate(event.button) - } - } - } - - Loader { id: panelLoader } - - Component { - id: fullContent - Row { - spacing: root.tokens.contentGap - Text { - anchors.verticalCenter: parent.verticalCenter - text: "PWR" - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.6) : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - font.letterSpacing: 0.5 - renderType: Text.NativeRendering - } - Text { - anchors.verticalCenter: parent.verticalCenter - text: root.shortName - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.tokens.labelSize - renderType: Text.NativeRendering - } - } - } - - Component { - id: compactContent - Text { - text: root.profileIcon - color: root.bar ? root.bar.urgent : Commons.Color.accent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: root.profile === "balanced" ? 13 : 14 - renderType: Text.QtRendering - } - } -} diff --git a/widgets/QuickAccessWidget.qml b/widgets/QuickAccessWidget.qml deleted file mode 100644 index 9718a32..0000000 --- a/widgets/QuickAccessWidget.qml +++ /dev/null @@ -1,160 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import qs.Commons as Commons - -Item { - id: root - - property var bar: null - property string moduleName: "hancore.shibumi.quick-access" - property var settings: ({}) - readonly property var tokens: bar ? bar.visualTokens : null - readonly property var picker: bar ? bar.pickerService : null - readonly property var targetScreen: root.QsWindow && root.QsWindow.window - ? root.QsWindow.window.screen : null - readonly property bool opened: picker && picker.opened - && picker.activeScreenName === (targetScreen ? String(targetScreen.name || "") : "") - readonly property bool panelLoaded: opened - - visible: bar !== null && tokens !== null && picker !== null - implicitWidth: visible ? actionRow.implicitWidth + 2 * tokens.pillPaddingX : 0 - implicitHeight: visible ? bar.barSize : 0 - - function showTip(owner, text) { - if (bar) bar.showTooltip(owner, text) - } - - function hideTip(owner) { - if (bar) bar.hideTooltip(owner) - } - - function openMode(mode) { - if (!picker) return false - if (bar) bar.requestPopout(root) - return picker.openMode(mode, targetScreen) - } - - function toggleMode(mode) { - if (!picker) return false - if (picker.opened && picker.mode === mode) { - picker.close() - return true - } - return openMode(mode) - } - - function open() { return openMode("wallpaper") } - function close() { if (picker) picker.close() } - function toggle() { return toggleMode("wallpaper") } - function closeForPopoutSwitch() { close() } - - function childPanelWidget(pluginId) { - const id = String(pluginId || "") - return id === "hancore.shibumi.picker" || id === "hancore.shibumi.media-browser" - ? root : null - } - - onOpenedChanged: { - if (!opened && bar) bar.releasePopout(root) - } - Component.onDestruction: { - if (opened) close() - if (bar) bar.releasePopout(root) - } - - PillSurface { - anchors.fill: parent - anchors.topMargin: root.bar && !root.bar.vertical - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - anchors.bottomMargin: anchors.topMargin - bar: root.bar - } - - Row { - id: actionRow - anchors.centerIn: parent - spacing: Commons.Style.space(4) - - ActionIcon { - icon: root.bar && root.bar.idleInhibited - ? String.fromCodePoint(0xF06E8) : String.fromCodePoint(0xF06E9) - nerdGlyph: true - inactiveOpacity: 0.45 - active: root.bar && root.bar.idleInhibited - tooltip: active ? "Idle inhibited: ON" : "Idle inhibited: OFF" - onTriggered: if (root.bar) root.bar.idleInhibited = !root.bar.idleInhibited - } - - ActionIcon { - icon: "collections" - active: root.picker && root.picker.opened && root.picker.mediaMode - tooltip: "Left: Screenshots Right: Videos" - onTriggered: function(button) { - root.toggleMode(button === Qt.RightButton ? "videos" : "screenshots") - } - } - - ActionIcon { - icon: "palette" - active: root.picker && root.picker.opened && root.picker.imageMode - tooltip: "Left: Themes Right: Wallpapers" - onTriggered: function(button) { - root.toggleMode(button === Qt.RightButton ? "wallpaper" : "theme") - } - } - } - - component ActionIcon: Item { - id: action - required property string icon - required property bool active - required property string tooltip - property bool nerdGlyph: false - property real inactiveOpacity: 0.62 - readonly property color iconColor: root.bar - ? (active ? root.bar.urgent - : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, inactiveOpacity)) : Commons.Color.foreground - signal triggered(int button) - - implicitWidth: Commons.Style.space(22) - implicitHeight: root.tokens ? root.tokens.slotHeight : Commons.Style.space(28) - - IconText { - anchors.centerIn: parent - visible: !action.nerdGlyph - text: action.icon - color: action.iconColor - font.pixelSize: 14 - font.weight: Font.Medium - fill: action.active ? 1 : 0 - Behavior on color { ColorAnimation { duration: 150 } } - } - - Text { - anchors.centerIn: parent - visible: action.nerdGlyph - text: action.icon - color: action.iconColor - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.space(14) - renderType: Text.QtRendering - Behavior on color { ColorAnimation { duration: 150 } } - } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: root.showTip(action, action.tooltip) - onExited: root.hideTip(action) - onClicked: function(mouse) { - root.hideTip(action) - action.triggered(mouse.button) - } - } - } -} diff --git a/widgets/StatusIndicators.qml b/widgets/StatusIndicators.qml deleted file mode 100644 index b1778f3..0000000 --- a/widgets/StatusIndicators.qml +++ /dev/null @@ -1,247 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Item { - id: root - - required property var bar - required property var statusService - - readonly property var stateService: bar && "shell" in bar && bar.shell - && typeof bar.shell.serviceFor === "function" - ? bar.shell.serviceFor("hancore.shibumi.state") : null - readonly property bool hasActive: statusService - && (statusService.stayAwake || statusService.notificationsSilenced - || statusService.recording || statusService.voxtypeActive) - readonly property string idleIconFamily: idleIcon.font.family - readonly property string dndIconFamily: dndIcon.font.family - readonly property string recordingIconFamily: recordingIcon.fontFamily - readonly property string recordingIconGlyph: recordingIcon.text - readonly property color recordingIconColor: stateService - && typeof stateService.paletteColor === "function" - ? stateService.paletteColor("color01") - : bar ? bar.urgent : Commons.Color.urgent - readonly property string voxtypeIconFamily: voxtypeIcon.font.family - readonly property real dndOpticalCenterOffset: 1 - readonly property int activeCount: (statusService && statusService.stayAwake ? 1 : 0) - + (statusService && statusService.notificationsSilenced ? 1 : 0) - + (statusService && statusService.recording ? 1 : 0) - + (statusService && statusService.voxtypeActive ? 1 : 0) - readonly property real activeWidth: - (statusService && statusService.stayAwake ? 20 : 0) - + (statusService && statusService.notificationsSilenced ? 20 : 0) - + (statusService && statusService.recording ? recordingIndicator.implicitWidth : 0) - + (statusService && statusService.voxtypeActive ? 20 : 0) - + Math.max(0, activeCount - 1) * Commons.Style.space(8) - - implicitWidth: hasActive ? activeWidth : 0 - implicitHeight: bar ? bar.barSize : Commons.Style.space(35) - visible: hasActive - - function pad(value) { - return Number(value) < 10 ? "0" + Number(value) : String(Number(value)) - } - - function elapsedText() { - const elapsed = statusService ? Number(statusService.recordingElapsed || 0) : 0 - const hours = Math.floor(elapsed / 3600) - const minutes = Math.floor((elapsed % 3600) / 60) - const seconds = Math.floor(elapsed % 60) - return hours > 0 - ? hours + ":" + pad(minutes) + ":" + pad(seconds) - : pad(minutes) + ":" + pad(seconds) - } - - function toggleStayAwake() { - return statusService && typeof statusService.toggleStayAwake === "function" - ? statusService.toggleStayAwake() : false - } - - function toggleNotifications() { - return statusService && typeof statusService.toggleNotifications === "function" - ? statusService.toggleNotifications() : false - } - - function stopRecording() { - return statusService && typeof statusService.stopRecording === "function" - ? statusService.stopRecording() : false - } - - function activateVoxtype(button) { - if (!statusService) return false - if (button === Qt.RightButton - && typeof statusService.openVoxtypeConfig === "function") - return statusService.openVoxtypeConfig() - if (typeof statusService.openVoxtypeModel === "function") - return statusService.openVoxtypeModel() - return false - } - - Row { - id: indicatorRow - anchors.centerIn: parent - spacing: Commons.Style.space(8) - - Item { - id: idleIndicator - visible: root.statusService && root.statusService.stayAwake - implicitWidth: visible ? 20 : 0 - implicitHeight: root.implicitHeight - width: implicitWidth - height: implicitHeight - - Text { - id: idleIcon - anchors.centerIn: parent - text: "\udb86\uded6" - color: root.bar ? root.bar.urgent : Commons.Color.urgent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 13 - } - - Ui.WidgetButton { - anchors.fill: parent - bar: root.bar - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - tooltipText: "Idle lock disabled" - onPressed: root.toggleStayAwake() - } - } - - Item { - id: dndIndicator - visible: root.statusService && root.statusService.notificationsSilenced - implicitWidth: visible ? 20 : 0 - implicitHeight: root.implicitHeight - width: implicitWidth - height: implicitHeight - - IconText { - id: dndIcon - anchors.centerIn: parent - anchors.horizontalCenterOffset: root.dndOpticalCenterOffset - text: "\ue7f6" - color: root.bar ? root.bar.urgent : Commons.Color.urgent - font.pixelSize: 14 - } - - Ui.WidgetButton { - anchors.fill: parent - bar: root.bar - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - tooltipText: "Notifications silenced" - onPressed: root.toggleNotifications() - } - } - - Item { - id: recordingIndicator - visible: root.statusService && root.statusService.recording - implicitWidth: visible ? recordingRow.implicitWidth + 6 : 0 - implicitHeight: root.implicitHeight - width: implicitWidth - height: implicitHeight - - Component.onCompleted: if (root.bar) root.bar.registerClickTarget(recordingIndicator) - Component.onDestruction: if (root.bar) root.bar.unregisterClickTarget(recordingIndicator) - - Row { - id: recordingRow - anchors.centerIn: parent - spacing: Commons.Style.space(5) - - Ui.OpticalGlyph { - id: recordingIcon - anchors.verticalCenter: parent.verticalCenter - width: Commons.Style.space(16) - height: width - text: "󰻂" - color: root.recordingIconColor - fontFamily: root.bar ? root.bar.fontFamily : Commons.Style.font.family - fontSize: 13 - - SequentialAnimation on opacity { - running: recordingIndicator.visible - loops: Animation.Infinite - NumberAnimation { to: 0.25; duration: 600; easing.type: Easing.InOutSine } - NumberAnimation { to: 1; duration: 600; easing.type: Easing.InOutSine } - } - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: root.elapsedText() - color: root.bar ? root.bar.urgent : Commons.Color.urgent - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) root.bar.showTooltip(recordingIndicator, - "Recording · " + root.elapsedText() + "\nClick to stop") - onExited: if (root.bar) root.bar.hideTooltip(recordingIndicator) - onClicked: { - if (root.bar) root.bar.hideTooltip(recordingIndicator) - root.stopRecording() - } - } - } - - Item { - id: voxtypeIndicator - visible: root.statusService && root.statusService.voxtypeActive - implicitWidth: visible ? 20 : 0 - implicitHeight: root.implicitHeight - width: implicitWidth - height: implicitHeight - - IconText { - id: voxtypeIcon - anchors.centerIn: parent - text: root.statusService && root.statusService.voxtypeState === "recording" - ? "\ue029" : "\ue65f" - color: root.statusService - && root.statusService.voxtypeState === "recording" && root.bar - ? root.bar.urgent : root.bar ? root.bar.foreground : Commons.Color.foreground - font.pixelSize: 14 - - SequentialAnimation on opacity { - running: voxtypeIndicator.visible && root.statusService - && root.statusService.voxtypeState === "recording" - loops: Animation.Infinite - NumberAnimation { to: 0.35; duration: 600; easing.type: Easing.InOutSine } - NumberAnimation { to: 1; duration: 600; easing.type: Easing.InOutSine } - } - } - - Ui.WidgetButton { - anchors.fill: parent - bar: root.bar - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - tooltipText: root.statusService && root.statusService.voxtypeHint - ? root.statusService.voxtypeHint - : root.statusService && root.statusService.voxtypeState === "recording" - ? "Voxtype recording" : "Voxtype transcribing" - onPressed: function(button) { - root.activateVoxtype(button) - } - } - } - } -} diff --git a/widgets/StatusWidget.qml b/widgets/StatusWidget.qml deleted file mode 100644 index 3195ba5..0000000 --- a/widgets/StatusWidget.qml +++ /dev/null @@ -1,542 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons - -Item { - id: root - - property var bar: null - property string moduleName: "hancore.shibumi.status" - property var settings: ({}) - readonly property url updateSource: registeredSource( - "hancore.shibumi.update-center") - readonly property url traySource: registeredSource("omarchy.tray") - property Component updateComponent: String(updateSource) ? null - : registeredComponent("hancore.shibumi.update-center") - property Component trayComponent: String(traySource) ? null - : registeredComponent("omarchy.tray") - property url trayDrawerSource: Qt.resolvedUrl("TrayDrawerPanel.qml") - property url trayAppMenuSource: Qt.resolvedUrl("TrayAppMenuPanel.qml") - property url notificationPanelSource: Qt.resolvedUrl("NotificationPanel.qml") - property bool trayDrawerOpen: false - property bool trayAppMenuOpen: false - property var trayAppMenuItem: null - property var trayAppMenuAnchor: null - property real trayAppMenuAnchorX: 0 - property bool notificationPanelOpen: false - - readonly property var tokens: bar ? bar.visualTokens : null - readonly property bool v2Mode: tokens && tokens.v2Shell === true - readonly property int statusActionSlot: Commons.Style.space(22) - readonly property int horizontalInset: tokens - && tokens.pillPaddingX !== undefined - ? Number(tokens.pillPaddingX) || Commons.Style.space(9) - : Commons.Style.space(9) - readonly property var updateWidget: updateLoader.item - readonly property var trayWidget: trayLoader.item - readonly property var notificationService: bar && bar.shell - && typeof bar.shell.firstPartyServiceFor === "function" - ? bar.shell.firstPartyServiceFor("omarchy.notifications") : null - readonly property var trayDrawerItem: trayDrawerLoader.item - readonly property var trayAppMenuPanelItem: trayAppMenuLoader.item - readonly property var notificationPanelItem: notificationPanelLoader.item - readonly property bool trayDrawerLoaded: trayDrawerItem !== null - readonly property bool notificationPanelLoaded: notificationPanelItem !== null - readonly property bool panelLoaded: notificationPanelLoaded - readonly property bool updatePresented: updateWidget !== null - readonly property bool trayPresented: trayView.presented - readonly property bool notificationPresented: notificationView.presented - readonly property int pendingCount: notificationView.pendingCount - readonly property int recentCount: notificationView.recentCount - readonly property int notificationCount: notificationView.notificationCount - readonly property bool ready: updateWidget !== null || trayWidget !== null - || notificationService !== null - readonly property bool opened: trayDrawerOpen || trayAppMenuOpen - || notificationPanelOpen - || childOpened(updateWidget) - || childOpened(trayWidget) - readonly property bool hasVisibleChild: updatePresented || trayPresented - || notificationPresented - readonly property real childGap: Commons.Style.space(4) - readonly property int presentedCount: (updatePresented ? 1 : 0) - + (trayPresented ? 1 : 0) - + (notificationPresented ? 1 : 0) - readonly property real contentWidth: - updateSlotWidth + traySlotWidth + notificationSlotWidth - + Math.max(0, presentedCount - 1) * childGap - readonly property real updateSlotWidth: updatePresented - ? updateSlot.implicitWidth : 0 - readonly property real traySlotWidth: trayPresented - ? trayView.implicitWidth : 0 - readonly property real notificationSlotWidth: notificationPresented - ? notificationView.implicitWidth : 0 - readonly property real trayPinnedIconOffset: - trayView.pinnedIconHorizontalOffset - readonly property real notificationIconOffset: - notificationView.iconHorizontalOffset - - visible: ready && hasVisibleChild - implicitWidth: visible ? contentWidth + 2 * horizontalInset : 0 - implicitHeight: visible ? (bar ? bar.barSize : Commons.Style.space(35)) : 0 - - function registeredComponent(id) { - if (bar && typeof bar.registeredWidgetComponent === "function") - return bar.registeredWidgetComponent(id) - const registry = bar ? bar.barWidgetRegistry : null - if (registry) void(registry.revision) - const widgets = registry && registry.widgets ? registry.widgets : ({}) - const entry = widgets[id] - return entry ? entry.component : null - } - - function registeredSource(id) { - return bar && typeof bar.registeredWidgetSource === "function" - ? bar.registeredWidgetSource(id) : "" - } - - function childSettings(id) { - return bar && typeof bar.widgetSettings === "function" - ? bar.widgetSettings("G3", id) : ({}) - } - - function persistEmbeddedTraySettings(moduleId, values) { - const id = String(moduleId || "") - if (id !== "omarchy.tray" || !values || typeof values !== "object" - || Array.isArray(values)) return false - const state = bar && bar.shell - && typeof bar.shell.serviceFor === "function" - ? bar.shell.serviceFor("hancore.shibumi.state") : null - if (!state || typeof state.setWidgetSetting !== "function") return false - let changed = false - for (const key in values) { - if (key === "id" - || !Object.prototype.hasOwnProperty.call(values, key)) continue - changed = state.setWidgetSetting("G3", id, key, values[key]) || changed - } - return changed - } - - function injectChild(item, id) { - if (!item) return - if ("bar" in item) item.bar = hostProxy - if ("moduleName" in item) item.moduleName = id - if ("settings" in item) item.settings = childSettings(id) - item.opacity = 0 - } - - function injectUpdateChild(item) { - if (!item) return - if ("bar" in item) item.bar = root.bar - if ("moduleName" in item) item.moduleName = "hancore.shibumi.update-center" - if ("settings" in item) - item.settings = childSettings("hancore.shibumi.update-center") - } - - function injectChildren() { - injectUpdateChild(updateWidget) - injectChild(trayWidget, "omarchy.tray") - } - - function syncLoader(loader, component, source, id) { - loader.sourceComponent = null - loader.source = "" - if (component !== null) { - loader.sourceComponent = component - } else if (String(source)) { - loader.setSource(source, { - bar: hostProxy, - moduleName: id, - settings: childSettings(id) - }) - } - } - - function syncTrayLoader() { - syncLoader(trayLoader, trayComponent, traySource, "omarchy.tray") - } - - function syncUpdateLoader() { - updateLoader.sourceComponent = null - updateLoader.source = "" - if (updateComponent !== null) { - updateLoader.sourceComponent = updateComponent - } else if (String(updateSource)) { - updateLoader.setSource(updateSource, { - bar: root.bar, - moduleName: "hancore.shibumi.update-center", - settings: childSettings("hancore.shibumi.update-center") - }) - } - } - - function syncTrayDrawerLoader() { - trayDrawerLoader.source = "" - if (!trayDrawerOpen || !trayWidget || !String(trayDrawerSource)) return - trayDrawerLoader.setSource(trayDrawerSource, { - ownerWidget: root, - trayBackend: trayWidget, - anchorItem: trayView, - bar: root.bar - }) - } - - function syncTrayAppMenuLoader() { - trayAppMenuLoader.source = "" - if (!trayAppMenuOpen || !trayAppMenuItem || !trayAppMenuAnchor - || !String(trayAppMenuSource)) return - trayAppMenuLoader.setSource(trayAppMenuSource, { - ownerWidget: root, - trayItem: trayAppMenuItem, - anchorItem: trayAppMenuAnchor, - bar: root.bar - }) - } - - function syncNotificationPanelLoader() { - notificationPanelLoader.source = "" - if (!notificationPanelOpen || !notificationService - || !String(notificationPanelSource)) return - notificationPanelLoader.setSource(notificationPanelSource, { - ownerWidget: root, - notificationService: root.notificationService, - anchorItem: notificationView, - bar: root.bar - }) - } - - function childOpened(item) { - if (!item) return false - return item.opened === true || item.popupOpen === true - || item.managePopupOpen === true || item.trayMenuOpen === true - } - - function open() { - closeTrayDrawer() - closeChild(updateWidget) - closeChild(trayWidget) - if (!notificationService || !String(notificationPanelSource)) return false - notificationPanelOpen = true - return true - } - - function closeChild(item) { - if (!item) return - if (typeof item.close === "function") item.close() - else { - if ("opened" in item) item.opened = false - if ("popupOpen" in item) item.popupOpen = false - if ("managePopupOpen" in item) item.managePopupOpen = false - if ("trayMenuOpen" in item) item.trayMenuOpen = false - } - } - - function closeTrayDrawer() { - trayDrawerOpen = false - closeTrayAppMenu() - // The host tray menu is a separate popup owned by omarchy.tray. Closing - // our drawer must close that popup too, otherwise Escape/outside-click - // can leave the app menu visible without its owning drawer. - closeChild(trayWidget) - } - - function trayItemName(item) { - if (!item) return "Tray App" - const title = String(item.title || "").trim() - if (title !== "") return title - const tooltipTitle = String(item.tooltipTitle || "").trim() - if (tooltipTitle !== "") return tooltipTitle - return String(item.id || "Tray App") - } - - function appMenuAnchorX(anchor) { - const fallback = trayView.x + trayView.width / 2 - if (!anchor || typeof anchor.mapToItem !== "function" - || typeof root.mapFromItem !== "function") return fallback - try { - const globalPoint = anchor.mapToItem(null, - Number(anchor.width || 0) / 2, Number(anchor.height || 0) / 2) - const localPoint = root.mapFromItem(null, globalPoint.x, globalPoint.y) - return Number.isFinite(localPoint.x) ? localPoint.x : fallback - } catch (error) { - return fallback - } - } - - function openTrayAppMenu(item, anchor) { - if (!item || !item.menu || !anchor || !String(trayAppMenuSource)) - return false - closeChild(trayWidget) - trayAppMenuItem = item - trayAppMenuAnchorX = appMenuAnchorX(anchor) - // The clicked button lives in the full-screen drawer window. Preserve its - // X position, but anchor the child panel to the real bar window so panel - // height and top/bottom placement are computed from the bar, as in V1. - trayAppMenuAnchor = trayAppMenuBarAnchor - // V1 popupOpened() makes the drawer and the DBus menu mutually exclusive. - // Closing only after preserving the X coordinate avoids leaving the - // full-screen drawer layer above the newly opened menu. - trayDrawerOpen = false - trayAppMenuOpen = true - return true - } - - function closeTrayAppMenu() { - trayAppMenuOpen = false - trayAppMenuItem = null - trayAppMenuAnchor = null - closeChild(trayWidget) - } - - function closeNotificationPanel() { - notificationPanelOpen = false - } - - function toggleTrayDrawer() { - if (!trayWidget || Number(trayWidget.drawerCount || 0) <= 0) return false - if (!trayDrawerOpen) { - closeChild(updateWidget) - closeNotificationPanel() - closeChild(trayWidget) - } - trayDrawerOpen = !trayDrawerOpen - return true - } - - function toggleNotificationDnd() { - const service = notificationService - if (!service || typeof service.setDoNotDisturb !== "function") return false - service.setDoNotDisturb(service.doNotDisturb !== true) - return true - } - - function close() { - closeTrayDrawer() - closeTrayAppMenu() - closeNotificationPanel() - closeChild(updateWidget) - closeChild(trayWidget) - } - - function toggle() { - if (notificationPanelOpen) { - closeNotificationPanel() - return true - } - return open() - } - - function switchPanel(direction) { - return bar && typeof bar.switchPanelFrom === "function" - ? bar.switchPanelFrom(root, direction) : false - } - - function childPanelWidget(pluginId) { - const id = String(pluginId || "") - if (id === "hancore.shibumi.update-center") return updateWidget - return id === "omarchy.notifications" ? root : null - } - - function ownsPanelWidget(owner) { - return !!owner && (owner === root || owner === updateWidget - || owner === notificationPanelItem - || owner === trayWidget || owner === trayDrawerItem - || owner === trayAppMenuPanelItem - || (trayAppMenuPanelItem && owner === trayAppMenuPanelItem.owner)) - } - - function scheduleChildSync() { - childSyncTimer.restart() - } - - onBarChanged: { - scheduleChildSync() - if (trayDrawerOpen) syncTrayDrawerLoader() - if (notificationPanelOpen) syncNotificationPanelLoader() - } - onSettingsChanged: injectChildren() - onUpdateComponentChanged: scheduleChildSync() - onUpdateSourceChanged: scheduleChildSync() - onTrayComponentChanged: scheduleChildSync() - onTraySourceChanged: scheduleChildSync() - onTrayDrawerOpenChanged: syncTrayDrawerLoader() - onTrayDrawerSourceChanged: syncTrayDrawerLoader() - onTrayWidgetChanged: if (trayDrawerOpen) syncTrayDrawerLoader() - onTrayAppMenuOpenChanged: syncTrayAppMenuLoader() - onTrayAppMenuSourceChanged: syncTrayAppMenuLoader() - onTrayAppMenuItemChanged: if (trayAppMenuOpen) syncTrayAppMenuLoader() - onTrayAppMenuAnchorChanged: if (trayAppMenuOpen) syncTrayAppMenuLoader() - onNotificationPanelOpenChanged: syncNotificationPanelLoader() - onNotificationPanelSourceChanged: syncNotificationPanelLoader() - onNotificationServiceChanged: if (notificationPanelOpen) - syncNotificationPanelLoader() - - Connections { - target: root.bar - function onLayoutConfigChanged() { root.injectChildren() } - } - - Component.onCompleted: { - scheduleChildSync() - } - Component.onDestruction: { - close() - if (updateWidget && "bar" in updateWidget) updateWidget.bar = null - if (trayWidget && "bar" in trayWidget) trayWidget.bar = null - } - - QtObject { - id: trayShellProxy - - readonly property var realShell: root.bar ? root.bar.shell : null - - function updateEntryInline(moduleId, values) { - if (String(moduleId || "") === "omarchy.tray") - return root.persistEmbeddedTraySettings(moduleId, values) - return realShell && typeof realShell.updateEntryInline === "function" - ? realShell.updateEntryInline(moduleId, values) : false - } - - function serviceFor(pluginId) { - return realShell && typeof realShell.serviceFor === "function" - ? realShell.serviceFor(pluginId) : null - } - - function firstPartyServiceFor(pluginId) { - return realShell && typeof realShell.firstPartyServiceFor === "function" - ? realShell.firstPartyServiceFor(pluginId) : null - } - } - - QtObject { - id: hostProxy - - readonly property var realBar: root.bar - readonly property bool vertical: realBar ? realBar.vertical === true : false - readonly property int barSize: realBar ? Number(realBar.barSize) : 35 - readonly property int sizeHorizontal: realBar - && realBar.sizeHorizontal !== undefined - ? Number(realBar.sizeHorizontal) : barSize - readonly property string position: realBar - ? String(realBar.position || "top") : "top" - readonly property string fontFamily: realBar - ? String(realBar.fontFamily || "monospace") : "monospace" - readonly property color foreground: realBar ? realBar.foreground : "#ffffff" - readonly property color barForeground: foreground - readonly property color urgent: realBar ? realBar.urgent : foreground - readonly property bool foregroundAnimationEnabled: realBar - ? realBar.foregroundAnimationEnabled !== false : false - readonly property var shell: trayShellProxy - readonly property var layoutConfig: realBar ? realBar.layoutConfig : ({}) - readonly property var activePopout: realBar ? realBar.activePopout : null - readonly property var clickTargets: realBar ? realBar.clickTargets : [] - - function registerClickTarget(_target) {} - function unregisterClickTarget(_target) {} - function showTooltip(_target, _text) {} - function hideTooltip(_target) {} - - function requestPopout(owner) { - if (realBar && typeof realBar.requestPopout === "function") - realBar.requestPopout(owner) - } - - function releasePopout(owner) { - if (realBar && typeof realBar.releasePopout === "function") - realBar.releasePopout(owner) - } - - function switchPanelFrom(_owner, direction) { - return realBar && typeof realBar.switchPanelFrom === "function" - ? realBar.switchPanelFrom(root, direction) : false - } - - function targetBelongsToWindow(target, window) { - return realBar && typeof realBar.targetBelongsToWindow === "function" - ? realBar.targetBelongsToWindow(target, window) : false - } - } - - Timer { - id: childSyncTimer - interval: 0 - onTriggered: { - root.syncUpdateLoader() - root.syncTrayLoader() - root.injectChildren() - } - } - - PillSurface { - anchors.fill: parent - anchors.topMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - anchors.bottomMargin: root.tokens - ? Math.round((parent.height - root.tokens.pillHeight) / 2) : 0 - bar: root.bar - } - - Row { - id: statusRow - anchors.centerIn: parent - spacing: root.childGap - z: 2 - - Item { - id: updateSlot - anchors.verticalCenter: parent.verticalCenter - visible: updateLoader.item !== null - implicitWidth: visible ? root.statusActionSlot : 0 - implicitHeight: updateLoader.implicitHeight - width: implicitWidth - height: implicitHeight - - Loader { - id: updateLoader - anchors.centerIn: parent - onLoaded: root.injectUpdateChild(item) - } - } - - TrayStatusView { - id: trayView - bar: root.bar - trayBackend: root.trayWidget - onDrawerRequested: root.toggleTrayDrawer() - } - - NotificationStatusView { - id: notificationView - bar: root.bar - slotWidth: root.statusActionSlot - notificationService: root.notificationService - onToggleRequested: root.toggle() - onDndRequested: root.toggleNotificationDnd() - } - } - - Loader { - id: trayLoader - x: statusRow.x + trayView.x - width: trayView.width - height: root.height - opacity: 0 - z: 0 - onLoaded: { - root.injectChild(item, "omarchy.tray") - } - } - - Loader { id: trayDrawerLoader } - Loader { id: trayAppMenuLoader } - - Item { - id: trayAppMenuBarAnchor - x: root.trayAppMenuAnchorX - y: 0 - width: 0 - height: root.height - visible: false - } - Loader { id: notificationPanelLoader } -} diff --git a/widgets/SystemUpdateWidget.qml b/widgets/SystemUpdateWidget.qml deleted file mode 100644 index bc25138..0000000 --- a/widgets/SystemUpdateWidget.qml +++ /dev/null @@ -1,63 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -// V1 presentation over Quattro's registered omarchy.system-update owner. -// The backend keeps update detection, IPC, and launch ownership. -Ui.BarWidget { - id: root - - moduleName: "omarchy.system-update" - property var backendWidget: null - - readonly property bool updateAvailable: backendWidget - ? backendWidget.updateAvailable === true : false - readonly property string tooltipText: "Omarchy update available" - readonly property string iconFamily: updateIcon.font.family - readonly property real opticalCenterOffset: 1 - readonly property var interactionTarget: updateMouse - - visible: updateAvailable - implicitWidth: updateAvailable ? 20 : 0 - implicitHeight: bar ? bar.barSize : Commons.Style.space(35) - width: implicitWidth - height: implicitHeight - - function activate() { - if (!backendWidget || typeof backendWidget.runUpdate !== "function") return false - backendWidget.runUpdate() - return true - } - - function refresh() { - if (!backendWidget || typeof backendWidget.refresh !== "function") return false - backendWidget.refresh() - return true - } - - IconText { - id: updateIcon - anchors.centerIn: parent - anchors.horizontalCenterOffset: root.opticalCenterOffset - text: "\ue627" - color: root.bar ? root.bar.urgent : Commons.Color.urgent - font.pixelSize: 15 - } - - MouseArea { - id: updateMouse - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) root.bar.showTooltip(root, root.tooltipText) - onExited: if (root.bar) root.bar.hideTooltip(root) - onClicked: function(mouse) { - if (root.bar) root.bar.hideTooltip(root) - if (mouse.button === Qt.RightButton) root.refresh() - else root.activate() - } - } -} diff --git a/widgets/TanzakuPickerView.qml b/widgets/TanzakuPickerView.qml deleted file mode 100644 index 79ca6fb..0000000 --- a/widgets/TanzakuPickerView.qml +++ /dev/null @@ -1,155 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons - -Item { - id: root - required property var bar - required property var controller - - readonly property real centerX: width / 2 - readonly property real centerY: height / 2 - Commons.Style.space(10) - readonly property real focusedWidth: Math.min(Commons.Style.space(460), width * 0.48) - readonly property real focusedHeight: focusedWidth * 9 / 16 - readonly property real peekWidth: Commons.Style.space(104) - readonly property real stripWidth: Commons.Style.space(24) - readonly property real gap: Commons.Style.space(8) - readonly property int maxVisible: 5 - property bool navigationAnimationsEnabled: false - property int layoutGeneration: 0 - - function settleInitialLayout() { - navigationAnimationsEnabled = false - const generation = ++layoutGeneration - if (!controller.opened || controller.filteredEntries.length === 0) return - Qt.callLater(function() { - if (generation !== root.layoutGeneration || !root.controller.opened - || root.controller.filteredEntries.length === 0) return - root.navigationAnimationsEnabled = true - }) - } - - function isCurrent(entry) { - if (!entry) return false - if (controller.mode === "theme") - return String(entry.label || "") === String(controller.currentSelection || "") - return String(entry.sourcePath || "") === String(controller.currentSelection || "") - } - - function itemWidth(relative) { - const distance = Math.abs(relative) - return distance === 0 ? focusedWidth - : distance === 1 ? peekWidth : stripWidth - } - - function itemX(relative) { - const targetWidth = itemWidth(relative) - if (relative === 0) return centerX - targetWidth / 2 - if (relative === -1) return centerX - focusedWidth / 2 - gap - targetWidth - if (relative === 1) return centerX + focusedWidth / 2 + gap - const offset = peekWidth + gap + (Math.abs(relative) - 2) * (stripWidth + gap) - return relative < 0 - ? centerX - focusedWidth / 2 - gap - offset - targetWidth - : centerX + focusedWidth / 2 + gap + offset - } - - Repeater { - model: root.controller.filteredEntries - - delegate: Item { - id: slice - required property int index - required property var modelData - readonly property int relative: index - root.controller.selectedIndex - readonly property bool focused: relative === 0 - readonly property bool neighbor: Math.abs(relative) === 1 - - visible: Math.abs(relative) <= root.maxVisible - width: root.itemWidth(relative) - height: root.focusedHeight - x: root.itemX(relative) - y: root.centerY - height / 2 - z: focused ? 20 : 10 - Math.abs(relative) - opacity: 1 - - Behavior on x { - enabled: root.navigationAnimationsEnabled - NumberAnimation { duration: 260; easing.type: Easing.OutCubic } - } - Behavior on width { - enabled: root.navigationAnimationsEnabled - NumberAnimation { duration: 260; easing.type: Easing.OutCubic } - } - Behavior on opacity { - enabled: root.navigationAnimationsEnabled - NumberAnimation { duration: 200 } - } - - PickerImage { - anchors.fill: parent - bar: root.bar - controller: root.controller - entry: slice.modelData - selected: slice.focused - current: root.isCurrent(slice.modelData) - imageRadius: Commons.Style.space(8) - imageInset: Commons.Style.space(3) - washOpacity: slice.focused ? 0 : slice.neighbor ? 0.28 : 0.5 - decodeWidth: root.focusedWidth - decodeHeight: root.focusedHeight - onActivated: slice.focused - ? root.controller.activateSelected() : root.controller.selectIndex(slice.index) - } - } - } - - Connections { - target: root.controller - function onRequestSerialChanged() { root.settleInitialLayout() } - function onOpenedChanged() { root.settleInitialLayout() } - function onFilteredEntriesChanged() { root.settleInitialLayout() } - } - - Component.onCompleted: settleInitialLayout() - - Text { - visible: root.controller.selectedEntry !== null - anchors.horizontalCenter: parent.horizontalCenter - y: root.centerY - root.focusedHeight / 2 - Commons.Style.space(22) - height - text: root.controller.mode === "theme" ? "THEME" - : root.controller.mode === "wallpaper" ? "WALLPAPER" - : String(root.controller.title || "").toUpperCase() - color: root.bar.urgent - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - font.letterSpacing: Commons.Style.space(3) - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - visible: root.controller.selectedEntry !== null - x: root.centerX + root.focusedWidth / 2 - width - y: root.centerY - root.focusedHeight / 2 - Commons.Style.space(23) - height - text: (root.controller.selectedIndex + 1) + " / " - + root.controller.filteredEntries.length - color: root.bar.visualTokens ? root.bar.visualTokens.mutedInk - : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.45) - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - - Text { - visible: root.controller.selectedEntry === null - anchors.centerIn: parent - text: root.controller.emptyText - color: root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: Commons.Style.font.body - font.letterSpacing: Commons.Style.space(1) - renderType: Text.NativeRendering - } -} diff --git a/widgets/TrayAppMenuPanel.qml b/widgets/TrayAppMenuPanel.qml deleted file mode 100644 index 7007632..0000000 --- a/widgets/TrayAppMenuPanel.qml +++ /dev/null @@ -1,362 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import qs.Commons as Commons -import qs.Ui as Ui - -// V1 tray-menu presentation over Quattro's authoritative SystemTrayItem.menu. -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var trayItem - - readonly property var rootMenu: trayItem ? trayItem.menu : null - property var menuStack: [] - readonly property var currentMenu: menuStack.length > 0 - ? menuStack[menuStack.length - 1] : null - readonly property var currentMenuChildren: menuStack.length > 1 - ? submenuOpener.children : rootMenuOpener.children - property real rootMenuImplicitHeight: 0 - readonly property string appName: ownerWidget - && typeof ownerWidget.trayItemName === "function" - ? ownerWidget.trayItemName(trayItem) : "App Menu" - - // Share the drawer's popout owner. A separate owner makes the coordinator - // close the status group while this menu is still opening. - owner: ownerWidget - open: ownerWidget.trayAppMenuOpen - focusTarget: keyCatcher - padding: Commons.Style.space(8) - contentWidth: fittedContentWidth(Commons.Style.space(220)) - contentHeight: fittedContentHeight( - Math.max(menuColumn.implicitHeight, rootMenuImplicitHeight)) - - function resetMenuStack() { - rootMenuImplicitHeight = 0 - menuStack = open && rootMenu ? [rootMenu] : [] - } - - function closeMenu() { - closeSubmenus() - ownerWidget.closeTrayAppMenu() - } - - function pushSubmenu(entry) { - if (!entry || entry.hasChildren !== true) return false - menuStack = menuStack.concat([entry]) - Qt.callLater(function() { - if (panel.currentMenu !== entry) return - if (typeof entry.updateLayout === "function") entry.updateLayout() - }) - return true - } - - function popSubmenu() { - if (menuStack.length <= 1) return false - menuStack = menuStack.slice(0, -1) - return true - } - - function closeSubmenus() { - menuStack = [] - } - - function stripMnemonic(value) { - return String(value || "").replace(/_([^_])/, "$1") - } - - onOpenChanged: resetMenuStack() - onRootMenuChanged: resetMenuStack() - Component.onCompleted: resetMenuStack() - - Item { - visible: false - width: 0 - height: 0 - - QsMenuOpener { - // Keep the DBusMenuHandle referenced for the complete panel lifetime. - // Releasing it while switching to a child destroys that child's tree. - id: rootMenuOpener - menu: panel.open ? panel.rootMenu : null - } - - QsMenuOpener { - id: submenuOpener - menu: panel.menuStack.length > 1 ? panel.currentMenu : null - } - } - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.closeMenu() - - Column { - id: menuColumn - width: parent.width - spacing: Commons.Style.space(1) - onImplicitHeightChanged: { - if (panel.menuStack.length <= 1) - panel.rootMenuImplicitHeight = implicitHeight - } - - Item { - width: parent.width - height: Commons.Style.space(28) - - Image { - id: appIcon - anchors.left: parent.left - anchors.leftMargin: Commons.Style.space(6) - anchors.verticalCenter: parent.verticalCenter - visible: String(panel.trayItem ? panel.trayItem.icon || "" : "") !== "" - source: panel.trayItem ? String(panel.trayItem.icon || "") : "" - sourceSize.width: Commons.Style.space(16) - sourceSize.height: Commons.Style.space(16) - width: visible ? Commons.Style.space(16) : 0 - height: Commons.Style.space(16) - fillMode: Image.PreserveAspectFit - smooth: true - } - - Text { - anchors.left: appIcon.right - anchors.leftMargin: appIcon.visible ? Commons.Style.space(8) : 0 - anchors.right: panel.shellStyle === "shibumi" - ? closeText.left : closeAction.left - anchors.rightMargin: Commons.Style.space(8) - anchors.verticalCenter: parent.verticalCenter - text: panel.appName !== "" ? panel.appName : "App Menu" - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: Font.Medium - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - id: closeText - anchors.right: parent.right - anchors.rightMargin: Commons.Style.space(6) - anchors.verticalCenter: parent.verticalCenter - visible: panel.shellStyle === "shibumi" - text: "\u2715" - color: closeMouse.containsMouse - ? panel.controlAccent : panel.controlMuted - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 12 - renderType: Text.NativeRendering - Behavior on color { ColorAnimation { duration: 120 } } - - MouseArea { - id: closeMouse - anchors.fill: parent - anchors.margins: -6 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.closeMenu() - } - } - - IconAction { - id: closeAction - anchors.right: parent.right - anchors.rightMargin: Commons.Style.space(6) - anchors.verticalCenter: parent.verticalCenter - visible: panel.shellStyle !== "shibumi" - icon: "close" - tooltip: "Close" - onClicked: panel.closeMenu() - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.dividerColor - } - - Rectangle { - visible: panel.menuStack.length > 1 - width: parent.width - height: visible ? Commons.Style.space(24) : 0 - radius: panel.controlRadius - color: backMouse.containsMouse - ? panel.controlHoverFillColor : "transparent" - - Text { - anchors.left: parent.left - anchors.leftMargin: Commons.Style.space(8) - anchors.verticalCenter: parent.verticalCenter - text: "\u2039 back" - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - renderType: Text.NativeRendering - } - - MouseArea { - id: backMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.popSubmenu() - } - } - - Rectangle { - visible: panel.menuStack.length > 1 - width: parent.width - height: visible ? 1 : 0 - color: panel.dividerColor - } - - Repeater { - model: panel.currentMenuChildren - - delegate: Item { - id: menuEntry - required property var modelData - - width: menuColumn.width - height: modelData.isSeparator - ? Commons.Style.space(7) : Commons.Style.space(26) - - Rectangle { - visible: menuEntry.modelData.isSeparator - anchors.verticalCenter: parent.verticalCenter - width: parent.width - height: 1 - color: panel.dividerColor - } - - Rectangle { - visible: !menuEntry.modelData.isSeparator - anchors.fill: parent - radius: panel.controlRadius - color: entryMouse.containsMouse && menuEntry.modelData.enabled - ? panel.controlActiveFillColor : "transparent" - Behavior on color { ColorAnimation { duration: 100 } } - - Text { - id: checkMark - anchors.left: parent.left - anchors.leftMargin: Commons.Style.space(6) - anchors.verticalCenter: parent.verticalCenter - width: Commons.Style.space(12) - text: menuEntry.modelData.checkState === Qt.Checked ? "\u2713" : "" - color: panel.controlAccent - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - renderType: Text.NativeRendering - } - - Image { - id: entryIcon - anchors.left: checkMark.right - anchors.leftMargin: Commons.Style.space(2) - anchors.verticalCenter: parent.verticalCenter - visible: String(menuEntry.modelData.icon || "") !== "" - source: String(menuEntry.modelData.icon || "") - sourceSize.width: Commons.Style.space(14) - sourceSize.height: Commons.Style.space(14) - width: visible ? Commons.Style.space(14) : 0 - height: Commons.Style.space(14) - fillMode: Image.PreserveAspectFit - smooth: true - } - - Text { - anchors.left: entryIcon.right - anchors.leftMargin: Commons.Style.space(6) - anchors.right: submenuArrow.left - anchors.rightMargin: Commons.Style.space(4) - anchors.verticalCenter: parent.verticalCenter - text: panel.stripMnemonic(menuEntry.modelData.text) - color: menuEntry.modelData.enabled - ? panel.controlForeground - : Commons.Util.alpha(panel.controlForeground, 0.35) - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - id: submenuArrow - anchors.right: parent.right - anchors.rightMargin: Commons.Style.space(8) - anchors.verticalCenter: parent.verticalCenter - visible: menuEntry.modelData.hasChildren - text: "\u203a" - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: Commons.Style.font.heading - renderType: Text.NativeRendering - } - - MouseArea { - id: entryMouse - anchors.fill: parent - enabled: menuEntry.modelData.enabled - hoverEnabled: true - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: { - if (menuEntry.modelData.hasChildren) { - panel.pushSubmenu(menuEntry.modelData) - } else { - menuEntry.modelData.triggered() - panel.closeMenu() - } - } - } - } - } - } - } - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: action.icon - color: action.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } -} diff --git a/widgets/TrayDrawerPanel.qml b/widgets/TrayDrawerPanel.qml deleted file mode 100644 index c22d1fa..0000000 --- a/widgets/TrayDrawerPanel.qml +++ /dev/null @@ -1,400 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell.Services.SystemTray -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var trayBackend - readonly property var drawerItems: trayBackend - && Array.isArray(trayBackend.drawerItems) ? trayBackend.drawerItems : [] - readonly property int attentionCount: { - let count = 0 - for (let index = 0; index < drawerItems.length; index++) { - if (itemNeedsAttention(drawerItems[index])) count++ - } - return count - } - - owner: ownerWidget - open: ownerWidget.trayDrawerOpen - focusTarget: keyCatcher - // V1 contract: 328px outer card with 304px usable row width. - gap: 8 - padding: 12 - contentWidth: fittedContentWidth(328) - contentHeight: fittedContentHeight(contentColumn.implicitHeight) - - function closePanel() { ownerWidget.closeTrayDrawer() } - - function itemName(item) { - if (!item) return "Tray App" - const title = String(item.title || "").trim() - if (title !== "") return title - const tooltipTitle = String(item.tooltipTitle || "").trim() - if (tooltipTitle !== "") return tooltipTitle - let fallback = String(item.id || "").trim() - const slash = fallback.lastIndexOf("/") - if (slash >= 0 && slash < fallback.length - 1) - fallback = fallback.substring(slash + 1) - fallback = fallback.replace(/^org\.(kde|ayatana|freedesktop)\./i, "") - .replace(/[_-]+/g, " ") - return fallback !== "" ? fallback : "Tray App" - } - - function itemDescription(item, displayName) { - if (!item) return "" - const description = String(item.tooltipDescription || "").trim() - const tooltipTitle = String(item.tooltipTitle || "").trim() - const name = String(displayName || "").trim().toLowerCase() - if (description !== "" && description.toLowerCase() !== name) - return description - if (tooltipTitle !== "" && tooltipTitle.toLowerCase() !== name) - return tooltipTitle - return "" - } - - function itemNeedsAttention(item) { - return !!item && item.status === Status.NeedsAttention - } - - function itemHasMenu(item) { - return !!item && (item.hasMenu === true || item.menu) - } - - function togglePin(item) { - const id = item ? String(item.id || "") : "" - if (!id || !trayBackend || typeof trayBackend.togglePin !== "function") - return false - trayBackend.togglePin(id) - return true - } - - function openAppMenu(item, anchor, mouse) { - if (!item || !anchor) return false - if (ownerWidget && typeof ownerWidget.openTrayAppMenu === "function" - && ownerWidget.openTrayAppMenu(item, anchor)) return true - if (!trayBackend || typeof trayBackend.openTrayMenu !== "function") - return false - trayBackend.openTrayMenu(item, anchor, mouse) - return true - } - - function activateApp(item) { - if (!item || typeof item.activate !== "function") return false - closePanel() - Qt.callLater(function() { item.activate() }) - return true - } - - onDrawerItemsChanged: if (open && drawerItems.length === 0) closePanel() - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.closePanel() - - Column { - id: contentColumn - width: parent.width - spacing: 8 - - Item { - width: parent.width - height: 24 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Tray Apps" - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 13 - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - anchors.right: panel.shellStyle === "shibumi" - ? closeText.left : closeAction.left - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: panel.drawerItems.length - + (panel.drawerItems.length === 1 ? " APP" : " APPS") - + (panel.attentionCount > 0 - ? " \u00b7 " + panel.attentionCount + " ATTENTION" : "") - color: panel.attentionCount > 0 - ? panel.controlAccent : panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 10 - renderType: Text.NativeRendering - } - - Text { - id: closeText - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - visible: panel.shellStyle === "shibumi" - text: "\u2715" - color: closeMouse.containsMouse - ? panel.controlAccent : panel.controlMuted - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 12 - renderType: Text.NativeRendering - Behavior on color { ColorAnimation { duration: 120 } } - - MouseArea { - id: closeMouse - anchors.fill: parent - anchors.margins: -6 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.closePanel() - } - } - - IconAction { - id: closeAction - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - visible: panel.shellStyle !== "shibumi" - icon: "close" - tooltip: "Close" - onClicked: panel.closePanel() - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.dividerColor - } - - Item { - width: parent.width - height: Math.min(trayRows.implicitHeight, 320) - - Flickable { - anchors.fill: parent - contentHeight: trayRows.implicitHeight - clip: true - interactive: contentHeight > height - boundsBehavior: Flickable.StopAtBounds - - Column { - id: trayRows - width: parent.width - spacing: 6 - - Repeater { - model: panel.drawerItems - - delegate: Item { - id: appRow - required property var modelData - - readonly property string appName: panel.itemName(modelData) - readonly property string appDescription: - panel.itemDescription(modelData, appName) - readonly property bool needsAttention: - panel.itemNeedsAttention(modelData) - readonly property bool hasMenu: panel.itemHasMenu(modelData) - readonly property string statusDescription: needsAttention - ? "\u26a0 " - + (appDescription !== "" ? appDescription : "Needs attention") - : appDescription - readonly property real cellWidth: 96 - - width: trayRows.width - height: 28 - - Rectangle { - id: appButton - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: appRow.cellWidth - height: parent.height - radius: panel.controlRadius - color: activateMouse.containsMouse - ? panel.controlHoverFillColor : panel.controlFillColor - border.color: activateMouse.containsMouse - ? panel.controlHoverBorderColor : panel.controlBorderColor - border.width: panel.controlBorderWidth - - Image { - id: appIcon - anchors.left: parent.left - anchors.leftMargin: 8 - anchors.verticalCenter: parent.verticalCenter - source: String(appRow.modelData.icon || "") - sourceSize.width: 16 - sourceSize.height: 16 - width: 16 - height: 16 - fillMode: Image.PreserveAspectFit - smooth: true - } - - Text { - anchors.left: appIcon.right - anchors.leftMargin: 6 - anchors.right: parent.right - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - text: (appRow.needsAttention ? "\u26a0 " : "") + appRow.appName - color: appRow.needsAttention || activateMouse.containsMouse - ? panel.controlAccent : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - font.weight: Font.Medium - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - ShibumiPanelToolTip { - panel: panel - visible: activateMouse.containsMouse - text: appRow.statusDescription !== "" - ? appRow.appName + "\n" + appRow.statusDescription - : appRow.appName - } - - MouseArea { - id: activateMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: function(mouse) { - if (appRow.modelData.onlyMenu === true && appRow.hasMenu) - panel.openAppMenu(appRow.modelData, appButton, mouse) - else - panel.activateApp(appRow.modelData) - } - } - } - - Rectangle { - id: pinButton - anchors.left: appButton.right - anchors.leftMargin: 8 - anchors.verticalCenter: parent.verticalCenter - width: appRow.cellWidth - height: parent.height - radius: panel.controlRadius - color: pinMouse.containsMouse - ? panel.controlHoverFillColor : panel.controlFillColor - border.color: pinMouse.containsMouse - ? panel.controlHoverBorderColor : panel.controlBorderColor - border.width: panel.controlBorderWidth - - Text { - anchors.centerIn: parent - text: "Pin" - color: pinMouse.containsMouse - ? panel.controlAccent : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - - MouseArea { - id: pinMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.togglePin(appRow.modelData) - } - } - - Rectangle { - id: menuButton - anchors.left: pinButton.right - anchors.leftMargin: 8 - anchors.verticalCenter: parent.verticalCenter - width: appRow.cellWidth - height: parent.height - radius: panel.controlRadius - color: !appRow.hasMenu ? "transparent" - : menuMouse.containsMouse - ? panel.controlHoverFillColor : panel.controlFillColor - border.color: !appRow.hasMenu - ? Commons.Util.alpha(panel.controlForeground, 0.10) - : menuMouse.containsMouse - ? panel.controlHoverBorderColor : panel.controlBorderColor - border.width: panel.controlBorderWidth - opacity: appRow.hasMenu ? 1 : 0.42 - - Text { - anchors.centerIn: parent - text: appRow.hasMenu ? "AppMenu" : "No Menu" - color: menuMouse.containsMouse && appRow.hasMenu - ? panel.controlAccent : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - - MouseArea { - id: menuMouse - anchors.fill: parent - enabled: appRow.hasMenu - hoverEnabled: enabled - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: function(mouse) { - panel.openAppMenu(appRow.modelData, menuButton, mouse) - } - } - } - } - } - } - } - } - } - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: action.icon - color: action.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } -} diff --git a/widgets/TrayStatusView.qml b/widgets/TrayStatusView.qml deleted file mode 100644 index f6eebfb..0000000 --- a/widgets/TrayStatusView.qml +++ /dev/null @@ -1,210 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons - -Item { - id: root - - required property var bar - property var trayBackend: null - readonly property var pinnedItems: trayBackend - && Array.isArray(trayBackend.pinnedItems) ? trayBackend.pinnedItems : [] - readonly property int drawerCount: trayBackend - && trayBackend.drawerCount !== undefined - ? Math.max(0, Number(trayBackend.drawerCount) || 0) : 0 - readonly property int pinnedCount: pinnedItems.length - readonly property int totalCount: pinnedItems.length + drawerCount - readonly property string drawerTooltipText: totalCount - + (totalCount === 1 ? " app" : " apps") - + (drawerCount > 0 ? " \u00b7 " + drawerCount + " hidden" : "") - readonly property bool presented: pinnedItems.length > 0 || drawerCount > 0 - readonly property int visibleItemCount: pinnedCount + (drawerCount > 0 ? 1 : 0) - readonly property real itemGap: Commons.Style.space(2) - readonly property real pinnedIconHorizontalOffset: Commons.Style.space(1) - signal drawerRequested() - - visible: presented - implicitWidth: presented - ? pinnedCount * Commons.Style.space(18) - + (drawerCount > 0 ? Commons.Style.space(22) : 0) - + Math.max(0, visibleItemCount - 1) * itemGap - : 0 - implicitHeight: bar ? bar.barSize : Commons.Style.space(35) - width: implicitWidth - height: implicitHeight - - function itemId(item) { - return item ? String(item.id || "") : "" - } - - function tooltip(item) { - if (!item) return "Tray application" - if (trayBackend && typeof trayBackend.trayTooltip === "function") - return String(trayBackend.trayTooltip(item) || "Tray application") - return String(item.tooltipTitle || item.title || item.id || "Tray application") - } - - function openMenu(item, anchor, mouse) { - if (!trayBackend || typeof trayBackend.openTrayMenu !== "function") return false - trayBackend.openTrayMenu(item, anchor, mouse) - return true - } - - function togglePin(item) { - const id = itemId(item) - if (!id || !trayBackend || typeof trayBackend.togglePin !== "function") - return false - trayBackend.togglePin(id) - return true - } - - Row { - id: trayRow - anchors.centerIn: parent - spacing: root.itemGap - - Repeater { - model: root.pinnedItems - - delegate: Item { - id: trayDelegate - required property var modelData - - implicitWidth: Commons.Style.space(18) - implicitHeight: root.implicitHeight - width: implicitWidth - height: implicitHeight - - Component.onCompleted: if (root.bar) root.bar.registerClickTarget(trayDelegate) - Component.onDestruction: { - if (root.bar) root.bar.unregisterClickTarget(trayDelegate) - } - - Image { - anchors.centerIn: parent - anchors.horizontalCenterOffset: root.pinnedIconHorizontalOffset - source: String(trayDelegate.modelData.icon || "") - sourceSize.width: Commons.Style.space(14) - sourceSize.height: Commons.Style.space(14) - width: Commons.Style.space(14) - height: Commons.Style.space(14) - fillMode: Image.PreserveAspectFit - smooth: true - } - - MouseArea { - id: trayMouse - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) - root.bar.showTooltip(trayDelegate, root.tooltip(trayDelegate.modelData)) - onExited: if (root.bar) root.bar.hideTooltip(trayDelegate) - onClicked: function(mouse) { - if (root.bar) root.bar.hideTooltip(trayDelegate) - if (mouse.button === Qt.RightButton) { - root.togglePin(trayDelegate.modelData) - } else if (mouse.button === Qt.MiddleButton - || trayDelegate.modelData.onlyMenu === true) { - root.openMenu(trayDelegate.modelData, trayDelegate, mouse) - } else if (typeof trayDelegate.modelData.activate === "function") { - trayDelegate.modelData.activate() - } - } - onWheel: function(wheel) { - if (typeof trayDelegate.modelData.scroll === "function") - trayDelegate.modelData.scroll(wheel.angleDelta.y, false) - } - } - - readonly property bool tooltipHovered: visible && trayMouse.containsMouse - } - } - - Item { - id: drawerToggle - property bool registered: false - visible: root.drawerCount > 0 - implicitWidth: visible ? Commons.Style.space(22) : 0 - implicitHeight: root.implicitHeight - width: implicitWidth - height: implicitHeight - - function syncRegistration() { - if (!root.bar) return - if (visible && !registered) { - root.bar.registerClickTarget(drawerToggle) - registered = true - } else if (!visible && registered) { - root.bar.unregisterClickTarget(drawerToggle) - registered = false - } - } - - onVisibleChanged: syncRegistration() - Component.onCompleted: syncRegistration() - Component.onDestruction: { - if (root.bar && registered) root.bar.unregisterClickTarget(drawerToggle) - } - - IconText { - id: moreIcon - anchors.centerIn: parent - text: "\uE5D3" - font.pixelSize: 16 - color: drawerMouse.containsMouse && root.bar - ? root.bar.foreground - : root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.7) : Commons.Color.foreground - - Behavior on color { ColorAnimation { duration: 150 } } - } - - Rectangle { - visible: root.drawerCount > 0 - width: Math.max(Commons.Style.space(12), badgeText.implicitWidth + 6) - height: Commons.Style.space(12) - radius: height / 2 - color: root.bar ? root.bar.urgent : Commons.Color.accent - anchors.verticalCenter: moreIcon.verticalCenter - anchors.verticalCenterOffset: -6 - anchors.horizontalCenter: moreIcon.horizontalCenter - anchors.horizontalCenterOffset: 7 - - Text { - id: badgeText - anchors.centerIn: parent - text: root.drawerCount > 99 ? "99" : String(root.drawerCount) - color: root.bar ? root.bar.background : Commons.Color.background - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 7 - font.weight: Font.Bold - } - } - - MouseArea { - id: drawerMouse - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) - root.bar.showTooltip(drawerToggle, root.drawerTooltipText) - onExited: if (root.bar) root.bar.hideTooltip(drawerToggle) - onClicked: function(mouse) { - if (root.bar) root.bar.hideTooltip(drawerToggle) - if (mouse.button === Qt.RightButton && root.trayBackend - && root.trayBackend.managePopupOpen !== undefined) { - root.trayBackend.managePopupOpen = !root.trayBackend.managePopupOpen - } else { - root.drawerRequested() - } - } - } - - readonly property bool tooltipHovered: visible && drawerMouse.containsMouse - } - } -} diff --git a/widgets/WeatherPanel.qml b/widgets/WeatherPanel.qml deleted file mode 100644 index d49092f..0000000 --- a/widgets/WeatherPanel.qml +++ /dev/null @@ -1,474 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var weatherService - - readonly property bool useImperial: ownerWidget.useImperial === true - readonly property var forecastDays: weatherService - && weatherService.forecastDays ? weatherService.forecastDays : [] - readonly property color primaryTextColor: shibumiTokens - && shibumiTokens.paper !== undefined - ? shibumiTokens.paper : renderedSurfaceColor - - owner: ownerWidget - open: ownerWidget.panelOpen && weatherService !== null - focusTarget: keyCatcher - centerOnBar: true - centerOnBarOffset: 1 - gap: 8 - padding: 12 - contentWidth: fittedContentWidth(300) - contentHeight: fittedContentHeight(contentColumn.implicitHeight, - 520) - - function closePanel() { - ownerWidget.close() - } - - function refresh() { - if (weatherService && typeof weatherService.refresh === "function") - weatherService.refresh(true) - } - - function dayLabel(dateString, index) { - if (index === 0) return "Today" - if (index === 1) return "Tomorrow" - const date = new Date(String(dateString || "") + "T00:00:00") - if (isNaN(date.getTime())) return String(dateString || "") - return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getDay()] - } - - function dayRange(day) { - if (!day) return "" - const minimum = useImperial ? day.minF : day.minC - const maximum = useImperial ? day.maxF : day.maxC - return minimum + "°/" + maximum + "°" + (useImperial ? "F" : "C") - } - - function feelsText() { - if (!weatherService) return "" - const value = useImperial ? weatherService.feelsF : weatherService.feelsC - return value === "" ? "" : value + "°" + (useImperial ? "F" : "C") - } - - function windText() { - if (!weatherService) return "" - const value = useImperial ? weatherService.windMph : weatherService.windKmh - return value === "" ? "" : value + (useImperial ? " mph" : " km/h") - } - - onOpenChanged: if (open && weatherService && !weatherService.loaded) refresh() - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onCloseRequested: panel.closePanel() - onTabRequested: function(direction) { - if (panel.ownerWidget - && typeof panel.ownerWidget.switchPanel === "function") - panel.ownerWidget.switchPanel(direction) - } - - Column { - id: contentColumn - width: parent.width - spacing: 8 - - Item { - width: parent.width - height: 24 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Weather" - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 13 - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - visible: panel.shellStyle === "shibumi" - text: "\u2715" - color: closeMouse.containsMouse - ? panel.controlAccent : panel.controlMuted - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 12 - renderType: Text.NativeRendering - Behavior on color { ColorAnimation { duration: 120 } } - - MouseArea { - id: closeMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.closePanel() - } - } - - IconAction { - icon: "close" - tooltip: "Close" - visible: panel.shellStyle !== "shibumi" - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - onClicked: panel.closePanel() - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.dividerColor - } - - Item { - width: parent.width - height: 36 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: panel.weatherService && panel.weatherService.loaded - ? panel.ownerWidget.temperature + panel.ownerWidget.unitSuffix : "—" - color: panel.controlAccent - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 26 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - width: parent.width * 0.55 - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: panel.weatherService - ? panel.weatherService.description : "" - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - horizontalAlignment: Text.AlignRight - wrapMode: Text.WordWrap - renderType: Text.NativeRendering - } - } - - Column { - width: parent.width - spacing: 4 - - Row { - width: parent.width - visible: panel.weatherService - && panel.weatherService.place !== "" - - Text { - width: parent.width * 0.4 - text: "Location" - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - Text { - width: parent.width * 0.6 - text: panel.weatherService ? panel.weatherService.place : "" - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - elide: Text.ElideRight - renderType: Text.NativeRendering - } - } - - Row { - width: parent.width - visible: panel.feelsText() !== "" - - Text { - width: parent.width * 0.4 - text: "Feels like" - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - Text { - text: panel.feelsText() - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - } - - Row { - width: parent.width - visible: panel.weatherService - && panel.weatherService.humidity !== "" - - Text { - width: parent.width * 0.4 - text: "Humidity" - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - Text { - text: panel.weatherService - ? panel.weatherService.humidity + "%" : "" - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - } - - Row { - width: parent.width - visible: panel.windText() !== "" - - Text { - width: parent.width * 0.4 - text: "Wind" - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - Text { - text: panel.windText() - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - } - } - - Rectangle { - width: parent.width - height: 1 - color: panel.dividerColor - } - - Column { - id: forecastColumn - width: parent.width - spacing: 5 - visible: panel.forecastDays.length > 0 - - Text { - text: "3-DAY FORECAST" - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 10 - font.letterSpacing: 1 - renderType: Text.NativeRendering - } - - Repeater { - model: panel.forecastDays - - delegate: Item { - id: forecastRow - required property var modelData - required property int index - - width: forecastColumn.width - height: 24 - - Text { - width: 66 - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: panel.dayLabel(forecastRow.modelData.date, - forecastRow.index) - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - anchors.left: parent.left - anchors.leftMargin: 76 - anchors.verticalCenter: parent.verticalCenter - text: panel.weatherService.glyphForCode( - forecastRow.modelData.code, false) - color: panel.controlAccent - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 14 - renderType: Text.NativeRendering - } - - Text { - width: 76 - anchors.left: parent.left - anchors.leftMargin: 106 - anchors.verticalCenter: parent.verticalCenter - text: panel.dayRange(forecastRow.modelData) - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - elide: Text.ElideRight - renderType: Text.NativeRendering - } - - Text { - width: 76 - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: Math.round(Number(forecastRow.modelData.rain || 0)) - + "% rain" - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 10 - horizontalAlignment: Text.AlignRight - renderType: Text.NativeRendering - } - } - } - } - - Rectangle { - width: parent.width - height: 1 - visible: forecastColumn.visible - color: panel.dividerColor - } - - Row { - width: parent.width - height: 28 - spacing: 6 - - Rectangle { - width: (parent.width - parent.spacing) / 2 - height: parent.height - radius: panel.controlRadius - color: panel.weatherService && panel.weatherService.refreshing - ? panel.controlActiveFillColor - : refreshMouse.containsMouse - ? panel.controlPrimaryHoverColor : panel.controlAccent - - Text { - anchors.centerIn: parent - text: panel.weatherService && panel.weatherService.refreshing - ? "Refreshing\u2026" : "Refresh" - color: panel.primaryTextColor - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - - MouseArea { - id: refreshMouse - anchors.fill: parent - enabled: !(panel.weatherService - && panel.weatherService.refreshing) - hoverEnabled: true - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: panel.refresh() - } - } - - Rectangle { - width: (parent.width - parent.spacing) / 2 - height: parent.height - radius: panel.controlRadius - color: unitMouse.containsMouse - ? panel.controlHoverFillColor : panel.controlFillColor - border.width: panel.controlBorderWidth - border.color: unitMouse.containsMouse - ? panel.controlHoverBorderColor : panel.controlBorderColor - - Text { - anchors.centerIn: parent - text: panel.useImperial ? "metric" : "imperial" - color: unitMouse.containsMouse - ? panel.controlAccent : panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } - - MouseArea { - id: unitMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: panel.ownerWidget.toggleUnit() - } - } - } - } - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius - foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground - accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: action.icon - color: action.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: panel - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } -} diff --git a/widgets/WeatherWidget.qml b/widgets/WeatherWidget.qml deleted file mode 100644 index 0b53b4b..0000000 --- a/widgets/WeatherWidget.qml +++ /dev/null @@ -1,127 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.BarWidget { - id: root - - moduleName: "omarchy.weather" - required property var weatherService - property url panelSource: Qt.resolvedUrl("WeatherPanel.qml") - property bool panelOpen: false - - readonly property var panelItem: panelLoader.item - readonly property bool panelLoaded: panelItem !== null - readonly property bool opened: panelOpen - readonly property var stateService: bar && bar.shell - && typeof bar.shell.serviceFor === "function" - ? bar.shell.serviceFor("hancore.shibumi.state") : null - readonly property string unitSetting: String(setting("unit", "") || "").toLowerCase() - readonly property bool useImperial: unitSetting === "imperial" - || (unitSetting !== "metric" && /^en[_-]US/.test(Qt.locale().name)) - readonly property string temperature: weatherService - ? (useImperial ? weatherService.tempF : weatherService.tempC) : "" - readonly property string unitSuffix: useImperial ? "°F" : "°C" - readonly property real iconSlotWidth: 20 - readonly property string tooltipText: { - if (!weatherService) return "Weather unavailable" - if (!weatherService.loaded) - return weatherService.unavailable ? "Weather offline" : "Loading weather" - const details = (weatherService.place ? weatherService.place + " · " : "") - + temperature + unitSuffix - + (weatherService.description ? " / " + weatherService.description : "") - return weatherService.unavailable ? "Weather stale · " + details : details - } - - implicitWidth: bar && bar.vertical ? bar.barSize : iconSlotWidth - implicitHeight: bar ? bar.barSize : Commons.Style.space(35) - - function refresh() { - if (weatherService) weatherService.refresh(true) - } - - function syncPanelLoader() { - panelLoader.source = "" - if (!panelOpen || !weatherService || !String(panelSource)) return - panelLoader.setSource(panelSource, { - ownerWidget: root, - weatherService: root.weatherService, - anchorItem: root, - bar: root.bar - }) - } - - function open() { - if (!weatherService || !String(panelSource)) return false - panelOpen = true - return true - } - - function close() { - panelOpen = false - } - - function toggle() { - if (panelOpen) close() - else return open() - return true - } - - function togglePanel() { - return toggle() - } - - function toggleUnit() { - return stateService && typeof stateService.setWidgetSetting === "function" - ? stateService.setWidgetSetting("G8", moduleName, "unit", - useImperial ? "metric" : "imperial") : false - } - - function switchPanel(direction) { - return bar && typeof bar.switchPanelFrom === "function" - ? bar.switchPanelFrom(root, direction) : false - } - - function ownsPanelWidget(item) { - return item === root || item === panelItem - } - - onPanelOpenChanged: syncPanelLoader() - onPanelSourceChanged: if (panelOpen) syncPanelLoader() - onWeatherServiceChanged: if (panelOpen) syncPanelLoader() - onBarChanged: if (panelOpen) syncPanelLoader() - - Text { - id: weatherIcon - anchors.centerIn: parent - text: root.weatherService && root.weatherService.loaded - ? root.weatherService.icon - : root.weatherService && root.weatherService.unavailable ? "?" : "·" - color: root.weatherService && root.weatherService.unavailable - && !root.weatherService.loaded && root.bar - ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.4) - : root.bar ? root.bar.foreground : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 14 - renderType: Text.NativeRendering - } - - Ui.WidgetButton { - anchors.fill: parent - bar: root.bar - text: " " - keepSpace: true - horizontalMargin: 0 - verticalPadding: 0 - tooltipText: root.tooltipText - onPressed: function(button) { - if (button === Qt.RightButton || button === Qt.MiddleButton) root.refresh() - else root.togglePanel() - } - } - - Loader { id: panelLoader } -} diff --git a/widgets/WidgetRegistry.qml b/widgets/WidgetRegistry.qml deleted file mode 100644 index 77678f8..0000000 --- a/widgets/WidgetRegistry.qml +++ /dev/null @@ -1,131 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import "../menu" as Menu - -Item { - id: root - - property var bar: null - - visible: false - width: 0 - height: 0 - - function componentFor(moduleName) { - if (moduleName === "hancore.shibumi.bar") return menuWidget - if (moduleName === "omarchy.workspaces" - || moduleName === "hancore.shibumi.workspaces") return workspaceWidget - if (moduleName === "hancore.shibumi.memory") return memoryWidget - if (moduleName === "hancore.shibumi.cpu") return cpuWidget - if (moduleName === "hancore.shibumi.audio") return audioWidget - if (moduleName === "hancore.shibumi.ai") return aiUsageWidget - if (moduleName === "hancore.shibumi.status") return statusWidget - if (moduleName === "hancore.shibumi.media") return mediaWidget - if (moduleName === "hancore.shibumi.quick-access") return quickAccessWidget - if (moduleName === "hancore.shibumi.network") return networkWidget - if (moduleName === "hancore.shibumi.brightness") return brightnessWidget - if (moduleName === "hancore.shibumi.battery") return batteryWidget - if (moduleName === "hancore.shibumi.power-profile") return powerProfileWidget - if (moduleName === "hancore.shibumi.bluetooth") return bluetoothWidget - if (moduleName === "hancore.shibumi.center") return centerWidget - if (moduleName === "omarchy.clock") return clockWidget - return null - } - - Component { - id: menuWidget - - Menu.BarWidget { bar: root.bar } - } - - Component { - id: workspaceWidget - - WorkspaceWidget { bar: root.bar } - } - - Component { - id: memoryWidget - - MemoryWidget { bar: root.bar } - } - - Component { - id: cpuWidget - - CpuWidget { bar: root.bar } - } - - Component { - id: statusWidget - - StatusWidget { bar: root.bar } - } - - Component { - id: audioWidget - - AudioWidget { bar: root.bar } - } - - Component { - id: aiUsageWidget - - AiUsageWidget { bar: root.bar } - } - - Component { - id: mediaWidget - - MediaWidget { bar: root.bar } - } - - Component { - id: quickAccessWidget - - QuickAccessWidget { bar: root.bar } - } - - Component { - id: networkWidget - - NetworkWidget { bar: root.bar } - } - - Component { - id: brightnessWidget - - BrightnessWidget { bar: root.bar } - } - - Component { - id: batteryWidget - - BatteryWidget { bar: root.bar } - } - - Component { - id: powerProfileWidget - - PowerProfileWidget { bar: root.bar } - } - - Component { - id: bluetoothWidget - - BluetoothWidget { bar: root.bar } - } - - Component { - id: centerWidget - - CenterWidget { bar: root.bar } - } - - Component { - id: clockWidget - - ClockWidget { bar: root.bar } - } -} diff --git a/widgets/WorkspacePanel.qml b/widgets/WorkspacePanel.qml deleted file mode 100644 index c757fa2..0000000 --- a/widgets/WorkspacePanel.qml +++ /dev/null @@ -1,60 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -ShibumiPanel { - id: panel - - required property var ownerWidget - required property var workspaceService - property int cursorIndex: workspaceService.entries.length > 0 ? 0 : -1 - readonly property var rows: workspaceService.entries - readonly property int renderedRowCount: panelContent.renderedRowCount - readonly property bool controlsFitWidth: panelContent.controlsFitWidth - - owner: ownerWidget - open: ownerWidget.opened - focusTarget: panelContent.focusTarget - gap: 8 - padding: 12 - contentWidth: fittedContentWidth(240) - contentHeight: fittedContentHeight(panelContent.implicitHeight) - - function focusedRowIndex() { - for (let index = 0; index < rows.length; index++) { - if (rows[index] && rows[index].focused === true) return index - } - return rows.length > 0 ? 0 : -1 - } - - function moveCursor(delta) { - if (rows.length === 0) { - cursorIndex = -1 - return false - } - cursorIndex = (Math.max(0, cursorIndex) + delta + rows.length) % rows.length - panelContent.positionAt(cursorIndex) - return true - } - - function activateCursor() { - if (cursorIndex < 0 || cursorIndex >= rows.length) return false - const focused = workspaceService.focusWorkspace(rows[cursorIndex].id) - if (focused) ownerWidget.close() - return focused - } - - onRowsChanged: { - if (rows.length === 0) cursorIndex = -1 - else cursorIndex = Math.max(0, Math.min(cursorIndex, rows.length - 1)) - } - onOpenChanged: if (open) cursorIndex = focusedRowIndex() - - WorkspacePanelContent { - id: panelContent - anchors.fill: parent - controller: panel - } -} diff --git a/widgets/WorkspacePanelContent.qml b/widgets/WorkspacePanelContent.qml deleted file mode 100644 index 5bf624d..0000000 --- a/widgets/WorkspacePanelContent.qml +++ /dev/null @@ -1,205 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Item { - id: root - - required property var controller - readonly property Item focusTarget: keyCatcher - readonly property int renderedRowCount: workspaceList.count - readonly property bool controlsFitWidth: true - readonly property var bar: controller.bar - readonly property var workspaceService: controller.workspaceService - - // Keep the V1 71 px one-row content contract independent of whether this - // component is hosted by a PanelWindow or an offscreen test Item. - implicitHeight: 24 + 8 + 1 + 8 + workspaceList.height - - function positionAt(index) { - if (index >= 0 && index < workspaceList.count) - workspaceList.positionViewAtIndex(index, ListView.Contain) - } - - Ui.PanelKeyCatcher { - id: keyCatcher - anchors.fill: parent - onMoveRequested: function(_dx, dy) { - if (dy !== 0) root.controller.moveCursor(dy) - } - onActivateRequested: root.controller.activateCursor() - onCloseRequested: root.controller.ownerWidget.close() - onTabRequested: function(direction) { - root.controller.ownerWidget.switchPanel(direction) - } - - Column { - id: panelColumn - width: parent.width - spacing: 8 - - Item { - width: parent.width - height: 24 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Workspaces" - color: root.bar ? root.bar.foreground : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 13 - font.letterSpacing: 2 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Text { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - visible: root.controller.shellStyle === "shibumi" - text: "\u2715" - color: closeMouse.containsMouse - ? root.controller.controlAccent : root.controller.controlMuted - font.family: root.bar ? root.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 12 - renderType: Text.NativeRendering - Behavior on color { ColorAnimation { duration: 120 } } - - MouseArea { - id: closeMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.controller.ownerWidget.close() - } - } - - IconAction { - id: closeButton - icon: "close" - tooltip: "Close" - visible: root.controller.shellStyle !== "shibumi" - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - onClicked: root.controller.ownerWidget.close() - } - } - - Rectangle { - width: parent.width - height: 1 - color: root.bar ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, - root.bar.foreground.b, 0.18) : Commons.Color.popups.border - } - - ListView { - id: workspaceList - width: parent.width - readonly property int visibleRows: Math.min(8, count) - height: visibleRows > 0 - ? visibleRows * 30 + Math.max(0, visibleRows - 1) * 4 : 0 - model: root.controller.rows - clip: true - boundsBehavior: Flickable.StopAtBounds - spacing: 4 - - delegate: Rectangle { - id: row - required property var modelData - required property int index - readonly property bool hasCursor: root.controller.cursorIndex === index - readonly property bool current: modelData.focused === true - width: ListView.view.width - height: 30 - radius: root.controller.controlRadius - color: current ? root.controller.controlActiveFillColor - : hasCursor ? root.controller.controlHoverFillColor - : root.controller.controlFillColor - border.width: root.controller.controlBorderWidth - border.color: current || hasCursor ? root.controller.controlAccent - : root.controller.controlBorderColor - - Behavior on color { ColorAnimation { duration: 120 } } - - Text { - id: workspaceLabel - anchors.left: parent.left - anchors.right: windowCountLabel.left - anchors.leftMargin: 10 - anchors.rightMargin: 10 - anchors.verticalCenter: parent.verticalCenter - text: "Workspace " + row.modelData.id - color: row.current || row.hasCursor ? root.controller.controlAccent - : root.bar ? root.bar.foreground : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 12 - font.weight: row.modelData.focused ? Font.Medium : Font.Normal - renderType: Text.NativeRendering - elide: Text.ElideRight - } - - Text { - id: windowCountLabel - anchors.right: parent.right - anchors.rightMargin: 10 - anchors.verticalCenter: parent.verticalCenter - text: String(row.modelData.windowCount) - color: root.controller.controlMutedHigh - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 10 - renderType: Text.NativeRendering - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: root.controller.cursorIndex = row.index - onClicked: { - root.controller.cursorIndex = row.index - root.controller.activateCursor() - } - } - } - } - } - } - - component IconAction: Ui.CursorSurface { - id: action - property string icon: "" - property string tooltip: "" - signal clicked() - implicitWidth: Commons.Style.space(28) - implicitHeight: Commons.Style.space(28) - radius: root.controller.controlRadius - foreground: root.bar ? root.bar.foreground : Commons.Color.foreground - accent: root.bar ? root.bar.urgent : Commons.Color.accent - - IconText { - anchors.centerIn: parent - text: action.icon - color: action.foreground - font.pixelSize: Commons.Style.font.body - } - - MouseArea { - id: actionMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: action.hasCursor = containsMouse - onClicked: action.clicked() - } - - ShibumiPanelToolTip { - panel: root.controller - visible: action.tooltip !== "" && actionMouse.containsMouse - text: action.tooltip - } - } -} diff --git a/widgets/WorkspaceWidget.qml b/widgets/WorkspaceWidget.qml deleted file mode 100644 index 12142fa..0000000 --- a/widgets/WorkspaceWidget.qml +++ /dev/null @@ -1,319 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Commons as Commons -import qs.Ui as Ui - -Ui.Panel { - id: root - - moduleName: "omarchy.workspaces" - manageIpc: false - property url panelSource: Qt.resolvedUrl("WorkspacePanel.qml") - readonly property var workspaceService: bar ? bar.workspaceService : null - readonly property var stateService: bar && bar.shell - && typeof bar.shell.serviceFor === "function" - ? bar.shell.serviceFor("hancore.shibumi.state") : null - readonly property var tokens: bar ? bar.visualTokens : null - readonly property var workspaceIds: workspaceService - ? workspaceService.visibleWorkspaceIds : [] - readonly property string workspaceStyle: workspaceService - ? workspaceService.style : "default" - readonly property color pacmanActiveColor: - paletteColor("color03", bar ? bar.urgent : Commons.Color.accent) - readonly property color pacmanBarColor: bar - ? bar.foreground : Commons.Color.foreground - readonly property color pacmanOccupiedColor: pacmanBarColor - readonly property color pacmanEmptyColor: pacmanBarColor - readonly property color pacmanHoverColor: pacmanBarColor - readonly property int renderedWorkspaceCount: workspaceRepeater.count - readonly property bool panelLoaded: panelLoader.item !== null - readonly property bool panelLoaderReady: panelLoader.item - ? panelLoader.item.ready === true : false - readonly property int workspacePadding: tokens - ? tokens.workspacePillPadding(workspaceStyle) : 4 - readonly property int workspaceGap: workspaceStyle === "rings" - || workspaceStyle === "aurora" ? Commons.Style.space(3) - : workspaceStyle === "pacman" ? Commons.Style.space(2) - : tokens ? tokens.contentGap : Commons.Style.space(5) - readonly property real workspaceContentWidth: { - void(workspaceIds) - void(workspaceStyle) - var total = 0 - var visibleCount = 0 - for (var i = 0; i < workspaceRepeater.count; i++) { - var item = workspaceRepeater.itemAt(i) - if (!item || item.implicitWidth <= 0) continue - total += item.implicitWidth - visibleCount++ - } - return total + Math.max(0, visibleCount - 1) * workspaceGap - } - - implicitWidth: bar && bar.vertical ? bar.barSize : workspaceSurface.implicitWidth - implicitHeight: bar && bar.vertical - ? workspaceSurface.implicitHeight : bar ? bar.barSize : 28 - - function activateWorkspace(id) { - return workspaceService ? workspaceService.focusWorkspace(id) : false - } - - function paletteColor(id, fallback) { - return stateService && typeof stateService.paletteColor === "function" - ? stateService.paletteColor(id) : fallback - } - - function workspaceState(id) { - return workspaceService - ? workspaceService.workspaceState(id) - : ({ id: Number(id) || 0, focused: false, occupied: false, windowCount: 0 }) - } - - function workspaceTooltip(id) { - const info = workspaceState(id) - const windows = info.windowCount === 1 ? "1 window" - : info.windowCount + " windows" - return "Workspace " + info.id + " · " + windows - + " · Right-click for workspace panel" - } - - function syncPanelLoader() { - if (!opened) { - panelLoader.source = "" - return - } - panelLoader.setSource(panelSource, { - anchorItem: workspaceSurface, - bar: root.bar, - ownerWidget: root, - workspaceService: root.workspaceService - }) - } - - onOpenedChanged: syncPanelLoader() - - Item { - id: workspaceSurface - anchors.centerIn: parent - implicitWidth: root.workspaceContentWidth + 2 * root.workspacePadding - implicitHeight: root.bar ? root.bar.barSize : Commons.Style.space(28) - width: implicitWidth - height: implicitHeight - - PillSurface { - bar: root.bar - anchors.fill: parent - anchors.topMargin: Math.round((parent.height - root.tokens.pillHeight) / 2) - anchors.bottomMargin: Math.round((parent.height - root.tokens.pillHeight) / 2) - } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.RightButton - cursorShape: Qt.PointingHandCursor - onClicked: root.toggle() - } - - Row { - id: workspaceRow - anchors.centerIn: parent - spacing: root.workspaceGap - width: root.workspaceContentWidth - - Repeater { - id: workspaceRepeater - model: root.workspaceIds - - delegate: Item { - id: cell - required property int modelData - readonly property var workspaceInfo: root.workspaceState(modelData) - readonly property bool focused: workspaceInfo.focused === true - readonly property bool occupied: workspaceInfo.occupied === true - readonly property bool empty: !focused && !occupied - readonly property int numberWidth: Commons.Style.space(20) - - implicitWidth: root.workspaceStyle === "numbers" ? Commons.Style.space(22) - : root.workspaceStyle === "kanji" ? Commons.Style.space(22) - : root.workspaceStyle === "magic" - ? Commons.Style.space(focused ? 20 : 18) - : root.workspaceStyle === "rings" ? Commons.Style.space(19) - : root.workspaceStyle === "aurora" - ? Commons.Style.space(focused ? 38 : 20) - : root.workspaceStyle === "pacman" - ? Commons.Style.space(22) - : Commons.Style.space(focused ? 32 : 16) - implicitHeight: workspaceSurface.height - - Behavior on implicitWidth { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } - } - Behavior on scale { NumberAnimation { duration: 120 } } - - Rectangle { - visible: root.workspaceStyle === "default" - anchors.centerIn: parent - width: Commons.Style.space(cell.focused ? 34 : 16) - height: Commons.Style.space(16) - radius: height / 2 - color: root.bar ? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, - root.bar.urgent.b, cell.focused ? 0.20 : cell.occupied ? 0.18 : 0.06) - : "transparent" - Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } - } - - Rectangle { - visible: root.workspaceStyle === "default" - anchors.centerIn: parent - width: Commons.Style.space(cell.focused ? 26 : 8) - height: Commons.Style.space(8) - radius: height / 2 - color: root.bar ? (cell.focused || cell.occupied - ? root.bar.urgent : Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, - root.bar.urgent.b, 0.25)) : Commons.Color.accent - Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } - } - - Rectangle { - visible: root.workspaceStyle === "numbers" - anchors.centerIn: parent - width: cell.numberWidth - height: Commons.Style.space(20) - radius: root.tokens.presentation.radius === "small" - ? Commons.Style.space(5) : height / 2 - color: root.bar ? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, - root.bar.urgent.b, cell.focused ? 0.30 : cell.occupied ? 0.12 : 0.04) - : "transparent" - - Text { - anchors.centerIn: parent - text: cell.modelData - color: root.bar ? (cell.focused - ? Qt.lighter(root.bar.urgent, 1.3) - : Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, - root.bar.urgent.b, cell.occupied ? 0.5 : 0.28)) - : Commons.Color.foreground - font.family: root.bar ? root.bar.fontFamily : Commons.Style.font.family - font.pixelSize: cell.focused - ? Commons.Style.font.subtitle : root.tokens.labelSize - font.weight: cell.focused ? Font.Bold : Font.Normal - renderType: Text.NativeRendering - } - } - - Text { - visible: root.workspaceStyle === "magic" - anchors.centerIn: parent - anchors.verticalCenterOffset: cell.focused ? 0 : 1 - text: cell.focused ? "✦" : cell.occupied ? "✧" : "·" - color: root.bar ? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, - root.bar.urgent.b, cell.focused ? 1 : cell.occupied ? 0.7 : 0.3) - : Commons.Color.foreground - font.family: "Adwaita Mono" - font.pixelSize: Commons.Style.space(cell.focused ? 22 : 18) - renderType: Text.NativeRendering - - Behavior on color { ColorAnimation { duration: 200 } } - } - - Text { - visible: root.workspaceStyle === "kanji" - anchors.centerIn: parent - text: cell.modelData >= 1 && cell.modelData <= 10 - ? ["一", "二", "三", "四", "五", - "六", "七", "八", "九", "十"][cell.modelData - 1] - : String(cell.modelData) - color: root.bar ? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, - root.bar.urgent.b, cell.focused ? 1 : cell.occupied ? 0.7 : 0.3) - : Commons.Color.foreground - font.family: "Noto Sans CJK JP" - font.pixelSize: Commons.Style.space(cell.focused ? 15 : 13) - font.weight: Font.Normal - renderType: Text.NativeRendering - - Behavior on color { ColorAnimation { duration: 200 } } - } - - Rectangle { - id: ringsMark - visible: root.workspaceStyle === "rings" - anchors.centerIn: parent - width: Commons.Style.space(cell.focused ? 13 : 12) - height: width - radius: width / 2 - color: cell.focused && root.bar ? root.bar.urgent : "transparent" - border.width: cell.focused ? 0 : 1 - border.color: root.bar ? root.bar.urgent : Commons.Color.accent - opacity: cellPointer.containsMouse ? 0.4 - : cell.focused ? 1 : cell.occupied ? 0.45 : 0.10 - antialiasing: true - - Behavior on width { - NumberAnimation { duration: 180; easing.type: Easing.OutCubic } - } - Behavior on opacity { - NumberAnimation { duration: 300; easing.type: Easing.InOutCubic } - } - } - - Rectangle { - visible: root.workspaceStyle === "aurora" - anchors.centerIn: parent - width: Commons.Style.space(cell.focused ? 36 : 18) - height: Commons.Style.space(16) - radius: height / 2 - antialiasing: true - color: root.bar ? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, - root.bar.urgent.b, - cell.empty ? (cellPointer.containsMouse ? 0.25 : 0.10) - : (cellPointer.containsMouse ? 0.55 : 1)) - : Commons.Color.accent - - Behavior on width { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } - } - Behavior on color { ColorAnimation { duration: 250 } } - } - - PacmanWorkspaceMarker { - visible: root.workspaceStyle === "pacman" - anchors.centerIn: parent - focused: cell.focused - occupied: cell.occupied - hovered: cellPointer.containsMouse - activeColor: root.pacmanActiveColor - occupiedColor: root.pacmanOccupiedColor - emptyColor: root.pacmanEmptyColor - hoverColor: root.pacmanHoverColor - } - - MouseArea { - id: cellPointer - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: { - cell.scale = root.workspaceStyle === "rings" ? 1.06 - : root.workspaceStyle === "aurora" - || root.workspaceStyle === "pacman" ? 1.03 : 1.15 - if (root.bar) root.bar.showTooltip( - workspaceSurface, root.workspaceTooltip(cell.modelData)) - } - onExited: { - cell.scale = 1 - if (root.bar) root.bar.hideTooltip(workspaceSurface) - } - onClicked: function(mouse) { - if (root.bar) root.bar.hideTooltip(workspaceSurface) - if (mouse.button === Qt.RightButton) root.toggle() - else root.activateWorkspace(cell.modelData) - } - } - } - } - } - } - - Loader { id: panelLoader } -} diff --git a/widgets/lan.svg b/widgets/lan.svg deleted file mode 100644 index d697199..0000000 --- a/widgets/lan.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -