Add omarchy-shell with a plugin registry
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,159 @@
|
||||
# Omarchy bar
|
||||
|
||||
This is the Quickshell implementation of the Omarchy status bar.
|
||||
|
||||
- `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly.
|
||||
- `bar-defaults.json` is the Omarchy-owned default layout and module settings.
|
||||
- `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy.
|
||||
- `common/` holds shared QML helpers (buttons, sliders, popup cards).
|
||||
- User overrides live in `~/.config/omarchy/bar.json` and are merged over defaults at runtime.
|
||||
- `omarchy-style-bar-position` updates only the user override file.
|
||||
|
||||
## Customizing
|
||||
|
||||
The bar reads `~/.local/share/omarchy/default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json`, then deep-merges `~/.config/omarchy/bar.json` on top of it. Each `layout.{left,center,right}` entry is an object: at minimum `{ "id": "<widget>" }`, plus any inline settings the widget reads.
|
||||
|
||||
Launch the visual editor with `omarchy launch bar-settings` (or run `omarchy-launch-bar-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand.
|
||||
|
||||
Example `bar.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"position": "top",
|
||||
"centerAnchor": "calendar",
|
||||
"layout": {
|
||||
"left": [
|
||||
{ "id": "omarchy" },
|
||||
{ "id": "spacer", "size": 12 },
|
||||
{ "id": "workspacesPro" }
|
||||
],
|
||||
"center": [
|
||||
{ "id": "media" },
|
||||
{ "id": "calendar", "format": "HH:mm" }
|
||||
],
|
||||
"right": [
|
||||
{ "id": "systemStats" },
|
||||
{ "id": "audioPanel" },
|
||||
{ "id": "battery" },
|
||||
{ "id": "controlCenter" },
|
||||
{ "id": "powerMenu" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`centerAnchor` pins one center module to the exact horizontal/vertical center and flanks others around it. Set to an empty string to disable anchoring (the center list is centered as a group).
|
||||
|
||||
## Module catalogue
|
||||
|
||||
### First-party interactive widgets (in `widgets/`)
|
||||
|
||||
| Name | What it does | Interactions |
|
||||
|---|---|---|
|
||||
| `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup |
|
||||
| `audioPanel` | Volume icon + popup with master slider, output-device picker, per-app mixer | left = popup · right = mute · middle = audio TUI · scroll = volume |
|
||||
| `networkPanel` | Wi-Fi/Ethernet icon + popup with Wi-Fi scan, signal, connect | left = popup · right = nmtui |
|
||||
| `bluetoothPanel` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI |
|
||||
| `calendar` | Clock + popup with month-grid calendar | left = popup · right = tz selector |
|
||||
| `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND |
|
||||
| `brightness` | Brightness slider + scroll | scroll = adjust · left = popup · middle = reset to 80% |
|
||||
| `powerProfile` | Current power profile + popup picker | left = popup |
|
||||
| `systemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal |
|
||||
| `weatherFlyout` | Weather icon + popup with forecast | left = popup · right = full notification |
|
||||
| `workspacesPro` | Animated focus indicator that slides between workspaces | left = focus · right = move window · scroll = cycle |
|
||||
| `powerMenu` | Power icon → popup with lock/suspend/log out/reboot/shutdown | left = popup |
|
||||
| `idleInhibitor` | Coffee-cup that toggles `omarchy-toggle-idle` | left = toggle |
|
||||
| `microphone` | Mic icon + scroll volume | left = mute toggle · middle = audio TUI · scroll = source volume |
|
||||
|
||||
### Built-in legacy modules (in `shell.qml`)
|
||||
|
||||
`omarchy`, `workspaces`, `clock`, `weather`, `update`, `voxtype`, `screenRecording`, `idle`, `notifications`, `tray`, `bluetooth`, `network`, `audio`, `cpu`, `battery`.
|
||||
|
||||
These remain available — set them in `layout` to use them instead of the richer widget versions.
|
||||
|
||||
## Orientation
|
||||
|
||||
All widgets work in `top`, `bottom`, `left`, and `right` positions. Popups anchor on the side opposite the bar edge, sliding into the workspace. Vertical bars use 28px width; widgets that show text fall back to compact icon-only forms (e.g. `media` hides its scrolling label).
|
||||
|
||||
## Custom user modules
|
||||
|
||||
The schema accepts arbitrary module ids that you provide. Set `type` to `command` for shell-driven output or `qml` for a custom QML widget.
|
||||
|
||||
Command module:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout": {
|
||||
"right": [
|
||||
{ "id": "tray" },
|
||||
{ "id": "vpn", "type": "command", "exec": "~/.config/omarchy/bar/scripts/vpn-status", "interval": 5, "tooltip": "VPN", "onClick": "nm-connection-editor" },
|
||||
{ "id": "audioPanel" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The command may print plain text or Waybar-style JSON, for example:
|
||||
|
||||
```json
|
||||
{"text":"","tooltip":"Work VPN","class":"active"}
|
||||
```
|
||||
|
||||
QML module:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout": {
|
||||
"right": [
|
||||
{ "id": "gpu", "type": "qml" },
|
||||
{ "id": "audioPanel" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then create `~/.config/omarchy/bar/modules/gpu.qml`. If you want to store it elsewhere, add a `source` path.
|
||||
|
||||
Custom QML modules should be an `Item` with `implicitWidth` and `implicitHeight`. They may optionally define these properties, which the bar fills after loading:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
property var bar
|
||||
property string moduleName
|
||||
property var settings
|
||||
|
||||
implicitWidth: 28
|
||||
implicitHeight: bar ? bar.barSize : 26
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "GPU"
|
||||
color: bar ? bar.foreground : "white"
|
||||
font.family: bar ? bar.fontFamily : "monospace"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: if (bar) bar.run("omarchy-launch-or-focus-tui btop")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Bar properties available to widgets
|
||||
|
||||
Widgets receive `bar` (the shell root), `moduleName` (string), and `settings` (object) injected at load time. The bar exposes:
|
||||
|
||||
- `bar.foreground`, `bar.background`, `bar.urgent` — theme colors (live-updated)
|
||||
- `bar.fontFamily` — current monospace family
|
||||
- `bar.position` — `"top" | "bottom" | "left" | "right"`
|
||||
- `bar.vertical` — boolean shortcut
|
||||
- `bar.barSize` — 26 horizontal / 28 vertical
|
||||
- `bar.run(command)` — fire-and-forget bash exec
|
||||
- `bar.shellQuote(value)` — safe shell-quote a string
|
||||
- `bar.showTooltip(target, text)` / `bar.hideTooltip(target)` — shared tooltip popup
|
||||
- `bar.requestPopout(owner)` / `bar.releasePopout(owner)` — one-popup-at-a-time coordinator
|
||||
|
||||
Drop new widgets into `widgets/<name>.qml`, add the name to the `firstPartyWidgets` registry in `shell.qml`, and reference it by name in any layout list.
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"position": "top",
|
||||
"fontFamily": "JetBrainsMono Nerd Font",
|
||||
"centerAnchor": "calendar",
|
||||
"layout": {
|
||||
"left": [
|
||||
{ "id": "omarchy" },
|
||||
{ "id": "workspacesPro" },
|
||||
{ "id": "activeWindow" }
|
||||
],
|
||||
"center": [
|
||||
{ "id": "media" },
|
||||
{ "id": "calendar", "format": "dddd HH:mm", "formatAlt": "dd MMMM 'W'ww yyyy", "verticalFormat": "HH\n—\nmm" },
|
||||
{ "id": "weatherFlyout" },
|
||||
{ "id": "update" },
|
||||
{ "id": "voxtype" },
|
||||
{ "id": "screenRecording" },
|
||||
{ "id": "idle" },
|
||||
{ "id": "notifications" }
|
||||
],
|
||||
"right": [
|
||||
{ "id": "tray" },
|
||||
{ "id": "systemStats" },
|
||||
{ "id": "microphone" },
|
||||
{ "id": "bluetoothPanel" },
|
||||
{ "id": "networkPanel" },
|
||||
{ "id": "audioPanel" },
|
||||
{ "id": "nightLight" },
|
||||
{ "id": "brightness" },
|
||||
{ "id": "powerProfile" },
|
||||
{ "id": "battery" },
|
||||
{ "id": "controlCenter" },
|
||||
{ "id": "powerMenu" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property string iconText: ""
|
||||
property color foreground: "#cacccc"
|
||||
property color background: "transparent"
|
||||
property color hoverBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.12)
|
||||
property color pressedBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.22)
|
||||
property string fontFamily: "JetBrainsMono Nerd Font"
|
||||
property real fontSize: 12
|
||||
property real iconSize: 14
|
||||
property real horizontalPadding: 10
|
||||
property real verticalPadding: 6
|
||||
property bool active: false
|
||||
property color activeBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
|
||||
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
|
||||
implicitWidth: row.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: row.implicitHeight + verticalPadding * 2
|
||||
radius: 4
|
||||
color: mouseArea.pressed ? pressedBackground : (mouseArea.containsMouse ? hoverBackground : (active ? activeBackground : background))
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 120 }
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
visible: root.iconText !== ""
|
||||
text: root.iconText
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: root.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.text !== ""
|
||||
text: root.text
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: root.fontSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.RightButton) root.rightClicked()
|
||||
else root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
|
||||
required property Item anchorItem
|
||||
required property QtObject bar
|
||||
property var owner: null
|
||||
property int margin: 8
|
||||
property int padding: 14
|
||||
property int contentWidth: 280
|
||||
property int contentHeight: 200
|
||||
property bool open: false
|
||||
|
||||
readonly property var coordinatorKey: owner || root
|
||||
|
||||
function closePopout() {
|
||||
if (owner && "closePopout" in owner) owner.closePopout()
|
||||
else root.open = false
|
||||
}
|
||||
|
||||
default property alias contentItem: contentHolder.children
|
||||
|
||||
visible: open
|
||||
color: "transparent"
|
||||
implicitWidth: contentWidth
|
||||
implicitHeight: contentHeight
|
||||
|
||||
onOpenChanged: {
|
||||
if (!bar) return
|
||||
if (open) bar.requestPopout(coordinatorKey)
|
||||
else if (bar.activePopout === coordinatorKey) bar.releasePopout(coordinatorKey)
|
||||
}
|
||||
|
||||
anchor {
|
||||
id: popupAnchor
|
||||
window: anchorItem ? anchorItem.QsWindow.window : null
|
||||
adjustment: PopupAdjustment.Slide
|
||||
edges: Edges.Top | Edges.Left
|
||||
gravity: Edges.Bottom | Edges.Right
|
||||
rect.width: 1
|
||||
rect.height: 1
|
||||
|
||||
onAnchoring: {
|
||||
if (!root.anchorItem || !root.bar) return
|
||||
|
||||
var target = root.anchorItem
|
||||
var popupWidth = root.implicitWidth
|
||||
var popupHeight = root.implicitHeight
|
||||
var localX = target.width / 2 - popupWidth / 2
|
||||
var localY = target.height + root.margin
|
||||
|
||||
if (root.bar.position === "bottom") {
|
||||
localY = -popupHeight - root.margin
|
||||
} else if (root.bar.position === "left") {
|
||||
localX = target.width + root.margin
|
||||
localY = target.height / 2 - popupHeight / 2
|
||||
} else if (root.bar.position === "right") {
|
||||
localX = -popupWidth - root.margin
|
||||
localY = target.height / 2 - popupHeight / 2
|
||||
}
|
||||
|
||||
var window = target.QsWindow.window
|
||||
if (!window) return
|
||||
|
||||
var point = window.contentItem.mapFromItem(target, localX, localY)
|
||||
popupAnchor.rect.x = Math.round(point.x)
|
||||
popupAnchor.rect.y = Math.round(point.y)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
anchors.fill: parent
|
||||
color: root.bar ? root.bar.background : "#101315"
|
||||
border.color: root.bar ? root.bar.foreground : "#cacccc"
|
||||
border.width: 1
|
||||
radius: 0
|
||||
opacity: root.open ? 1.0 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentHolder
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.padding
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property real value: 0
|
||||
property real minimum: 0
|
||||
property real maximum: 1
|
||||
property real step: 0.05
|
||||
property bool integer: false
|
||||
property color trackColor: bar ? Qt.rgba(bar.foreground.r, bar.foreground.g, bar.foreground.b, 0.18) : "#333"
|
||||
property color fillColor: bar ? bar.foreground : "#cacccc"
|
||||
property color knobColor: bar ? bar.foreground : "#cacccc"
|
||||
property bool dragging: false
|
||||
property real trackHeight: 4
|
||||
property real liveValue: value
|
||||
|
||||
onValueChanged: if (!dragging) liveValue = value
|
||||
|
||||
signal moved(real value)
|
||||
signal released(real value)
|
||||
|
||||
implicitWidth: 200
|
||||
implicitHeight: 22
|
||||
|
||||
readonly property real range: Math.max(0.0001, maximum - minimum)
|
||||
readonly property real progress: Math.max(0, Math.min(1, (liveValue - minimum) / range))
|
||||
|
||||
Rectangle {
|
||||
id: track
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: root.trackHeight
|
||||
radius: height / 2
|
||||
color: root.trackColor
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: fill
|
||||
anchors.verticalCenter: track.verticalCenter
|
||||
anchors.left: track.left
|
||||
height: track.height
|
||||
radius: track.radius
|
||||
color: root.fillColor
|
||||
width: track.width * root.progress
|
||||
|
||||
Behavior on width {
|
||||
enabled: !root.dragging
|
||||
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: knob
|
||||
width: 14
|
||||
height: 14
|
||||
radius: 7
|
||||
color: root.knobColor
|
||||
border.color: root.bar ? root.bar.background : "#101315"
|
||||
border.width: 2
|
||||
anchors.verticalCenter: track.verticalCenter
|
||||
x: Math.max(0, Math.min(track.width - width, track.width * root.progress - width / 2))
|
||||
scale: mouseArea.containsMouse || root.dragging ? 1.15 : 1.0
|
||||
|
||||
Behavior on x {
|
||||
enabled: !root.dragging
|
||||
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 110; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
|
||||
function valueFromX(x) {
|
||||
var clamped = Math.max(0, Math.min(track.width, x))
|
||||
var raw = root.minimum + (clamped / track.width) * root.range
|
||||
if (root.integer) raw = Math.round(raw)
|
||||
return Math.max(root.minimum, Math.min(root.maximum, raw))
|
||||
}
|
||||
|
||||
onPressed: function(mouse) {
|
||||
root.dragging = true
|
||||
var next = valueFromX(mouse.x)
|
||||
root.liveValue = next
|
||||
root.moved(next)
|
||||
}
|
||||
onPositionChanged: function(mouse) {
|
||||
if (!root.dragging) return
|
||||
var next = valueFromX(mouse.x)
|
||||
root.liveValue = next
|
||||
root.moved(next)
|
||||
}
|
||||
onReleased: function(mouse) {
|
||||
root.dragging = false
|
||||
root.released(root.liveValue)
|
||||
root.liveValue = root.value
|
||||
}
|
||||
onWheel: function(wheel) {
|
||||
var delta = wheel.angleDelta.y > 0 ? root.step : -root.step
|
||||
var next = Math.max(root.minimum, Math.min(root.maximum, root.liveValue + delta))
|
||||
if (root.integer) next = Math.round(next)
|
||||
root.liveValue = next
|
||||
root.moved(next)
|
||||
root.released(next)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var bar: null
|
||||
property string text: ""
|
||||
property string fontFamily: bar ? bar.fontFamily : "JetBrainsMono Nerd Font"
|
||||
property real fontSize: 12
|
||||
property color foreground: bar ? bar.foreground : "#cacccc"
|
||||
property color activeColor: bar ? bar.urgent : "#a55555"
|
||||
property bool active: false
|
||||
property real horizontalMargin: 7.5
|
||||
property real verticalPadding: 6
|
||||
property real fixedWidth: -1
|
||||
property real fixedHeight: -1
|
||||
property real textRotation: 0
|
||||
property bool keepSpace: false
|
||||
property string tooltipText: ""
|
||||
property real hoverScale: 1.0
|
||||
property real pressScale: 0.92
|
||||
|
||||
signal pressed(int button)
|
||||
signal wheelMoved(int delta)
|
||||
|
||||
readonly property bool vertical: bar ? bar.vertical : false
|
||||
readonly property int barSize: bar ? bar.barSize : 26
|
||||
|
||||
visible: text !== "" || keepSpace
|
||||
opacity: text === "" ? 0 : 1
|
||||
implicitWidth: fixedWidth > 0 ? fixedWidth : (vertical ? barSize : Math.max(12, label.implicitWidth + horizontalMargin * 2))
|
||||
implicitHeight: fixedHeight > 0 ? fixedHeight : (vertical ? Math.max(12, label.implicitHeight + verticalPadding * 2) : barSize)
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
color: root.active ? root.activeColor : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: root.fontSize
|
||||
rotation: root.textRotation
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
scale: mouseArea.pressed ? root.pressScale : (mouseArea.containsMouse ? root.hoverScale : 1.0)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 160 }
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 110; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: true
|
||||
onEntered: if (root.bar) root.bar.showTooltip(root, root.tooltipText)
|
||||
onExited: if (root.bar) root.bar.hideTooltip(root)
|
||||
onClicked: function(mouse) { root.pressed(mouse.button) }
|
||||
onWheel: function(wheel) { root.wheelMoved(wheel.angleDelta.y) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "omarchy.bar",
|
||||
"name": "Bar",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Status bar with widgets",
|
||||
"kinds": ["bar"],
|
||||
"activation": "persistent",
|
||||
"entryPoints": { "bar": "Bar.qml" }
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "activeWindow"
|
||||
property var settings: ({})
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
readonly property var toplevel: ToplevelManager.activeToplevel
|
||||
readonly property string title: toplevel ? (toplevel.title || toplevel.appId || "") : ""
|
||||
readonly property int maxLabelWidth: Number(setting("maxWidth", 280))
|
||||
|
||||
readonly property bool vertical: bar ? bar.vertical : false
|
||||
|
||||
visible: title !== "" && !vertical
|
||||
implicitWidth: visible ? Math.min(maxLabelWidth, labelText.implicitWidth) + 16 : 0
|
||||
implicitHeight: bar ? bar.barSize : 26
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
width: parent.width
|
||||
text: root.title
|
||||
color: root.bar ? root.bar.foreground : "#cacccc"
|
||||
font.family: root.bar ? root.bar.fontFamily : "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
opacity: 0.85
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (!root.toplevel) return
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
root.toplevel.close()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.toplevel.close()
|
||||
} else {
|
||||
root.toplevel.activate()
|
||||
}
|
||||
}
|
||||
onEntered: if (root.bar) root.bar.showTooltip(root, root.title)
|
||||
onExited: if (root.bar) root.bar.hideTooltip(root)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "audioPanel"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property var source: Pipewire.defaultAudioSource
|
||||
readonly property var nodes: Pipewire.nodes ? Pipewire.nodes.values : []
|
||||
|
||||
readonly property var candidateSinks: {
|
||||
var list = []
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i]
|
||||
if (node && node.isSink && !node.isStream) list.push(node)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
readonly property var candidateStreams: {
|
||||
var list = []
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i]
|
||||
if (node && node.isStream && !node.isSink) list.push(node)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
readonly property var audioSinks: {
|
||||
var list = []
|
||||
for (var i = 0; i < candidateSinks.length; i++)
|
||||
if (candidateSinks[i].audio) list.push(candidateSinks[i])
|
||||
return list
|
||||
}
|
||||
|
||||
readonly property var audioStreams: {
|
||||
var list = []
|
||||
for (var i = 0; i < candidateStreams.length; i++)
|
||||
if (candidateStreams[i].audio) list.push(candidateStreams[i])
|
||||
return list
|
||||
}
|
||||
|
||||
readonly property real currentVolume: sink && sink.audio ? sink.audio.volume : 0
|
||||
readonly property bool muted: sink && sink.audio ? sink.audio.muted : false
|
||||
|
||||
readonly property string volumeIcon: {
|
||||
if (!sink || !sink.audio) return ""
|
||||
if (muted) return ""
|
||||
var v = currentVolume
|
||||
if (v >= 0.67) return ""
|
||||
if (v >= 0.34) return ""
|
||||
if (v > 0) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
function setVolume(v) {
|
||||
if (!sink || !sink.audio) return
|
||||
sink.audio.volume = Math.max(0, Math.min(1, v))
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (sink && sink.audio) sink.audio.muted = !sink.audio.muted
|
||||
}
|
||||
|
||||
function setDefaultSink(node) {
|
||||
Pipewire.preferredDefaultAudioSink = node
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
PwObjectTracker { objects: root.candidateSinks }
|
||||
PwObjectTracker { objects: root.candidateStreams }
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.volumeIcon
|
||||
tooltipText: root.sink ? (root.sink.description || root.sink.nickname || "Audio") + " · " + Math.round(root.currentVolume * 100) + "%" : "No audio"
|
||||
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.RightButton) root.toggleMute()
|
||||
else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-audio")
|
||||
else root.popupOpen = !root.popupOpen
|
||||
}
|
||||
|
||||
onWheelMoved: function(delta) {
|
||||
var step = 0.05
|
||||
root.setVolume(root.currentVolume + (delta > 0 ? step : -step))
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 340
|
||||
contentHeight: panelColumn.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: panelColumn
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
|
||||
// Master volume
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: root.volumeIcon
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggleMute()
|
||||
}
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
bar: root.bar
|
||||
width: parent.width - 50
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
step: 0.05
|
||||
value: root.currentVolume
|
||||
opacity: root.muted ? 0.5 : 1.0
|
||||
|
||||
onMoved: function(v) { root.setVolume(v) }
|
||||
}
|
||||
}
|
||||
|
||||
// Output device picker
|
||||
Column {
|
||||
spacing: 4
|
||||
width: parent.width
|
||||
visible: root.audioSinks.length > 0
|
||||
|
||||
Text {
|
||||
text: "Output"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.audioSinks
|
||||
|
||||
Common.PillButton {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
text: modelData ? (modelData.description || modelData.nickname || modelData.name || "Unknown") : ""
|
||||
iconText: root.sinkGlyph(modelData)
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
active: root.sink && modelData && root.sink.id === modelData.id
|
||||
onClicked: { root.setDefaultSink(modelData); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-app streams
|
||||
Column {
|
||||
spacing: 4
|
||||
width: parent.width
|
||||
visible: root.audioStreams.length > 0
|
||||
|
||||
Text {
|
||||
text: "Playing"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.audioStreams
|
||||
|
||||
Row {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: modelData && modelData.properties ? (modelData.properties["application.name"] || modelData.properties["node.name"] || "Stream") : "Stream"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
width: 110
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
bar: root.bar
|
||||
width: parent.width - 124
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minimum: 0
|
||||
maximum: 1.5
|
||||
step: 0.05
|
||||
value: modelData && modelData.audio ? modelData.audio.volume : 0
|
||||
|
||||
onMoved: function(v) {
|
||||
if (modelData && modelData.audio) modelData.audio.volume = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sinkGlyph(node) {
|
||||
if (!node) return ""
|
||||
var blob = String([
|
||||
node.name, node.description, node.nickname,
|
||||
node.properties ? node.properties["device.icon-name"] : "",
|
||||
node.properties ? node.properties["device.product.name"] : ""
|
||||
].join(" ")).toLowerCase()
|
||||
if (blob.indexOf("headphone") !== -1 || blob.indexOf("headset") !== -1) return ""
|
||||
if (blob.indexOf("bluetooth") !== -1) return ""
|
||||
if (blob.indexOf("hdmi") !== -1 || blob.indexOf("display") !== -1) return ""
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "bluetoothPanel"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
readonly property var adapter: Bluetooth.defaultAdapter
|
||||
readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : []
|
||||
|
||||
readonly property var connectedDevices: {
|
||||
var list = []
|
||||
for (var i = 0; i < devices.length; i++)
|
||||
if (devices[i] && devices[i].connected) list.push(devices[i])
|
||||
return list
|
||||
}
|
||||
|
||||
readonly property var knownDevices: {
|
||||
var list = []
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
var d = devices[i]
|
||||
if (d && (d.paired || d.connected || d.bonded || d.trusted)) list.push(d)
|
||||
}
|
||||
list.sort(function(a, b) {
|
||||
if (a.connected !== b.connected) return a.connected ? -1 : 1
|
||||
return (a.name || a.deviceName || "").localeCompare(b.name || b.deviceName || "")
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
readonly property string icon: {
|
||||
if (!adapter) return ""
|
||||
if (!adapter.enabled) return ""
|
||||
if (connectedDevices.length > 0) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
visible: adapter !== null
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.icon
|
||||
horizontalMargin: 8.5
|
||||
tooltipText: root.adapter ? (root.adapter.enabled ? "Bluetooth: " + root.connectedDevices.length + " connected" : "Bluetooth off") : ""
|
||||
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.RightButton && root.adapter) root.adapter.enabled = !root.adapter.enabled
|
||||
else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-bluetooth")
|
||||
else root.popupOpen = !root.popupOpen
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 320
|
||||
contentHeight: column.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Bluetooth"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: parent.width - 200; height: 1 }
|
||||
|
||||
Common.PillButton {
|
||||
iconText: root.adapter && root.adapter.enabled ? "" : ""
|
||||
text: root.adapter && root.adapter.enabled ? "On" : "Off"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
active: root.adapter && root.adapter.enabled
|
||||
onClicked: if (root.adapter) root.adapter.enabled = !root.adapter.enabled
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
enabled: root.adapter !== null && root.adapter.enabled
|
||||
opacity: enabled ? 1 : 0.4
|
||||
active: root.adapter && root.adapter.discovering
|
||||
onClicked: if (root.adapter) root.adapter.discovering = !root.adapter.discovering
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.knownDevices
|
||||
|
||||
Common.PillButton {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
iconText: modelData && modelData.connected ? "" : ""
|
||||
text: {
|
||||
var label = modelData ? (modelData.deviceName || modelData.name || modelData.address || "Device") : ""
|
||||
if (modelData && modelData.batteryAvailable) label += " " + Math.round(modelData.battery * 100) + "%"
|
||||
return label
|
||||
}
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
active: modelData && modelData.connected
|
||||
|
||||
onClicked: {
|
||||
if (!modelData) return
|
||||
if (modelData.connected) modelData.disconnect()
|
||||
else modelData.connect()
|
||||
}
|
||||
onRightClicked: if (modelData) modelData.forget()
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.knownDevices.length === 0
|
||||
text: root.adapter && root.adapter.enabled ? "Scanning for devices…" : "Turn Bluetooth on to scan"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "brightness"
|
||||
property var settings: ({})
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
property int currentPercent: -1
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
readonly property string iconGlyph: {
|
||||
if (currentPercent < 0) return ""
|
||||
if (currentPercent > 66) return ""
|
||||
if (currentPercent > 33) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
visible: currentPercent >= 0
|
||||
|
||||
function refresh() {
|
||||
if (!readProc.running) readProc.running = true
|
||||
}
|
||||
|
||||
property int pendingPercent: -1
|
||||
|
||||
function setBrightness(percent) {
|
||||
var clamped = Math.max(1, Math.min(100, Math.round(percent)))
|
||||
currentPercent = clamped
|
||||
pendingPercent = clamped
|
||||
writeTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: writeTimer
|
||||
interval: 60
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (writeProc.running) {
|
||||
writeTimer.restart()
|
||||
return
|
||||
}
|
||||
if (pendingPercent < 0) return
|
||||
writeProc.command = ["bash", "-lc", "brightnessctl set " + pendingPercent + "% >/dev/null"]
|
||||
pendingPercent = -1
|
||||
writeProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Process {
|
||||
id: readProc
|
||||
command: ["bash", "-lc", "if command -v brightnessctl >/dev/null; then echo $(( 100 * $(brightnessctl get) / $(brightnessctl max) )); fi"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var n = parseInt(String(text || "").trim(), 10)
|
||||
if (!isNaN(n)) root.currentPercent = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: writeProc }
|
||||
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.iconGlyph
|
||||
horizontalMargin: 6.5
|
||||
tooltipText: root.currentPercent >= 0 ? "Brightness " + root.currentPercent + "%" : ""
|
||||
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.MiddleButton) {
|
||||
root.popupOpen = false
|
||||
root.setBrightness(80)
|
||||
} else {
|
||||
root.popupOpen = !root.popupOpen
|
||||
}
|
||||
}
|
||||
|
||||
onWheelMoved: function(delta) {
|
||||
var step = Number(root.setting("step", 5))
|
||||
root.setBrightness(root.currentPercent + (delta > 0 ? step : -step))
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 280
|
||||
contentHeight: 80
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Row {
|
||||
spacing: 10
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
text: root.iconGlyph
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Brightness"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: 10; height: 1 }
|
||||
|
||||
Text {
|
||||
text: root.currentPercent + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
bar: root.bar
|
||||
width: parent.width
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
step: 5
|
||||
integer: true
|
||||
value: root.currentPercent
|
||||
|
||||
onMoved: function(v) { root.setBrightness(v) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "calendar"
|
||||
property var settings: ({})
|
||||
|
||||
property date now: new Date()
|
||||
property date viewMonth: new Date()
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
function formatLabel() {
|
||||
if (!bar) return ""
|
||||
var fmt = bar.vertical
|
||||
? String(setting("verticalFormat", "HH\n—\nmm"))
|
||||
: String(setting("format", "dddd HH:mm"))
|
||||
return Qt.formatDateTime(now, fmt)
|
||||
}
|
||||
|
||||
function shiftMonth(delta) {
|
||||
var date = new Date(viewMonth)
|
||||
date.setDate(1)
|
||||
date.setMonth(date.getMonth() + delta)
|
||||
viewMonth = date
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
SystemClock {
|
||||
id: clockTimer
|
||||
precision: SystemClock.Minutes
|
||||
onDateChanged: root.now = clockTimer.date
|
||||
}
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.formatLabel()
|
||||
horizontalMargin: 8.75
|
||||
verticalPadding: 8.75
|
||||
tooltipText: Qt.formatDateTime(root.now, "dddd, MMMM d, yyyy")
|
||||
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.RightButton) {
|
||||
root.bar.run("omarchy-launch-floating-terminal-with-presentation omarchy-tz-select")
|
||||
} else {
|
||||
root.viewMonth = new Date(root.now)
|
||||
root.popupOpen = !root.popupOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
id: popup
|
||||
anchorItem: button
|
||||
bar: root.bar
|
||||
owner: root
|
||||
open: root.popupOpen
|
||||
contentWidth: 300
|
||||
contentHeight: header.implicitHeight + grid.implicitHeight + 36
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
Item {
|
||||
id: header
|
||||
width: parent.width
|
||||
implicitHeight: 28
|
||||
|
||||
Common.PillButton {
|
||||
id: prevButton
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
onClicked: root.shiftMonth(-1)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: Qt.formatDate(root.viewMonth, "MMMM yyyy")
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
id: nextButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
onClicked: root.shiftMonth(1)
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
id: grid
|
||||
columns: 7
|
||||
rowSpacing: 4
|
||||
columnSpacing: 4
|
||||
width: parent.width
|
||||
|
||||
Repeater {
|
||||
model: ["S", "M", "T", "W", "T", "F", "S"]
|
||||
|
||||
Item {
|
||||
required property string modelData
|
||||
width: (grid.width - grid.columnSpacing * 6) / 7
|
||||
height: 18
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: Qt.darker(root.bar.foreground, 1.6)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: 42
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
|
||||
readonly property var startOfMonth: {
|
||||
var d = new Date(root.viewMonth)
|
||||
d.setDate(1)
|
||||
return d
|
||||
}
|
||||
readonly property int firstDayOffset: startOfMonth.getDay()
|
||||
readonly property var dayDate: {
|
||||
var d = new Date(startOfMonth)
|
||||
d.setDate(d.getDate() + index - firstDayOffset)
|
||||
return d
|
||||
}
|
||||
readonly property bool inMonth: dayDate.getMonth() === root.viewMonth.getMonth()
|
||||
readonly property bool isToday: {
|
||||
var n = root.now
|
||||
return dayDate.getDate() === n.getDate() && dayDate.getMonth() === n.getMonth() && dayDate.getFullYear() === n.getFullYear()
|
||||
}
|
||||
|
||||
width: (grid.width - grid.columnSpacing * 6) / 7
|
||||
height: 28
|
||||
radius: 4
|
||||
color: isToday ? root.bar.foreground : "transparent"
|
||||
border.color: isToday ? root.bar.foreground : "transparent"
|
||||
border.width: isToday ? 1 : 0
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: dayDate.getDate()
|
||||
color: isToday ? root.bar.background : (inMonth ? root.bar.foreground : Qt.darker(root.bar.foreground, 2.2))
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: isToday
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Services.UPower
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "controlCenter"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
function run(command) {
|
||||
if (root.bar) root.bar.run(command)
|
||||
}
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property real currentVolume: sink && sink.audio && isFinite(sink.audio.volume) ? sink.audio.volume : 0
|
||||
readonly property bool sinkMuted: sink && sink.audio ? sink.audio.muted : false
|
||||
PwObjectTracker { objects: root.sink ? [root.sink] : [] }
|
||||
|
||||
property int currentBrightness: -1
|
||||
property int pendingBrightness: -1
|
||||
|
||||
property bool dndActive: false
|
||||
property bool idleInhibited: false
|
||||
property bool nightLightActive: false
|
||||
property bool nightLightAvailable: false
|
||||
property string themeName: ""
|
||||
|
||||
readonly property bool powerProfileAvailable: PowerProfiles.hasPerformanceProfile || PowerProfiles.profile === PowerProfile.PowerSaver || PowerProfiles.profile === PowerProfile.Balanced
|
||||
readonly property int currentProfile: PowerProfiles.profile
|
||||
|
||||
function setVolume(value) {
|
||||
if (!sink || !sink.audio) return
|
||||
sink.audio.volume = Math.max(0, Math.min(1, value))
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (!sink || !sink.audio) return
|
||||
sink.audio.muted = !sink.audio.muted
|
||||
}
|
||||
|
||||
function setBrightness(percent) {
|
||||
var clamped = Math.max(1, Math.min(100, Math.round(percent)))
|
||||
currentBrightness = clamped
|
||||
pendingBrightness = clamped
|
||||
brightnessWriteTimer.restart()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!brightnessProc.running) brightnessProc.running = true
|
||||
if (!dndProc.running) dndProc.running = true
|
||||
if (!idleProc.running) idleProc.running = true
|
||||
if (!nightLightProc.running) nightLightProc.running = true
|
||||
if (!themeProc.running) themeProc.running = true
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Process {
|
||||
id: brightnessProc
|
||||
command: ["bash", "-lc", "command -v brightnessctl >/dev/null || { echo missing; exit; }; cur=$(brightnessctl get); max=$(brightnessctl max); [[ $max -gt 0 ]] && echo $((cur*100/max)) || echo 0"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var v = String(text || "").trim()
|
||||
root.currentBrightness = v === "missing" ? -1 : (parseInt(v, 10) || 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: brightnessWriteProc
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: brightnessWriteTimer
|
||||
interval: 60
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (brightnessWriteProc.running) { brightnessWriteTimer.restart(); return }
|
||||
if (root.pendingBrightness < 0) return
|
||||
brightnessWriteProc.command = ["bash", "-lc", "brightnessctl set " + root.pendingBrightness + "% >/dev/null"]
|
||||
root.pendingBrightness = -1
|
||||
brightnessWriteProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: dndProc
|
||||
command: ["bash", "-lc", "if command -v makoctl >/dev/null && makoctl mode 2>/dev/null | grep -q '^do-not-disturb$'; then echo on; else echo off; fi"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.dndActive = String(text || "").trim() === "on"
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: idleProc
|
||||
command: ["bash", "-lc", "if pgrep -x hypridle >/dev/null 2>&1; then echo running; else echo inhibited; fi"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.idleInhibited = String(text || "").trim() === "inhibited"
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: nightLightProc
|
||||
command: ["bash", "-lc", "command -v hyprsunset >/dev/null || { echo missing; exit; }; if pgrep -x hyprsunset >/dev/null 2>&1; then hyprctl hyprsunset temperature 2>/dev/null | grep -oE '[0-9]+' | head -1; else echo idle; fi"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var state = String(text || "").trim()
|
||||
if (state === "missing") {
|
||||
root.nightLightAvailable = false
|
||||
root.nightLightActive = false
|
||||
return
|
||||
}
|
||||
root.nightLightAvailable = true
|
||||
var temp = parseInt(state, 10)
|
||||
root.nightLightActive = !isNaN(temp) && temp < 6000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: themeProc
|
||||
command: ["bash", "-lc", "readlink ~/.config/omarchy/current/theme 2>/dev/null | xargs -r basename"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.themeName = String(text || "").trim()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 3000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: ""
|
||||
fontSize: 14
|
||||
tooltipText: "Quick Settings"
|
||||
onPressed: function() { root.popupOpen = !root.popupOpen }
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
id: popup
|
||||
anchorItem: button
|
||||
bar: root.bar
|
||||
owner: root
|
||||
open: root.popupOpen
|
||||
contentWidth: 320
|
||||
contentHeight: layout.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: layout
|
||||
anchors.fill: parent
|
||||
spacing: 14
|
||||
|
||||
Text {
|
||||
text: "Quick settings"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
visible: root.sink !== null
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Common.PillButton {
|
||||
iconText: root.sinkMuted ? "" : ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 6
|
||||
iconSize: 16
|
||||
onClicked: root.toggleMute()
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
bar: root.bar
|
||||
width: parent.width - 90
|
||||
value: root.currentVolume
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
step: 0.05
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onMoved: function(v) { root.setVolume(v) }
|
||||
onReleased: function(v) { root.setVolume(v) }
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Math.round(root.currentVolume * 100) + "%"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 32
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
visible: root.currentBrightness >= 0
|
||||
|
||||
Common.PillButton {
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 6
|
||||
iconSize: 16
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
bar: root.bar
|
||||
width: parent.width - 90
|
||||
value: Math.max(0, root.currentBrightness / 100)
|
||||
minimum: 0.01
|
||||
maximum: 1
|
||||
step: 0.05
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onMoved: function(v) { root.setBrightness(v * 100) }
|
||||
onReleased: function(v) { root.setBrightness(v * 100) }
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.currentBrightness + "%"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 32
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
}
|
||||
|
||||
Grid {
|
||||
id: tileGrid
|
||||
width: parent.width
|
||||
columns: 2
|
||||
columnSpacing: 8
|
||||
rowSpacing: 8
|
||||
|
||||
Tile {
|
||||
width: (tileGrid.width - tileGrid.columnSpacing) / 2
|
||||
glyph: root.dndActive ? "" : ""
|
||||
title: "Do Not Disturb"
|
||||
subtitle: root.dndActive ? "On" : "Off"
|
||||
active: root.dndActive
|
||||
onClicked: { root.run("omarchy-toggle-notification-silencing"); dndProc.running = true }
|
||||
}
|
||||
|
||||
Tile {
|
||||
width: (tileGrid.width - tileGrid.columnSpacing) / 2
|
||||
glyph: root.nightLightActive ? "" : ""
|
||||
title: "Night Light"
|
||||
subtitle: !root.nightLightAvailable ? "—" : (root.nightLightActive ? "On" : "Off")
|
||||
active: root.nightLightActive
|
||||
tileEnabled: root.nightLightAvailable
|
||||
onClicked: { root.run("omarchy-toggle-nightlight"); nightLightProc.running = true }
|
||||
}
|
||||
|
||||
Tile {
|
||||
width: (tileGrid.width - tileGrid.columnSpacing) / 2
|
||||
glyph: root.idleInhibited ? "" : ""
|
||||
title: "Keep Awake"
|
||||
subtitle: root.idleInhibited ? "On" : "Off"
|
||||
active: root.idleInhibited
|
||||
onClicked: { root.run("omarchy-toggle-idle"); idleProc.running = true }
|
||||
}
|
||||
|
||||
Tile {
|
||||
width: (tileGrid.width - tileGrid.columnSpacing) / 2
|
||||
glyph: ""
|
||||
title: "Theme"
|
||||
subtitle: root.themeName || "—"
|
||||
onClicked: { root.run("omarchy-menu themes"); root.popupOpen = false }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.powerProfileAvailable
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Customize bar…"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: { root.run("omarchy-launch-bar-settings"); root.popupOpen = false }
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
visible: root.powerProfileAvailable
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{ profile: PowerProfile.PowerSaver, label: "Saver", glyph: "" },
|
||||
{ profile: PowerProfile.Balanced, label: "Balanced", glyph: "" },
|
||||
{ profile: PowerProfile.Performance, label: "Performance", glyph: "" }
|
||||
]
|
||||
|
||||
Common.PillButton {
|
||||
required property var modelData
|
||||
width: (parent.width - 12) / 3
|
||||
iconText: modelData.glyph
|
||||
text: modelData.label
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 8
|
||||
active: root.currentProfile === modelData.profile
|
||||
enabled: modelData.profile !== PowerProfile.Performance || PowerProfiles.hasPerformanceProfile
|
||||
opacity: enabled ? 1 : 0.4
|
||||
onClicked: PowerProfiles.profile = modelData.profile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Tile: Rectangle {
|
||||
id: tile
|
||||
|
||||
property string glyph: ""
|
||||
property string title: ""
|
||||
property string subtitle: ""
|
||||
property bool active: false
|
||||
property bool tileEnabled: true
|
||||
|
||||
signal clicked()
|
||||
|
||||
implicitHeight: 56
|
||||
radius: 6
|
||||
color: tileArea.containsMouse
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.16)
|
||||
: (active ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.10)
|
||||
: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.04))
|
||||
border.color: active ? root.bar.foreground : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
border.width: 1
|
||||
opacity: tileEnabled ? 1 : 0.4
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: tile.glyph
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
width: parent.width - parent.children[0].implicitWidth - 8
|
||||
|
||||
Text {
|
||||
text: tile.title
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
text: tile.subtitle
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tileArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: tile.tileEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: tile.tileEnabled
|
||||
onClicked: tile.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "idleInhibitor"
|
||||
property var settings: ({})
|
||||
|
||||
property bool active: false
|
||||
|
||||
readonly property string icon: active ? "" : ""
|
||||
|
||||
function refresh() {
|
||||
if (!statusProc.running) statusProc.running = true
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (root.bar) root.bar.run("omarchy-toggle-idle")
|
||||
refreshTimer.restart()
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Process {
|
||||
id: statusProc
|
||||
command: ["bash", "-lc", "if pgrep -x hyprlock >/dev/null 2>&1; then echo locked; elif pgrep -f 'systemd-inhibit' >/dev/null 2>&1; then echo inhibited; elif [[ -f /tmp/omarchy-idle-off ]]; then echo off; else echo idle; fi"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var state = String(text || "").trim()
|
||||
root.active = state === "inhibited" || state === "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 1500
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.icon
|
||||
active: root.active
|
||||
tooltipText: root.active ? "Idle inhibited — click to allow sleep" : "System can idle — click to keep awake"
|
||||
onPressed: function() { root.toggle() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "keyboardLayout"
|
||||
property var settings: ({})
|
||||
|
||||
property string layoutLabel: ""
|
||||
property string layoutFull: ""
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!queryProc.running) queryProc.running = true
|
||||
}
|
||||
|
||||
function cycleLayout() {
|
||||
Hyprland.dispatch("switchxkblayout current next")
|
||||
refreshTimer.restart()
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Connections {
|
||||
target: Hyprland
|
||||
function onRawEvent(event) {
|
||||
if (!event || !event.name) return
|
||||
if (String(event.name).indexOf("activelayout") !== -1) root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: queryProc
|
||||
command: ["bash", "-lc", "hyprctl -j devices 2>/dev/null | sed -n '/keyboards/,$p' | head -200"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var match = String(text || "").match(/"active_keymap":\s*"([^"]+)"/)
|
||||
if (!match) return
|
||||
var full = match[1]
|
||||
root.layoutFull = full
|
||||
var token = full.split(/\s+/)[0]
|
||||
root.layoutLabel = token.substring(0, 3).toUpperCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 600
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 10000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
visible: layoutLabel !== ""
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.layoutLabel
|
||||
fontSize: 10
|
||||
horizontalMargin: 6
|
||||
tooltipText: root.layoutFull
|
||||
onPressed: function() { root.cycleLayout() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "lockKeys"
|
||||
property var settings: ({})
|
||||
|
||||
property bool capsOn: false
|
||||
property bool numOn: false
|
||||
property bool scrollOn: false
|
||||
property bool hideWhenOff: true
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
hideWhenOff = setting("hideWhenOff", true) === true
|
||||
refresh()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!stateProc.running) stateProc.running = true
|
||||
}
|
||||
|
||||
property bool ledsAvailable: true
|
||||
|
||||
Process {
|
||||
id: stateProc
|
||||
command: ["bash", "-lc", "read_led() { for path in /sys/class/leds/input*::$1; do if [[ -r $path/brightness ]]; then cat $path/brightness; return; fi; done; echo missing; }; read_led capslock; read_led numlock; read_led scrolllock"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var lines = String(text || "").split("\n")
|
||||
var caps = String(lines[0] || "").trim()
|
||||
var num = String(lines[1] || "").trim()
|
||||
var scroll = String(lines[2] || "").trim()
|
||||
root.capsOn = caps !== "missing" && parseInt(caps, 10) > 0
|
||||
root.numOn = num !== "missing" && parseInt(num, 10) > 0
|
||||
root.scrollOn = scroll !== "missing" && parseInt(scroll, 10) > 0
|
||||
root.ledsAvailable = caps !== "missing" || num !== "missing" || scroll !== "missing"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 2000
|
||||
running: root.ledsAvailable
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
readonly property bool anyOn: capsOn || numOn || scrollOn
|
||||
visible: ledsAvailable && (hideWhenOff ? anyOn : true)
|
||||
|
||||
readonly property bool vertical: bar ? bar.vertical : false
|
||||
|
||||
implicitWidth: vertical ? (bar ? bar.barSize : 28) : (lay.item ? lay.item.implicitWidth + 8 : 0)
|
||||
implicitHeight: vertical ? (lay.item ? lay.item.implicitHeight + 8 : 0) : (bar ? bar.barSize : 26)
|
||||
|
||||
Loader {
|
||||
id: lay
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: root.vertical ? colLayout : rowLayout
|
||||
}
|
||||
|
||||
Component {
|
||||
id: rowLayout
|
||||
Row {
|
||||
spacing: 4
|
||||
LockGlyph { glyph: "A"; active: root.capsOn; visible: !root.hideWhenOff || root.capsOn }
|
||||
LockGlyph { glyph: "1"; active: root.numOn; visible: !root.hideWhenOff || root.numOn }
|
||||
LockGlyph { glyph: "S"; active: root.scrollOn; visible: !root.hideWhenOff || root.scrollOn }
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: colLayout
|
||||
Column {
|
||||
spacing: 2
|
||||
LockGlyph { glyph: "A"; active: root.capsOn; visible: !root.hideWhenOff || root.capsOn }
|
||||
LockGlyph { glyph: "1"; active: root.numOn; visible: !root.hideWhenOff || root.numOn }
|
||||
LockGlyph { glyph: "S"; active: root.scrollOn; visible: !root.hideWhenOff || root.scrollOn }
|
||||
}
|
||||
}
|
||||
|
||||
component LockGlyph: Text {
|
||||
property string glyph: ""
|
||||
property bool active: false
|
||||
|
||||
text: glyph
|
||||
color: active ? (root.bar ? root.bar.foreground : "#cacccc") : Qt.rgba(0.7, 0.7, 0.7, 0.3)
|
||||
font.family: root.bar ? root.bar.fontFamily : "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "media"
|
||||
property var settings: ({})
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
readonly property var players: Mpris.players ? Mpris.players.values : []
|
||||
readonly property var activePlayer: {
|
||||
var playing = null
|
||||
for (var i = 0; i < players.length; i++) {
|
||||
var p = players[i]
|
||||
if (!p) continue
|
||||
if (p.isPlaying) return p
|
||||
if (!playing && p.trackTitle) playing = p
|
||||
}
|
||||
return playing
|
||||
}
|
||||
|
||||
readonly property bool hasMedia: activePlayer !== null && (activePlayer.trackTitle || activePlayer.trackArtist)
|
||||
readonly property string playIcon: activePlayer && activePlayer.isPlaying ? "" : ""
|
||||
readonly property string title: activePlayer ? (activePlayer.trackTitle || "") : ""
|
||||
readonly property string artist: activePlayer ? (activePlayer.trackArtist || "") : ""
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
property real maxLabelWidth: 180
|
||||
|
||||
visible: hasMedia
|
||||
implicitWidth: hasMedia ? row.implicitWidth + 14 : 0
|
||||
implicitHeight: bar ? bar.barSize : 26
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
id: glyph
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.playIcon
|
||||
color: activePlayer && activePlayer.isPlaying ? root.bar.foreground : Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 160 } }
|
||||
}
|
||||
|
||||
Item {
|
||||
id: scrollClip
|
||||
width: Math.min(root.maxLabelWidth, labelText.implicitWidth)
|
||||
height: glyph.height
|
||||
clip: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !root.bar.vertical && root.title !== ""
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
text: root.title + (root.artist ? " · " + root.artist : "")
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
property bool needsScroll: implicitWidth > scrollClip.width
|
||||
|
||||
NumberAnimation on x {
|
||||
id: scrollAnim
|
||||
running: labelText.needsScroll && !root.popupOpen && !root.bar.vertical
|
||||
loops: Animation.Infinite
|
||||
duration: Math.max(6000, labelText.implicitWidth * 25)
|
||||
from: scrollClip.width
|
||||
to: -labelText.implicitWidth
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (!root.activePlayer) return
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
if (root.activePlayer.canGoNext) root.activePlayer.next()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.popupOpen = !root.popupOpen
|
||||
} else {
|
||||
if (root.activePlayer.canTogglePlaying) root.activePlayer.togglePlaying()
|
||||
}
|
||||
}
|
||||
onWheel: function(wheel) {
|
||||
if (!root.activePlayer) return
|
||||
if (wheel.angleDelta.y > 0 && root.activePlayer.canGoPrevious) root.activePlayer.previous()
|
||||
else if (wheel.angleDelta.y < 0 && root.activePlayer.canGoNext) root.activePlayer.next()
|
||||
}
|
||||
onEntered: if (root.bar) root.bar.showTooltip(root, root.hasMedia ? (root.title + (root.artist ? " — " + root.artist : "")) : "")
|
||||
onExited: if (root.bar) root.bar.hideTooltip(root)
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
id: popup
|
||||
anchorItem: root
|
||||
bar: root.bar
|
||||
owner: root
|
||||
open: root.popupOpen
|
||||
contentWidth: 320
|
||||
contentHeight: column.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Row {
|
||||
spacing: 10
|
||||
width: parent.width
|
||||
|
||||
Rectangle {
|
||||
width: 64
|
||||
height: 64
|
||||
radius: 4
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.08)
|
||||
border.color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
source: root.activePlayer && root.activePlayer.trackArtUrl ? root.activePlayer.trackArtUrl : ""
|
||||
visible: source !== ""
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !root.activePlayer || !root.activePlayer.trackArtUrl
|
||||
text: ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 28
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 4
|
||||
width: parent.width - 74
|
||||
|
||||
Text {
|
||||
text: root.title || "Nothing playing"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.artist
|
||||
color: Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.activePlayer && root.activePlayer.trackAlbum ? root.activePlayer.trackAlbum : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.6)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 6
|
||||
|
||||
Common.PillButton {
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
enabled: root.activePlayer && root.activePlayer.canGoPrevious
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
onClicked: if (root.activePlayer) root.activePlayer.previous()
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
iconText: root.activePlayer && root.activePlayer.isPlaying ? "" : ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 14
|
||||
verticalPadding: 6
|
||||
iconSize: 18
|
||||
enabled: root.activePlayer && root.activePlayer.canTogglePlaying
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
onClicked: if (root.activePlayer) root.activePlayer.togglePlaying()
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
enabled: root.activePlayer && root.activePlayer.canGoNext
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
onClicked: if (root.activePlayer) root.activePlayer.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "microphone"
|
||||
property var settings: ({})
|
||||
|
||||
readonly property var source: Pipewire.defaultAudioSource
|
||||
readonly property bool muted: source && source.audio ? source.audio.muted : true
|
||||
readonly property real volume: source && source.audio ? source.audio.volume : 0
|
||||
readonly property var nodes: Pipewire.nodes ? Pipewire.nodes.values : []
|
||||
|
||||
readonly property var activeStreams: {
|
||||
var list = []
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i]
|
||||
if (node && node.isStream && node.isSink === false && !node.audio?.muted) list.push(node)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
readonly property bool inUse: activeStreams.length > 0 && !muted
|
||||
|
||||
visible: source !== null
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
function toggleMute() {
|
||||
if (source && source.audio) source.audio.muted = !source.audio.muted
|
||||
}
|
||||
|
||||
PwObjectTracker { objects: root.source ? [root.source] : [] }
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.muted ? "" : ""
|
||||
active: root.inUse
|
||||
tooltipText: root.muted ? "Microphone muted" : (root.inUse ? "Microphone in use" : "Microphone live")
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-audio")
|
||||
else root.toggleMute()
|
||||
}
|
||||
onWheelMoved: function(delta) {
|
||||
if (!root.source || !root.source.audio) return
|
||||
var step = 0.05
|
||||
root.source.audio.volume = Math.max(0, Math.min(1, root.volume + (delta > 0 ? step : -step)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "networkPanel"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
property var networks: []
|
||||
property bool scanning: false
|
||||
|
||||
readonly property string kind: bar ? bar.networkKind : "disconnected"
|
||||
readonly property string label: bar ? bar.networkLabel : ""
|
||||
readonly property int signalStrength: bar ? bar.networkSignal : -1
|
||||
|
||||
readonly property string icon: {
|
||||
if (kind === "wifi") {
|
||||
var icons = ["", "", "", "", ""]
|
||||
var index = Math.max(0, Math.min(4, Math.ceil(signalStrength / 20) - 1))
|
||||
return icons[index]
|
||||
}
|
||||
if (kind === "ethernet") return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!scanProc.running) {
|
||||
scanning = true
|
||||
scanProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
function updateScan(raw) {
|
||||
var list = []
|
||||
var lines = String(raw || "").split("\n")
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim()
|
||||
if (!line) continue
|
||||
var parts = line.split("\t")
|
||||
if (parts.length < 3) continue
|
||||
list.push({
|
||||
inUse: parts[0] === "*",
|
||||
ssid: parts[1],
|
||||
signalStrength: parseInt(parts[2], 10) || 0,
|
||||
security: parts[3] || ""
|
||||
})
|
||||
}
|
||||
list.sort(function(a, b) {
|
||||
if (a.inUse !== b.inUse) return a.inUse ? -1 : 1
|
||||
return b.signalStrength - a.signalStrength
|
||||
})
|
||||
networks = list
|
||||
scanning = false
|
||||
}
|
||||
|
||||
function wifiIconFor(signalStrength) {
|
||||
var icons = ["", "", "", "", ""]
|
||||
var index = Math.max(0, Math.min(4, Math.ceil(signalStrength / 20) - 1))
|
||||
return icons[index]
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Process {
|
||||
id: scanProc
|
||||
command: ["bash", "-lc", "command -v nmcli >/dev/null && nmcli -t -f IN-USE,SSID,SIGNAL,SECURITY device wifi list --rescan no 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.updateScan(text)
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: actionProc }
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.icon
|
||||
horizontalMargin: 6.5
|
||||
tooltipText: bar ? bar.networkTooltip() : ""
|
||||
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.RightButton) root.bar.run("OMARCHY_PATH=" + root.bar.shellQuote(root.bar.omarchyPath) + " " + root.bar.omarchyPath + "/bin/omarchy-launch-wifi")
|
||||
else {
|
||||
root.popupOpen = !root.popupOpen
|
||||
if (root.popupOpen) root.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 320
|
||||
contentHeight: column.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: root.icon
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
width: parent.width - 100
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: root.kind === "disconnected" ? "Disconnected" : (root.label || root.kind)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.kind !== "disconnected"
|
||||
text: root.kind === "wifi" ? "Wi-Fi · " + root.signalStrength + "%" : "Ethernet"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
active: root.scanning
|
||||
onClicked: {
|
||||
if (scanProc.running) return
|
||||
scanProc.command = ["bash", "-lc", "command -v nmcli >/dev/null && nmcli device wifi rescan 2>/dev/null; nmcli -t -f IN-USE,SSID,SIGNAL,SECURITY device wifi list --rescan no 2>/dev/null"]
|
||||
root.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.15)
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
width: parent.width
|
||||
|
||||
Repeater {
|
||||
model: root.networks.slice(0, 6)
|
||||
|
||||
Common.PillButton {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
iconText: root.wifiIconFor(modelData.signalStrength)
|
||||
text: (modelData.ssid || "Hidden") + (modelData.security ? " · " : "") + (modelData.security || "")
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
active: modelData.inUse
|
||||
|
||||
onClicked: {
|
||||
if (modelData.inUse) return
|
||||
if (actionProc.running) return
|
||||
actionProc.command = ["bash", "-lc", "command -v nmcli >/dev/null && nmcli device wifi connect " + root.bar.shellQuote(modelData.ssid)]
|
||||
actionProc.running = true
|
||||
root.popupOpen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.networks.length === 0
|
||||
text: root.scanning ? "Scanning…" : "No networks found"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Open Wi-Fi manager"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
onClicked: { root.bar.run(root.bar.omarchyPath + "/bin/omarchy-launch-wifi"); root.popupOpen = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "nightLight"
|
||||
property var settings: ({})
|
||||
|
||||
property bool active: false
|
||||
property bool toolAvailable: false
|
||||
property bool toggling: false
|
||||
|
||||
readonly property int onTemp: 4000
|
||||
readonly property int offTemp: 6000
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!statusProc.running) statusProc.running = true
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (toggling) return
|
||||
toggling = true
|
||||
if (root.bar) root.bar.run("omarchy-toggle-nightlight")
|
||||
refreshTimer.restart()
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Process {
|
||||
id: statusProc
|
||||
command: ["bash", "-lc", "command -v hyprsunset >/dev/null || { echo missing; exit; }; if pgrep -x hyprsunset >/dev/null 2>&1; then hyprctl hyprsunset temperature 2>/dev/null | grep -oE '[0-9]+' | head -1; else echo idle; fi"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var state = String(text || "").trim()
|
||||
root.toggling = false
|
||||
if (state === "missing") {
|
||||
root.toolAvailable = false
|
||||
root.active = false
|
||||
return
|
||||
}
|
||||
root.toolAvailable = true
|
||||
var temp = parseInt(state, 10)
|
||||
root.active = !isNaN(temp) && temp < root.offTemp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 1500
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 10000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
visible: toolAvailable
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.active ? "" : ""
|
||||
active: root.active
|
||||
tooltipText: root.active ? "Night light on" : "Night light off"
|
||||
onPressed: function() { root.toggle() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "notificationCenter"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
property var stored: []
|
||||
property bool dnd: false
|
||||
|
||||
readonly property int count: stored.length
|
||||
readonly property string icon: {
|
||||
if (dnd) return ""
|
||||
if (count > 0) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
property bool replaceMako: settings && settings.replaceMako === true
|
||||
|
||||
Loader {
|
||||
active: root.replaceMako
|
||||
sourceComponent: serverComponent
|
||||
}
|
||||
|
||||
Component {
|
||||
id: serverComponent
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
keepOnReload: false
|
||||
bodySupported: true
|
||||
actionsSupported: true
|
||||
imageSupported: true
|
||||
|
||||
onNotification: function(notification) {
|
||||
if (root.dnd) {
|
||||
notification.expire()
|
||||
return
|
||||
}
|
||||
notification.tracked = true
|
||||
var snapshot = {
|
||||
id: notification.id,
|
||||
app: notification.appName,
|
||||
summary: notification.summary,
|
||||
body: notification.body,
|
||||
time: new Date(),
|
||||
ref: notification
|
||||
}
|
||||
var next = root.stored.slice()
|
||||
next.unshift(snapshot)
|
||||
if (next.length > 30) next.pop()
|
||||
root.stored = next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss(index) {
|
||||
var item = stored[index]
|
||||
if (item && item.ref && !item.ref.closed) item.ref.dismiss()
|
||||
var next = stored.slice()
|
||||
next.splice(index, 1)
|
||||
stored = next
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
for (var i = 0; i < stored.length; i++) {
|
||||
if (stored[i].ref && !stored[i].ref.closed) stored[i].ref.dismiss()
|
||||
}
|
||||
stored = []
|
||||
}
|
||||
|
||||
function relativeTime(date) {
|
||||
if (!date) return ""
|
||||
var diff = (Date.now() - date.getTime()) / 1000
|
||||
if (diff < 60) return "just now"
|
||||
if (diff < 3600) return Math.floor(diff / 60) + "m"
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + "h"
|
||||
return Math.floor(diff / 86400) + "d"
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.icon
|
||||
active: root.count > 0 && !root.dnd
|
||||
tooltipText: root.dnd ? "Do Not Disturb" : (root.count > 0 ? root.count + " notifications" : "No notifications")
|
||||
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.RightButton) root.dnd = !root.dnd
|
||||
else root.popupOpen = !root.popupOpen
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 340
|
||||
contentHeight: Math.min(420, listColumn.implicitHeight + 60)
|
||||
|
||||
Column {
|
||||
id: listColumn
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Notifications"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: parent.width - 220; height: 1 }
|
||||
|
||||
Common.PillButton {
|
||||
iconText: root.dnd ? "" : ""
|
||||
text: root.dnd ? "DND" : ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
active: root.dnd
|
||||
onClicked: root.dnd = !root.dnd
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
enabled: root.count > 0
|
||||
opacity: enabled ? 1 : 0.4
|
||||
onClicked: root.clearAll()
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: Math.min(320, contentHeight)
|
||||
contentHeight: feedColumn.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: feedColumn
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: root.stored
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: feedColumn.width
|
||||
radius: 4
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.06)
|
||||
implicitHeight: notifContent.implicitHeight + 16
|
||||
|
||||
Column {
|
||||
id: notifContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
text: modelData ? (modelData.app || "App") : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - timeText.implicitWidth - dismissBtn.width - 12
|
||||
}
|
||||
|
||||
Text {
|
||||
id: timeText
|
||||
text: modelData ? root.relativeTime(modelData.time) : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.6)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
}
|
||||
|
||||
Item { width: 6; height: 1 }
|
||||
|
||||
Common.PillButton {
|
||||
id: dismissBtn
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 4
|
||||
verticalPadding: 0
|
||||
iconSize: 10
|
||||
onClicked: root.dismiss(index)
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData ? (modelData.summary || "") : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: modelData && modelData.body !== ""
|
||||
text: modelData ? (modelData.body || "") : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.2)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.stored.length === 0
|
||||
text: root.dnd ? "Do Not Disturb is on" : "Nothing new"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "powerMenu"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
function run(command) {
|
||||
if (root.bar) root.bar.run(command)
|
||||
popupOpen = false
|
||||
}
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: ""
|
||||
fontSize: 14
|
||||
tooltipText: "Power menu"
|
||||
onPressed: function() { root.popupOpen = !root.popupOpen }
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 220
|
||||
contentHeight: column.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: "Power"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Lock"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: root.run("loginctl lock-session")
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Suspend"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: root.run("systemctl suspend")
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Log out"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: root.run("hyprctl dispatch exit")
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Reboot"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: root.run("systemctl reboot")
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Shut down"
|
||||
foreground: root.bar.urgent
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: root.run("systemctl poweroff")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "powerProfile"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
readonly property var profileGlyphs: ({
|
||||
[PowerProfile.PowerSaver]: "",
|
||||
[PowerProfile.Balanced]: "",
|
||||
[PowerProfile.Performance]: ""
|
||||
})
|
||||
|
||||
readonly property var profileLabels: ({
|
||||
[PowerProfile.PowerSaver]: "Power Saver",
|
||||
[PowerProfile.Balanced]: "Balanced",
|
||||
[PowerProfile.Performance]: "Performance"
|
||||
})
|
||||
|
||||
readonly property bool available: PowerProfiles.hasPerformanceProfile || PowerProfiles.profile === PowerProfile.PowerSaver || PowerProfiles.profile === PowerProfile.Balanced
|
||||
readonly property int current: PowerProfiles.profile
|
||||
|
||||
visible: available
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
function setProfile(profile) {
|
||||
PowerProfiles.profile = profile
|
||||
}
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.profileGlyphs[root.current] || ""
|
||||
tooltipText: "Power profile: " + (root.profileLabels[root.current] || "Unknown")
|
||||
onPressed: function() { root.popupOpen = !root.popupOpen }
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 240
|
||||
contentHeight: column.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: "Power Profile"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{ profile: PowerProfile.PowerSaver, label: "Power Saver", glyph: "" },
|
||||
{ profile: PowerProfile.Balanced, label: "Balanced", glyph: "" },
|
||||
{ profile: PowerProfile.Performance, label: "Performance", glyph: "" }
|
||||
]
|
||||
|
||||
Common.PillButton {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
iconText: modelData.glyph
|
||||
text: modelData.label
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
active: root.current === modelData.profile
|
||||
enabled: modelData.profile !== PowerProfile.Performance || PowerProfiles.hasPerformanceProfile
|
||||
opacity: enabled ? 1 : 0.4
|
||||
onClicked: { root.setProfile(modelData.profile); root.popupOpen = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "spacer"
|
||||
property var settings: ({})
|
||||
|
||||
readonly property bool vertical: bar ? bar.vertical : false
|
||||
readonly property int span: settings && settings.size !== undefined ? Number(settings.size) : 12
|
||||
|
||||
implicitWidth: vertical ? (bar ? bar.barSize : 28) : span
|
||||
implicitHeight: vertical ? span : (bar ? bar.barSize : 26)
|
||||
visible: span > 0
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "systemStats"
|
||||
property var settings: ({})
|
||||
|
||||
property real cpuPercent: 0
|
||||
property real memPercent: 0
|
||||
property var cpuHistory: []
|
||||
property var memHistory: []
|
||||
property real loadAvg: 0
|
||||
|
||||
property var prevCpu: ({ idle: 0, total: 0 })
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
readonly property int historyLimit: 30
|
||||
|
||||
function refresh() {
|
||||
if (!cpuProc.running) cpuProc.running = true
|
||||
if (!memProc.running) memProc.running = true
|
||||
}
|
||||
|
||||
function pushHistory(arr, value) {
|
||||
var next = arr.slice()
|
||||
next.push(value)
|
||||
if (next.length > historyLimit) next.shift()
|
||||
return next
|
||||
}
|
||||
|
||||
function updateCpu(raw) {
|
||||
var fields = String(raw || "").trim().split(/\s+/)
|
||||
if (fields.length < 8) return
|
||||
var user = parseInt(fields[1], 10) || 0
|
||||
var nice = parseInt(fields[2], 10) || 0
|
||||
var sys = parseInt(fields[3], 10) || 0
|
||||
var idle = parseInt(fields[4], 10) || 0
|
||||
var iowait = parseInt(fields[5], 10) || 0
|
||||
var irq = parseInt(fields[6], 10) || 0
|
||||
var softirq = parseInt(fields[7], 10) || 0
|
||||
|
||||
var total = user + nice + sys + idle + iowait + irq + softirq
|
||||
var totalDiff = total - prevCpu.total
|
||||
var idleDiff = idle - prevCpu.idle
|
||||
|
||||
if (prevCpu.total > 0 && totalDiff > 0) {
|
||||
var usage = (1 - idleDiff / totalDiff) * 100
|
||||
cpuPercent = Math.max(0, Math.min(100, usage))
|
||||
cpuHistory = pushHistory(cpuHistory, cpuPercent)
|
||||
}
|
||||
|
||||
prevCpu = { idle: idle, total: total }
|
||||
}
|
||||
|
||||
function updateMem(raw) {
|
||||
var lines = String(raw || "").split("\n")
|
||||
var total = 0
|
||||
var available = 0
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i]
|
||||
if (line.indexOf("MemTotal:") === 0) total = parseInt(line.replace(/[^0-9]/g, ""), 10) || 0
|
||||
else if (line.indexOf("MemAvailable:") === 0) available = parseInt(line.replace(/[^0-9]/g, ""), 10) || 0
|
||||
}
|
||||
if (total > 0) {
|
||||
memPercent = ((total - available) / total) * 100
|
||||
memHistory = pushHistory(memHistory, memPercent)
|
||||
}
|
||||
}
|
||||
|
||||
function updateLoad(raw) {
|
||||
var n = parseFloat(String(raw || "").trim().split(/\s+/)[0])
|
||||
if (!isNaN(n)) loadAvg = n
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Process {
|
||||
id: cpuProc
|
||||
command: ["bash", "-lc", "head -n1 /proc/stat"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.updateCpu(text)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: memProc
|
||||
command: ["bash", "-lc", "head -n3 /proc/meminfo"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.updateMem(text)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: loadProc
|
||||
command: ["bash", "-lc", "cat /proc/loadavg"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.updateLoad(text)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 2000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
root.refresh()
|
||||
if (!loadProc.running) loadProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool vertical: bar ? bar.vertical : false
|
||||
|
||||
implicitWidth: vertical ? (bar ? bar.barSize : 28) : (statLayout.item ? statLayout.item.implicitWidth + 6 : 0)
|
||||
implicitHeight: vertical ? (statLayout.item ? statLayout.item.implicitHeight + 6 : 0) : (bar ? bar.barSize : 26)
|
||||
|
||||
readonly property color statColor: bar ? bar.foreground : "#cacccc"
|
||||
readonly property string statFont: bar ? bar.fontFamily : "JetBrainsMono Nerd Font"
|
||||
|
||||
Loader {
|
||||
id: statLayout
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: root.vertical ? statColumn : statRow
|
||||
}
|
||||
|
||||
Component {
|
||||
id: statRow
|
||||
Row {
|
||||
spacing: 8
|
||||
StatPill {
|
||||
glyph: ""
|
||||
percent: root.cpuPercent
|
||||
history: root.cpuHistory
|
||||
vertical: false
|
||||
barFg: root.statColor
|
||||
fontFamily: root.statFont
|
||||
}
|
||||
StatPill {
|
||||
glyph: ""
|
||||
percent: root.memPercent
|
||||
history: root.memHistory
|
||||
vertical: false
|
||||
barFg: root.statColor
|
||||
fontFamily: root.statFont
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: statColumn
|
||||
Column {
|
||||
spacing: 4
|
||||
StatPill {
|
||||
glyph: ""
|
||||
percent: root.cpuPercent
|
||||
history: root.cpuHistory
|
||||
vertical: true
|
||||
barFg: root.statColor
|
||||
fontFamily: root.statFont
|
||||
}
|
||||
StatPill {
|
||||
glyph: ""
|
||||
percent: root.memPercent
|
||||
history: root.memHistory
|
||||
vertical: true
|
||||
barFg: root.statColor
|
||||
fontFamily: root.statFont
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.RightButton) root.bar.run("alacritty")
|
||||
else root.popupOpen = !root.popupOpen
|
||||
}
|
||||
onEntered: if (root.bar) root.bar.showTooltip(root, "CPU " + Math.round(root.cpuPercent) + "% · Mem " + Math.round(root.memPercent) + "% · Load " + root.loadAvg.toFixed(2))
|
||||
onExited: if (root.bar) root.bar.hideTooltip(root)
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: root
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 320
|
||||
contentHeight: detailColumn.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: detailColumn
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: "System"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
DetailStat {
|
||||
title: "CPU"
|
||||
value: Math.round(root.cpuPercent) + "%"
|
||||
history: root.cpuHistory
|
||||
barFg: root.statColor
|
||||
fontFamily: root.bar.fontFamily
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
DetailStat {
|
||||
title: "Memory"
|
||||
value: Math.round(root.memPercent) + "%"
|
||||
history: root.memHistory
|
||||
barFg: root.statColor
|
||||
fontFamily: root.bar.fontFamily
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
Text {
|
||||
text: "Load"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
Text {
|
||||
text: root.loadAvg.toFixed(2)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Open btop"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: { root.bar.run("omarchy-launch-or-focus-tui btop"); root.popupOpen = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component StatPill: Item {
|
||||
id: pill
|
||||
property string glyph: ""
|
||||
property real percent: 0
|
||||
property var history: []
|
||||
property bool vertical: false
|
||||
property color barFg: "#cacccc"
|
||||
property string fontFamily: "JetBrainsMono Nerd Font"
|
||||
|
||||
implicitWidth: vertical ? 22 : 56
|
||||
implicitHeight: vertical ? 32 : 22
|
||||
|
||||
Row {
|
||||
visible: !pill.vertical
|
||||
anchors.fill: parent
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: pill.glyph
|
||||
color: pill.barFg
|
||||
font.family: pill.fontFamily
|
||||
font.pixelSize: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: spark
|
||||
width: 36
|
||||
height: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
property var history: pill.history
|
||||
onHistoryChanged: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
if (!pill.history || pill.history.length === 0) return
|
||||
|
||||
ctx.strokeStyle = pill.barFg
|
||||
ctx.fillStyle = Qt.rgba(pill.barFg.r, pill.barFg.g, pill.barFg.b, 0.2)
|
||||
ctx.lineWidth = 1
|
||||
|
||||
ctx.beginPath()
|
||||
var step = width / Math.max(1, pill.history.length - 1)
|
||||
for (var i = 0; i < pill.history.length; i++) {
|
||||
var x = i * step
|
||||
var y = height - (pill.history[i] / 100) * height
|
||||
if (i === 0) ctx.moveTo(x, y)
|
||||
else ctx.lineTo(x, y)
|
||||
}
|
||||
ctx.stroke()
|
||||
ctx.lineTo(width, height)
|
||||
ctx.lineTo(0, height)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
visible: pill.vertical
|
||||
anchors.fill: parent
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: pill.glyph
|
||||
color: pill.barFg
|
||||
font.family: pill.fontFamily
|
||||
font.pixelSize: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Math.round(pill.percent) + ""
|
||||
color: pill.barFg
|
||||
font.family: pill.fontFamily
|
||||
font.pixelSize: 9
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component DetailStat: Column {
|
||||
id: detail
|
||||
|
||||
property string title: ""
|
||||
property string value: ""
|
||||
property var history: []
|
||||
property color barFg: "#cacccc"
|
||||
property string fontFamily: "JetBrainsMono Nerd Font"
|
||||
|
||||
spacing: 4
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
Text {
|
||||
text: detail.title
|
||||
color: Qt.darker(detail.barFg, 1.4)
|
||||
font.family: detail.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
Item { width: detail.width - parent.children[0].implicitWidth - parent.children[2].implicitWidth; height: 1 }
|
||||
Text {
|
||||
text: detail.value
|
||||
color: detail.barFg
|
||||
font.family: detail.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: detailCanvas
|
||||
width: parent.width
|
||||
height: 40
|
||||
property var history: detail.history
|
||||
onHistoryChanged: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
if (!detail.history || detail.history.length === 0) return
|
||||
|
||||
ctx.strokeStyle = detail.barFg
|
||||
ctx.fillStyle = Qt.rgba(detail.barFg.r, detail.barFg.g, detail.barFg.b, 0.25)
|
||||
ctx.lineWidth = 1.5
|
||||
|
||||
ctx.beginPath()
|
||||
var step = width / Math.max(1, detail.history.length - 1)
|
||||
for (var i = 0; i < detail.history.length; i++) {
|
||||
var x = i * step
|
||||
var y = height - (detail.history[i] / 100) * (height - 2) - 1
|
||||
if (i === 0) ctx.moveTo(x, y)
|
||||
else ctx.lineTo(x, y)
|
||||
}
|
||||
ctx.stroke()
|
||||
ctx.lineTo(width, height)
|
||||
ctx.lineTo(0, height)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "weatherFlyout"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
property string fullReport: ""
|
||||
|
||||
readonly property string label: bar ? bar.weatherText : ""
|
||||
readonly property string klass: bar ? bar.weatherClass : ""
|
||||
|
||||
visible: label !== ""
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
function refresh() {
|
||||
if (!forecastProc.running) forecastProc.running = true
|
||||
}
|
||||
|
||||
|
||||
Process {
|
||||
id: forecastProc
|
||||
command: ["bash", "-lc", "curl -fsS --max-time 5 'wttr.in/?T0&format=%l:+%C+%t+%f+wind+%w+%h+humidity' 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.fullReport = String(text || "").trim()
|
||||
}
|
||||
}
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.label
|
||||
active: root.klass === "active"
|
||||
horizontalMargin: 7.5
|
||||
tooltipText: root.fullReport || "Weather"
|
||||
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.RightButton) root.bar.run("omarchy-notification-send \"$(omarchy-weather-status)\"")
|
||||
else {
|
||||
root.popupOpen = !root.popupOpen
|
||||
if (root.popupOpen) root.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 320
|
||||
contentHeight: column.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Row {
|
||||
spacing: 12
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
text: root.label || "—"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 28
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: "Weather"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.fullReport || "Fetching forecast…"
|
||||
color: Qt.darker(root.bar.foreground, 1.2)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
wrapMode: Text.WordWrap
|
||||
width: 220
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Refresh"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
onClicked: root.refresh()
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Open wttr.in"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
onClicked: { root.bar.run("xdg-open https://wttr.in"); root.popupOpen = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "workspacesPro"
|
||||
property var settings: ({})
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
readonly property var ids: bar ? bar.workspaceIds() : [1, 2, 3, 4, 5]
|
||||
readonly property bool vertical: bar ? bar.vertical : false
|
||||
readonly property int barSize: bar ? bar.barSize : 26
|
||||
readonly property int slotSize: vertical ? barSize : 26
|
||||
readonly property int spacing: vertical ? 4 : 4
|
||||
|
||||
readonly property int focusedIndex: {
|
||||
if (!Hyprland.focusedWorkspace) return -1
|
||||
var focused = Hyprland.focusedWorkspace.id
|
||||
for (var i = 0; i < ids.length; i++) if (ids[i] === focused) return i
|
||||
return -1
|
||||
}
|
||||
|
||||
implicitWidth: vertical ? barSize : ids.length * slotSize + (ids.length - 1) * spacing + 6
|
||||
implicitHeight: vertical ? ids.length * slotSize + (ids.length - 1) * spacing + 6 : barSize
|
||||
|
||||
Rectangle {
|
||||
id: indicator
|
||||
width: vertical ? root.slotSize - 8 : root.slotSize - 6
|
||||
height: vertical ? root.slotSize - 6 : root.slotSize - 8
|
||||
radius: 4
|
||||
color: root.bar ? root.bar.foreground : "#cacccc"
|
||||
opacity: root.focusedIndex >= 0 ? 0.25 : 0
|
||||
x: vertical ? (root.width - width) / 2 : 3 + root.focusedIndex * (root.slotSize + root.spacing) + (root.slotSize - width) / 2
|
||||
y: vertical ? 3 + root.focusedIndex * (root.slotSize + root.spacing) + (root.slotSize - height) / 2 : (root.height - height) / 2
|
||||
|
||||
Behavior on x { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
|
||||
Behavior on y { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
|
||||
Behavior on opacity { NumberAnimation { duration: 160 } }
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.vertical ? verticalLayout : horizontalLayout
|
||||
}
|
||||
|
||||
Component {
|
||||
id: horizontalLayout
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: root.spacing
|
||||
|
||||
Repeater {
|
||||
model: root.ids
|
||||
WorkspaceButton {
|
||||
required property int modelData
|
||||
workspaceId: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: verticalLayout
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: root.spacing
|
||||
|
||||
Repeater {
|
||||
model: root.ids
|
||||
WorkspaceButton {
|
||||
required property int modelData
|
||||
workspaceId: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component WorkspaceButton: Item {
|
||||
id: ws
|
||||
|
||||
property int workspaceId: 0
|
||||
readonly property var workspace: root.bar ? root.bar.workspaceById(workspaceId) : null
|
||||
readonly property bool occupied: workspace !== null && workspace.toplevels.values.length > 0
|
||||
readonly property bool focused: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === workspaceId
|
||||
|
||||
implicitWidth: root.slotSize
|
||||
implicitHeight: root.slotSize
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ws.focused ? "" : (ws.workspaceId === 10 ? "0" : String(ws.workspaceId))
|
||||
color: root.bar ? root.bar.foreground : "#cacccc"
|
||||
font.family: root.bar ? root.bar.fontFamily : "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: ws.focused ? 13 : 11
|
||||
opacity: ws.focused ? 1 : (ws.occupied || ws.workspaceId <= 5 ? 0.8 : 0.4)
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 160 } }
|
||||
Behavior on font.pixelSize { NumberAnimation { duration: 160 } }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
Hyprland.dispatch("movetoworkspace " + ws.workspaceId)
|
||||
} else {
|
||||
root.bar.focusWorkspace(ws.workspaceId)
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: function(wheel) {
|
||||
if (wheel.angleDelta.y > 0) Hyprland.dispatch("workspace e-1")
|
||||
else Hyprland.dispatch("workspace e+1")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user