rewrite bar with quickshell
This commit is contained in:
@@ -73,7 +73,7 @@
|
||||
(scale :min 0 :max 101 :class "brightness-scale" :width 150
|
||||
:value {brightness == "" ? 0 : brightness}
|
||||
:round-digits 0
|
||||
:onchange { brightness-hover ? "brightnessctl set {}\% -d $DISPLAY_DEVICE" : "" })))
|
||||
:onchange { brightness-hover ? "brightnessctl set {}\% --class=backlight" : "" })))
|
||||
(box :class "cpu-stats" :hexpand "false" :vexpand "false" :space-evenly "false"
|
||||
(label :tooltip "${cpu}%" :class "cpu-icon" :text "")
|
||||
(scale :min 0 :max 101 :active false :value {cpu == "" ? 0 : cpu} :class "cpu-scale" :width 150))
|
||||
|
||||
@@ -111,8 +111,15 @@ animations {
|
||||
layer-rule {
|
||||
match namespace="^swww-daemonbackdrop$"
|
||||
place-within-backdrop true
|
||||
|
||||
}
|
||||
|
||||
layer-rule {
|
||||
match namespace="^quickshell-bar$"
|
||||
place-within-backdrop false
|
||||
}
|
||||
|
||||
|
||||
/************************Autostart************************/
|
||||
|
||||
// Switch configs
|
||||
@@ -247,7 +254,6 @@ window-rule {
|
||||
open-on-workspace "special"
|
||||
}
|
||||
|
||||
|
||||
/************************Others************************/
|
||||
|
||||
cursor {
|
||||
@@ -300,7 +306,7 @@ binds {
|
||||
Mod+Shift+C { spawn-sh "hyprpicker -a"; }
|
||||
|
||||
// Session
|
||||
Mod+L { spawn "loginctl lock-session"; }
|
||||
Mod+L { spawn-sh "loginctl lock-session"; }
|
||||
|
||||
// Media
|
||||
XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ && wp-vol"; }
|
||||
|
||||
@@ -111,8 +111,10 @@ animations {
|
||||
layer-rule {
|
||||
match namespace="^swww-daemonbackdrop$"
|
||||
place-within-backdrop true
|
||||
|
||||
}
|
||||
|
||||
|
||||
/************************Autostart************************/
|
||||
|
||||
// Switch configs
|
||||
@@ -247,7 +249,6 @@ window-rule {
|
||||
open-on-workspace "special"
|
||||
}
|
||||
|
||||
|
||||
/************************Others************************/
|
||||
|
||||
cursor {
|
||||
@@ -300,7 +301,7 @@ binds {
|
||||
Mod+Shift+C { spawn-sh "hyprpicker -a"; }
|
||||
|
||||
// Session
|
||||
Mod+L { spawn "loginctl lock-session"; }
|
||||
Mod+L { spawn-sh "loginctl lock-session"; }
|
||||
|
||||
// Media
|
||||
XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ && wp-vol"; }
|
||||
|
||||
16
quickshell/Assets/Fonts/tabler/tabler-icons-license.txt
Normal file
16
quickshell/Assets/Fonts/tabler/tabler-icons-license.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Tabler Licenses - Detailed Usage Rights and Guidelines
|
||||
|
||||
This is a legal agreement between you, the Purchaser, and Tabler. Purchasing or downloading of any Tabler product (Tabler Admin Template, Tabler Icons, Tabler Emails, Tabler Illustrations), constitutes your acceptance of the terms of this license, Tabler terms of service and Tabler private policy.
|
||||
|
||||
Tabler Admin Template and Tabler Icons License*
|
||||
Tabler Admin Template and Tabler Icons are available under MIT License.
|
||||
|
||||
Copyright (c) 2018-2025 Tabler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
See more at Tabler Admin Template MIT License See more at Tabler Icons MIT License
|
||||
BIN
quickshell/Assets/Fonts/tabler/tabler-icons.ttf
Normal file
BIN
quickshell/Assets/Fonts/tabler/tabler-icons.ttf
Normal file
Binary file not shown.
2
quickshell/Assets/Ip/.gitignore
vendored
Normal file
2
quickshell/Assets/Ip/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
token.txt
|
||||
# cache.json
|
||||
38
quickshell/Constants/Colors.qml
Normal file
38
quickshell/Constants/Colors.qml
Normal file
@@ -0,0 +1,38 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property color transparent: "transparent"
|
||||
readonly property color rosewater: "#f5e0dc"
|
||||
readonly property color flamingo: "#f2cdcd"
|
||||
readonly property color pink: "#f5c2e7"
|
||||
readonly property color mauve: "#cba6f7"
|
||||
readonly property color red: "#f38ba8"
|
||||
readonly property color maroon: "#eba0ac"
|
||||
readonly property color peach: "#fab387"
|
||||
readonly property color yellow: "#f9e2af"
|
||||
readonly property color green: "#a6e3a1"
|
||||
readonly property color teal: "#94e2d5"
|
||||
readonly property color sky: "#89dceb"
|
||||
readonly property color sapphire: "#74c7ec"
|
||||
readonly property color blue: "#89b4fa"
|
||||
readonly property color lavender: "#b4befe"
|
||||
readonly property color text: "#cdd6f4"
|
||||
readonly property color subtext1: "#bac2de"
|
||||
readonly property color subtext0: "#a6adc8"
|
||||
readonly property color overlay2: "#9399b2"
|
||||
readonly property color overlay1: "#7f849c"
|
||||
readonly property color overlay0: "#6c7086"
|
||||
readonly property color surface2: "#585b70"
|
||||
readonly property color surface1: "#45475a"
|
||||
readonly property color surface0: "#313244"
|
||||
readonly property color base: "#1e1e2e"
|
||||
readonly property color mantle: "#181825"
|
||||
readonly property color crust: "#11111b"
|
||||
readonly property color accent: "#89b4fa"
|
||||
readonly property color distroColor: "#74c7ec"
|
||||
readonly property var cavaList: ["#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5", "#a6e3a1", "#f9e2af", "#fab387"]
|
||||
}
|
||||
15
quickshell/Constants/Fonts.qml
Normal file
15
quickshell/Constants/Fonts.qml
Normal file
@@ -0,0 +1,15 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string primary: "Sour Gummy Light"
|
||||
readonly property string nerd: "Meslo LGM Nerd Font Mono"
|
||||
readonly property string sans: "Noto Sans"
|
||||
readonly property int small: 10
|
||||
readonly property int medium: 12
|
||||
readonly property int large: 14
|
||||
readonly property int icon: 14
|
||||
}
|
||||
97
quickshell/Constants/Icons.qml
Normal file
97
quickshell/Constants/Icons.qml
Normal file
@@ -0,0 +1,97 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Nerd fonts icons
|
||||
readonly property string distro: ""
|
||||
readonly property string tray: ""
|
||||
readonly property string idleInhibitorActivated: ""
|
||||
readonly property string idleInhibitorDeactivated: ""
|
||||
readonly property string powerMenu: ""
|
||||
readonly property string volumeHigh: ""
|
||||
readonly property string volumeMedium: ""
|
||||
readonly property string volumeLow: ""
|
||||
readonly property string volumeMuted: ""
|
||||
readonly property string brightness: ""
|
||||
readonly property string charging: ""
|
||||
readonly property string battery100: ""
|
||||
readonly property string battery75: ""
|
||||
readonly property string battery50: ""
|
||||
readonly property string battery25: ""
|
||||
readonly property string battery00: ""
|
||||
readonly property string cpu: ""
|
||||
readonly property string memory: ""
|
||||
readonly property string tempHigh: ""
|
||||
readonly property string tempMedium: ""
|
||||
readonly property string tempLow: ""
|
||||
readonly property string global: ""
|
||||
readonly property string upload: ""
|
||||
readonly property string download: ""
|
||||
// Expose the font family name for easy access
|
||||
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
|
||||
readonly property string defaultIcon: TablerIcons.defaultIcon
|
||||
readonly property var icons: TablerIcons.icons
|
||||
readonly property var aliases: TablerIcons.aliases
|
||||
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf"
|
||||
// Current active font loader
|
||||
property FontLoader currentFontLoader: null
|
||||
property int fontVersion: 0
|
||||
// Create a unique cache-busting path
|
||||
readonly property string cacheBustingPath: Quickshell.shellDir + fontPath + "?v=" + fontVersion + "&t=" + Date.now()
|
||||
|
||||
// Signal emitted when font is reloaded
|
||||
signal fontReloaded()
|
||||
|
||||
// ---------------------------------------
|
||||
function get(iconName) {
|
||||
// Check in aliases first
|
||||
if (aliases[iconName] !== undefined)
|
||||
iconName = aliases[iconName];
|
||||
|
||||
// Find the appropriate codepoint
|
||||
return icons[iconName];
|
||||
}
|
||||
|
||||
function loadFontWithCacheBusting() {
|
||||
// Destroy old loader first
|
||||
if (currentFontLoader) {
|
||||
currentFontLoader.destroy();
|
||||
currentFontLoader = null;
|
||||
}
|
||||
// Create new loader with cache-busting URL
|
||||
currentFontLoader = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
FontLoader {
|
||||
source: "${cacheBustingPath}"
|
||||
}
|
||||
`, root, "dynamicFontLoader_" + fontVersion);
|
||||
// Connect to the new loader's status changes
|
||||
currentFontLoader.statusChanged.connect(function() {
|
||||
if (currentFontLoader.status === FontLoader.Ready)
|
||||
fontReloaded();
|
||||
else if (currentFontLoader.status === FontLoader.Error)
|
||||
Logger.error("Font failed to load (version " + fontVersion + ")");
|
||||
});
|
||||
}
|
||||
|
||||
function reloadFont() {
|
||||
fontVersion++;
|
||||
loadFontWithCacheBusting();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
loadFontWithCacheBusting();
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onReloadCompleted() {
|
||||
reloadFont();
|
||||
}
|
||||
|
||||
target: Quickshell
|
||||
}
|
||||
|
||||
}
|
||||
6169
quickshell/Constants/TablerIcons.qml
Normal file
6169
quickshell/Constants/TablerIcons.qml
Normal file
File diff suppressed because it is too large
Load Diff
260
quickshell/Modules/Bar/Bar.qml
Normal file
260
quickshell/Modules/Bar/Bar.qml
Normal file
@@ -0,0 +1,260 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.UPower
|
||||
import Quickshell.Wayland
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Components
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Modules.Misc
|
||||
import qs.Services
|
||||
|
||||
Scope {
|
||||
id: rootScope
|
||||
|
||||
property var shell
|
||||
|
||||
Item {
|
||||
id: barRootItem
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
Item {
|
||||
property var modelData
|
||||
|
||||
PanelWindow {
|
||||
id: panel
|
||||
|
||||
screen: modelData
|
||||
WlrLayershell.namespace: "quickshell-bar"
|
||||
color: Colors.transparent
|
||||
implicitHeight: 45
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
right: true
|
||||
top: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: barBackground
|
||||
|
||||
anchors.fill: parent
|
||||
color: Niri.noFocus ? null : Colors.base
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0.8 : 1)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0 : 1)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: leftLayout
|
||||
|
||||
height: parent.height - 10
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: 5
|
||||
}
|
||||
|
||||
SymbolButton {
|
||||
symbol: Icons.distro
|
||||
buttonColor: Colors.distroColor
|
||||
onRightClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
return ;
|
||||
}
|
||||
action.exec(["rofi", "-show", "drun"]);
|
||||
}
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
Workspace {
|
||||
screen: modelData
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
}
|
||||
|
||||
CavaBar {
|
||||
count: 6
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
}
|
||||
|
||||
FocusedWindow {
|
||||
maxWidth: 400
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: middleLayout
|
||||
|
||||
height: parent.height - 10
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Time {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rightLayout
|
||||
|
||||
height: parent.height - 10
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
rightMargin: 5
|
||||
}
|
||||
|
||||
NetworkSpeed {
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
}
|
||||
|
||||
Ip {
|
||||
showCountryCode: true
|
||||
}
|
||||
|
||||
CpuTemp {
|
||||
}
|
||||
|
||||
MemUsage {
|
||||
}
|
||||
|
||||
CpuUsage {
|
||||
}
|
||||
|
||||
Battery {
|
||||
}
|
||||
|
||||
Brightness {
|
||||
screen: modelData
|
||||
}
|
||||
|
||||
Volume {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 5
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 5
|
||||
}
|
||||
|
||||
TrayExpander {
|
||||
screen: modelData
|
||||
}
|
||||
|
||||
SymbolButton {
|
||||
symbol: Caffeine.isInhibited ? Icons.idleInhibitorActivated : Icons.idleInhibitorDeactivated
|
||||
buttonColor: Caffeine.isInhibited ? Colors.peach : Colors.yellow
|
||||
onClicked: {
|
||||
Caffeine.manualToggle();
|
||||
}
|
||||
|
||||
Behavior on buttonColor {
|
||||
ColorAnimation {
|
||||
duration: 500
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SymbolButton {
|
||||
symbol: Icons.powerMenu
|
||||
buttonColor: Colors.red
|
||||
onClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
return ;
|
||||
}
|
||||
action.exec(["wlogout"]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
22
quickshell/Modules/Bar/Components/Battery.qml
Normal file
22
quickshell/Modules/Bar/Components/Battery.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
readonly property var battery: UPower.displayDevice
|
||||
readonly property bool isReady: (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||
readonly property real percent: (isReady ? (battery.percentage * 100) : 0)
|
||||
readonly property bool charging: (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||
property int lowBatteryThreshold: 15
|
||||
|
||||
symbol: {
|
||||
return charging ? Icons.charging : percent >= 80 ? Icons.battery100 : percent >= 60 ? Icons.battery75 : percent >= 40 ? Icons.battery50 : percent >= 20 ? Icons.battery25 : Icons.battery00;
|
||||
}
|
||||
fillColor: !isReady || charging || percent > lowBatteryThreshold ? Colors.sapphire : Colors.red
|
||||
value: percent
|
||||
maxValue: 100
|
||||
textSuffix: "%"
|
||||
pointerCursor: false
|
||||
}
|
||||
34
quickshell/Modules/Bar/Components/Brightness.qml
Normal file
34
quickshell/Modules/Bar/Components/Brightness.qml
Normal file
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
property ShellScreen screen: null
|
||||
|
||||
function getMonitor() {
|
||||
return BrightnessService.getMonitorForScreen(screen) || null;
|
||||
}
|
||||
|
||||
symbol: Icons.brightness
|
||||
fillColor: Colors.blue
|
||||
value: {
|
||||
const monitor = getMonitor();
|
||||
return monitor ? Math.round(monitor.brightness * 100) : "N/A";
|
||||
}
|
||||
maxValue: 100
|
||||
textSuffix: "%"
|
||||
onWheelUp: {
|
||||
const monitor = getMonitor();
|
||||
if (monitor)
|
||||
monitor.increaseBrightness();
|
||||
|
||||
}
|
||||
onWheelDown: {
|
||||
const monitor = getMonitor();
|
||||
if (monitor)
|
||||
monitor.decreaseBrightness();
|
||||
|
||||
}
|
||||
}
|
||||
69
quickshell/Modules/Bar/Components/CavaBar.qml
Normal file
69
quickshell/Modules/Bar/Components/CavaBar.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
import qs.Modules.Misc
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int count: 6
|
||||
property int barWidth: 5
|
||||
property int barSpacing: 3
|
||||
|
||||
implicitWidth: root.barWidth * root.count + root.barSpacing * (root.count - 1)
|
||||
implicitHeight: parent.height - 10
|
||||
|
||||
Cava {
|
||||
id: cavaProcess
|
||||
|
||||
count: root.count
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: root.barSpacing
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: cavaProcess.values
|
||||
|
||||
Rectangle {
|
||||
width: root.barWidth
|
||||
implicitHeight: Math.max(1, modelData * (parent.height - 10))
|
||||
color: Colors.cavaList[Math.min(Math.floor(modelData * (Colors.cavaList.length - 1)), Colors.cavaList.length - 1)]
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
MusicManager.playPause();
|
||||
}
|
||||
onWheel: function(wheel) {
|
||||
if (wheel.angleDelta.y > 0)
|
||||
MusicManager.previous();
|
||||
else if (wheel.angleDelta.y < 0)
|
||||
MusicManager.next();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
27
quickshell/Modules/Bar/Components/CpuTemp.qml
Normal file
27
quickshell/Modules/Bar/Components/CpuTemp.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
symbol: Icons.cpuTemp > 80 ? Icons.tempHigh : Icons.cpuTemp > 50 ? Icons.tempMedium : Icons.tempLow
|
||||
fillColor: Icons.cpuTemp > 80 ? Colors.red : Colors.yellow
|
||||
value: Math.round(SystemStatService.cpuTemp)
|
||||
maxValue: 120
|
||||
textSuffix: "°C"
|
||||
onClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
return ;
|
||||
}
|
||||
action.exec(["ghostty", "-e", "btop"]);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
27
quickshell/Modules/Bar/Components/CpuUsage.qml
Normal file
27
quickshell/Modules/Bar/Components/CpuUsage.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
symbol: Icons.cpu
|
||||
fillColor: Colors.teal
|
||||
value: Math.round(SystemStatService.cpuUsage)
|
||||
maxValue: 100
|
||||
textSuffix: "%"
|
||||
onClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
return ;
|
||||
}
|
||||
action.exec(["ghostty", "-e", "btop"]);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
144
quickshell/Modules/Bar/Components/FocusedWindow.qml
Normal file
144
quickshell/Modules/Bar/Components/FocusedWindow.qml
Normal file
@@ -0,0 +1,144 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real maxWidth: 250
|
||||
property string fallbackIcon: "application-x-executable"
|
||||
|
||||
function getAppIcon() {
|
||||
try {
|
||||
const focusedWindow = Niri.getFocusedWindow();
|
||||
if (focusedWindow && focusedWindow.appId) {
|
||||
try {
|
||||
const idValue = focusedWindow.appId;
|
||||
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue);
|
||||
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase());
|
||||
if (iconResult && iconResult !== "")
|
||||
return iconResult;
|
||||
|
||||
} catch (iconError) {
|
||||
console.warn("Error getting icon from CompositorService:", iconError);
|
||||
}
|
||||
}
|
||||
return ThemeIcons.iconFromName(root.fallbackIcon);
|
||||
} catch (e) {
|
||||
console.warn("Error in getAppIcon:", e);
|
||||
return ThemeIcons.iconFromName(root.fallbackIcon);
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: parent.height
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
visible: Niri.focusedWindowId !== -1
|
||||
|
||||
Item {
|
||||
// Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
id: iconContainer
|
||||
|
||||
implicitWidth: 18
|
||||
implicitHeight: 18
|
||||
|
||||
IconImage {
|
||||
id: windowIcon
|
||||
|
||||
anchors.fill: parent
|
||||
source: getAppIcon()
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: titleContainer
|
||||
|
||||
implicitWidth: root.maxWidth
|
||||
implicitHeight: parent.height
|
||||
// Layout.alignment: Qt.AlignVCenter
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
id: windowTitle
|
||||
|
||||
text: Niri.focusedWindowTitle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
onEntered: {
|
||||
if (windowTitle.implicitWidth > titleContainer.width)
|
||||
windowTitle.x = titleContainer.width - windowTitle.implicitWidth;
|
||||
|
||||
}
|
||||
onExited: {
|
||||
windowTitle.x = 0;
|
||||
}
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
action.command = ["niri", "msg", "action", "close-window"];
|
||||
action.startDetached();
|
||||
} else if (mouse.button === Qt.LeftButton) {
|
||||
action.command = ["niri", "msg", "action", "center-window"];
|
||||
action.startDetached();
|
||||
}
|
||||
}
|
||||
onWheel: function(wheel) {
|
||||
if (wheel.angleDelta.y > 0) {
|
||||
action.command = ["niri", "msg", "action", "set-column-width", "+10%"];
|
||||
action.startDetached();
|
||||
} else if (wheel.angleDelta.y < 0) {
|
||||
action.command = ["niri", "msg", "action", "set-column-width", "-10%"];
|
||||
action.startDetached();
|
||||
} else if (wheel.angleDelta.x > 0) {
|
||||
action.command = ["niri", "msg", "action", "focus-column-left"];
|
||||
action.startDetached();
|
||||
} else if (wheel.angleDelta.x < 0) {
|
||||
action.command = ["niri", "msg", "action", "focus-column-right"];
|
||||
action.startDetached();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
85
quickshell/Modules/Bar/Components/Ip.qml
Normal file
85
quickshell/Modules/Bar/Components/Ip.qml
Normal file
@@ -0,0 +1,85 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
// Text {
|
||||
// id: ipText
|
||||
// anchors.verticalCenter: parent.verticalCenter
|
||||
// text: Icons.global + " " + (showCountryCode ? IpService.countryCode : IpService.ip)
|
||||
// font.pixelSize: Fonts.medium
|
||||
// color: Colors.peach
|
||||
// }
|
||||
|
||||
id: root
|
||||
|
||||
property bool showCountryCode: true
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: layout.width + 10
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
text: Icons.global
|
||||
font.pointSize: Fonts.icon + 5
|
||||
color: Colors.peach
|
||||
}
|
||||
|
||||
Item {
|
||||
id: expander
|
||||
|
||||
implicitWidth: mouseArea.containsMouse ? ipText.implicitWidth + 10 : 0
|
||||
implicitHeight: parent.height
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
id: ipText
|
||||
|
||||
text: showCountryCode ? IpService.countryCode : IpService.ip
|
||||
font.pointSize: showCountryCode ? Fonts.medium : Fonts.small
|
||||
font.family: Fonts.primary
|
||||
color: Colors.peach
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
WriteClipboard.write(showCountryCode ? IpService.countryCode : IpService.ip);
|
||||
SendNotification.show("Copied to clipboard", showCountryCode ? IpService.countryCode : IpService.ip);
|
||||
} else if (mouse.button === Qt.RightButton)
|
||||
showCountryCode = !showCountryCode;
|
||||
else if (mouse.button === Qt.MiddleButton)
|
||||
IpService.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
28
quickshell/Modules/Bar/Components/MemUsage.qml
Normal file
28
quickshell/Modules/Bar/Components/MemUsage.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
symbol: Icons.memory
|
||||
fillColor: Colors.green
|
||||
value: Math.round(SystemStatService.memPercent)
|
||||
maxValue: 100
|
||||
textValue: SystemStatService.memGb
|
||||
textSuffix: "G"
|
||||
onClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
return ;
|
||||
}
|
||||
action.exec(["ghostty", "-e", "btop"]);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
52
quickshell/Modules/Bar/Components/NetworkSpeed.qml
Normal file
52
quickshell/Modules/Bar/Components/NetworkSpeed.qml
Normal file
@@ -0,0 +1,52 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: layout.width + 10
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: Icons.download
|
||||
font.pointSize: Fonts.icon - 3
|
||||
color: Colors.accent
|
||||
Layout.leftMargin: 10
|
||||
}
|
||||
|
||||
Text {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 5
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Icons.upload
|
||||
font.pointSize: Fonts.icon - 3
|
||||
color: Colors.accent
|
||||
}
|
||||
|
||||
Text {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
18
quickshell/Modules/Bar/Components/Separator.qml
Normal file
18
quickshell/Modules/Bar/Components/Separator.qml
Normal file
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitHeight: parent.height
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 1.5
|
||||
height: parent.height * 0.32
|
||||
color: Colors.text
|
||||
}
|
||||
|
||||
}
|
||||
10
quickshell/Modules/Bar/Components/Time.qml
Normal file
10
quickshell/Modules/Bar/Components/Time.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Text {
|
||||
text: TimeService.time + " | " + TimeService.dateString
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
}
|
||||
60
quickshell/Modules/Bar/Components/TrayExpander.qml
Normal file
60
quickshell/Modules/Bar/Components/TrayExpander.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: layout.implicitWidth
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
SymbolButton {
|
||||
symbol: Icons.tray
|
||||
buttonColor: Colors.green
|
||||
}
|
||||
|
||||
Item {
|
||||
id: trayContainer
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: mouseArea.containsMouse ? expandedTray.implicitWidth : 0
|
||||
clip: true
|
||||
|
||||
SystemTray {
|
||||
id: expandedTray
|
||||
|
||||
screen: root.screen
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
}
|
||||
21
quickshell/Modules/Bar/Components/Volume.qml
Normal file
21
quickshell/Modules/Bar/Components/Volume.qml
Normal file
@@ -0,0 +1,21 @@
|
||||
import QtQuick
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
symbol: AudioService.muted ? Icons.volumeMuted : (AudioService.volume >= 0.66 ? Icons.volumeHigh : (AudioService.volume >= 0.33 ? Icons.volumeMedium : Icons.volumeLow))
|
||||
fillColor: Colors.lavender
|
||||
value: Math.round(AudioService.volume * 100)
|
||||
maxValue: 100
|
||||
textSuffix: "%"
|
||||
onWheelUp: {
|
||||
AudioService.increaseVolume();
|
||||
}
|
||||
onWheelDown: {
|
||||
AudioService.decreaseVolume();
|
||||
}
|
||||
onClicked: {
|
||||
AudioService.toggleMute();
|
||||
}
|
||||
}
|
||||
299
quickshell/Modules/Bar/Components/Workspace.qml
Normal file
299
quickshell/Modules/Bar/Components/Workspace.qml
Normal file
@@ -0,0 +1,299 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
property bool hovered: false
|
||||
property ListModel localWorkspaces
|
||||
property real masterProgress: 0
|
||||
property bool effectsActive: false
|
||||
property color effectColor: Colors.accent
|
||||
property int horizontalPadding: 16
|
||||
property int spacingBetweenPills: 8
|
||||
property bool isDestroying: false
|
||||
|
||||
signal workspaceChanged(int workspaceId, color accentColor)
|
||||
|
||||
function triggerUnifiedWave() {
|
||||
effectColor = Colors.accent;
|
||||
masterAnimation.restart();
|
||||
}
|
||||
|
||||
function updateWorkspaceFocus() {
|
||||
for (let i = 0; i < localWorkspaces.count; i++) {
|
||||
const ws = localWorkspaces.get(i);
|
||||
if (ws.isFocused === true) {
|
||||
root.triggerUnifiedWave();
|
||||
root.workspaceChanged(ws.id, Colors.accent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: {
|
||||
let total = 0;
|
||||
for (let i = 0; i < localWorkspaces.count; i++) {
|
||||
const ws = localWorkspaces.get(i);
|
||||
if (ws.isFocused)
|
||||
total += 44;
|
||||
else if (ws.isActive)
|
||||
total += 28;
|
||||
else
|
||||
total += 16;
|
||||
}
|
||||
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills;
|
||||
total += horizontalPadding * 2;
|
||||
return total;
|
||||
}
|
||||
height: parent.height
|
||||
Component.onCompleted: {
|
||||
localWorkspaces.clear();
|
||||
for (let i = 0; i < WorkspaceManager.workspaces.count; i++) {
|
||||
const ws = WorkspaceManager.workspaces.get(i);
|
||||
if (ws.output.toLowerCase() === screen.name.toLowerCase())
|
||||
localWorkspaces.append(ws);
|
||||
|
||||
}
|
||||
workspaceRepeater.model = localWorkspaces;
|
||||
updateWorkspaceFocus();
|
||||
}
|
||||
Component.onDestruction: {
|
||||
root.isDestroying = true;
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onWorkspacesChanged() {
|
||||
localWorkspaces.clear();
|
||||
for (let i = 0; i < WorkspaceManager.workspaces.count; i++) {
|
||||
const ws = WorkspaceManager.workspaces.get(i);
|
||||
if (ws.output.toLowerCase() === screen.name.toLowerCase())
|
||||
localWorkspaces.append(ws);
|
||||
|
||||
}
|
||||
workspaceRepeater.model = localWorkspaces;
|
||||
updateWorkspaceFocus();
|
||||
}
|
||||
|
||||
target: WorkspaceManager
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: masterAnimation
|
||||
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "effectsActive"
|
||||
value: true
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "masterProgress"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 1000
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "effectsActive"
|
||||
value: false
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "masterProgress"
|
||||
value: 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: workspaceBackground
|
||||
|
||||
width: parent.width - 15
|
||||
height: 26
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: 12
|
||||
color: Colors.transparent
|
||||
layer.enabled: true
|
||||
|
||||
layer.effect: DropShadow {
|
||||
color: "black"
|
||||
radius: 12
|
||||
samples: 24
|
||||
verticalOffset: 0
|
||||
horizontalOffset: 0
|
||||
opacity: 0.1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
id: pillRow
|
||||
|
||||
spacing: spacingBetweenPills
|
||||
anchors.verticalCenter: workspaceBackground.verticalCenter
|
||||
width: root.width - horizontalPadding * 2
|
||||
x: horizontalPadding
|
||||
|
||||
Repeater {
|
||||
id: workspaceRepeater
|
||||
|
||||
model: localWorkspaces
|
||||
|
||||
Item {
|
||||
id: workspacePillContainer
|
||||
|
||||
height: 12
|
||||
width: {
|
||||
if (model.isFocused)
|
||||
return 44;
|
||||
else if (model.isActive)
|
||||
return 28;
|
||||
else
|
||||
return 16;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
// half of focused height (if you want to animate this too)
|
||||
|
||||
id: workspacePill
|
||||
|
||||
anchors.fill: parent
|
||||
radius: {
|
||||
if (model.isFocused)
|
||||
return 12;
|
||||
else
|
||||
return 6;
|
||||
}
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Colors.accent;
|
||||
|
||||
if (model.isActive)
|
||||
return Colors.accent.lighter(130);
|
||||
|
||||
if (model.isUrgent)
|
||||
return Theme.error;
|
||||
|
||||
return Colors.surface2;
|
||||
}
|
||||
scale: model.isFocused ? 1 : 0.9
|
||||
z: 0
|
||||
|
||||
MouseArea {
|
||||
id: pillMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
WorkspaceManager.switchToWorkspace(model.idx);
|
||||
}
|
||||
z: 20
|
||||
hoverEnabled: true
|
||||
}
|
||||
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pillBurst
|
||||
|
||||
anchors.centerIn: workspacePillContainer
|
||||
width: workspacePillContainer.width + 18 * root.masterProgress
|
||||
height: workspacePillContainer.height + 18 * root.masterProgress
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: root.effectColor
|
||||
border.width: 2 + 6 * (1 - root.masterProgress)
|
||||
opacity: root.effectsActive && model.isFocused ? (1 - root.masterProgress) * 0.7 : 0
|
||||
visible: root.effectsActive && model.isFocused
|
||||
z: 1
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
localWorkspaces: ListModel {
|
||||
}
|
||||
|
||||
}
|
||||
140
quickshell/Modules/Bar/Misc/MonitorItem.qml
Normal file
140
quickshell/Modules/Bar/Misc/MonitorItem.qml
Normal file
@@ -0,0 +1,140 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string symbol
|
||||
property real maxValue: 100
|
||||
property real value: 100
|
||||
property string textValue: "" // override value in textDisplay if set
|
||||
property color fillColor: Colors.accent
|
||||
property string textSuffix: ""
|
||||
property bool pointerCursor: true
|
||||
property alias hovered: mouseArea.containsMouse
|
||||
readonly property real ratio: value / maxValue
|
||||
|
||||
signal wheelUp()
|
||||
signal wheelDown()
|
||||
signal clicked()
|
||||
|
||||
implicitHeight: parent.height - 5
|
||||
implicitWidth: parent.height + (hovered ? textDisplay.width : 0)
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: pointerCursor ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton)
|
||||
root.clicked();
|
||||
else if (mouse.button === Qt.RightButton)
|
||||
root.rightClicked();
|
||||
}
|
||||
onWheel: (wheel) => {
|
||||
if (wheel.angleDelta.y > 0)
|
||||
root.wheelUp();
|
||||
else if (wheel.angleDelta.y < 0)
|
||||
root.wheelDown();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
id: progressDisplay
|
||||
|
||||
Layout.preferredHeight: parent.height
|
||||
Layout.preferredWidth: parent.height
|
||||
|
||||
Canvas {
|
||||
id: progressCircle
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.centerIn: parent
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
var centerX = width / 2;
|
||||
var centerY = height / 2;
|
||||
var radius = width / 2 - 3;
|
||||
var startAngle = -Math.PI / 2;
|
||||
var endAngle = startAngle - (2 * Math.PI * root.ratio);
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, endAngle, startAngle, false);
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = root.fillColor;
|
||||
ctx.lineCap = "round";
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onRatioChanged() {
|
||||
progressCircle.requestPaint();
|
||||
}
|
||||
|
||||
function onFillColorChanged() {
|
||||
progressCircle.requestPaint();
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
id: symbolText
|
||||
|
||||
anchors.fill: parent
|
||||
text: symbol
|
||||
font.family: Fonts.nerd
|
||||
font.pointSize: Fonts.icon
|
||||
color: fillColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: textDisplay
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: root.hovered ? textLabel.implicitWidth + 10 : 0
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
id: textLabel
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
text: (textValue || Math.round(root.value)) + root.textSuffix
|
||||
font.pointSize: Fonts.small
|
||||
font.family: Fonts.primary
|
||||
color: root.fillColor
|
||||
opacity: root.hovered ? 1 : 0
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
60
quickshell/Modules/Bar/Misc/SymbolButton.qml
Normal file
60
quickshell/Modules/Bar/Misc/SymbolButton.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string symbol
|
||||
property color buttonColor: Colors.distroColor
|
||||
readonly property alias hovered: mouseArea.containsMouse
|
||||
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: parent.height
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton)
|
||||
root.rightClicked();
|
||||
else if (mouse.button === Qt.LeftButton)
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
text: symbol
|
||||
font.family: Fonts.nerd
|
||||
font.pointSize: Fonts.icon
|
||||
font.bold: false
|
||||
color: buttonColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: parent.hovered ? buttonColor : Colors.transparent
|
||||
opacity: 0.3
|
||||
radius: 14
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 120
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
160
quickshell/Modules/Bar/Misc/SystemTray.qml
Normal file
160
quickshell/Modules/Bar/Misc/SystemTray.qml
Normal file
@@ -0,0 +1,160 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
implicitWidth: trayFlow.implicitWidth + 20
|
||||
implicitHeight: parent.height
|
||||
radius: 0
|
||||
color: Colors.transparent
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Flow {
|
||||
id: trayFlow
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
flow: Flow.LeftToRight
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Item {
|
||||
width: 18
|
||||
height: 18
|
||||
visible: modelData
|
||||
|
||||
IconImage {
|
||||
id: trayIcon
|
||||
|
||||
property ShellScreen screen: root.screen
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 14
|
||||
height: 14
|
||||
smooth: false
|
||||
asynchronous: true
|
||||
backer.fillMode: Image.PreserveAspectFit
|
||||
source: {
|
||||
let icon = modelData?.icon || ""
|
||||
if (!icon) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Process icon path
|
||||
if (icon.includes("?path=")) {
|
||||
// Seems qmlfmt does not support the following ES6 syntax: const[name, path] = icon.split
|
||||
const chunks = icon.split("?path=")
|
||||
const name = chunks[0]
|
||||
const path = chunks[1]
|
||||
const fileName = name.substring(name.lastIndexOf("/") + 1)
|
||||
return `file://${path}/${fileName}`
|
||||
}
|
||||
return icon
|
||||
}
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: mouse => {
|
||||
if (!modelData) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Close any open menu first
|
||||
trayPanel.close()
|
||||
|
||||
if (!modelData.onlyMenu) {
|
||||
modelData.activate()
|
||||
}
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
// Close any open menu first
|
||||
trayPanel.close()
|
||||
|
||||
modelData.secondaryActivate && modelData.secondaryActivate()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
// Close the menu if it was visible
|
||||
if (trayPanel && trayPanel.visible) {
|
||||
trayPanel.close()
|
||||
return
|
||||
}
|
||||
|
||||
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
|
||||
trayPanel.open()
|
||||
|
||||
// Position menu based on bar position
|
||||
let menuX, menuY
|
||||
// For horizontal bars: center horizontally and position below
|
||||
menuX = (width / 2) - (trayMenu.item.width / 2)
|
||||
menuY = root.height
|
||||
trayMenu.item.menu = modelData.menu
|
||||
trayMenu.item.showAt(parent, menuX, menuY)
|
||||
} else {
|
||||
// Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
|
||||
console.log("No menu available for", modelData.id, "or trayMenu not set")
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
trayPanel.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: trayPanel
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
visible: false
|
||||
color: Colors.transparent
|
||||
screen: root.screen
|
||||
|
||||
function open() {
|
||||
visible = true
|
||||
PanelService.willOpenPanel(trayPanel)
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false
|
||||
if (trayMenu.item) {
|
||||
trayMenu.item.hideMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Clicking outside of the rectangle to close
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: trayPanel.close()
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: trayMenu
|
||||
Component.onCompleted: {
|
||||
setSource("../Misc/TrayMenu.qml", {
|
||||
"screen": root.screen
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
282
quickshell/Modules/Bar/Misc/TrayMenu.qml
Normal file
282
quickshell/Modules/Bar/Misc/TrayMenu.qml
Normal file
@@ -0,0 +1,282 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
property QsMenuHandle menu
|
||||
property var anchorItem: null
|
||||
property real anchorX
|
||||
property real anchorY
|
||||
property bool isSubMenu: false
|
||||
property bool isHovered: rootMouseArea.containsMouse
|
||||
property ShellScreen screen
|
||||
|
||||
readonly property int menuWidth: 180
|
||||
|
||||
implicitWidth: menuWidth
|
||||
|
||||
// Use the content height of the Flickable for implicit height
|
||||
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + 20)
|
||||
visible: false
|
||||
color: Colors.transparent
|
||||
anchor.item: anchorItem
|
||||
anchor.rect.x: anchorX
|
||||
anchor.rect.y: anchorY - (isSubMenu ? 0 : 4)
|
||||
|
||||
function showAt(item, x, y) {
|
||||
if (!item) {
|
||||
console.warn("AnchorItem is undefined, won't show menu.")
|
||||
return
|
||||
}
|
||||
|
||||
if (!opener.children || opener.children.values.length === 0) {
|
||||
Qt.callLater(() => showAt(item, x, y))
|
||||
return
|
||||
}
|
||||
|
||||
anchorItem = item
|
||||
anchorX = x
|
||||
anchorY = y
|
||||
|
||||
visible = true
|
||||
forceActiveFocus()
|
||||
|
||||
// Force update after showing.
|
||||
Qt.callLater(() => {
|
||||
root.anchor.updateAnchor()
|
||||
})
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
visible = false
|
||||
|
||||
// Clean up all submenus recursively
|
||||
for (var i = 0; i < columnLayout.children.length; i++) {
|
||||
const child = columnLayout.children[i]
|
||||
if (child?.subMenu) {
|
||||
child.subMenu.hideMenu()
|
||||
child.subMenu.destroy()
|
||||
child.subMenu = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full-sized, transparent MouseArea to track the mouse.
|
||||
MouseArea {
|
||||
id: rootMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
Keys.onEscapePressed: root.hideMenu()
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: opener
|
||||
menu: root.menu
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Colors.base
|
||||
border.color: Colors.accent
|
||||
border.width: 2
|
||||
radius: 14
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
contentHeight: columnLayout.implicitHeight
|
||||
interactive: true
|
||||
|
||||
// Use a ColumnLayout to handle menu item arrangement
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
width: flickable.width
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: opener.children ? [...opener.children.values] : []
|
||||
|
||||
delegate: Rectangle {
|
||||
id: entry
|
||||
required property var modelData
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: {
|
||||
if (modelData?.isSeparator) {
|
||||
return 8
|
||||
} else {
|
||||
// Calculate based on text content
|
||||
const textHeight = text.contentHeight || (Fonts.small)
|
||||
return textHeight + 16
|
||||
}
|
||||
}
|
||||
|
||||
color: Colors.transparent
|
||||
property var subMenu: null
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 16
|
||||
height: 1
|
||||
anchors.centerIn: parent
|
||||
visible: modelData?.isSeparator ?? false
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Colors.transparent
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.1
|
||||
color: Colors.accent
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.9
|
||||
color: Colors.accent
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Colors.transparent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: mouseArea.containsMouse ? Colors.accent : Colors.transparent
|
||||
radius: 10
|
||||
visible: !(modelData?.isSeparator ?? false)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
id: text
|
||||
Layout.fillWidth: true
|
||||
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Colors.base : Colors.text) : Colors.text
|
||||
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
|
||||
font.pointSize: Fonts.small
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 14
|
||||
Layout.preferredHeight: 14
|
||||
source: modelData?.icon ?? ""
|
||||
visible: (modelData?.icon ?? "") !== ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
Text {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: modelData?.hasChildren ?? false
|
||||
color: (mouseArea.containsMouse ? Colors.base : Colors.text)
|
||||
|
||||
text: {
|
||||
const icon = modelData?.hasChildren ? "menu" : ""
|
||||
if ((icon === undefined) || (icon === "")) {
|
||||
return ""
|
||||
}
|
||||
if (Icons.get(icon) === undefined) {
|
||||
console.warn("Icon", `"${icon}"`, "doesn't exist in the icons font")
|
||||
return Icons.get(Icons.defaultIcon)
|
||||
}
|
||||
return Icons.get(icon)
|
||||
}
|
||||
font.family: Icons.fontFamily
|
||||
font.pointSize: Fonts.small
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && root.visible
|
||||
|
||||
onClicked: {
|
||||
if (modelData && !modelData.isSeparator && !modelData.hasChildren) {
|
||||
modelData.triggered()
|
||||
root.hideMenu()
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
if (!root.visible)
|
||||
return
|
||||
|
||||
// Close all sibling submenus
|
||||
for (var i = 0; i < columnLayout.children.length; i++) {
|
||||
const sibling = columnLayout.children[i]
|
||||
if (sibling !== entry && sibling?.subMenu) {
|
||||
sibling.subMenu.hideMenu()
|
||||
sibling.subMenu.destroy()
|
||||
sibling.subMenu = null
|
||||
}
|
||||
}
|
||||
|
||||
// Create submenu if needed
|
||||
if (modelData?.hasChildren) {
|
||||
if (entry.subMenu) {
|
||||
entry.subMenu.hideMenu()
|
||||
entry.subMenu.destroy()
|
||||
}
|
||||
|
||||
// Need a slight overlap so that menu don't close when moving the mouse to a submenu
|
||||
const submenuWidth = menuWidth // Assuming a similar width as the parent
|
||||
const overlap = 4 // A small overlap to bridge the mouse path
|
||||
|
||||
// Position with overlap
|
||||
const anchorX = -submenuWidth + overlap
|
||||
|
||||
// Create submenu
|
||||
entry.subMenu = Qt.createComponent("TrayMenu.qml").createObject(root, {
|
||||
"menu": modelData,
|
||||
"anchorItem": entry,
|
||||
"anchorX": anchorX,
|
||||
"anchorY": 0,
|
||||
"isSubMenu": true,
|
||||
"screen": root.screen
|
||||
})
|
||||
|
||||
if (entry.subMenu) {
|
||||
entry.subMenu.showAt(entry, anchorX, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
Qt.callLater(() => {
|
||||
if (entry.subMenu && !entry.subMenu.isHovered) {
|
||||
entry.subMenu.hideMenu()
|
||||
entry.subMenu.destroy()
|
||||
entry.subMenu = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (subMenu) {
|
||||
subMenu.destroy()
|
||||
subMenu = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
quickshell/Modules/Misc/Cava.qml
Normal file
81
quickshell/Modules/Misc/Cava.qml
Normal file
@@ -0,0 +1,81 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property int count: 32
|
||||
property int noiseReduction: 60
|
||||
property string channels: "mono"
|
||||
property string monoOption: "average"
|
||||
property var config: ({
|
||||
"general": {
|
||||
"bars": count,
|
||||
"framerate": 30,
|
||||
"autosens": 1
|
||||
},
|
||||
"smoothing": {
|
||||
"monstercat": 1,
|
||||
"gravity": 1e+06,
|
||||
"noise_reduction": noiseReduction
|
||||
},
|
||||
"output": {
|
||||
"method": "raw",
|
||||
"bit_format": 8,
|
||||
"channels": channels,
|
||||
"mono_option": monoOption
|
||||
}
|
||||
})
|
||||
property var values: Array(count).fill(0)
|
||||
|
||||
Process {
|
||||
id: process
|
||||
|
||||
property int index: 0
|
||||
|
||||
stdinEnabled: true
|
||||
running: !MusicManager.isAllPaused()
|
||||
command: ["cava", "-p", "/dev/stdin"]
|
||||
onExited: {
|
||||
stdinEnabled = true;
|
||||
index = 0;
|
||||
values = Array(count).fill(0);
|
||||
}
|
||||
onStarted: {
|
||||
for (const k in config) {
|
||||
if (typeof config[k] !== "object") {
|
||||
write(k + "=" + config[k] + "\n");
|
||||
continue;
|
||||
}
|
||||
write("[" + k + "]\n");
|
||||
const obj = config[k];
|
||||
for (const k2 in obj) {
|
||||
write(k2 + "=" + obj[k2] + "\n");
|
||||
}
|
||||
}
|
||||
stdinEnabled = false;
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: (data) => {
|
||||
const newValues = Array(count).fill(0);
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
newValues[i] = values[i];
|
||||
}
|
||||
if (process.index + data.length > count)
|
||||
process.index = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
newValues[i + process.index] = Math.min(data.charCodeAt(i), 128) / 128;
|
||||
}
|
||||
process.index += data.length;
|
||||
values = newValues;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
10
quickshell/Modules/Misc/CavaColorList.qml
Normal file
10
quickshell/Modules/Misc/CavaColorList.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var colorList: [Colors.lavender, Colors.blue, Colors.sapphire, Colors.sky, Colors.teal, Colors.green, Colors.yellow, Colors.peach]
|
||||
}
|
||||
87
quickshell/Modules/Misc/Corner.qml
Normal file
87
quickshell/Modules/Misc/Corner.qml
Normal file
@@ -0,0 +1,87 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Constants
|
||||
|
||||
Shape {
|
||||
id: root
|
||||
|
||||
property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright
|
||||
property real size: 1 // Scale multiplier for entire corner
|
||||
property int concaveWidth: 100 * size
|
||||
property int concaveHeight: 60 * size
|
||||
property int offsetX: -20
|
||||
property int offsetY: -20
|
||||
property color fillColor: Colors.base
|
||||
property int arcRadius: 20 * size
|
||||
property var modelData: null
|
||||
// Position flags derived from position string
|
||||
property bool _isTop: position.includes("top")
|
||||
property bool _isLeft: position.includes("left")
|
||||
property bool _isRight: position.includes("right")
|
||||
property bool _isBottom: position.includes("bottom")
|
||||
// Shift the path vertically if offsetY is negative to pull shape up
|
||||
property real pathOffsetY: Math.min(offsetY, 0)
|
||||
// Base coordinates for left corner shape, shifted by pathOffsetY vertically
|
||||
property real _baseStartX: 30 * size
|
||||
property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY
|
||||
property real _baseLineX: 30 * size
|
||||
property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY
|
||||
property real _baseArcX: 50 * size
|
||||
property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY
|
||||
// Mirror coordinates for right corners
|
||||
property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX
|
||||
property real _startY: _baseStartY
|
||||
property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX
|
||||
property real _lineY: _baseLineY
|
||||
property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX
|
||||
property real _arcY: _baseArcY
|
||||
// Arc direction varies by corner to maintain proper concave shape
|
||||
property int _arcDirection: {
|
||||
if (_isTop && _isLeft)
|
||||
return PathArc.Counterclockwise;
|
||||
|
||||
if (_isTop && _isRight)
|
||||
return PathArc.Clockwise;
|
||||
|
||||
if (_isBottom && _isLeft)
|
||||
return PathArc.Clockwise;
|
||||
|
||||
if (_isBottom && _isRight)
|
||||
return PathArc.Counterclockwise;
|
||||
|
||||
return PathArc.Counterclockwise;
|
||||
}
|
||||
|
||||
width: concaveWidth
|
||||
height: concaveHeight
|
||||
// Position relative to parent based on corner type
|
||||
x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0)
|
||||
y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0)
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
layer.enabled: true
|
||||
layer.samples: 4
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: root.fillColor
|
||||
strokeColor: root.fillColor
|
||||
startX: root._startX
|
||||
startY: root._startY
|
||||
|
||||
PathLine {
|
||||
x: root._lineX
|
||||
y: root._lineY
|
||||
}
|
||||
|
||||
PathArc {
|
||||
x: root._arcX
|
||||
y: root._arcY
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
useLargeArc: false
|
||||
direction: root._arcDirection
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
178
quickshell/Modules/Misc/Corners.qml
Normal file
178
quickshell/Modules/Misc/Corners.qml
Normal file
@@ -0,0 +1,178 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Constants
|
||||
import qs.Modules.Misc
|
||||
import qs.Services
|
||||
|
||||
Scope {
|
||||
id: rootScope
|
||||
|
||||
property var shell
|
||||
property string namespace: "quickshell-corners"
|
||||
property int topMargin: 45
|
||||
property int cornerHeight: 20
|
||||
property real cornerSize: 1
|
||||
property real opacity: Niri.noFocus ? 0 : 1
|
||||
|
||||
Item {
|
||||
id: cornersRootItem
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
Item {
|
||||
property var modelData
|
||||
|
||||
PanelWindow {
|
||||
id: fakeBar
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: true
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
aboveWindows: false
|
||||
WlrLayershell.namespace: namespace
|
||||
implicitHeight: topMargin
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Colors.base
|
||||
opacity: rootScope.opacity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: topLeftPanel
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
margins.top: topMargin
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: true
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
aboveWindows: false
|
||||
WlrLayershell.namespace: namespace
|
||||
implicitHeight: cornerHeight
|
||||
|
||||
Corner {
|
||||
id: topLeftCorner
|
||||
|
||||
position: "bottomleft"
|
||||
size: rootScope.cornerSize
|
||||
offsetX: -32
|
||||
offsetY: 0
|
||||
anchors.top: parent.top
|
||||
opacity: rootScope.opacity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: topRightPanel
|
||||
|
||||
anchors.top: true
|
||||
anchors.right: true
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
margins.top: topMargin
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: true
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
aboveWindows: false
|
||||
WlrLayershell.namespace: namespace
|
||||
implicitHeight: cornerHeight
|
||||
|
||||
Corner {
|
||||
id: topRightCorner
|
||||
|
||||
position: "bottomright"
|
||||
size: rootScope.cornerSize
|
||||
offsetX: 32
|
||||
offsetY: 0
|
||||
anchors.top: parent.top
|
||||
opacity: rootScope.opacity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: bottomLeftPanel
|
||||
|
||||
anchors.bottom: true
|
||||
anchors.left: true
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: true
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
aboveWindows: false
|
||||
WlrLayershell.namespace: namespace
|
||||
implicitHeight: cornerHeight
|
||||
|
||||
Corner {
|
||||
id: bottomLeftCorner
|
||||
|
||||
position: "topleft"
|
||||
size: rootScope.cornerSize
|
||||
offsetX: -32
|
||||
offsetY: 0
|
||||
anchors.top: parent.top
|
||||
opacity: rootScope.opacity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: bottomRightPanel
|
||||
|
||||
anchors.bottom: true
|
||||
anchors.right: true
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: true
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
aboveWindows: false
|
||||
WlrLayershell.namespace: namespace
|
||||
implicitHeight: cornerHeight
|
||||
|
||||
Corner {
|
||||
id: bottomRightCorner
|
||||
|
||||
position: "topright"
|
||||
size: rootScope.cornerSize
|
||||
offsetX: 32
|
||||
offsetY: 0
|
||||
anchors.top: parent.top
|
||||
opacity: rootScope.opacity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
143
quickshell/Services/AudioService.qml
Normal file
143
quickshell/Services/AudioService.qml
Normal file
@@ -0,0 +1,143 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => {
|
||||
if (!node.isStream) {
|
||||
if (node.isSink) {
|
||||
acc.sinks.push(node)
|
||||
} else if (node.audio) {
|
||||
acc.sources.push(node)
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {
|
||||
"sources": [],
|
||||
"sinks": []
|
||||
})
|
||||
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
readonly property list<PwNode> sinks: nodes.sinks
|
||||
readonly property list<PwNode> sources: nodes.sources
|
||||
|
||||
// Volume [0..1] is readonly from outside
|
||||
readonly property alias volume: root._volume
|
||||
property real _volume: sink?.audio?.volume ?? 0
|
||||
|
||||
readonly property alias muted: root._muted
|
||||
property bool _muted: !!sink?.audio?.muted
|
||||
|
||||
// Input volume [0..1] is readonly from outside
|
||||
readonly property alias inputVolume: root._inputVolume
|
||||
property real _inputVolume: source?.audio?.volume ?? 0
|
||||
|
||||
readonly property alias inputMuted: root._inputMuted
|
||||
property bool _inputMuted: !!source?.audio?.muted
|
||||
|
||||
readonly property real stepVolume: 5 / 100.0
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [...root.sinks, ...root.sources]
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: sink?.audio ? sink?.audio : null
|
||||
|
||||
function onVolumeChanged() {
|
||||
var vol = (sink?.audio.volume ?? 0)
|
||||
if (isNaN(vol)) {
|
||||
vol = 0
|
||||
}
|
||||
root._volume = vol
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
root._muted = (sink?.audio.muted ?? true)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: source?.audio ? source?.audio : null
|
||||
|
||||
function onVolumeChanged() {
|
||||
var vol = (source?.audio.volume ?? 0)
|
||||
if (isNaN(vol)) {
|
||||
vol = 0
|
||||
}
|
||||
root._inputVolume = vol
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
root._inputMuted = (source?.audio.muted ?? true)
|
||||
}
|
||||
}
|
||||
|
||||
function increaseVolume() {
|
||||
setVolume(volume + stepVolume)
|
||||
}
|
||||
|
||||
function decreaseVolume() {
|
||||
setVolume(volume - stepVolume)
|
||||
}
|
||||
|
||||
function setVolume(newVolume: real) {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
// Clamp it accordingly
|
||||
sink.audio.muted = false
|
||||
sink.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
||||
} else {
|
||||
console.warn("No sink available")
|
||||
}
|
||||
}
|
||||
|
||||
function setOutputMuted(muted: bool) {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
sink.audio.muted = muted
|
||||
} else {
|
||||
console.warn("No sink available")
|
||||
}
|
||||
}
|
||||
|
||||
function setInputVolume(newVolume: real) {
|
||||
if (source?.ready && source?.audio) {
|
||||
// Clamp it accordingly
|
||||
source.audio.muted = false
|
||||
source.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
||||
} else {
|
||||
console.warn("No source available")
|
||||
}
|
||||
}
|
||||
|
||||
function setInputMuted(muted: bool) {
|
||||
if (source?.ready && source?.audio) {
|
||||
source.audio.muted = muted
|
||||
} else {
|
||||
console.warn("No source available")
|
||||
}
|
||||
}
|
||||
|
||||
function setAudioSink(newSink: PwNode): void {
|
||||
Pipewire.preferredDefaultAudioSink = newSink
|
||||
// Volume is changed by the sink change
|
||||
root._volume = newSink?.audio?.volume ?? 0
|
||||
root._muted = !!newSink?.audio?.muted
|
||||
}
|
||||
|
||||
function setAudioSource(newSource: PwNode): void {
|
||||
Pipewire.preferredDefaultAudioSource = newSource
|
||||
// Volume is changed by the source change
|
||||
root._inputVolume = newSource?.audio?.volume ?? 0
|
||||
root._inputMuted = !!newSource?.audio?.muted
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
setOutputMuted(!muted)
|
||||
}
|
||||
}
|
||||
303
quickshell/Services/BrightnessService.qml
Normal file
303
quickshell/Services/BrightnessService.qml
Normal file
@@ -0,0 +1,303 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<var> ddcMonitors: []
|
||||
readonly property list<Monitor> monitors: variants.instances
|
||||
property bool appleDisplayPresent: false
|
||||
|
||||
function getMonitorForScreen(screen: ShellScreen): var {
|
||||
return monitors.find(m => m.modelData === screen)
|
||||
}
|
||||
|
||||
// Signal emitted when a specific monitor's brightness changes, includes monitor context
|
||||
signal monitorBrightnessChanged(var monitor, real newBrightness)
|
||||
|
||||
function getAvailableMethods(): list<string> {
|
||||
var methods = []
|
||||
if (monitors.some(m => m.isDdc))
|
||||
methods.push("ddcutil")
|
||||
if (monitors.some(m => !m.isDdc))
|
||||
methods.push("internal")
|
||||
if (appleDisplayPresent)
|
||||
methods.push("apple")
|
||||
return methods
|
||||
}
|
||||
|
||||
// Global helpers for IPC and shortcuts
|
||||
function increaseBrightness(): void {
|
||||
monitors.forEach(m => m.increaseBrightness())
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
monitors.forEach(m => m.decreaseBrightness())
|
||||
}
|
||||
|
||||
function getDetectedDisplays(): list<var> {
|
||||
return detectedDisplays
|
||||
}
|
||||
|
||||
reloadableId: "brightness"
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = []
|
||||
ddcProc.running = true
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: variants
|
||||
model: Quickshell.screens
|
||||
Monitor {}
|
||||
}
|
||||
|
||||
// Check for Apple Display support
|
||||
Process {
|
||||
running: true
|
||||
command: ["sh", "-c", "which asdbctl >/dev/null 2>&1 && asdbctl get || echo ''"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.appleDisplayPresent = text.trim().length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Detect DDC monitors
|
||||
Process {
|
||||
id: ddcProc
|
||||
property list<var> ddcMonitors: []
|
||||
command: ["ddcutil", "detect", "--sleep-multiplier=0.5"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var displays = text.trim().split("\n\n")
|
||||
ddcProc.ddcMonitors = displays.map(d => {
|
||||
|
||||
var ddcModelMatch = d.match(/(This monitor does not support DDC\/CI|Invalid display)/)
|
||||
var modelMatch = d.match(/Model:\s*(.*)/)
|
||||
var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)
|
||||
var ddcModel = ddcModelMatch ? ddcModelMatch.length > 0 : false
|
||||
var model = modelMatch ? modelMatch[1] : "Unknown"
|
||||
var bus = busMatch ? busMatch[1] : "Unknown"
|
||||
console.log("Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel)
|
||||
return {
|
||||
"model": model,
|
||||
"busNum": bus,
|
||||
"isDdc": !ddcModel
|
||||
}
|
||||
})
|
||||
root.ddcMonitors = ddcProc.ddcMonitors.filter(m => m.isDdc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Monitor: QtObject {
|
||||
id: monitor
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model)
|
||||
readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? ""
|
||||
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
||||
readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal")
|
||||
|
||||
property real brightness
|
||||
property real lastBrightness: 0
|
||||
property real queuedBrightness: NaN
|
||||
|
||||
// For internal displays - store the backlight device path
|
||||
property string backlightDevice: ""
|
||||
property string brightnessPath: ""
|
||||
property string maxBrightnessPath: ""
|
||||
property int maxBrightness: 100
|
||||
property bool ignoreNextChange: false
|
||||
|
||||
// Signal for brightness changes
|
||||
signal brightnessUpdated(real newBrightness)
|
||||
|
||||
// Execute a system command to get the current brightness value directly
|
||||
readonly property Process refreshProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var dataText = text.trim()
|
||||
if (dataText === "") {
|
||||
return
|
||||
}
|
||||
|
||||
var lines = dataText.split("\n")
|
||||
if (lines.length >= 2) {
|
||||
var current = parseInt(lines[0].trim())
|
||||
var max = parseInt(lines[1].trim())
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
var newBrightness = current / max
|
||||
// Only update if it's actually different (avoid feedback loops)
|
||||
if (Math.abs(newBrightness - monitor.brightness) > 0.01) {
|
||||
// Update internal value to match system state
|
||||
monitor.brightness = newBrightness
|
||||
monitor.brightnessUpdated(monitor.brightness)
|
||||
root.monitorBrightnessChanged(monitor, monitor.brightness)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to actively refresh the brightness from system
|
||||
function refreshBrightnessFromSystem() {
|
||||
if (!monitor.isDdc && !monitor.isAppleDisplay) {
|
||||
// For internal displays, query the system directly
|
||||
refreshProc.command = ["sh", "-c", "cat " + monitor.brightnessPath + " && " + "cat " + monitor.maxBrightnessPath]
|
||||
refreshProc.running = true
|
||||
} else if (monitor.isDdc) {
|
||||
// For DDC displays, get the current value
|
||||
refreshProc.command = ["ddcutil", "-b", monitor.busNum, "getvcp", "10", "--brief"]
|
||||
refreshProc.running = true
|
||||
} else if (monitor.isAppleDisplay) {
|
||||
// For Apple displays, get the current value
|
||||
refreshProc.command = ["asdbctl", "get"]
|
||||
refreshProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
// FileView to watch for external brightness changes (internal displays only)
|
||||
readonly property FileView brightnessWatcher: FileView {
|
||||
id: brightnessWatcher
|
||||
// Only set path for internal displays with a valid brightness path
|
||||
path: (!monitor.isDdc && !monitor.isAppleDisplay && monitor.brightnessPath !== "") ? monitor.brightnessPath : ""
|
||||
watchChanges: path !== ""
|
||||
onFileChanged: {
|
||||
// When a file change is detected, actively refresh from system
|
||||
// to ensure we get the most up-to-date value
|
||||
Qt.callLater(() => {
|
||||
monitor.refreshBrightnessFromSystem()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize brightness
|
||||
readonly property Process initProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var dataText = text.trim()
|
||||
if (dataText === "") {
|
||||
return
|
||||
}
|
||||
|
||||
if (monitor.isAppleDisplay) {
|
||||
var val = parseInt(dataText)
|
||||
if (!isNaN(val)) {
|
||||
monitor.brightness = val / 101
|
||||
console.log("Apple display brightness:", monitor.brightness)
|
||||
}
|
||||
} else if (monitor.isDdc) {
|
||||
var parts = dataText.split(" ")
|
||||
if (parts.length >= 4) {
|
||||
var current = parseInt(parts[3])
|
||||
var max = parseInt(parts[4])
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.brightness = current / max
|
||||
console.log("DDC brightness:", monitor.brightness)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Internal backlight - parse the response which includes device path
|
||||
var lines = dataText.split("\n")
|
||||
if (lines.length >= 3) {
|
||||
monitor.backlightDevice = lines[0]
|
||||
monitor.brightnessPath = monitor.backlightDevice + "/brightness"
|
||||
monitor.maxBrightnessPath = monitor.backlightDevice + "/max_brightness"
|
||||
|
||||
var current = parseInt(lines[1])
|
||||
var max = parseInt(lines[2])
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.maxBrightness = max
|
||||
monitor.brightness = current / max
|
||||
console.log("Internal brightness:", monitor.brightness)
|
||||
console.log("Using backlight device:", monitor.backlightDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always update
|
||||
monitor.brightnessUpdated(monitor.brightness)
|
||||
root.monitorBrightnessChanged(monitor, monitor.brightness)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real stepSize: 5.0 / 100.0
|
||||
|
||||
// Timer for debouncing rapid changes
|
||||
readonly property Timer timer: Timer {
|
||||
interval: 100
|
||||
onTriggered: {
|
||||
if (!isNaN(monitor.queuedBrightness)) {
|
||||
monitor.setBrightness(monitor.queuedBrightness)
|
||||
monitor.queuedBrightness = NaN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightnessDebounced(value: real): void {
|
||||
monitor.queuedBrightness = value
|
||||
timer.start()
|
||||
}
|
||||
|
||||
function increaseBrightness(): void {
|
||||
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness
|
||||
setBrightnessDebounced(value + stepSize)
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness
|
||||
setBrightnessDebounced(value - stepSize)
|
||||
}
|
||||
|
||||
function setBrightness(value: real): void {
|
||||
value = Math.max(0, Math.min(1, value))
|
||||
var rounded = Math.round(value * 100)
|
||||
|
||||
if (timer.running) {
|
||||
monitor.queuedBrightness = value
|
||||
return
|
||||
}
|
||||
|
||||
// Update internal value and trigger UI feedback
|
||||
monitor.brightness = value
|
||||
monitor.brightnessUpdated(value)
|
||||
root.monitorBrightnessChanged(monitor, monitor.brightness)
|
||||
|
||||
if (isAppleDisplay) {
|
||||
monitor.ignoreNextChange = true
|
||||
Quickshell.execDetached(["asdbctl", "set", rounded])
|
||||
} else if (isDdc) {
|
||||
monitor.ignoreNextChange = true
|
||||
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded])
|
||||
} else {
|
||||
monitor.ignoreNextChange = true
|
||||
Quickshell.execDetached(["brightnessctl", "s", rounded + "%"])
|
||||
}
|
||||
|
||||
if (isDdc) {
|
||||
timer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function initBrightness(): void {
|
||||
if (isAppleDisplay) {
|
||||
initProc.command = ["asdbctl", "get"]
|
||||
} else if (isDdc) {
|
||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]
|
||||
} else {
|
||||
// Internal backlight - find the first available backlight device and get its info
|
||||
// This now returns: device_path, current_brightness, max_brightness (on separate lines)
|
||||
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do " + " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; " + " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"]
|
||||
}
|
||||
initProc.running = true
|
||||
}
|
||||
|
||||
onBusNumChanged: initBrightness()
|
||||
Component.onCompleted: initBrightness()
|
||||
}
|
||||
}
|
||||
146
quickshell/Services/Caffeine.qml
Normal file
146
quickshell/Services/Caffeine.qml
Normal file
@@ -0,0 +1,146 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
// "systemd", "wayland", or "auto"
|
||||
// systemd-inhibit not found, try Wayland tools
|
||||
// wayhibitor not found
|
||||
|
||||
id: root
|
||||
|
||||
property string reason: "Application request"
|
||||
property bool isInhibited: false
|
||||
property var activeInhibitors: []
|
||||
// Different inhibitor strategies
|
||||
property string strategy: "systemd"
|
||||
|
||||
// Auto-detect the best strategy
|
||||
function detectStrategy() {
|
||||
if (strategy === "auto") {
|
||||
// Check if systemd-inhibit is available
|
||||
try {
|
||||
var systemdResult = Quickshell.execDetached(["which", "systemd-inhibit"]);
|
||||
strategy = "systemd";
|
||||
return ;
|
||||
} catch (e) {
|
||||
}
|
||||
try {
|
||||
var waylandResult = Quickshell.execDetached(["which", "wayhibitor"]);
|
||||
strategy = "wayland";
|
||||
return ;
|
||||
} catch (e) {
|
||||
}
|
||||
strategy = "systemd"; // Fallback to systemd even if not detected
|
||||
}
|
||||
}
|
||||
|
||||
// Add an inhibitor
|
||||
function addInhibitor(id, reason = "Application request") {
|
||||
if (activeInhibitors.includes(id))
|
||||
return false;
|
||||
|
||||
activeInhibitors.push(id);
|
||||
updateInhibition(reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove an inhibitor
|
||||
function removeInhibitor(id) {
|
||||
const index = activeInhibitors.indexOf(id);
|
||||
if (index === -1) {
|
||||
console.log("Inhibitor not found:", id);
|
||||
return false;
|
||||
}
|
||||
activeInhibitors.splice(index, 1);
|
||||
updateInhibition();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update the actual system inhibition
|
||||
function updateInhibition(newReason = reason) {
|
||||
const shouldInhibit = activeInhibitors.length > 0;
|
||||
if (shouldInhibit === isInhibited)
|
||||
return ;
|
||||
|
||||
// No change needed
|
||||
if (shouldInhibit)
|
||||
startInhibition(newReason);
|
||||
else
|
||||
stopInhibition();
|
||||
}
|
||||
|
||||
// Start system inhibition
|
||||
function startInhibition(newReason) {
|
||||
reason = newReason;
|
||||
if (strategy === "systemd")
|
||||
startSystemdInhibition();
|
||||
else if (strategy === "wayland")
|
||||
startWaylandInhibition();
|
||||
else
|
||||
return ;
|
||||
isInhibited = true;
|
||||
}
|
||||
|
||||
// Stop system inhibition
|
||||
function stopInhibition() {
|
||||
if (!isInhibited)
|
||||
return ;
|
||||
|
||||
if (inhibitorProcess.running)
|
||||
inhibitorProcess.signal(15);
|
||||
|
||||
// SIGTERM
|
||||
isInhibited = false;
|
||||
}
|
||||
|
||||
// Systemd inhibition using systemd-inhibit
|
||||
function startSystemdInhibition() {
|
||||
inhibitorProcess.command = ["systemd-inhibit", "--what=idle", "--why=" + reason, "--mode=block", "sleep", "infinity"];
|
||||
inhibitorProcess.running = true;
|
||||
}
|
||||
|
||||
// Wayland inhibition using wayhibitor or similar
|
||||
function startWaylandInhibition() {
|
||||
inhibitorProcess.command = ["wayhibitor"];
|
||||
inhibitorProcess.running = true;
|
||||
}
|
||||
|
||||
// Manual toggle for user control
|
||||
function manualToggle() {
|
||||
if (activeInhibitors.includes("manual")) {
|
||||
removeInhibitor("manual");
|
||||
return false;
|
||||
} else {
|
||||
addInhibitor("manual", "Manually activated by user");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
detectStrategy();
|
||||
}
|
||||
// Clean up on shutdown
|
||||
Component.onDestruction: {
|
||||
stopInhibition();
|
||||
}
|
||||
|
||||
// Process for maintaining the inhibition
|
||||
Process {
|
||||
id: inhibitorProcess
|
||||
|
||||
running: false
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
if (isInhibited)
|
||||
isInhibited = false;
|
||||
|
||||
console.log("Inhibitor process exited with code:", exitCode, "status:", exitStatus);
|
||||
}
|
||||
onStarted: function() {
|
||||
console.log("Inhibitor process started with strategy:", root.strategy);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
131
quickshell/Services/IpService.qml
Normal file
131
quickshell/Services/IpService.qml
Normal file
@@ -0,0 +1,131 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
property string ip: "N/A"
|
||||
property string countryCode: "N/A"
|
||||
property real fetchInterval: 30 // in s
|
||||
property real fetchTimeout: 10 // in s
|
||||
property string ipURL: "https://api.uyanide.com/ip"
|
||||
property string geoURL: "curl https://api.ipinfo.io/lite/"
|
||||
property string geoURLToken: ""
|
||||
|
||||
function fetchIP() {
|
||||
if (fetchIPProcess.running) {
|
||||
console.warn("Fetch IP process is still running, skipping fetchIP");
|
||||
return ;
|
||||
}
|
||||
fetchIPProcess.running = true;
|
||||
}
|
||||
|
||||
function fetchGeoInfo() {
|
||||
if (fetchGeoProcess.running) {
|
||||
console.warn("Fetch geo process is still running, skipping fetchGeoInfo");
|
||||
return ;
|
||||
}
|
||||
if (!ip || ip === "N/A") {
|
||||
countryCode = "N/A";
|
||||
return ;
|
||||
}
|
||||
fetchGeoProcess.command = ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${geoURL}${ip}${geoURLToken ? "?token=" + geoURLToken : ""}`];
|
||||
fetchGeoProcess.running = true;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetchTimer.stop();
|
||||
ip = "N/A";
|
||||
fetchIP();
|
||||
fetchTimer.start();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: tokenFile
|
||||
|
||||
path: Qt.resolvedUrl("../Assets/Ip/token.txt")
|
||||
onLoaded: {
|
||||
geoURLToken = tokenFile.text();
|
||||
if (!geoURLToken)
|
||||
console.warn("No token found for geoIP service, assuming none is required");
|
||||
|
||||
fetchIP();
|
||||
fetchTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fetchTimer
|
||||
|
||||
interval: fetchInterval * 1000
|
||||
repeat: true
|
||||
running: false
|
||||
onTriggered: {
|
||||
fetchIP();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fetchIPProcess
|
||||
|
||||
command: ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${ipURL}`]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: (data) => {
|
||||
let newIP = "";
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
if (response && response.ip) {
|
||||
newIP = response.ip;
|
||||
console.log("Fetched IP: " + newIP);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse IP response: " + e);
|
||||
}
|
||||
if (newIP && newIP !== ip) {
|
||||
ip = newIP;
|
||||
fetchGeoInfo();
|
||||
} else if (!newIP) {
|
||||
ip = "N/A";
|
||||
countryCode = "N/A";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fetchGeoProcess
|
||||
|
||||
command: []
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: (data) => {
|
||||
let newCountryCode = "";
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
if (response && response.country) {
|
||||
newCountryCode = response.country_code;
|
||||
console.log("Fetched country code: " + newCountryCode);
|
||||
SendNotification.show("New IP", `IP: ${ip}\nCountry: ${newCountryCode}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse geo response: " + e);
|
||||
}
|
||||
if (newCountryCode && newCountryCode !== countryCode)
|
||||
countryCode = newCountryCode;
|
||||
else if (!newCountryCode)
|
||||
countryCode = "N/A";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
164
quickshell/Services/MusicManager.qml
Normal file
164
quickshell/Services/MusicManager.qml
Normal file
@@ -0,0 +1,164 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Modules.Misc
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: manager
|
||||
|
||||
// Properties
|
||||
property var currentPlayer: null
|
||||
property real currentPosition: 0
|
||||
property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false
|
||||
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
|
||||
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
|
||||
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
|
||||
property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : ""
|
||||
property real trackLength: currentPlayer ? currentPlayer.length : 0
|
||||
property bool canPlay: currentPlayer ? currentPlayer.canPlay : false
|
||||
property bool canPause: currentPlayer ? currentPlayer.canPause : false
|
||||
property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false
|
||||
property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false
|
||||
property bool canSeek: currentPlayer ? currentPlayer.canSeek : false
|
||||
property bool hasPlayer: getAvailablePlayers().length > 0
|
||||
// Expose cava values
|
||||
property alias cavaValues: cava.values
|
||||
|
||||
// Returns available MPRIS players
|
||||
function getAvailablePlayers() {
|
||||
if (!Mpris.players || !Mpris.players.values)
|
||||
return [];
|
||||
|
||||
let allPlayers = Mpris.players.values;
|
||||
let controllablePlayers = [];
|
||||
for (let i = 0; i < allPlayers.length; i++) {
|
||||
let player = allPlayers[i];
|
||||
if (player && player.canControl)
|
||||
controllablePlayers.push(player);
|
||||
|
||||
}
|
||||
return controllablePlayers;
|
||||
}
|
||||
|
||||
// Returns active player or first available
|
||||
function findActivePlayer() {
|
||||
let availablePlayers = getAvailablePlayers();
|
||||
if (availablePlayers.length === 0)
|
||||
return null;
|
||||
|
||||
// Get the first playing player
|
||||
for (let i = availablePlayers.length - 1; i >= 0; i--) {
|
||||
if (availablePlayers[i].isPlaying)
|
||||
return availablePlayers[i];
|
||||
|
||||
}
|
||||
// Fallback to last player
|
||||
return availablePlayers[availablePlayers.length - 1];
|
||||
}
|
||||
|
||||
// Updates currentPlayer and currentPosition
|
||||
function updateCurrentPlayer() {
|
||||
let newPlayer = findActivePlayer();
|
||||
if (newPlayer !== currentPlayer) {
|
||||
currentPlayer = newPlayer;
|
||||
currentPosition = currentPlayer ? currentPlayer.position : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Player control functions
|
||||
function playPause() {
|
||||
if (currentPlayer) {
|
||||
if (currentPlayer.isPlaying)
|
||||
currentPlayer.pause();
|
||||
else
|
||||
currentPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
function isAllPaused() {
|
||||
let availablePlayers = getAvailablePlayers();
|
||||
for (let i = 0; i < availablePlayers.length; i++) {
|
||||
if (availablePlayers[i].isPlaying)
|
||||
return false;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function play() {
|
||||
if (currentPlayer && currentPlayer.canPlay)
|
||||
currentPlayer.play();
|
||||
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (currentPlayer && currentPlayer.canPause)
|
||||
currentPlayer.pause();
|
||||
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (currentPlayer && currentPlayer.canGoNext)
|
||||
currentPlayer.next();
|
||||
|
||||
}
|
||||
|
||||
function previous() {
|
||||
if (currentPlayer && currentPlayer.canGoPrevious)
|
||||
currentPlayer.previous();
|
||||
|
||||
}
|
||||
|
||||
function seek(position) {
|
||||
if (currentPlayer && currentPlayer.canSeek) {
|
||||
currentPlayer.position = position;
|
||||
currentPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
function seekByRatio(ratio) {
|
||||
if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) {
|
||||
let seekPosition = ratio * currentPlayer.length;
|
||||
currentPlayer.position = seekPosition;
|
||||
currentPosition = seekPosition;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
Item {
|
||||
Component.onCompleted: {
|
||||
updateCurrentPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
// Updates progress bar every second
|
||||
Timer {
|
||||
id: positionTimer
|
||||
|
||||
interval: 1000
|
||||
running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (currentPlayer && currentPlayer.isPlaying)
|
||||
currentPosition = currentPlayer.position;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Reacts to player list changes
|
||||
Connections {
|
||||
function onValuesChanged() {
|
||||
updateCurrentPlayer();
|
||||
}
|
||||
|
||||
target: Mpris.players
|
||||
}
|
||||
|
||||
Cava {
|
||||
id: cava
|
||||
|
||||
count: 44
|
||||
}
|
||||
|
||||
}
|
||||
184
quickshell/Services/Niri.qml
Normal file
184
quickshell/Services/Niri.qml
Normal file
@@ -0,0 +1,184 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var workspaces: []
|
||||
property var windows: {}
|
||||
property int focusedWindowId: -1
|
||||
property bool noFocus: focusedWindowId === -1
|
||||
property bool inOverview: false
|
||||
property string focusedWindowTitle: ""
|
||||
// property string focusedWindowAppId: ""
|
||||
|
||||
function updateFocusedWindowTitle() {
|
||||
if (windows && windows[focusedWindowId]) {
|
||||
focusedWindowTitle = windows[focusedWindowId].title || "";
|
||||
// focusedWindowAppId = windows[focusedWindowId].appId || "";
|
||||
} else {
|
||||
focusedWindowTitle = "";
|
||||
// focusedWindowAppId = "";
|
||||
}
|
||||
}
|
||||
|
||||
function getFocusedWindow() {
|
||||
return (windows && windows[focusedWindowId]) || null;
|
||||
}
|
||||
|
||||
onWindowsChanged: updateFocusedWindowTitle()
|
||||
onFocusedWindowIdChanged: updateFocusedWindowTitle()
|
||||
Component.onCompleted: {
|
||||
eventStream.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: workspaceProcess
|
||||
|
||||
running: false
|
||||
command: ["niri", "msg", "--json", "workspaces"]
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: function(line) {
|
||||
try {
|
||||
const workspacesData = JSON.parse(line);
|
||||
const workspacesList = [];
|
||||
for (const ws of workspacesData) {
|
||||
workspacesList.push({
|
||||
"id": ws.id,
|
||||
"idx": ws.idx,
|
||||
"name": ws.name || "",
|
||||
"output": ws.output || "",
|
||||
"isFocused": ws.is_focused === true,
|
||||
"isActive": ws.is_active === true,
|
||||
"isUrgent": ws.is_urgent === true,
|
||||
"activeWindowId": ws.active_window_id
|
||||
});
|
||||
}
|
||||
workspacesList.sort((a, b) => {
|
||||
if (a.output !== b.output)
|
||||
return a.output.localeCompare(b.output);
|
||||
|
||||
return a.id - b.id;
|
||||
});
|
||||
root.workspaces = workspacesList;
|
||||
} catch (e) {
|
||||
console.error("Failed to parse workspaces:", e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: eventStream
|
||||
|
||||
running: false
|
||||
command: ["niri", "msg", "--json", "event-stream"]
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: (data) => {
|
||||
try {
|
||||
const event = JSON.parse(data.trim());
|
||||
if (event.WorkspacesChanged) {
|
||||
workspaceProcess.running = true;
|
||||
} else if (event.WindowsChanged) {
|
||||
try {
|
||||
const windowsData = event.WindowsChanged.windows;
|
||||
const windowsMap = {};
|
||||
for (const win of windowsData) {
|
||||
if (win.is_focused === true) {
|
||||
root.focusedWindowId = win.id;
|
||||
}
|
||||
windowsMap[win.id] = {
|
||||
"title": win.title || "",
|
||||
"appId": win.app_id || "",
|
||||
"workspaceId": win.workspace_id || null,
|
||||
"isFocused": win.is_focused === true
|
||||
};
|
||||
}
|
||||
root.windows = windowsMap;
|
||||
} catch (e) {
|
||||
console.error("Error parsing windows event:", e);
|
||||
}
|
||||
} else if (event.WorkspaceActivated) {
|
||||
workspaceProcess.running = true;
|
||||
} else if (event.WindowFocusChanged) {
|
||||
try {
|
||||
const focusedId = event.WindowFocusChanged.id;
|
||||
if (focusedId) {
|
||||
if (root.windows[focusedId]) {
|
||||
root.focusedWindowId = focusedId;
|
||||
} else {
|
||||
root.focusedWindowId = -1;
|
||||
}
|
||||
|
||||
} else {
|
||||
root.focusedWindowId = -1;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window focus event:", e);
|
||||
}
|
||||
} else if (event.OverviewOpenedOrClosed) {
|
||||
try {
|
||||
root.inOverview = event.OverviewOpenedOrClosed.is_open === true;
|
||||
} catch (e) {
|
||||
console.error("Error parsing overview state:", e);
|
||||
}
|
||||
} else if (event.WindowOpenedOrChanged) {
|
||||
try {
|
||||
const targetWin = event.WindowOpenedOrChanged.window;
|
||||
const id = targetWin.id;
|
||||
const isFocused = targetWin.is_focused === true;
|
||||
let needUpdateTitle = false;
|
||||
if (id) {
|
||||
if (root.windows && root.windows[id]) {
|
||||
const win = root.windows[id];
|
||||
// Update existing window
|
||||
needUpdateTitle = win.title !== targetWin.title;
|
||||
win.title = targetWin.title || win.title;
|
||||
win.appId = targetWin.app_id || win.appId;
|
||||
win.workspaceId = targetWin.workspace_id || win.workspaceId;
|
||||
win.isFocused = isFocused;
|
||||
} else {
|
||||
// New window
|
||||
const newWin = {
|
||||
"title": targetWin.title || "",
|
||||
"appId": targetWin.app_id || "",
|
||||
"workspaceId": targetWin.workspace_id || null,
|
||||
"isFocused": isFocused
|
||||
};
|
||||
root.windows[id] = targetWin;
|
||||
}
|
||||
if (isFocused) {
|
||||
root.focusedWindowId = id;
|
||||
if (needUpdateTitle)
|
||||
root.updateFocusedWindowTitle();
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window opened/changed event:", e);
|
||||
}
|
||||
} else if (event.windowClosed) {
|
||||
try {
|
||||
const closedId = event.windowClosed.id;
|
||||
if (closedId && (root.windows && root.windows[closedId])) {
|
||||
delete root.windows[closedId];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window closed event:", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing event stream:", e, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
67
quickshell/Services/PanelService.qml
Normal file
67
quickshell/Services/PanelService.qml
Normal file
@@ -0,0 +1,67 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// A ref. to the lockScreen, so it's accessible from anywhere
|
||||
// This is not a panel...
|
||||
property var lockScreen: null
|
||||
|
||||
// Panels
|
||||
property var registeredPanels: ({})
|
||||
property var openedPanel: null
|
||||
signal willOpen
|
||||
|
||||
// Currently opened popups, can have more than one.
|
||||
// ex: when opening an NIconPicker from a widget setting.
|
||||
property var openedPopups: []
|
||||
property bool hasOpenedPopup: false
|
||||
signal popupChanged
|
||||
|
||||
// Register this panel
|
||||
function registerPanel(panel) {
|
||||
registeredPanels[panel.objectName] = panel
|
||||
}
|
||||
|
||||
// Returns a panel
|
||||
function getPanel(name) {
|
||||
return registeredPanels[name] || null
|
||||
}
|
||||
|
||||
// Check if a panel exists
|
||||
function hasPanel(name) {
|
||||
return name in registeredPanels
|
||||
}
|
||||
|
||||
// Helper to keep only one panel open at any time
|
||||
function willOpenPanel(panel) {
|
||||
if (openedPanel && openedPanel !== panel) {
|
||||
openedPanel.close()
|
||||
}
|
||||
openedPanel = panel
|
||||
|
||||
// emit signal
|
||||
willOpen()
|
||||
}
|
||||
|
||||
function closedPanel(panel) {
|
||||
if (openedPanel && openedPanel === panel) {
|
||||
openedPanel = null
|
||||
}
|
||||
}
|
||||
|
||||
// Popups
|
||||
function willOpenPopup(popup) {
|
||||
openedPopups.push(popup)
|
||||
hasOpenedPopup = (openedPopups.length !== 0)
|
||||
popupChanged()
|
||||
}
|
||||
|
||||
function willClosePopup(popup) {
|
||||
openedPopups = openedPopups.filter(p => p !== popup)
|
||||
hasOpenedPopup = (openedPopups.length !== 0)
|
||||
popupChanged()
|
||||
}
|
||||
}
|
||||
23
quickshell/Services/SendNotification.qml
Normal file
23
quickshell/Services/SendNotification.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function show(title, message, icon = "", urgency = "normal", timeout = 5000) {
|
||||
if (icon)
|
||||
action.command = ["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message];
|
||||
else
|
||||
action.command = ["notify-send", "-u", urgency, "-t", timeout.toString(), title, message];
|
||||
action.startDetached();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
392
quickshell/Services/SystemStatService.qml
Normal file
392
quickshell/Services/SystemStatService.qml
Normal file
@@ -0,0 +1,392 @@
|
||||
import Qt.labs.folderlistmodel
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
// For Intel coretemp, start averaging all available sensors/cores
|
||||
|
||||
id: root
|
||||
|
||||
// Public values
|
||||
property real cpuUsage: 0
|
||||
property real cpuTemp: 0
|
||||
property real memGb: 0
|
||||
property real memPercent: 0
|
||||
property real diskPercent: 0
|
||||
property real rxSpeed: 0
|
||||
property real txSpeed: 0
|
||||
// Configuration
|
||||
property int sleepDuration: 3000
|
||||
property int fasterSleepDuration: 1000
|
||||
// Internal state for CPU calculation
|
||||
property var prevCpuStats: null
|
||||
// Internal state for network speed calculation
|
||||
// Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered
|
||||
// since the computer started, so their value will easily overlfow a 32bit int.
|
||||
property real prevRxBytes: 0
|
||||
property real prevTxBytes: 0
|
||||
property real prevTime: 0
|
||||
// Cpu temperature is the most complex
|
||||
readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"]
|
||||
property string cpuTempSensorName: ""
|
||||
property string cpuTempHwmonPath: ""
|
||||
// For Intel coretemp averaging of all cores/sensors
|
||||
property var intelTempValues: []
|
||||
property int intelTempFilesChecked: 0
|
||||
property int intelTempMaxFiles: 20 // Will test up to temp20_input
|
||||
|
||||
// -------------------------------------------------------
|
||||
// -------------------------------------------------------
|
||||
// Parse memory info from /proc/meminfo
|
||||
function parseMemoryInfo(text) {
|
||||
if (!text)
|
||||
return ;
|
||||
|
||||
const lines = text.split('\n');
|
||||
let memTotal = 0;
|
||||
let memAvailable = 0;
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('MemTotal:'))
|
||||
memTotal = parseInt(line.split(/\s+/)[1]) || 0;
|
||||
else if (line.startsWith('MemAvailable:'))
|
||||
memAvailable = parseInt(line.split(/\s+/)[1]) || 0;
|
||||
}
|
||||
if (memTotal > 0) {
|
||||
const usageKb = memTotal - memAvailable;
|
||||
root.memGb = (usageKb / 1e+06).toFixed(1);
|
||||
root.memPercent = Math.round((usageKb / memTotal) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Calculate CPU usage from /proc/stat
|
||||
function calculateCpuUsage(text) {
|
||||
if (!text)
|
||||
return ;
|
||||
|
||||
const lines = text.split('\n');
|
||||
const cpuLine = lines[0];
|
||||
// First line is total CPU
|
||||
if (!cpuLine.startsWith('cpu '))
|
||||
return ;
|
||||
|
||||
const parts = cpuLine.split(/\s+/);
|
||||
const stats = {
|
||||
"user": parseInt(parts[1]) || 0,
|
||||
"nice": parseInt(parts[2]) || 0,
|
||||
"system": parseInt(parts[3]) || 0,
|
||||
"idle": parseInt(parts[4]) || 0,
|
||||
"iowait": parseInt(parts[5]) || 0,
|
||||
"irq": parseInt(parts[6]) || 0,
|
||||
"softirq": parseInt(parts[7]) || 0,
|
||||
"steal": parseInt(parts[8]) || 0,
|
||||
"guest": parseInt(parts[9]) || 0,
|
||||
"guestNice": parseInt(parts[10]) || 0
|
||||
};
|
||||
const totalIdle = stats.idle + stats.iowait;
|
||||
const total = Object.values(stats).reduce((sum, val) => {
|
||||
return sum + val;
|
||||
}, 0);
|
||||
if (root.prevCpuStats) {
|
||||
const prevTotalIdle = root.prevCpuStats.idle + root.prevCpuStats.iowait;
|
||||
const prevTotal = Object.values(root.prevCpuStats).reduce((sum, val) => {
|
||||
return sum + val;
|
||||
}, 0);
|
||||
const diffTotal = total - prevTotal;
|
||||
const diffIdle = totalIdle - prevTotalIdle;
|
||||
if (diffTotal > 0)
|
||||
root.cpuUsage = (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1);
|
||||
|
||||
}
|
||||
root.prevCpuStats = stats;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Calculate RX and TX speed from /proc/net/dev
|
||||
// Average speed of all interfaces excepted 'lo'
|
||||
function calculateNetworkSpeed(text) {
|
||||
if (!text)
|
||||
return ;
|
||||
|
||||
const currentTime = Date.now() / 1000;
|
||||
const lines = text.split('\n');
|
||||
let totalRx = 0;
|
||||
let totalTx = 0;
|
||||
for (var i = 2; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line)
|
||||
continue;
|
||||
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex === -1)
|
||||
continue;
|
||||
|
||||
const iface = line.substring(0, colonIndex).trim();
|
||||
if (iface === 'lo')
|
||||
continue;
|
||||
|
||||
const statsLine = line.substring(colonIndex + 1).trim();
|
||||
const stats = statsLine.split(/\s+/);
|
||||
const rxBytes = parseInt(stats[0], 10) || 0;
|
||||
const txBytes = parseInt(stats[8], 10) || 0;
|
||||
totalRx += rxBytes;
|
||||
totalTx += txBytes;
|
||||
}
|
||||
// Compute only if we have a previous run to compare to.
|
||||
if (root.prevTime > 0) {
|
||||
const timeDiff = currentTime - root.prevTime;
|
||||
// Avoid division by zero if time hasn't passed.
|
||||
if (timeDiff > 0) {
|
||||
let rxDiff = totalRx - root.prevRxBytes;
|
||||
let txDiff = totalTx - root.prevTxBytes;
|
||||
// Handle counter resets (e.g., WiFi reconnect), which would cause a negative value.
|
||||
if (rxDiff < 0)
|
||||
rxDiff = 0;
|
||||
|
||||
if (txDiff < 0)
|
||||
txDiff = 0;
|
||||
|
||||
root.rxSpeed = Math.round(rxDiff / timeDiff); // Speed in Bytes/s
|
||||
root.txSpeed = Math.round(txDiff / timeDiff);
|
||||
}
|
||||
}
|
||||
root.prevRxBytes = totalRx;
|
||||
root.prevTxBytes = totalTx;
|
||||
root.prevTime = currentTime;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Helper function to format network speeds
|
||||
function formatSpeed(bytesPerSecond) {
|
||||
if (bytesPerSecond < 1024 * 1024) {
|
||||
const kb = bytesPerSecond / 1024;
|
||||
if (kb < 10)
|
||||
return kb.toFixed(1) + "KB";
|
||||
else
|
||||
return Math.round(kb) + "KB";
|
||||
} else if (bytesPerSecond < 1024 * 1024 * 1024) {
|
||||
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB";
|
||||
} else {
|
||||
return (bytesPerSecond / (1024 * 1024 * 1024)).toFixed(1) + "GB";
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Compact speed formatter for vertical bar display
|
||||
function formatCompactSpeed(bytesPerSecond) {
|
||||
if (!bytesPerSecond || bytesPerSecond <= 0)
|
||||
return "0";
|
||||
|
||||
const units = ["", "K", "M", "G"];
|
||||
let value = bytesPerSecond;
|
||||
let unitIndex = 0;
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value = value / 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
// Promote at ~100 of current unit (e.g., 100k -> ~0.1M shown as 0.1M or 0M if rounded)
|
||||
if (unitIndex < units.length - 1 && value >= 100) {
|
||||
value = value / 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
const display = Math.round(value).toString();
|
||||
return display + units[unitIndex];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Function to start fetching and computing the cpu temperature
|
||||
function updateCpuTemperature() {
|
||||
// For AMD sensors (k10temp and zenpower), only use Tctl sensor
|
||||
// temp1_input corresponds to Tctl (Temperature Control) on these sensors
|
||||
if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") {
|
||||
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input`;
|
||||
cpuTempReader.reload();
|
||||
} else if (root.cpuTempSensorName === "coretemp") {
|
||||
root.intelTempValues = [];
|
||||
root.intelTempFilesChecked = 0;
|
||||
checkNextIntelTemp();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Function to check next Intel temperature sensor
|
||||
function checkNextIntelTemp() {
|
||||
if (root.intelTempFilesChecked >= root.intelTempMaxFiles) {
|
||||
// Calculate average of all found temperatures
|
||||
if (root.intelTempValues.length > 0) {
|
||||
let sum = 0;
|
||||
for (var i = 0; i < root.intelTempValues.length; i++) {
|
||||
sum += root.intelTempValues[i];
|
||||
}
|
||||
root.cpuTemp = Math.round(sum / root.intelTempValues.length);
|
||||
} else {
|
||||
console.warn("No temperature sensors found for coretemp");
|
||||
root.cpuTemp = 0;
|
||||
}
|
||||
return ;
|
||||
}
|
||||
// Check next temperature file
|
||||
root.intelTempFilesChecked++;
|
||||
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input`;
|
||||
cpuTempReader.reload();
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
Component.onCompleted: {
|
||||
// Kickoff the cpu name detection for temperature
|
||||
cpuTempNameReader.checkNext();
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Timer for periodic updates
|
||||
Timer {
|
||||
id: updateTimer
|
||||
|
||||
interval: root.sleepDuration
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
// Trigger all direct system files reads
|
||||
memInfoFile.reload();
|
||||
cpuStatFile.reload();
|
||||
// Run df (disk free) one time
|
||||
dfProcess.running = true;
|
||||
updateCpuTemperature();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fasterUpdateTimer
|
||||
|
||||
interval: root.fasterSleepDuration
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
netDevFile.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// FileView components for reading system files
|
||||
FileView {
|
||||
id: memInfoFile
|
||||
|
||||
path: "/proc/meminfo"
|
||||
onLoaded: parseMemoryInfo(text())
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: cpuStatFile
|
||||
|
||||
path: "/proc/stat"
|
||||
onLoaded: calculateCpuUsage(text())
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: netDevFile
|
||||
|
||||
path: "/proc/net/dev"
|
||||
onLoaded: calculateNetworkSpeed(text())
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Process to fetch disk usage in percent
|
||||
// Uses 'df' aka 'disk free'
|
||||
Process {
|
||||
id: dfProcess
|
||||
|
||||
command: ["df", "--output=pcent", "/"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const lines = text.trim().split('\n');
|
||||
if (lines.length >= 2) {
|
||||
const percent = lines[1].replace(/[^0-9]/g, '');
|
||||
root.diskPercent = parseInt(percent) || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// --------------------------------------------
|
||||
// CPU Temperature
|
||||
// It's more complex.
|
||||
// ----
|
||||
// #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower"
|
||||
FileView {
|
||||
id: cpuTempNameReader
|
||||
|
||||
property int currentIndex: 0
|
||||
|
||||
function checkNext() {
|
||||
if (currentIndex >= 16) {
|
||||
// Check up to hwmon10
|
||||
console.warn("No supported temperature sensor found");
|
||||
return ;
|
||||
}
|
||||
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`;
|
||||
cpuTempNameReader.reload();
|
||||
}
|
||||
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
const name = text().trim();
|
||||
if (root.supportedTempCpuSensorNames.includes(name)) {
|
||||
root.cpuTempSensorName = name;
|
||||
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`;
|
||||
console.log(`Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`);
|
||||
} else {
|
||||
currentIndex++;
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNext();
|
||||
});
|
||||
}
|
||||
}
|
||||
onLoadFailed: function(error) {
|
||||
currentIndex++;
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNext();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
// #2 - Read sensor value
|
||||
FileView {
|
||||
id: cpuTempReader
|
||||
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
const data = text().trim();
|
||||
if (root.cpuTempSensorName === "coretemp") {
|
||||
// For Intel, collect all temperature values
|
||||
const temp = parseInt(data) / 1000;
|
||||
//console.log(temp, cpuTempReader.path)
|
||||
root.intelTempValues.push(temp);
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNextIntelTemp();
|
||||
});
|
||||
} else {
|
||||
// For AMD sensors (k10temp and zenpower), directly set the temperature
|
||||
root.cpuTemp = Math.round(parseInt(data) / 1000);
|
||||
}
|
||||
}
|
||||
onLoadFailed: function(error) {
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNextIntelTemp();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
46
quickshell/Services/ThemeIcons.qml
Normal file
46
quickshell/Services/ThemeIcons.qml
Normal file
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
// ignore and fall back
|
||||
|
||||
id: root
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback);
|
||||
if (p && p !== "")
|
||||
return p;
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
|
||||
} catch (e2) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve icon path for a DesktopEntries appId - safe on missing entries
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback);
|
||||
|
||||
try {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return iconFromName(fallback, fallback);
|
||||
|
||||
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId);
|
||||
const name = entry && entry.icon ? entry.icon : "";
|
||||
return iconFromName(name || fallback, fallback);
|
||||
} catch (e) {
|
||||
return iconFromName(fallback, fallback);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
44
quickshell/Services/TimeService.qml
Normal file
44
quickshell/Services/TimeService.qml
Normal file
@@ -0,0 +1,44 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var date: new Date()
|
||||
property string time: Qt.formatDateTime(date, "HH:mm")
|
||||
property string dateString: {
|
||||
let now = date;
|
||||
let dayName = now.toLocaleDateString(Qt.locale(), "ddd");
|
||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1);
|
||||
let day = now.getDate();
|
||||
let suffix;
|
||||
if (day > 3 && day < 21)
|
||||
suffix = 'th';
|
||||
else
|
||||
switch (day % 10) {
|
||||
case 1:
|
||||
suffix = "st";
|
||||
break;
|
||||
case 2:
|
||||
suffix = "nd";
|
||||
break;
|
||||
case 3:
|
||||
suffix = "rd";
|
||||
break;
|
||||
default:
|
||||
suffix = "th";
|
||||
};
|
||||
let month = now.toLocaleDateString(Qt.locale(), "MMMM");
|
||||
let year = now.toLocaleDateString(Qt.locale(), "yyyy");
|
||||
return `${dayName}, ` + `${day}${suffix} ${month} ${year}`;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: root.date = new Date()
|
||||
}
|
||||
|
||||
}
|
||||
55
quickshell/Services/WorkspaceManager.qml
Normal file
55
quickshell/Services/WorkspaceManager.qml
Normal file
@@ -0,0 +1,55 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property ListModel workspaces
|
||||
|
||||
workspaces: ListModel {
|
||||
}
|
||||
|
||||
function initNiri() {
|
||||
updateNiriWorkspaces();
|
||||
}
|
||||
|
||||
function updateNiriWorkspaces() {
|
||||
const niriWorkspaces = Niri.workspaces || [];
|
||||
workspaces.clear();
|
||||
for (let i = 0; i < niriWorkspaces.length; i++) {
|
||||
const ws = niriWorkspaces[i];
|
||||
workspaces.append({
|
||||
"id": ws.id,
|
||||
"idx": ws.idx || 1,
|
||||
"name": ws.name || "",
|
||||
"output": ws.output || "",
|
||||
"isFocused": ws.isFocused === true,
|
||||
"isActive": ws.isActive === true,
|
||||
"isUrgent": ws.isUrgent === true
|
||||
});
|
||||
}
|
||||
workspacesChanged();
|
||||
}
|
||||
|
||||
function switchToWorkspace(workspaceId) {
|
||||
try {
|
||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]);
|
||||
} catch (e) {
|
||||
console.error("Error switching Niri workspace:", e);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onWorkspacesChanged() {
|
||||
updateNiriWorkspaces();
|
||||
}
|
||||
|
||||
target: Niri
|
||||
}
|
||||
|
||||
}
|
||||
20
quickshell/Services/WriteClipboard.qml
Normal file
20
quickshell/Services/WriteClipboard.qml
Normal file
@@ -0,0 +1,20 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function write(text) {
|
||||
action.command = ["sh", "-c", `echo ${text} | wl-copy -n`];
|
||||
action.startDetached();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
23
quickshell/shell.qml
Normal file
23
quickshell/shell.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar
|
||||
import qs.Modules.Misc
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
Bar {
|
||||
id: bar
|
||||
|
||||
shell: root
|
||||
}
|
||||
|
||||
Corners {
|
||||
id: corners
|
||||
|
||||
shell: root
|
||||
}
|
||||
|
||||
}
|
||||
@@ -136,8 +136,8 @@
|
||||
"format-alt-click": "click-right",
|
||||
//"format-icons": ["", ""],
|
||||
"format-icons": [""],
|
||||
"on-scroll-down": "brightnessctl -d $HYPR_DISPLAY_DEVICE set 5%-",
|
||||
"on-scroll-up": "brightnessctl -d $HYPR_DISPLAY_DEVICE set +5%",
|
||||
"on-scroll-down": "brightnessctl --class=backlight set -5%",
|
||||
"on-scroll-up": "brightnessctl --class=backlight set +5%",
|
||||
"max-length": 6,
|
||||
"min-length": 6
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user