diff --git a/.licenses/noctalia-dev/noctalia-shell b/.licenses/noctalia-dev/noctalia-shell
new file mode 100644
index 0000000..2bdaed9
--- /dev/null
+++ b/.licenses/noctalia-dev/noctalia-shell
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 noctalia-dev
+
+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.
diff --git a/.scripts/change-colortheme b/.scripts/change-colortheme
index bcf45e4..93c372d 100755
--- a/.scripts/change-colortheme
+++ b/.scripts/change-colortheme
@@ -34,6 +34,8 @@
- fuzzel: edit $HOME/.config/fuzzel/fuzzel.ini
- niri: edit $HOME/.config/niri/config.kdl
+
+- quickshell qs ipc call colors setPrimary $hex
'''
import os
@@ -216,6 +218,11 @@ def _change_niri(palette: dict[str, str], flavor: str):
replace_placeholders(niri_dist, palette, flavor)
+def _change_quickshell(palette: dict[str, str], flavor: str):
+ hex_color = palette[flavor]
+ os.system(f'qs ipc call colors setPrimary {hex_color}')
+
+
apply_theme_funcs: dict[str, Callable[[dict[str, str], str], None]] = {
'kvantum': _change_kvantum,
# 'nwg-look': _change_nwglook,
@@ -230,6 +237,7 @@ apply_theme_funcs: dict[str, Callable[[dict[str, str], str], None]] = {
'wlogout': _change_wlogout,
'fuzzel': _change_fuzzel,
'niri': _change_niri,
+ 'quickshell': _change_quickshell,
}
@@ -333,7 +341,8 @@ def main():
parser.add_argument('-i', '--image', type=str, help="Path to the image")
parser.add_argument('-f', '--flavor', type=str, help="Flavor to apply")
parser.add_argument('-c', '--color', type=str, help="Color to match from the palette")
- parser.add_argument('arguments', nargs='*', help="List of applications to change the color theme of")
+ parser.add_argument('arguments', nargs='*',
+ help="List of applications to change the color theme of, or !app to exclude an application. Available apps: " + ', '.join(apply_theme_funcs.keys()))
arguments = parser.parse_args()
@@ -363,15 +372,34 @@ def main():
return flavor
def parse_apps() -> list[str]:
+ apps = set()
if not arguments.arguments:
- return list(apply_theme_funcs.keys())
- apps = []
+ apps = set(apply_theme_funcs.keys())
+ else:
+ allExclude = True
+ for arg in arguments.arguments:
+ if arg[0] == '!':
+ continue
+ allExclude = False
+ if arg not in apply_theme_funcs:
+ print(f"Unknown app: {arg}. Available apps: {', '.join(apply_theme_funcs.keys())}")
+ sys.exit(1)
+ apps.add(arg)
+
+ # If all arguments are exclusions, start with all apps
+ if allExclude:
+ apps = set(apply_theme_funcs.keys())
+
for arg in arguments.arguments:
- if arg not in apply_theme_funcs:
- print(f"Unknown app: {arg}. Available apps: {', '.join(apply_theme_funcs.keys())}")
- sys.exit(1)
- apps.append(arg)
- return apps
+ if arg[0] == '!':
+ print(f"Excluding app: {arg[1:]}")
+ app = arg[1:]
+ if app not in apply_theme_funcs:
+ print(f"Unknown app to exclude: {app}. Available apps: {', '.join(apply_theme_funcs.keys())}")
+ sys.exit(1)
+ apps.discard(app)
+
+ return list(apps)
palette_name = parse_palette_name()
palette = PALETTES[palette_name]
diff --git a/.scripts/change-wallpaper b/.scripts/change-wallpaper
index 78cff48..789ef91 100755
--- a/.scripts/change-wallpaper
+++ b/.scripts/change-wallpaper
@@ -80,13 +80,13 @@ if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
notify-send "Wallpaper Changed" "$image"
- change-colortheme -i "$image_copied" || exit 1
+ change-colortheme -i "$image_copied" !quickshell || exit 1
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
swww img -n background "$image_copied" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
notify-send "Wallpaper Changed" "$image"
- change-colortheme -i "$image_copied" || exit 1
+ change-colortheme -i "$image_copied" !waybar !eww || exit 1
else
echo "Unsupported desktop environment: $XDG_CURRENT_DESKTOP"
exit 1
diff --git a/.scripts/config-switch b/.scripts/config-switch
index cbdc2ab..c88bdb9 100755
--- a/.scripts/config-switch
+++ b/.scripts/config-switch
@@ -6,7 +6,7 @@ else
desktop="$1"
fi
-for item in "waybar" "kitty" "ghostty" "wlogout"; do
+for item in "kitty" "ghostty" "wlogout"; do
[ -L "$HOME/.config/$item" ] || exit 1
rm "$HOME/.config/$item"
diff --git a/.scripts/sl-wrap b/.scripts/sl-wrap
new file mode 100755
index 0000000..81e10fd
--- /dev/null
+++ b/.scripts/sl-wrap
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+pgrep -f spotify-lyrics && (killall spotify-lyrics || exit 1)
+spotify-lyrics "$@"
diff --git a/README.md b/README.md
index d9302cc..18fdfdb 100644
--- a/README.md
+++ b/README.md
@@ -1,35 +1,43 @@
## Screenshots
-- desktop with a few widgets:
-
+- Hyprland & Waybar & Eww:
-- dynamic colortheme based on Catppuccin Mocha:
-
+
-- the grub menu looks like:
+- Niri & Quickshell
+
+
+
+
+- Grub menu:
## Setup Overview
- **OS**: Archlinux
-- **WM**: Niri & Hyprland (looks similar through screenshots)
-- **Bar**: Waybar
+- **WM**: Hyprland | Niri
+- **Bar**: Waybar | Quickshell
- **Shell**: Fish
- **Prompt**: Oh My Posh
- **Terminal**: Kitty & Ghostty
- **Colorscheme**: Catppuccin Mocha
- **App Launcher**: Rofi
- **Logout Screen**: Wlogout
-- **Desktop Widgets**: Eww
+- **Desktop Widgets**: Eww | Quickshell
+- **Wallpaper Darmon**: swww
- **Notification Daemon**: Mako
## Hyprland & friends
-Based on [end-4/dots-hyprland](https://github.com/end-4/dots-hyprland) but without ags amd tons of other stuff.
+Based on an old version of [end-4/dots-hyprland](https://github.com/end-4/dots-hyprland) but without ags amd tons of other stuff.
## Niri
-Ported from Hyprland, and shares most of the desktop components such as hyprlock & eww widgets & rofi & waybar & mako.
+Ported from Hyprland, and shares some of the desktop components such as hyprlock & hypridle & mako, but uses quickshell as bar and desktop-widgets instead of the combination of waybar and eww.
+
+## Quickshell
+
+Not based on, but heaviely depends on modules from [noctalia-shell](https://github.com/noctalia-dev/noctalia-shell). This setup is currently only adapted for Niri.
## Eww
@@ -37,13 +45,17 @@ Ported from Hyprland, and shares most of the desktop components such as hyprlock
- `lyrics`, scrolling lyrics player, depends on [a small program](https://github.com/Uyanide/spotify-lyrics) from myself (which also happens to be my frist Golang program :D).
- `lyrics-single`, similar to `lyrics`, but only with a single line and can be easily embeded into the status bar.
+# SWWW
+
+In Niri, the wallpaper will be automatically blurred when there are windows in focus. And the backdrop also has a blurred wallpaper applied to it. These are implemented in [wallpaper-daemon](https://github.com/Uyanide/dotfiles/blob/main/.scripts/wallpaper-daemon).
+
## Rofi
Based on [codeopshq/dotfiles](https://github.com/codeopshq/dotfiles), also serves as cliphist browser and emojis picker.
## Grub theme
-Based on [vinceliuice/Elegant-grub2-themes](https://github.com/vinceliuice/Elegant-grub2-themes) with [illustration from 紺屋鴉江](https://www.pixiv.net/artworks/119683453).
+Based on [vinceliuice/Elegant-grub2-themes](https://github.com/vinceliuice/Elegant-grub2-themes) with the [illustration from 紺屋鴉江](https://www.pixiv.net/artworks/119683453).
## MPV
@@ -57,8 +69,8 @@ See [backgrounds repo for personal usage](https://github.com/Uyanide/backgrounds
including:
-- MesloLGM Nerd Font (& Mono)
- Maple Mono NF CN
+- MesloLGM Nerd Font (& Mono)
- WenQuanYi Micro Hei
- Sour Gummy
- Noto Sans
diff --git a/backgrounds b/backgrounds
index 49aad19..ec1022f 160000
--- a/backgrounds
+++ b/backgrounds
@@ -1 +1 @@
-Subproject commit 49aad198141797bb8a67d51b94fe1bd2006d2e71
+Subproject commit ec1022ffec5c8f7909b42f6fee684527ed7d62bf
diff --git a/niri/config.kdl b/niri/config.kdl
index de7a316..904f147 100644
--- a/niri/config.kdl
+++ b/niri/config.kdl
@@ -27,7 +27,7 @@ input {
warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
- focus-follows-mouse max-scroll-amount="75%"
+ focus-follows-mouse max-scroll-amount="100%"
}
/************************Output************************/
@@ -114,11 +114,6 @@ layer-rule {
}
-layer-rule {
- match namespace="^quickshell-bar$"
- place-within-backdrop false
-}
-
/************************Autostart************************/
@@ -126,7 +121,7 @@ layer-rule {
spawn-sh-at-startup "config-switch niri"
// Bar
-spawn-at-startup "waybar"
+spawn-at-startup "quickshell"
// Wallpaper
spawn-at-startup "wallpaper-daemon"
@@ -141,7 +136,7 @@ spawn-sh-at-startup "gnome-keyring-daemon --start --components=secrets"
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
spawn-at-startup "mako"
-// idle
+// Idle
spawn-sh-at-startup "hypridle"
// Clipboard history
@@ -286,8 +281,8 @@ binds {
Mod+Shift+W { spawn "wallpaper-chooser"; }
// EWW
- Mod+Space { spawn-sh "eww open main --toggle"; }
- Mod+Shift+L { spawn-sh "lyrics-widgets"; }
+ Mod+Space { spawn-sh "qs ipc call panels toggleControlCenter"; }
+ Mod+Shift+L { spawn-sh "qs ipc call lyrics toggleBarLyrics"; }
// Waybar
Mod+Shift+K { spawn-sh "waybar-toggle"; }
diff --git a/niri/config.kdl.template b/niri/config.kdl.template
index d836191..7376d22 100644
--- a/niri/config.kdl.template
+++ b/niri/config.kdl.template
@@ -27,7 +27,7 @@ input {
warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
- focus-follows-mouse max-scroll-amount="75%"
+ focus-follows-mouse max-scroll-amount="100%"
}
/************************Output************************/
@@ -121,7 +121,7 @@ layer-rule {
spawn-sh-at-startup "config-switch niri"
// Bar
-spawn-at-startup "waybar"
+spawn-at-startup "quickshell"
// Wallpaper
spawn-at-startup "wallpaper-daemon"
@@ -136,7 +136,7 @@ spawn-sh-at-startup "gnome-keyring-daemon --start --components=secrets"
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
spawn-at-startup "mako"
-// idle
+// Idle
spawn-sh-at-startup "hypridle"
// Clipboard history
@@ -281,8 +281,8 @@ binds {
Mod+Shift+W { spawn "wallpaper-chooser"; }
// EWW
- Mod+Space { spawn-sh "eww open main --toggle"; }
- Mod+Shift+L { spawn-sh "lyrics-widgets"; }
+ Mod+Space { spawn-sh "qs ipc call panels toggleControlCenter"; }
+ Mod+Shift+L { spawn-sh "qs ipc call lyrics toggleBarLyrics"; }
// Waybar
Mod+Shift+K { spawn-sh "waybar-toggle"; }
diff --git a/quickshell/Assets/Config/.gitignore b/quickshell/Assets/Config/.gitignore
new file mode 100644
index 0000000..f1b0917
--- /dev/null
+++ b/quickshell/Assets/Config/.gitignore
@@ -0,0 +1,4 @@
+
+Location.json
+
+GeoInfoToken.txt
\ No newline at end of file
diff --git a/quickshell/Assets/Config/LyricsOffset.txt b/quickshell/Assets/Config/LyricsOffset.txt
new file mode 100644
index 0000000..c227083
--- /dev/null
+++ b/quickshell/Assets/Config/LyricsOffset.txt
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/quickshell/Assets/Config/Settings.json b/quickshell/Assets/Config/Settings.json
new file mode 100644
index 0000000..8f52f69
--- /dev/null
+++ b/quickshell/Assets/Config/Settings.json
@@ -0,0 +1,4 @@
+{
+ "primaryColor": "#89b4fa",
+ "showLyricsBar": true
+}
diff --git a/quickshell/Assets/Images/Avatar.jpg b/quickshell/Assets/Images/Avatar.jpg
new file mode 100644
index 0000000..a39dbbc
Binary files /dev/null and b/quickshell/Assets/Images/Avatar.jpg differ
diff --git a/quickshell/Assets/Ip/.gitignore b/quickshell/Assets/Ip/.gitignore
deleted file mode 100644
index 9497d36..0000000
--- a/quickshell/Assets/Ip/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-token.txt
-# cache.json
\ No newline at end of file
diff --git a/quickshell/Constants/Color.qml b/quickshell/Constants/Color.qml
new file mode 100644
index 0000000..17a0e35
--- /dev/null
+++ b/quickshell/Constants/Color.qml
@@ -0,0 +1,25 @@
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Constants
+pragma Singleton
+
+Singleton {
+ id: root
+
+ property color mPrimary: Colors.primary
+ property color mOnPrimary: Colors.base
+ property color mSecondary: Colors.primary
+ property color mOnSecondary: Colors.base
+ property color mTertiary: Colors.primary
+ property color mOnTertiary: Colors.base
+ property color mError: Colors.red
+ property color mOnError: Colors.base
+ property color mSurface: Colors.base
+ property color mOnSurface: Colors.text
+ property color mSurfaceVariant: Colors.surface
+ property color mOnSurfaceVariant: Colors.text
+ property color mOutline: Colors.primary
+ property color mShadow: Colors.crust
+ property color transparent: "transparent"
+}
diff --git a/quickshell/Constants/Colors.qml b/quickshell/Constants/Colors.qml
index f075d82..567fdd0 100644
--- a/quickshell/Constants/Colors.qml
+++ b/quickshell/Constants/Colors.qml
@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
+import qs.Services
pragma Singleton
Singleton {
@@ -29,10 +30,11 @@ Singleton {
readonly property color surface2: "#585b70"
readonly property color surface1: "#45475a"
readonly property color surface0: "#313244"
+ readonly property color surface: "#292a3c"
readonly property color base: "#1e1e2e"
readonly property color mantle: "#181825"
readonly property color crust: "#11111b"
- readonly property color accent: "#89b4fa"
+ property color primary: SettingsService.primaryColor
readonly property color distroColor: "#74c7ec"
readonly property var cavaList: ["#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5", "#a6e3a1", "#f9e2af", "#fab387"]
}
diff --git a/quickshell/Constants/Fonts.qml b/quickshell/Constants/Fonts.qml
index 91e0a6e..fb882da 100644
--- a/quickshell/Constants/Fonts.qml
+++ b/quickshell/Constants/Fonts.qml
@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
+import qs.Constants
pragma Singleton
Singleton {
@@ -8,8 +9,8 @@ Singleton {
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
+ readonly property int small: Style.fontSizeS
+ readonly property int medium: Style.fontSizeM
+ readonly property int large: Style.fontSizeL
+ readonly property int icon: 14 // for nerd font
}
diff --git a/quickshell/Constants/Icons.qml b/quickshell/Constants/Icons.qml
index 3f0a509..edc8af2 100644
--- a/quickshell/Constants/Icons.qml
+++ b/quickshell/Constants/Icons.qml
@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
+import qs.Utils
pragma Singleton
Singleton {
@@ -30,6 +31,11 @@ Singleton {
readonly property string global: ""
readonly property string upload: ""
readonly property string download: ""
+ readonly property string speedSlower: ""
+ readonly property string speedFaster: ""
+ readonly property string speedReset: ""
+ readonly property string reset: ""
+ readonly property string lines: ""
// Expose the font family name for easy access
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
readonly property string defaultIcon: TablerIcons.defaultIcon
@@ -73,7 +79,7 @@ Singleton {
if (currentFontLoader.status === FontLoader.Ready)
fontReloaded();
else if (currentFontLoader.status === FontLoader.Error)
- Logger.error("Font failed to load (version " + fontVersion + ")");
+ Logger.error("Icons", "Font failed to load (version " + fontVersion + ")");
});
}
diff --git a/quickshell/Constants/Style.qml b/quickshell/Constants/Style.qml
new file mode 100644
index 0000000..6b4f9ad
--- /dev/null
+++ b/quickshell/Constants/Style.qml
@@ -0,0 +1,68 @@
+import QtQuick
+import Quickshell
+import Quickshell.Io
+pragma Singleton
+
+Singleton {
+ /*
+ Preset sizes for font, radii, ?
+ */
+
+ id: root
+
+ // Font size
+ property real fontSizeXXS: 8
+ property real fontSizeXS: 9
+ property real fontSizeS: 10
+ property real fontSizeM: 11
+ property real fontSizeL: 13
+ property real fontSizeXL: 16
+ property real fontSizeXXL: 18
+ property real fontSizeXXXL: 24
+ // Font weight
+ property int fontWeightRegular: 400
+ property int fontWeightMedium: 500
+ property int fontWeightSemiBold: 600
+ property int fontWeightBold: 700
+ // Radii
+ property int radiusXXS: 4
+ property int radiusXS: 8
+ property int radiusS: 12
+ property int radiusM: 16
+ property int radiusL: 20
+ //screen Radii
+ property int screenRadius: 20
+ // Border
+ property int borderS: 2
+ property int borderM: 3
+ property int borderL: 4
+ // Margins (for margins and spacing)
+ property int marginXXS: 2
+ property int marginXS: 4
+ property int marginS: 8
+ property int marginM: 12
+ property int marginL: 16
+ property int marginXL: 24
+ // Opacity
+ property real opacityNone: 0
+ property real opacityLight: 0.25
+ property real opacityMedium: 0.5
+ property real opacityHeavy: 0.75
+ property real opacityAlmost: 0.95
+ property real opacityFull: 1
+ // Animation duration (ms)
+ property int animationFast: 150
+ property int animationNormal: 300
+ property int animationSlow: 450
+ property int animationSlowest: 750
+ // Delays
+ property int tooltipDelay: 300
+ property int tooltipDelayLong: 1200
+ property int pillDelay: 500
+ // Settings widgets base size
+ property real baseWidgetSize: 33
+ property real sliderWidth: 200
+ // Bar Dimensions
+ property real barHeight: 45
+ property real capsuleHeight: 35
+}
diff --git a/quickshell/Modules/Bar/Bar.qml b/quickshell/Modules/Bar/Bar.qml
index 20a3a45..b96282e 100644
--- a/quickshell/Modules/Bar/Bar.qml
+++ b/quickshell/Modules/Bar/Bar.qml
@@ -33,7 +33,7 @@ Scope {
screen: modelData
WlrLayershell.namespace: "quickshell-bar"
color: Colors.transparent
- implicitHeight: 45
+ implicitHeight: Style.barHeight
anchors {
left: true
@@ -94,6 +94,9 @@ Scope {
SymbolButton {
symbol: Icons.distro
buttonColor: Colors.distroColor
+ onClicked: {
+ PanelService.getPanel("controlCenterPanel")?.toggle(this)
+ }
onRightClicked: {
if (action.running) {
action.signal(15);
@@ -164,37 +167,49 @@ Scope {
rightMargin: 5
}
- NetworkSpeed {
+ RowLayout {
+ id: monitorsLayout
+ visible: !SettingsService.showLyricsBar
+
+ height: parent.height
+ NetworkSpeed {
+ }
+
+ Separator {
+ }
+
+ Item {
+ width: 10
+ }
+
+ Ip {
+ showCountryCode: true
+ }
+
+ CpuTemp {
+ }
+
+ MemUsage {
+ }
+
+ CpuUsage {
+ }
+
+ Battery {
+ }
+
+ Brightness {
+ screen: modelData
+ }
+
+ Volume {
+ }
}
- Separator {
- }
-
- Item {
- width: 10
- }
-
- Ip {
- showCountryCode: true
- }
-
- CpuTemp {
- }
-
- MemUsage {
- }
-
- CpuUsage {
- }
-
- Battery {
- }
-
- Brightness {
- screen: modelData
- }
-
- Volume {
+ LyricsBar {
+ id: lyricsBar
+ visible: SettingsService.showLyricsBar
+ width: 600
}
Item {
diff --git a/quickshell/Modules/Bar/Components/CavaBar.qml b/quickshell/Modules/Bar/Components/CavaBar.qml
index f6d6de3..59319ee 100644
--- a/quickshell/Modules/Bar/Components/CavaBar.qml
+++ b/quickshell/Modules/Bar/Components/CavaBar.qml
@@ -2,8 +2,8 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
-import qs.Modules.Misc
import qs.Services
+import qs.Utils
Item {
id: root
diff --git a/quickshell/Modules/Bar/Components/FocusedWindow.qml b/quickshell/Modules/Bar/Components/FocusedWindow.qml
index 721d276..7df1628 100644
--- a/quickshell/Modules/Bar/Components/FocusedWindow.qml
+++ b/quickshell/Modules/Bar/Components/FocusedWindow.qml
@@ -78,7 +78,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Fonts.medium
font.family: Fonts.primary
- color: Colors.accent
+ color: Colors.primary
Process {
id: action
diff --git a/quickshell/Modules/Bar/Components/Ip.qml b/quickshell/Modules/Bar/Components/Ip.qml
index 3727079..8110375 100644
--- a/quickshell/Modules/Bar/Components/Ip.qml
+++ b/quickshell/Modules/Bar/Components/Ip.qml
@@ -29,7 +29,7 @@ Item {
Text {
text: Icons.global
- font.pointSize: Fonts.icon + 5
+ font.pointSize: Fonts.icon + 6
color: Colors.peach
}
@@ -54,7 +54,7 @@ Item {
Behavior on implicitWidth {
NumberAnimation {
- duration: 200
+ duration: Style.animationFast
easing.type: Easing.InOutCubic
}
diff --git a/quickshell/Modules/Bar/Components/LyricsBar.qml b/quickshell/Modules/Bar/Components/LyricsBar.qml
new file mode 100644
index 0000000..9adab54
--- /dev/null
+++ b/quickshell/Modules/Bar/Components/LyricsBar.qml
@@ -0,0 +1,88 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.Constants
+import qs.Noctalia
+import qs.Services
+
+Rectangle {
+ implicitHeight: parent.height
+ radius: Style.radiusS
+ color: Colors.base
+ border.color: Colors.primary
+ border.width: Style.borderS
+
+ Connections {
+ target: SettingsService
+ onShowLyricsBarChanged: {
+ visible = SettingsService.showLyricsBar;
+ if (visible)
+ LyricsService.startSyncing();
+ else
+ LyricsService.stopSyncing();
+ }
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: Style.marginM
+ anchors.rightMargin: Style.marginM
+ spacing: Style.marginS
+
+ Item {
+ implicitWidth: parent.width - slowerButton.implicitWidth * 3 - parent.spacing * 3 - parent.anchors.leftMargin - parent.anchors.rightMargin
+ Layout.fillHeight: true
+ clip: true
+
+ NText {
+ text: LyricsService.lyrics[LyricsService.currentIndex] || ""
+ family: Fonts.sans
+ pointSize: Style.fontSizeS
+ maximumLineCount: 1
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ }
+
+ NIconButton {
+ id: slowerButton
+
+ baseSize: 24
+ colorBg: Color.transparent
+ colorBgHover: Colors.blue
+ colorFg: Colors.blue
+ icon: "rotate-2"
+ onClicked: {
+ LyricsService.increaseOffset();
+ }
+ }
+
+ NIconButton {
+ id: playPauseButton
+
+ baseSize: 24
+ colorBg: Color.transparent
+ colorBgHover: Colors.yellow
+ colorFg: Colors.yellow
+ icon: "rotate-clockwise-2"
+ onClicked: {
+ LyricsService.decreaseOffset();
+ }
+ }
+
+ NIconButton {
+ id: nextButton
+
+ baseSize: 24
+ colorBg: Color.transparent
+ colorBgHover: Colors.green
+ colorFg: Colors.green
+ icon: "rotate-clockwise"
+ onClicked: {
+ LyricsService.resetOffset();
+ }
+ }
+
+ }
+
+}
diff --git a/quickshell/Modules/Bar/Components/NetworkSpeed.qml b/quickshell/Modules/Bar/Components/NetworkSpeed.qml
index 61e671a..20e0442 100644
--- a/quickshell/Modules/Bar/Components/NetworkSpeed.qml
+++ b/quickshell/Modules/Bar/Components/NetworkSpeed.qml
@@ -19,15 +19,15 @@ Item {
Text {
text: Icons.download
font.pointSize: Fonts.icon - 3
- color: Colors.accent
+ color: Colors.primary
Layout.leftMargin: 10
}
Text {
- text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
+ text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.pointSize: Fonts.medium
font.family: Fonts.primary
- color: Colors.accent
+ color: Colors.primary
}
Item {
@@ -37,14 +37,14 @@ Item {
Text {
text: Icons.upload
font.pointSize: Fonts.icon - 3
- color: Colors.accent
+ color: Colors.primary
}
Text {
- text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
+ text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.pointSize: Fonts.medium
font.family: Fonts.primary
- color: Colors.accent
+ color: Colors.primary
}
}
diff --git a/quickshell/Modules/Bar/Components/Time.qml b/quickshell/Modules/Bar/Components/Time.qml
index 9216d27..008108e 100644
--- a/quickshell/Modules/Bar/Components/Time.qml
+++ b/quickshell/Modules/Bar/Components/Time.qml
@@ -6,5 +6,13 @@ Text {
text: TimeService.time + " | " + TimeService.dateString
font.pointSize: Fonts.medium
font.family: Fonts.primary
- color: Colors.accent
+ color: Colors.primary
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ PanelService.getPanel("calendarPanel")?.toggle(this)
+ }
+ }
}
diff --git a/quickshell/Modules/Bar/Components/Workspace.qml b/quickshell/Modules/Bar/Components/Workspace.qml
index c9f65f9..7600a7e 100644
--- a/quickshell/Modules/Bar/Components/Workspace.qml
+++ b/quickshell/Modules/Bar/Components/Workspace.qml
@@ -16,15 +16,15 @@ Item {
property ListModel localWorkspaces
property real masterProgress: 0
property bool effectsActive: false
- property color effectColor: Colors.accent
+ property color effectColor: Colors.primary
property int horizontalPadding: 16
property int spacingBetweenPills: 8
property bool isDestroying: false
- signal workspaceChanged(int workspaceId, color accentColor)
+ signal workspaceChanged(int workspaceId, color primaryColor)
function triggerUnifiedWave() {
- effectColor = Colors.accent;
+ effectColor = Colors.primary;
masterAnimation.restart();
}
@@ -33,7 +33,7 @@ Item {
const ws = localWorkspaces.get(i);
if (ws.isFocused === true) {
root.triggerUnifiedWave();
- root.workspaceChanged(ws.id, Colors.accent);
+ root.workspaceChanged(ws.id, Colors.primary);
break;
}
}
@@ -180,10 +180,10 @@ Item {
}
color: {
if (model.isFocused)
- return Colors.accent;
+ return Colors.primary;
if (model.isActive)
- return Colors.accent.lighter(130);
+ return Colors.primary.lighter(130);
if (model.isUrgent)
return Theme.error;
diff --git a/quickshell/Modules/Bar/Misc/MonitorItem.qml b/quickshell/Modules/Bar/Misc/MonitorItem.qml
index 7a3c75b..131ca4c 100644
--- a/quickshell/Modules/Bar/Misc/MonitorItem.qml
+++ b/quickshell/Modules/Bar/Misc/MonitorItem.qml
@@ -11,7 +11,7 @@ Item {
property real maxValue: 100
property real value: 100
property string textValue: "" // override value in textDisplay if set
- property color fillColor: Colors.accent
+ property color fillColor: Colors.primary
property string textSuffix: ""
property bool pointerCursor: true
property alias hovered: mouseArea.containsMouse
@@ -127,7 +127,7 @@ Item {
Behavior on implicitWidth {
NumberAnimation {
- duration: 200
+ duration: Style.animationNormal
easing.type: Easing.InOutCubic
}
diff --git a/quickshell/Modules/Bar/Misc/SymbolButton.qml b/quickshell/Modules/Bar/Misc/SymbolButton.qml
index 4a722ec..5499a7e 100644
--- a/quickshell/Modules/Bar/Misc/SymbolButton.qml
+++ b/quickshell/Modules/Bar/Misc/SymbolButton.qml
@@ -9,6 +9,8 @@ Item {
required property string symbol
property color buttonColor: Colors.distroColor
readonly property alias hovered: mouseArea.containsMouse
+ property real iconSize: Fonts.icon
+ property real radius: Style.radiusS
signal clicked()
signal rightClicked()
@@ -35,7 +37,7 @@ Item {
anchors.fill: parent
text: symbol
font.family: Fonts.nerd
- font.pointSize: Fonts.icon
+ font.pointSize: iconSize
font.bold: false
color: buttonColor
horizontalAlignment: Text.AlignHCenter
@@ -46,11 +48,12 @@ Item {
anchors.fill: parent
color: parent.hovered ? buttonColor : Colors.transparent
opacity: 0.3
- radius: 14
+ radius: root.radius
Behavior on color {
ColorAnimation {
- duration: 120
+ duration: Style.animationNormal
+ easing.type: Easing.InOutCubic
}
}
diff --git a/quickshell/Modules/Bar/Misc/SystemTray.qml b/quickshell/Modules/Bar/Misc/SystemTray.qml
index 4ee5469..47f1255 100644
--- a/quickshell/Modules/Bar/Misc/SystemTray.qml
+++ b/quickshell/Modules/Bar/Misc/SystemTray.qml
@@ -8,6 +8,7 @@ import Quickshell.Widgets
import qs.Modules.Bar.Misc
import qs.Constants
import qs.Services
+import qs.Utils
Rectangle {
id: root
@@ -107,8 +108,7 @@ Rectangle {
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")
+ Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
}
}
}
diff --git a/quickshell/Modules/Bar/Misc/TrayMenu.qml b/quickshell/Modules/Bar/Misc/TrayMenu.qml
index 4844cba..6a8ee3a 100644
--- a/quickshell/Modules/Bar/Misc/TrayMenu.qml
+++ b/quickshell/Modules/Bar/Misc/TrayMenu.qml
@@ -3,6 +3,8 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Constants
+import qs.Utils
+import qs.Noctalia
PopupWindow {
id: root
@@ -19,7 +21,7 @@ PopupWindow {
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)
+ implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2))
visible: false
color: Colors.transparent
anchor.item: anchorItem
@@ -28,7 +30,7 @@ PopupWindow {
function showAt(item, x, y) {
if (!item) {
- console.warn("AnchorItem is undefined, won't show menu.")
+ Logger.warn("TrayMenu", "AnchorItem is undefined, won't show menu.");
return
}
@@ -84,15 +86,15 @@ PopupWindow {
Rectangle {
anchors.fill: parent
color: Colors.base
- border.color: Colors.accent
+ border.color: Colors.primary
border.width: 2
- radius: 14
+ radius: Style.radiusM
}
Flickable {
id: flickable
anchors.fill: parent
- anchors.margins: 10
+ anchors.margins: Style.marginS
contentHeight: columnLayout.implicitHeight
interactive: true
@@ -115,88 +117,56 @@ PopupWindow {
return 8
} else {
// Calculate based on text content
- const textHeight = text.contentHeight || (Fonts.small)
- return textHeight + 16
+ const textHeight = text.contentHeight || (Style.fontSizeS * 1.2)
+ return Math.max(28, textHeight + (Style.marginS * 2))
}
}
color: Colors.transparent
property var subMenu: null
- Rectangle {
- width: parent.width - 16
- height: 1
+ NDivider {
anchors.centerIn: parent
+ width: parent.width - (Style.marginM * 2)
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
+ color: mouseArea.containsMouse ? Colors.primary : Colors.transparent
+ radius: Style.radiusS
visible: !(modelData?.isSeparator ?? false)
RowLayout {
anchors.fill: parent
- anchors.leftMargin: 8
- anchors.rightMargin: 8
- spacing: 8
+ anchors.leftMargin: Style.marginM
+ anchors.rightMargin: Style.marginM
+ spacing: Style.marginS
- Text {
+ NText {
id: text
Layout.fillWidth: true
- color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Colors.base : Colors.text) : Colors.text
+ color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
- font.pointSize: Fonts.small
+ pointSize: Style.fontSizeS
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
}
Image {
- Layout.preferredWidth: 14
- Layout.preferredHeight: 14
+ Layout.preferredWidth: Style.marginL
+ Layout.preferredHeight: Style.marginL
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
- Text {
+ NIcon {
+ icon: modelData?.hasChildren ? "menu" : ""
+ pointSize: Style.fontSizeS
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
+ color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
}
}
diff --git a/quickshell/Modules/Panel/CalendarPanel.qml b/quickshell/Modules/Panel/CalendarPanel.qml
new file mode 100644
index 0000000..eb562b2
--- /dev/null
+++ b/quickshell/Modules/Panel/CalendarPanel.qml
@@ -0,0 +1,537 @@
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Wayland
+import qs.Constants
+import qs.Noctalia
+import qs.Services
+import qs.Utils
+
+NPanel {
+ id: root
+
+ preferredWidth: 400
+ preferredHeight: 520
+
+ panelContent: ColumnLayout {
+ id: content
+
+ readonly property int firstDayOfWeek: Qt.locale().firstDayOfWeek
+ property bool isCurrentMonth: checkIsCurrentMonth()
+ readonly property bool weatherReady: (LocationService.data.weather !== null)
+
+ function checkIsCurrentMonth() {
+ return (Time.date.getMonth() === grid.month) && (Time.date.getFullYear() === grid.year);
+ }
+
+ function getISOWeekNumber(date) {
+ const target = new Date(date.getTime());
+ target.setHours(0, 0, 0, 0);
+ const dayOfWeek = target.getDay() || 7;
+ target.setDate(target.getDate() + 4 - dayOfWeek);
+ const yearStart = new Date(target.getFullYear(), 0, 1);
+ const weekNumber = Math.ceil(((target - yearStart) / 8.64e+07 + 1) / 7);
+ return weekNumber;
+ }
+
+ anchors.fill: parent
+ anchors.margins: Style.marginL
+ spacing: Style.marginM
+
+ Connections {
+ function onDateChanged() {
+ isCurrentMonth = checkIsCurrentMonth();
+ }
+
+ target: Time
+ }
+
+ // Combined blue banner with date/time and weather summary
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: blueColumn.implicitHeight + Style.marginM * 2
+ radius: Style.radiusL
+ color: Color.mSurfaceVariant
+ layer.enabled: true
+
+ ColumnLayout {
+ id: blueColumn
+
+ anchors.fill: parent
+ anchors.margins: Style.marginM
+ spacing: 0
+
+ // Combined layout for weather icon, date, and weather text
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 60
+ spacing: Style.marginS
+
+ // Weather icon and temperature
+ ColumnLayout {
+ Layout.alignment: Qt.AlignVCenter
+ spacing: Style.marginXXS
+
+ NIcon {
+ Layout.alignment: Qt.AlignHCenter
+ icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "cloud"
+ pointSize: Style.fontSizeXXL
+ color: Color.mOnSurfaceVariant
+ }
+
+ NText {
+ Layout.alignment: Qt.AlignHCenter
+ text: {
+ if (!weatherReady)
+ return "";
+
+ var temp = LocationService.data.weather.current_weather.temperature;
+ var suffix = "C";
+ temp = Math.round(temp);
+ return `${temp}°${suffix}`;
+ }
+ pointSize: Style.fontSizeM
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurfaceVariant
+ }
+
+ }
+
+ // Today day number
+ NText {
+ visible: content.isCurrentMonth
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+ text: Time.date.getDate()
+ pointSize: Style.fontSizeXXXL * 1.5
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurfaceVariant
+ }
+
+ Item {
+ visible: !content.isCurrentMonth
+ }
+
+ // Month, year, location
+ ColumnLayout {
+ Layout.fillWidth: false
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+ spacing: -Style.marginXS
+
+ RowLayout {
+ spacing: 0
+
+ NText {
+ text: Qt.locale().monthName(grid.month, Locale.LongFormat).toUpperCase()
+ pointSize: Style.fontSizeXL * 1.2
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurfaceVariant
+ Layout.alignment: Qt.AlignBaseline
+ Layout.maximumWidth: 150
+ elide: Text.ElideRight
+ }
+
+ NText {
+ text: ` ${grid.year}`
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Qt.alpha(Color.mOnSurfaceVariant, 0.7)
+ Layout.alignment: Qt.AlignBaseline
+ }
+
+ }
+
+ RowLayout {
+ spacing: 0
+
+ NText {
+ text: {
+ if (!weatherReady)
+ return "Weather unavailable";
+
+ const chunks = LocationService.data.name.split(",");
+ return chunks[0];
+ }
+ pointSize: Style.fontSizeM
+ font.weight: Style.fontWeightMedium
+ color: Color.mOnSurfaceVariant
+ Layout.maximumWidth: 150
+ elide: Text.ElideRight
+ }
+
+ NText {
+ text: weatherReady ? ` (${LocationService.data.weather.timezone_abbreviation})` : ""
+ pointSize: Style.fontSizeXS
+ font.weight: Style.fontWeightMedium
+ color: Qt.alpha(Color.mOnSurfaceVariant, 0.7)
+ }
+
+ }
+
+ }
+
+ // Spacer between date and clock
+ Item {
+ Layout.fillWidth: true
+ }
+
+ // Digital clock with circular progress
+ Item {
+ width: Style.fontSizeXXXL * 1.9
+ height: Style.fontSizeXXXL * 1.9
+ Layout.alignment: Qt.AlignVCenter
+
+ // Seconds circular progress
+ Canvas {
+ id: secondsProgress
+
+ property real progress: Time.date.getSeconds() / 60
+
+ anchors.fill: parent
+ onProgressChanged: requestPaint()
+ onPaint: {
+ var ctx = getContext("2d");
+ var centerX = width / 2;
+ var centerY = height / 2;
+ var radius = Math.min(width, height) / 2 - 3;
+ ctx.reset();
+ // Background circle
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
+ ctx.lineWidth = 2.5;
+ ctx.strokeStyle = Qt.alpha(Color.mOnSurfaceVariant, 0.15);
+ ctx.stroke();
+ // Progress arc
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI);
+ ctx.lineWidth = 2.5;
+ ctx.strokeStyle = Color.mOnSurfaceVariant;
+ ctx.lineCap = "round";
+ ctx.stroke();
+ }
+
+ Connections {
+ function onDateChanged() {
+ secondsProgress.progress = Time.date.getSeconds() / 60;
+ }
+
+ target: Time
+ }
+
+ }
+
+ // Digital clock
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: -Style.marginXXS
+
+ NText {
+ text: {
+ var t = Qt.locale().toString(new Date(), "HH");
+ return t.split(" ")[0];
+ }
+ pointSize: Style.fontSizeXS
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurfaceVariant
+ family: Fonts.sans
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ NText {
+ text: Qt.formatTime(Time.date, "mm")
+ pointSize: Style.fontSizeXXS
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurfaceVariant
+ family: Fonts.sans
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ layer.effect: DropShadow {
+ horizontalOffset: 6
+ verticalOffset: 6
+ radius: 8
+ samples: 12
+ color: Qt.rgba(0, 0, 0, 0.3)
+ }
+
+ }
+
+ // 6-day forecast (outside blue banner)
+ RowLayout {
+ visible: weatherReady
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+ spacing: Style.marginL
+
+ Repeater {
+ model: weatherReady ? Math.min(6, LocationService.data.weather.daily.time.length) : 0
+
+ delegate: ColumnLayout {
+ Layout.preferredWidth: 0
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+ spacing: Style.marginS
+
+ NText {
+ text: {
+ var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"));
+ return Qt.locale().toString(weatherDate, "ddd");
+ }
+ color: Color.mOnSurfaceVariant
+ pointSize: Style.fontSizeM
+ font.weight: Style.fontWeightMedium
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ NIcon {
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+ icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
+ pointSize: Style.fontSizeXXL * 1.5
+ color: LocationService.weatherColorFromCode(LocationService.data.weather.daily.weathercode[index])
+ }
+
+ NText {
+ Layout.alignment: Qt.AlignHCenter
+ text: {
+ var max = LocationService.data.weather.daily.temperature_2m_max[index];
+ var min = LocationService.data.weather.daily.temperature_2m_min[index];
+ max = Math.round(max);
+ min = Math.round(min);
+ return `${max}°/${min}°`;
+ }
+ pointSize: Style.fontSizeXS
+ color: Color.mOnSurfaceVariant
+ font.weight: Style.fontWeightMedium
+ }
+
+ }
+
+ }
+
+ }
+
+ // Loading indicator for weather
+ RowLayout {
+ visible: !weatherReady
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+
+ NBusyIndicator {
+ }
+
+ }
+
+ // Spacer
+ Item {
+ }
+
+ // Navigation and divider
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ NDivider {
+ Layout.fillWidth: true
+ }
+
+ NIconButton {
+ icon: "chevron-left"
+ colorBg: Color.transparent
+ colorBorder: Color.transparent
+ colorBorderHover: Color.transparent
+ onClicked: {
+ let newDate = new Date(grid.year, grid.month - 1, 1);
+ grid.year = newDate.getFullYear();
+ grid.month = newDate.getMonth();
+ content.isCurrentMonth = content.checkIsCurrentMonth();
+ }
+ }
+
+ NIconButton {
+ icon: "calendar"
+ colorBg: Color.transparent
+ colorBorder: Color.transparent
+ colorBorderHover: Color.transparent
+ onClicked: {
+ grid.month = Time.date.getMonth();
+ grid.year = Time.date.getFullYear();
+ content.isCurrentMonth = true;
+ }
+ }
+
+ NIconButton {
+ icon: "chevron-right"
+ colorBg: Color.transparent
+ colorBorder: Color.transparent
+ colorBorderHover: Color.transparent
+ onClicked: {
+ let newDate = new Date(grid.year, grid.month + 1, 1);
+ grid.year = newDate.getFullYear();
+ grid.month = newDate.getMonth();
+ content.isCurrentMonth = content.checkIsCurrentMonth();
+ }
+ }
+
+ }
+
+ // Names of days of the week
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ Item {
+ Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ columns: 7
+ rows: 1
+ columnSpacing: 0
+ rowSpacing: 0
+
+ Repeater {
+ model: 7
+
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Style.baseWidgetSize * 0.6
+
+ NText {
+ anchors.centerIn: parent
+ text: {
+ let dayIndex = (content.firstDayOfWeek + index) % 7;
+ const dayNames = ["S", "M", "T", "W", "T", "F", "S"];
+ return dayNames[dayIndex];
+ }
+ color: Color.mPrimary
+ pointSize: Style.fontSizeS
+ font.weight: Style.fontWeightBold
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ // Grid with weeks and days
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: 0
+
+ // Column of week numbers
+ ColumnLayout {
+ Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
+ Layout.fillHeight: true
+ spacing: 0
+
+ Repeater {
+ model: 6
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ NText {
+ anchors.centerIn: parent
+ color: Color.mOutline
+ pointSize: Style.fontSizeXXS
+ font.weight: Style.fontWeightMedium
+ text: {
+ let firstOfMonth = new Date(grid.year, grid.month, 1);
+ let firstDayOfWeek = content.firstDayOfWeek;
+ let firstOfMonthDayOfWeek = firstOfMonth.getDay();
+ let daysBeforeFirst = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7;
+ if (daysBeforeFirst === 0)
+ daysBeforeFirst = 7;
+
+ let gridStartDate = new Date(grid.year, grid.month, 1 - daysBeforeFirst);
+ let rowStartDate = new Date(gridStartDate);
+ rowStartDate.setDate(gridStartDate.getDate() + (index * 7));
+ let thursday = new Date(rowStartDate);
+ if (firstDayOfWeek === 0) {
+ thursday.setDate(rowStartDate.getDate() + 4);
+ } else if (firstDayOfWeek === 1) {
+ thursday.setDate(rowStartDate.getDate() + 3);
+ } else {
+ let daysToThursday = (4 - firstDayOfWeek + 7) % 7;
+ thursday.setDate(rowStartDate.getDate() + daysToThursday);
+ }
+ return `${getISOWeekNumber(thursday)}`;
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+ // Days Grid
+ MonthGrid {
+ id: grid
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: Style.marginXXS
+ month: Time.date.getMonth()
+ year: Time.date.getFullYear()
+ locale: Qt.locale()
+
+ delegate: Item {
+ Rectangle {
+ width: Style.baseWidgetSize * 0.9
+ height: Style.baseWidgetSize * 0.9
+ anchors.centerIn: parent
+ radius: Style.radiusM
+ color: model.today ? Color.mSecondary : Color.transparent
+
+ NText {
+ anchors.centerIn: parent
+ text: model.day
+ color: {
+ if (model.today)
+ return Color.mOnSecondary;
+
+ if (model.month === grid.month)
+ return Color.mOnSurface;
+
+ return Color.mOnSurfaceVariant;
+ }
+ opacity: model.month === grid.month ? 1 : 0.4
+ pointSize: Style.fontSizeM
+ font.weight: model.today ? Style.fontWeightBold : Style.fontWeightMedium
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/quickshell/Modules/Panel/Cards/LyricsCard.qml b/quickshell/Modules/Panel/Cards/LyricsCard.qml
new file mode 100644
index 0000000..262d32f
--- /dev/null
+++ b/quickshell/Modules/Panel/Cards/LyricsCard.qml
@@ -0,0 +1,46 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.Constants
+import qs.Noctalia
+import qs.Services
+import qs.Utils
+
+NBox {
+ id: lyricsBox
+
+ Component.onCompleted: {
+ LyricsService.startSyncing();
+ }
+ Component.onDestruction: {
+ LyricsService.stopSyncing();
+ }
+
+ ColumnLayout {
+ id: lyricsColumn
+
+ anchors.fill: parent
+ anchors.margins: Style.marginS
+
+ Repeater {
+ model: LyricsService.lyrics
+
+ NText {
+ Layout.fillWidth: true
+ text: modelData
+ font.pointSize: index === LyricsService.currentIndex ? Style.fontSizeM : Style.fontSizeS
+ font.weight: index === LyricsService.currentIndex ? Style.fontWeightBold : Style.fontWeightRegular
+ font.family: Fonts.sans
+ color: index === LyricsService.currentIndex ? Color.mOnSurface : Color.mOnSurfaceVariant
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ wrapMode: Text.WrapAnywhere
+ maximumLineCount: 1
+ }
+
+ }
+
+ }
+
+}
diff --git a/quickshell/Modules/Panel/Cards/LyricsControl.qml b/quickshell/Modules/Panel/Cards/LyricsControl.qml
new file mode 100644
index 0000000..056d9c9
--- /dev/null
+++ b/quickshell/Modules/Panel/Cards/LyricsControl.qml
@@ -0,0 +1,96 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.Constants
+import qs.Modules.Bar.Misc
+import qs.Noctalia
+import qs.Services
+
+GridLayout {
+ id: buttonsGrid
+
+ columns: 2
+ columnSpacing: 10
+ rowSpacing: 10
+ Layout.margins: 10
+
+ NIconButton {
+ id: slowerButton
+
+ baseSize: 32
+ colorBg: Color.transparent
+ colorBgHover: Colors.blue
+ colorFg: Colors.blue
+ icon: "arrow-bar-up"
+ onClicked: {
+ LyricsService.increaseOffset();
+ }
+ }
+
+ NIconButton {
+ id: playPauseButton
+
+ baseSize: 32
+ colorBg: Color.transparent
+ colorBgHover: Colors.yellow
+ colorFg: Colors.yellow
+ icon: "arrow-bar-down"
+ onClicked: {
+ LyricsService.decreaseOffset();
+ }
+ }
+
+ NIconButton {
+ id: nextButton
+
+ baseSize: 32
+ colorBg: Color.transparent
+ colorBgHover: Colors.green
+ colorFg: Colors.green
+ icon: "rotate-clockwise"
+ onClicked: {
+ LyricsService.resetOffset();
+ }
+ }
+
+ NIconButton {
+ id: fasterButton
+
+ baseSize: 32
+ colorBg: Color.transparent
+ colorBgHover: Colors.red
+ colorFg: Colors.red
+ icon: "trash"
+ onClicked: {
+ LyricsService.clearCache();
+ }
+ }
+
+ NIconButton {
+ id: barLyricsButton
+
+ baseSize: 32
+ colorBg: SettingsService.showLyricsBar ? Colors.peach : Color.transparent
+ colorBgHover: Colors.peach
+ colorFg: SettingsService.showLyricsBar ? Colors.base : Colors.peach
+ icon: "app-window"
+ onClicked: {
+ SettingsService.showLyricsBar = !SettingsService.showLyricsBar;
+ }
+ }
+
+ NIconButton {
+ id: textButton
+
+ baseSize: 32
+ colorBg: Color.transparent
+ colorBgHover: Colors.subtext1
+ colorFg: Colors.subtext1
+ icon: "align-box-left-bottom"
+ onClicked: {
+ LyricsService.showLyricsText();
+ controlCenterPanel.close();
+ }
+ }
+
+}
diff --git a/quickshell/Modules/Panel/Cards/MediaCard.qml b/quickshell/Modules/Panel/Cards/MediaCard.qml
new file mode 100644
index 0000000..9ef375d
--- /dev/null
+++ b/quickshell/Modules/Panel/Cards/MediaCard.qml
@@ -0,0 +1,458 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell
+import qs.Constants
+import qs.Noctalia
+import qs.Services
+import qs.Utils
+
+NBox {
+ id: root
+
+ // Background artwork that covers everything
+ Item {
+ anchors.fill: parent
+ clip: true
+
+ NImageRounded {
+ id: bgArtImage
+
+ anchors.fill: parent
+ imagePath: MusicManager.trackArtUrl
+ imageRadius: Style.radiusM
+ visible: MusicManager.trackArtUrl !== ""
+ }
+
+ // Dark overlay for readability
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mSurfaceVariant
+ opacity: 0.85
+ radius: Style.radiusM
+ }
+
+ // Border
+ Rectangle {
+ anchors.fill: parent
+ color: Color.transparent
+ radius: Style.radiusM
+ }
+
+ }
+
+ // Background visualizer on top of the artwork
+ Item {
+ id: visualizerContainer
+
+ anchors.fill: parent
+ layer.enabled: true
+
+ Item {
+ anchors.fill: parent
+
+ Cava {
+ id: cava
+
+ count: 32
+ }
+
+ Repeater {
+ model: cava.values
+
+ Rectangle {
+ anchors.bottom: parent.bottom
+ width: (parent.width - (cava.count - 1) * Style.marginXS) / cava.count
+ height: modelData * parent.height
+ x: index * (width + Style.marginXS)
+ color: Color.mPrimary
+ radius: width / 2
+ opacity: 0.25
+ }
+
+ }
+
+ }
+
+ layer.effect: MultiEffect {
+ maskEnabled: true
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 0
+
+ maskSource: ShaderEffectSource {
+
+ sourceItem: Rectangle {
+ width: root.width
+ height: root.height
+ radius: Style.radiusM
+ color: "white"
+ }
+
+ }
+
+ }
+
+ }
+
+ // Player selector - positioned at the very top
+ Rectangle {
+ id: playerSelectorButton
+
+ property var currentPlayer: MusicManager.getAvailablePlayers()[MusicManager.selectedPlayerIndex]
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: Style.marginXS
+ anchors.leftMargin: Style.marginM
+ anchors.rightMargin: Style.marginM
+ height: Style.barHeight
+ visible: MusicManager.getAvailablePlayers().length > 1
+ radius: Style.radiusM
+ color: Color.transparent
+ Component.onCompleted: {
+ MusicManager.selectedPlayerIndex = -1;
+ }
+ Component.onDestruction: {
+ MusicManager.selectedPlayerIndex = -1;
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: Style.marginS
+
+ NIcon {
+ icon: "caret-down"
+ pointSize: Style.fontSizeXXL
+ color: Color.mOnSurfaceVariant
+ }
+
+ NText {
+ text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : ""
+ pointSize: Style.fontSizeXS
+ color: Color.mOnSurfaceVariant
+ Layout.fillWidth: true
+ }
+
+ }
+
+ MouseArea {
+ id: playerSelectorMouseArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ var menuItems = [];
+ var players = MusicManager.getAvailablePlayers();
+ for (var i = 0; i < players.length; i++) {
+ menuItems.push({
+ "label": players[i].identity,
+ "action": i.toString(),
+ "icon": "disc",
+ "enabled": true,
+ "visible": true
+ });
+ }
+ playerContextMenu.model = menuItems;
+ playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height);
+ }
+ }
+
+ NContextMenu {
+ id: playerContextMenu
+
+ parent: root
+ width: 200
+ onTriggered: function(action) {
+ var index = parseInt(action);
+ if (!isNaN(index)) {
+ MusicManager.selectedPlayerIndex = index;
+ MusicManager.updateCurrentPlayer();
+ }
+ }
+ }
+
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Style.marginM
+
+ // No media player detected
+ ColumnLayout {
+ id: fallback
+
+ visible: !main.visible
+ spacing: Style.marginS
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: Style.marginL
+
+ Item {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.preferredWidth: Style.fontSizeXXXL * 4
+ Layout.preferredHeight: Style.fontSizeXXXL * 4
+
+ // Pulsating audio circles (background)
+ Repeater {
+ model: 3
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: parent.width * (1 + index * 0.2)
+ height: width
+ radius: width / 2
+ color: "transparent"
+ border.color: Color.mOnSurfaceVariant
+ border.width: 2
+ opacity: 0
+
+ SequentialAnimation on opacity {
+ running: true
+ loops: Animation.Infinite
+
+ PauseAnimation {
+ duration: index * 600
+ }
+
+ NumberAnimation {
+ from: 1
+ to: 0
+ duration: 2000
+ easing.type: Easing.OutQuad
+ }
+
+ }
+
+ SequentialAnimation on scale {
+ running: true
+ loops: Animation.Infinite
+
+ PauseAnimation {
+ duration: index * 600
+ }
+
+ NumberAnimation {
+ from: 0.5
+ to: 1.2
+ duration: 2000
+ easing.type: Easing.OutQuad
+ }
+
+ }
+
+ }
+
+ }
+
+ // Spinning disc
+ NIcon {
+ anchors.centerIn: parent
+ icon: "disc"
+ pointSize: Style.fontSizeXXXL * 3
+ color: Color.mOnSurfaceVariant
+
+ RotationAnimator on rotation {
+ from: 0
+ to: 360
+ duration: 8000
+ loops: Animation.Infinite
+ running: true
+ }
+
+ }
+
+ }
+
+ // Descriptive text
+ ColumnLayout {
+ Layout.alignment: Qt.AlignHCenter
+ spacing: Style.marginXS
+ }
+
+ }
+
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ }
+
+ // MediaPlayer Main Content
+ ColumnLayout {
+ id: main
+
+ visible: MusicManager.currentPlayer && MusicManager.canPlay
+ spacing: Style.marginS
+
+ // Spacer to push content down
+ Item {
+ Layout.preferredHeight: Style.marginM
+ }
+
+ // Metadata at the bottom left
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignLeft
+ spacing: Style.marginXS
+
+ NText {
+ visible: MusicManager.trackTitle !== ""
+ text: MusicManager.trackTitle
+ pointSize: Style.fontSizeM
+ font.weight: Style.fontWeightBold
+ elide: Text.ElideRight
+ wrapMode: Text.Wrap
+ Layout.fillWidth: true
+ maximumLineCount: 1
+ }
+
+ NText {
+ visible: MusicManager.trackArtist !== ""
+ text: MusicManager.trackArtist
+ color: Color.mPrimary
+ pointSize: Style.fontSizeS
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ maximumLineCount: 1
+ }
+
+ NText {
+ visible: MusicManager.trackAlbum !== ""
+ text: MusicManager.trackAlbum
+ color: Color.mOnSurfaceVariant
+ pointSize: Style.fontSizeM
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ maximumLineCount: 1
+ }
+
+ }
+
+ // Progress slider
+ Item {
+ id: progressWrapper
+
+ property real localSeekRatio: -1
+ property real lastSentSeekRatio: -1
+ property real seekEpsilon: 0.01
+ property real progressRatio: {
+ if (!MusicManager.currentPlayer || MusicManager.trackLength <= 0)
+ return 0;
+
+ const r = MusicManager.currentPosition / MusicManager.trackLength;
+ if (isNaN(r) || !isFinite(r))
+ return 0;
+
+ return Math.max(0, Math.min(1, r));
+ }
+ property real effectiveRatio: (MusicManager.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio
+
+ visible: (MusicManager.currentPlayer && MusicManager.trackLength > 0)
+ Layout.fillWidth: true
+ height: Style.baseWidgetSize * 0.5
+
+ Timer {
+ id: seekDebounce
+
+ interval: 75
+ repeat: false
+ onTriggered: {
+ if (MusicManager.isSeeking && progressWrapper.localSeekRatio >= 0) {
+ const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio));
+ if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
+ MusicManager.seekByRatio(next);
+ progressWrapper.lastSentSeekRatio = next;
+ }
+ }
+ }
+ }
+
+ NSlider {
+ id: progressSlider
+
+ anchors.fill: parent
+ from: 0
+ to: 1
+ stepSize: 0
+ snapAlways: false
+ enabled: MusicManager.trackLength > 0 && MusicManager.canSeek
+ heightRatio: 0.65
+ onMoved: {
+ progressWrapper.localSeekRatio = value;
+ seekDebounce.restart();
+ }
+ onPressedChanged: {
+ if (pressed) {
+ MusicManager.isSeeking = true;
+ progressWrapper.localSeekRatio = value;
+ MusicManager.seekByRatio(value);
+ progressWrapper.lastSentSeekRatio = value;
+ } else {
+ seekDebounce.stop();
+ MusicManager.seekByRatio(value);
+ MusicManager.isSeeking = false;
+ progressWrapper.localSeekRatio = -1;
+ progressWrapper.lastSentSeekRatio = -1;
+ }
+ }
+ }
+
+ Binding {
+ target: progressSlider
+ property: "value"
+ value: progressWrapper.progressRatio
+ when: !MusicManager.isSeeking
+ }
+
+ }
+
+ // Media controls
+ RowLayout {
+ spacing: Style.marginS
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+
+ NIconButton {
+ icon: "media-prev"
+ visible: MusicManager.canGoPrevious
+ onClicked: MusicManager.canGoPrevious ? MusicManager.previous() : {
+ }
+ }
+
+ NIconButton {
+ icon: MusicManager.isPlaying ? "media-pause" : "media-play"
+ visible: (MusicManager.canPlay || MusicManager.canPause)
+ onClicked: (MusicManager.canPlay || MusicManager.canPause) ? MusicManager.playPause() : {
+ }
+ }
+
+ NIconButton {
+ icon: "media-next"
+ visible: MusicManager.canGoNext
+ onClicked: MusicManager.canGoNext ? MusicManager.next() : {
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/quickshell/Modules/Panel/Cards/SystemMonitorCard.qml b/quickshell/Modules/Panel/Cards/SystemMonitorCard.qml
new file mode 100644
index 0000000..ad24efa
--- /dev/null
+++ b/quickshell/Modules/Panel/Cards/SystemMonitorCard.qml
@@ -0,0 +1,61 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.Constants
+import qs.Modules.Panel.Misc
+import qs.Noctalia
+import qs.Services
+import qs.Utils
+
+// Unified system card: monitors CPU, temp, memory, disk
+NBox {
+ id: root
+
+ compact: true
+
+ ColumnLayout {
+ id: content
+
+ anchors.fill: parent
+ anchors.margins: Style.marginXS
+ spacing: Style.marginS
+
+ MonitorSlider {
+ icon: "cpu-usage"
+ value: SystemStatService.cpuUsage
+ from: 0
+ to: 100
+ colorFill: Colors.teal
+ Layout.fillWidth: true
+ }
+
+ MonitorSlider {
+ icon: "memory"
+ value: SystemStatService.memPercent
+ from: 0
+ to: 100
+ colorFill: Colors.green
+ Layout.fillWidth: true
+ }
+
+ MonitorSlider {
+ icon: "cpu-temperature"
+ value: SystemStatService.cpuTemp
+ from: 0
+ to: 100
+ colorFill: Colors.yellow
+ Layout.fillWidth: true
+ }
+
+ MonitorSlider {
+ icon: "storage"
+ value: SystemStatService.diskPercent
+ from: 0
+ to: 100
+ colorFill: Colors.peach
+ Layout.fillWidth: true
+ }
+
+ }
+
+}
diff --git a/quickshell/Modules/Panel/Cards/TopLeftCard.qml b/quickshell/Modules/Panel/Cards/TopLeftCard.qml
new file mode 100644
index 0000000..cbfd7bb
--- /dev/null
+++ b/quickshell/Modules/Panel/Cards/TopLeftCard.qml
@@ -0,0 +1,175 @@
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Services.UPower
+import Quickshell.Widgets
+import qs.Constants
+import qs.Noctalia
+import qs.Services
+import qs.Utils
+
+ColumnLayout {
+ id: root
+
+ readonly property bool hasPP: PowerProfileService.available
+
+ spacing: Style.marginM
+
+ NBox {
+ id: whoamiBox
+
+ property string uptimeText: "--"
+ property string hostname: "--"
+
+ function updateSystemInfo() {
+ uptimeProcess.running = true;
+ hostnameProcess.running = true;
+ }
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ RowLayout {
+ id: content
+
+ spacing: root.spacing
+ anchors.fill: parent
+ anchors.margins: root.spacing
+
+ NImageCircled {
+ width: Style.baseWidgetSize * 1.5
+ height: Style.baseWidgetSize * 1.5
+ imagePath: Quickshell.shellDir + "/Assets/Images/Avatar.jpg"
+ fallbackIcon: "person"
+ borderColor: Color.mPrimary
+ borderWidth: Math.max(1, Style.borderM)
+ Layout.alignment: Qt.AlignVCenter
+ Layout.topMargin: Style.marginXS
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginXXS
+
+ NText {
+ text: `${Quickshell.env("USER") || "user"} @ ${whoamiBox.hostname}`
+ font.weight: Style.fontWeightBold
+ font.pointSize: Style.fontSizeL
+ font.capitalization: Font.Capitalize
+ }
+
+ NText {
+ text: "Uptime: " + whoamiBox.uptimeText
+ font.pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ }
+
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ }
+
+ // ----------------------------------
+ // Uptime
+ Timer {
+ interval: 60000
+ repeat: true
+ running: true
+ onTriggered: uptimeProcess.running = true
+ }
+
+ Process {
+ id: uptimeProcess
+
+ command: ["cat", "/proc/uptime"]
+ running: true
+
+ stdout: StdioCollector {
+ onStreamFinished: {
+ var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]);
+ whoamiBox.uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds);
+ uptimeProcess.running = false;
+ }
+ }
+
+ }
+
+ Process {
+ id: hostnameProcess
+
+ command: ["cat", "/etc/hostname"]
+ running: true
+
+ stdout: StdioCollector {
+ onStreamFinished: {
+ whoamiBox.hostname = this.text.trim();
+ hostnameProcess.running = false;
+ }
+ }
+
+ }
+
+ }
+
+ RowLayout {
+ id: utilitiesRow
+
+ Layout.fillWidth: true
+
+ // Performance
+ NIconButton {
+ implicitHeight: 32
+ implicitWidth: 32
+ icon: PowerProfileService.getIcon(PowerProfile.Performance)
+ enabled: hasPP
+ opacity: enabled ? Style.opacityFull : Style.opacityMedium
+ colorBgHover: Colors.red
+ colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Colors.red : Color.transparent
+ colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Colors.red
+ onClicked: PowerProfileService.setProfile(PowerProfile.Performance)
+ }
+
+ // Balanced
+ NIconButton {
+ implicitHeight: 32
+ implicitWidth: 32
+ icon: PowerProfileService.getIcon(PowerProfile.Balanced)
+ enabled: hasPP
+ opacity: enabled ? Style.opacityFull : Style.opacityMedium
+ colorBgHover: Colors.blue
+ colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Colors.blue : Color.transparent
+ colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Colors.blue
+ onClicked: PowerProfileService.setProfile(PowerProfile.Balanced)
+ }
+
+ // Eco
+ NIconButton {
+ implicitHeight: 32
+ implicitWidth: 32
+ icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
+ enabled: hasPP
+ opacity: enabled ? Style.opacityFull : Style.opacityMedium
+ colorBgHover: Colors.green
+ colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Colors.green : Color.transparent
+ colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Colors.green
+ onClicked: PowerProfileService.setProfile(PowerProfile.PowerSaver)
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ // Lyrics Offset
+ NText {
+ text: `Lyrics Offset: ${LyricsService.offset >= 0 ? '+' : ''}${LyricsService.offset} ms`
+ Layout.alignment: Qt.AlignVCenter
+ }
+
+ }
+
+}
diff --git a/quickshell/Modules/Panel/ControlCenterPanel.qml b/quickshell/Modules/Panel/ControlCenterPanel.qml
new file mode 100644
index 0000000..8f2cfb4
--- /dev/null
+++ b/quickshell/Modules/Panel/ControlCenterPanel.qml
@@ -0,0 +1,86 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import qs.Constants
+import qs.Modules.Panel.Cards
+import qs.Noctalia
+import qs.Services
+import qs.Utils
+
+NPanel {
+ id: root
+
+ // Positioning
+ readonly property string controlCenterPosition: "top_left"
+ property real topCardHeight: 120
+ property real middleCardHeight: 100
+ property real bottomCardHeight: 200
+
+ preferredWidth: 480
+ preferredHeight: topCardHeight + middleCardHeight + bottomCardHeight + Style.marginL * 4
+ panelKeyboardFocus: false
+ panelAnchorHorizontalCenter: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_center")
+ panelAnchorVerticalCenter: false
+ panelAnchorLeft: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left")
+ panelAnchorRight: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right")
+ panelAnchorBottom: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_")
+ panelAnchorTop: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("top_")
+
+ panelContent: Item {
+ id: content
+
+ property real cardSpacing: Style.marginL
+
+ // Layout content
+ ColumnLayout {
+ id: layout
+
+ anchors.fill: parent
+ anchors.margins: content.cardSpacing
+ spacing: content.cardSpacing
+
+ // Top Card: profile + utilities
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.preferredHeight: topCardHeight
+
+ TopLeftCard {
+ Layout.fillWidth: true
+ Layout.maximumHeight: topCardHeight
+ }
+
+ LyricsControl {
+ Layout.preferredHeight: topCardHeight
+ }
+
+ }
+
+ LyricsCard {
+ Layout.fillWidth: true
+ Layout.preferredHeight: middleCardHeight
+ }
+
+ // Media + stats column
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.preferredHeight: bottomCardHeight
+ spacing: content.cardSpacing
+
+ SystemMonitorCard {
+ Layout.fillWidth: true
+ Layout.preferredHeight: bottomCardHeight
+ }
+
+ MediaCard {
+ Layout.preferredWidth: 270
+ Layout.preferredHeight: bottomCardHeight
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/quickshell/Modules/Panel/Misc/MonitorSlider.qml b/quickshell/Modules/Panel/Misc/MonitorSlider.qml
new file mode 100644
index 0000000..778d113
--- /dev/null
+++ b/quickshell/Modules/Panel/Misc/MonitorSlider.qml
@@ -0,0 +1,54 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.Constants
+import qs.Noctalia
+
+Item {
+ id: root
+
+ property string icon: "volume-high"
+ property real value: 50
+ property real from: 0
+ property real to: 100
+ property color colorFill: Colors.primary
+ property color colorRest: Colors.surface0
+
+ implicitHeight: layout.implicitHeight
+
+ RowLayout {
+ id: layout
+
+ anchors.fill: parent
+ spacing: Style.marginS
+
+ NIcon {
+ id: iconItem
+
+ icon: root.icon
+ color: root.colorFill
+ }
+
+ Rectangle {
+ id: whole
+
+ Layout.fillWidth: true
+ color: root.colorRest
+ radius: height / 2
+ height: Style.baseWidgetSize * 0.3
+
+ Rectangle {
+ id: fill
+
+ width: Math.max(0, Math.min(whole.width, (root.value - root.from) / (root.to - root.from) * whole.width))
+ height: parent.height
+ color: root.colorFill
+ radius: height / 2
+ anchors.left: parent.left
+ }
+
+ }
+
+ }
+
+}
diff --git a/quickshell/Noctalia/NBox.qml b/quickshell/Noctalia/NBox.qml
new file mode 100644
index 0000000..c753d07
--- /dev/null
+++ b/quickshell/Noctalia/NBox.qml
@@ -0,0 +1,28 @@
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Layouts
+import qs.Constants
+import qs.Noctalia
+
+// Rounded group container using the variant surface color.
+// To be used in side panels and settings panes to group fields or buttons.
+Rectangle {
+ id: root
+
+ property bool compact: false
+
+ implicitWidth: childrenRect.width
+ implicitHeight: childrenRect.height
+ color: compact ? Color.transparent : Color.mSurfaceVariant
+ radius: Style.radiusM
+ layer.enabled: !compact
+
+ layer.effect: DropShadow {
+ horizontalOffset: 6
+ verticalOffset: 6
+ radius: 8
+ samples: 12
+ color: Qt.rgba(0, 0, 0, 0.3)
+ }
+
+}
diff --git a/quickshell/Noctalia/NBusyIndicator.qml b/quickshell/Noctalia/NBusyIndicator.qml
new file mode 100644
index 0000000..137f37b
--- /dev/null
+++ b/quickshell/Noctalia/NBusyIndicator.qml
@@ -0,0 +1,53 @@
+import QtQuick
+import qs.Constants
+import qs.Noctalia
+
+Item {
+ id: root
+
+ property bool running: true
+ property color color: Color.mPrimary
+ property int size: Style.baseWidgetSize
+ property int strokeWidth: Style.borderL
+ property int duration: Style.animationSlow * 2
+
+ implicitWidth: size
+ implicitHeight: size
+
+ Canvas {
+ id: canvas
+
+ property real rotationAngle: 0
+
+ anchors.fill: parent
+ onPaint: {
+ var ctx = getContext("2d");
+ ctx.reset();
+ var centerX = width / 2;
+ var centerY = height / 2;
+ var radius = Math.min(width, height) / 2 - strokeWidth / 2;
+ ctx.strokeStyle = root.color;
+ ctx.lineWidth = Math.max(1, root.strokeWidth);
+ ctx.lineCap = "round";
+ // Draw arc with gap (270 degrees with 90 degree gap)
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, -Math.PI / 2 + rotationAngle, -Math.PI / 2 + rotationAngle + Math.PI * 1.5);
+ ctx.stroke();
+ }
+ onRotationAngleChanged: {
+ requestPaint();
+ }
+
+ NumberAnimation {
+ target: canvas
+ property: "rotationAngle"
+ running: root.running
+ from: 0
+ to: 2 * Math.PI
+ duration: root.duration
+ loops: Animation.Infinite
+ }
+
+ }
+
+}
diff --git a/quickshell/Noctalia/NCircleStat.qml b/quickshell/Noctalia/NCircleStat.qml
new file mode 100644
index 0000000..59dc94f
--- /dev/null
+++ b/quickshell/Noctalia/NCircleStat.qml
@@ -0,0 +1,122 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Noctalia
+import qs.Services
+import qs.Utils
+
+// Compact circular statistic display using Layout management
+Rectangle {
+ id: root
+
+ property real value: 0 // 0..100 (or any range visually mapped)
+ property string icon: ""
+ property string suffix: "%"
+ // When nested inside a parent group (NBox), you can make it flat
+ property bool flat: false
+ // Scales the internal content (labels, gauge, icon) without changing the
+ // outer width/height footprint of the component
+ property real contentScale: 1
+
+ width: 68
+ height: 92
+ color: flat ? Color.transparent : Color.mSurface
+ radius: Style.radiusS
+ border.color: flat ? Color.transparent : Color.mSurfaceVariant
+ border.width: flat ? 0 : Math.max(1, Style.borderS)
+ // Repaint gauge when the bound value changes
+ onValueChanged: gauge.requestPaint()
+
+ ColumnLayout {
+ id: mainLayout
+
+ anchors.fill: parent
+ anchors.margins: Style.marginS * contentScale
+ spacing: 0
+
+ // Main gauge container
+ Item {
+ id: gaugeContainer
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.alignment: Qt.AlignCenter
+ Layout.preferredWidth: 68 * contentScale
+ Layout.preferredHeight: 68 * contentScale
+
+ Canvas {
+ // 390° (equivalent to 30°)
+
+ id: gauge
+
+ anchors.fill: parent
+ renderStrategy: Canvas.Cooperative
+ onPaint: {
+ const ctx = getContext("2d");
+ const w = width, h = height;
+ const cx = w / 2, cy = h / 2;
+ const r = Math.min(w, h) / 2 - 5 * contentScale;
+ // Rotated 90° to the right: gap at the bottom
+ // Start at 150° and end at 390° (30°) → bottom opening
+ const start = Math.PI * 5 / 6;
+ // 150°
+ const endBg = Math.PI * 13 / 6;
+ ctx.reset();
+ ctx.lineWidth = 6 * contentScale;
+ // Track uses surfaceVariant for stronger contrast
+ ctx.strokeStyle = Color.mSurface;
+ ctx.beginPath();
+ ctx.arc(cx, cy, r, start, endBg);
+ ctx.stroke();
+ // Value arc
+ const ratio = Math.max(0, Math.min(1, root.value / 100));
+ const end = start + (endBg - start) * ratio;
+ ctx.strokeStyle = Color.mPrimary;
+ ctx.beginPath();
+ ctx.arc(cx, cy, r, start, end);
+ ctx.stroke();
+ }
+ }
+
+ // Percent centered in the circle
+ NText {
+ id: valueLabel
+
+ anchors.centerIn: parent
+ anchors.verticalCenterOffset: -4 * contentScale
+ text: `${root.value}${root.suffix}`
+ pointSize: Style.fontSizeM * contentScale
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ // Tiny circular badge for the icon, positioned inside below the percentage
+ Rectangle {
+ id: iconBadge
+
+ width: iconText.implicitWidth + Style.marginXXS
+ height: width
+ radius: width / 2
+ color: Color.mPrimary
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: valueLabel.bottom
+ anchors.topMargin: 8 * contentScale
+
+ NIcon {
+ id: iconText
+
+ anchors.centerIn: parent
+ icon: root.icon
+ color: Color.mOnPrimary
+ pointSize: Style.fontSizeS
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/quickshell/Noctalia/NContextMenu.qml b/quickshell/Noctalia/NContextMenu.qml
new file mode 100644
index 0000000..c2d735d
--- /dev/null
+++ b/quickshell/Noctalia/NContextMenu.qml
@@ -0,0 +1,124 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import qs.Constants
+import qs.Noctalia
+import qs.Services
+import qs.Utils
+
+Popup {
+ id: root
+
+ property alias model: listView.model
+ property real itemHeight: 36
+ property real itemPadding: Style.marginM
+
+ signal triggered(string action)
+
+ // Helper function to open at mouse position
+ function openAt(x, y) {
+ root.x = x;
+ root.y = y;
+ root.open();
+ }
+
+ // Helper function to open at item
+ function openAtItem(item, mouseX, mouseY) {
+ var pos = item.mapToItem(root.parent, mouseX || 0, mouseY || 0);
+ openAt(pos.x, pos.y);
+ }
+
+ width: 180
+ padding: Style.marginS
+ onOpened: PanelService.willOpenPopup(root)
+ onClosed: PanelService.willClosePopup(root)
+
+ background: Rectangle {
+ color: Color.mSurfaceVariant
+ border.color: Color.mOutline
+ border.width: Math.max(1, Style.borderS)
+ radius: Style.radiusM
+ }
+
+ contentItem: NListView {
+ id: listView
+
+ implicitHeight: contentHeight
+ spacing: Style.marginXXS
+ interactive: contentHeight > root.height
+
+ delegate: ItemDelegate {
+ id: menuItem
+
+ // Store reference to the popup
+ property var popup: root
+
+ width: listView.width
+ height: modelData.visible !== false ? root.itemHeight : 0
+ visible: modelData.visible !== false
+ opacity: modelData.enabled !== false ? 1 : 0.5
+ enabled: modelData.enabled !== false
+ onClicked: {
+ if (enabled) {
+ popup.triggered(modelData.action || modelData.key || index.toString());
+ popup.close();
+ }
+ }
+
+ background: Rectangle {
+ color: menuItem.hovered && menuItem.enabled ? Color.mTertiary : Color.transparent
+ radius: Style.radiusS
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ }
+
+ contentItem: RowLayout {
+ spacing: Style.marginS
+
+ // Optional icon
+ NIcon {
+ visible: modelData.icon !== undefined
+ icon: modelData.icon || ""
+ pointSize: Style.fontSizeM
+ color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface
+ Layout.leftMargin: root.itemPadding
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ }
+
+ NText {
+ text: modelData.label || modelData.text || ""
+ pointSize: Style.fontSizeM
+ color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface
+ verticalAlignment: Text.AlignVCenter
+ Layout.fillWidth: true
+ Layout.leftMargin: modelData.icon === undefined ? root.itemPadding : 0
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/quickshell/Noctalia/NDivider.qml b/quickshell/Noctalia/NDivider.qml
new file mode 100644
index 0000000..d476143
--- /dev/null
+++ b/quickshell/Noctalia/NDivider.qml
@@ -0,0 +1,35 @@
+import QtQuick
+import Quickshell
+import Quickshell.Widgets
+import qs.Constants
+
+Rectangle {
+ width: parent.width
+ height: Math.max(1, Style.borderS)
+
+ gradient: Gradient {
+ orientation: Gradient.Horizontal
+
+ GradientStop {
+ position: 0
+ color: Color.transparent
+ }
+
+ GradientStop {
+ position: 0.1
+ color: Color.mOutline
+ }
+
+ GradientStop {
+ position: 0.9
+ color: Color.mOutline
+ }
+
+ GradientStop {
+ position: 1
+ color: Color.transparent
+ }
+
+ }
+
+}
diff --git a/quickshell/Noctalia/NIcon.qml b/quickshell/Noctalia/NIcon.qml
new file mode 100644
index 0000000..831aa60
--- /dev/null
+++ b/quickshell/Noctalia/NIcon.qml
@@ -0,0 +1,28 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Constants
+import qs.Noctalia
+
+Text {
+ id: root
+
+ property string icon: Icons.defaultIcon
+ property real pointSize: Style.fontSizeL
+
+ visible: (icon !== undefined) && (icon !== "")
+ text: {
+ if ((icon === undefined) || (icon === ""))
+ return "";
+
+ if (Icons.get(icon) === undefined) {
+ Logger.warn("Icon", `"${icon}"`, "doesn't exist in the icons font");
+ Logger.callStack();
+ return Icons.get(Icons.defaultIcon);
+ }
+ return Icons.get(icon);
+ }
+ font.family: Icons.fontFamily
+ font.pointSize: root.pointSize
+ color: Color.mOnSurface
+ verticalAlignment: Text.AlignVCenter
+}
diff --git a/quickshell/Noctalia/NIconButton.qml b/quickshell/Noctalia/NIconButton.qml
new file mode 100644
index 0000000..bebffe4
--- /dev/null
+++ b/quickshell/Noctalia/NIconButton.qml
@@ -0,0 +1,92 @@
+import QtQuick
+import Quickshell
+import Quickshell.Widgets
+import qs.Constants
+import qs.Noctalia
+
+Rectangle {
+ id: root
+
+ property real baseSize: Style.baseWidgetSize
+ property string icon
+ property bool enabled: true
+ property bool allowClickWhenDisabled: false
+ property bool hovering: false
+ property bool compact: false
+ property color colorBg: Color.mSurfaceVariant
+ property color colorFg: Color.mPrimary
+ property color colorBgHover: Color.mTertiary
+ property color colorFgHover: Color.mOnTertiary
+ property color colorBorder: Color.transparent
+ property color colorBorderHover: Color.transparent
+
+ signal entered()
+ signal exited()
+ signal clicked()
+ signal rightClicked()
+ signal middleClicked()
+
+ implicitWidth: Math.round(baseSize)
+ implicitHeight: Math.round(baseSize)
+ opacity: root.enabled ? Style.opacityFull : Style.opacityMedium
+ color: root.enabled && root.hovering ? colorBgHover : colorBg
+ radius: width * 0.5
+ border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder
+ border.width: Math.max(1, Style.borderS)
+
+ NIcon {
+ icon: root.icon
+ pointSize: Math.max(1, root.compact ? root.width * 0.65 : root.width * 0.48)
+ color: root.enabled && root.hovering ? colorFgHover : colorFg
+ // Center horizontally
+ x: (root.width - width) / 2
+ // Center vertically accounting for font metrics
+ y: (root.height - height) / 2 + (height - contentHeight) / 2
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ easing.type: Easing.InOutQuad
+ }
+
+ }
+
+ }
+
+ MouseArea {
+ // Always enabled to allow hover/tooltip even when the button is disabled
+ enabled: true
+ anchors.fill: parent
+ cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ hoverEnabled: true
+ onEntered: {
+ hovering = root.enabled ? true : false;
+ root.entered();
+ }
+ onExited: {
+ hovering = false;
+ root.exited();
+ }
+ onClicked: function(mouse) {
+ if (!root.enabled && !allowClickWhenDisabled)
+ return ;
+
+ if (mouse.button === Qt.LeftButton)
+ root.clicked();
+ else if (mouse.button === Qt.RightButton)
+ root.rightClicked();
+ else if (mouse.button === Qt.MiddleButton)
+ root.middleClicked();
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationNormal
+ easing.type: Easing.InOutQuad
+ }
+
+ }
+
+}
diff --git a/quickshell/Noctalia/NImageCircled.qml b/quickshell/Noctalia/NImageCircled.qml
new file mode 100644
index 0000000..d091fe1
--- /dev/null
+++ b/quickshell/Noctalia/NImageCircled.qml
@@ -0,0 +1,85 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Widgets
+import qs.Constants
+import qs.Noctalia
+import qs.Services
+
+Rectangle {
+ id: root
+
+ property string imagePath: ""
+ property color borderColor: Color.transparent
+ property real borderWidth: 0
+ property string fallbackIcon: ""
+ property real fallbackIconSize: Style.fontSizeXXL
+
+ color: Color.transparent
+ radius: parent.width * 0.5
+ anchors.margins: Style.marginXXS
+
+ Rectangle {
+ color: Color.transparent
+ anchors.fill: parent
+
+ Image {
+ id: img
+
+ anchors.fill: parent
+ source: imagePath
+ visible: false // Hide since we're using it as shader source
+ mipmap: true
+ smooth: true
+ asynchronous: true
+ antialiasing: true
+ fillMode: Image.PreserveAspectCrop
+ }
+
+ ShaderEffect {
+ property var source
+ property real imageOpacity: root.opacity
+
+ anchors.fill: parent
+ fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/circled_image.frag.qsb")
+ supportsAtlasTextures: false
+ blending: true
+
+ source: ShaderEffectSource {
+ sourceItem: img
+ hideSource: true
+ live: true
+ recursive: false
+ format: ShaderEffectSource.RGBA
+ }
+
+ }
+
+ // Fallback icon
+ Loader {
+ active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
+ anchors.centerIn: parent
+
+ sourceComponent: NIcon {
+ anchors.centerIn: parent
+ icon: fallbackIcon
+ pointSize: fallbackIconSize
+ z: 0
+ }
+
+ }
+
+ }
+
+ // Border
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: Color.transparent
+ border.color: parent.borderColor
+ border.width: parent.borderWidth
+ antialiasing: true
+ z: 10
+ }
+
+}
diff --git a/quickshell/Noctalia/NImageRounded.qml b/quickshell/Noctalia/NImageRounded.qml
new file mode 100644
index 0000000..d7dfea3
--- /dev/null
+++ b/quickshell/Noctalia/NImageRounded.qml
@@ -0,0 +1,103 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Widgets
+import qs.Constants
+import qs.Noctalia
+
+Rectangle {
+ id: root
+
+ property string imagePath: ""
+ property color borderColor: Color.transparent
+ property real borderWidth: 0
+ property real imageRadius: width * 0.5
+ property string fallbackIcon: ""
+ property real fallbackIconSize: Style.fontSizeXXL
+ property real scaledRadius: imageRadius
+
+ signal statusChanged(int status)
+
+ color: Color.transparent
+ radius: scaledRadius
+ anchors.margins: Style.marginXXS
+
+ Rectangle {
+ color: Color.transparent
+ anchors.fill: parent
+
+ Image {
+ id: img
+
+ anchors.fill: parent
+ source: imagePath
+ visible: false // Hide since we're using it as shader source
+ mipmap: true
+ smooth: true
+ asynchronous: true
+ antialiasing: true
+ fillMode: Image.PreserveAspectCrop
+ onStatusChanged: root.statusChanged(status)
+ }
+
+ ShaderEffect {
+ property var source
+ // Use custom property names to avoid conflicts with final properties
+ property real itemWidth: root.width
+ property real itemHeight: root.height
+ property real cornerRadius: root.radius
+ property real imageOpacity: root.opacity
+
+ anchors.fill: parent
+ fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
+ // Qt6 specific properties - ensure proper blending
+ supportsAtlasTextures: false
+ blending: true
+
+ // Make sure the background is transparent
+ Rectangle {
+ id: background
+
+ anchors.fill: parent
+ color: Color.transparent
+ z: -1
+ }
+
+ source: ShaderEffectSource {
+ sourceItem: img
+ hideSource: true
+ live: true
+ recursive: false
+ format: ShaderEffectSource.RGBA
+ }
+
+ }
+
+ // Fallback icon
+ Loader {
+ active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
+ anchors.centerIn: parent
+
+ sourceComponent: NIcon {
+ anchors.centerIn: parent
+ icon: fallbackIcon
+ pointSize: fallbackIconSize
+ z: 0
+ }
+
+ }
+
+ }
+
+ // Border
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: Color.transparent
+ border.color: parent.borderColor
+ border.width: parent.borderWidth
+ antialiasing: true
+ z: 10
+ }
+
+}
diff --git a/quickshell/Noctalia/NListView.qml b/quickshell/Noctalia/NListView.qml
new file mode 100644
index 0000000..2d79633
--- /dev/null
+++ b/quickshell/Noctalia/NListView.qml
@@ -0,0 +1,217 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Templates as T
+import qs.Constants
+import qs.Noctalia
+
+Item {
+ id: root
+
+ property color handleColor: Qt.alpha(Color.mTertiary, 0.8)
+ property color handleHoverColor: handleColor
+ property color handlePressedColor: handleColor
+ property color trackColor: Color.transparent
+ property real handleWidth: 6
+ property real handleRadius: Style.radiusM
+ property int verticalPolicy: ScrollBar.AsNeeded
+ property int horizontalPolicy: ScrollBar.AsNeeded
+ // Forward ListView properties
+ property alias model: listView.model
+ property alias delegate: listView.delegate
+ property alias spacing: listView.spacing
+ property alias orientation: listView.orientation
+ property alias currentIndex: listView.currentIndex
+ property alias count: listView.count
+ property alias contentHeight: listView.contentHeight
+ property alias contentWidth: listView.contentWidth
+ property alias contentY: listView.contentY
+ property alias contentX: listView.contentX
+ property alias currentItem: listView.currentItem
+ property alias highlightItem: listView.highlightItem
+ property alias headerItem: listView.headerItem
+ property alias footerItem: listView.footerItem
+ property alias section: listView.section
+ property alias highlightFollowsCurrentItem: listView.highlightFollowsCurrentItem
+ property alias highlightMoveDuration: listView.highlightMoveDuration
+ property alias highlightMoveVelocity: listView.highlightMoveVelocity
+ property alias preferredHighlightBegin: listView.preferredHighlightBegin
+ property alias preferredHighlightEnd: listView.preferredHighlightEnd
+ property alias highlightRangeMode: listView.highlightRangeMode
+ property alias snapMode: listView.snapMode
+ property alias keyNavigationWraps: listView.keyNavigationWraps
+ property alias cacheBuffer: listView.cacheBuffer
+ property alias displayMarginBeginning: listView.displayMarginBeginning
+ property alias displayMarginEnd: listView.displayMarginEnd
+ property alias layoutDirection: listView.layoutDirection
+ property alias effectiveLayoutDirection: listView.effectiveLayoutDirection
+ property alias verticalLayoutDirection: listView.verticalLayoutDirection
+ property alias boundsBehavior: listView.boundsBehavior
+ property alias flickableDirection: listView.flickableDirection
+ property alias interactive: listView.interactive
+ property alias moving: listView.moving
+ property alias flicking: listView.flicking
+ property alias dragging: listView.dragging
+ property alias horizontalVelocity: listView.horizontalVelocity
+ property alias verticalVelocity: listView.verticalVelocity
+
+ // Forward ListView methods
+ function positionViewAtIndex(index, mode) {
+ listView.positionViewAtIndex(index, mode);
+ }
+
+ function positionViewAtBeginning() {
+ listView.positionViewAtBeginning();
+ }
+
+ function positionViewAtEnd() {
+ listView.positionViewAtEnd();
+ }
+
+ function forceLayout() {
+ listView.forceLayout();
+ }
+
+ function cancelFlick() {
+ listView.cancelFlick();
+ }
+
+ function flick(xVelocity, yVelocity) {
+ listView.flick(xVelocity, yVelocity);
+ }
+
+ function incrementCurrentIndex() {
+ listView.incrementCurrentIndex();
+ }
+
+ function decrementCurrentIndex() {
+ listView.decrementCurrentIndex();
+ }
+
+ function indexAt(x, y) {
+ return listView.indexAt(x, y);
+ }
+
+ function itemAt(x, y) {
+ return listView.itemAt(x, y);
+ }
+
+ function itemAtIndex(index) {
+ return listView.itemAtIndex(index);
+ }
+
+ // Set reasonable implicit sizes for Layout usage
+ implicitWidth: 200
+ implicitHeight: 200
+
+ ListView {
+ id: listView
+
+ anchors.fill: parent
+ // Enable clipping to keep content within bounds
+ clip: true
+ // Enable flickable for smooth scrolling
+ boundsBehavior: Flickable.StopAtBounds
+
+ ScrollBar.vertical: ScrollBar {
+ parent: listView
+ x: listView.mirrored ? 0 : listView.width - width
+ y: 0
+ height: listView.height
+ active: listView.ScrollBar.horizontal.active
+ policy: root.verticalPolicy
+
+ contentItem: Rectangle {
+ implicitWidth: root.handleWidth
+ implicitHeight: 100
+ radius: root.handleRadius
+ color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
+ opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ }
+
+ background: Rectangle {
+ implicitWidth: root.handleWidth
+ implicitHeight: 100
+ color: root.trackColor
+ opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0
+ radius: root.handleRadius / 2
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ }
+
+ }
+
+ ScrollBar.horizontal: ScrollBar {
+ id: horizontalScrollBar
+
+ parent: listView
+ x: 0
+ y: listView.height - height
+ width: listView.width
+ active: listView.ScrollBar.vertical.active
+ policy: root.horizontalPolicy
+
+ contentItem: Rectangle {
+ implicitWidth: 100
+ implicitHeight: root.handleWidth
+ radius: root.handleRadius
+ color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
+ opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ }
+
+ background: Rectangle {
+ implicitWidth: 100
+ implicitHeight: root.handleWidth
+ color: root.trackColor
+ opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0
+ radius: root.handleRadius / 2
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/quickshell/Noctalia/NPanel.qml b/quickshell/Noctalia/NPanel.qml
new file mode 100644
index 0000000..107bf44
--- /dev/null
+++ b/quickshell/Noctalia/NPanel.qml
@@ -0,0 +1,459 @@
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import qs.Noctalia
+import qs.Services
+import qs.Constants
+import qs.Utils
+
+Loader {
+ id: root
+
+ property ShellScreen screen
+
+ property Component panelContent: null
+ property real preferredWidth: 700
+ property real preferredHeight: 900
+ property real preferredWidthRatio
+ property real preferredHeightRatio
+ property color panelBackgroundColor: Color.mSurface
+ property bool draggable: false
+ property var buttonItem: null
+ property string buttonName: ""
+
+ property bool panelAnchorHorizontalCenter: false
+ property bool panelAnchorVerticalCenter: false
+ property bool panelAnchorTop: false
+ property bool panelAnchorBottom: false
+ property bool panelAnchorLeft: false
+ property bool panelAnchorRight: false
+
+ property bool isMasked: false
+
+ // Properties to support positioning relative to the opener (button)
+ property bool useButtonPosition: false
+ property point buttonPosition: Qt.point(0, 0)
+ property int buttonWidth: 0
+ property int buttonHeight: 0
+
+ property bool panelKeyboardFocus: false
+ property bool backgroundClickEnabled: true
+
+ // Animation properties
+ readonly property real originalScale: 0.7
+ readonly property real originalOpacity: 0.0
+ property real scaleValue: originalScale
+ property real opacityValue: originalOpacity
+ property real dimmingOpacity: 0
+
+ signal opened
+ signal closed
+
+ active: false
+ asynchronous: true
+
+ Component.onCompleted: {
+ PanelService.registerPanel(root)
+ }
+
+ // -----------------------------------------
+ // Functions to control background click behavior
+ function disableBackgroundClick() {
+ backgroundClickEnabled = false
+ }
+
+ function enableBackgroundClick() {
+ // Add a small delay to prevent immediate close after drag release
+ enableBackgroundClickTimer.restart()
+ }
+
+ Timer {
+ id: enableBackgroundClickTimer
+ interval: 100
+ repeat: false
+ onTriggered: backgroundClickEnabled = true
+ }
+
+ // -----------------------------------------
+ function toggle(buttonItem, buttonName) {
+ if (!active) {
+ open(buttonItem, buttonName)
+ } else {
+ close()
+ }
+ }
+
+ // -----------------------------------------
+ function open(buttonItem, buttonName) {
+ root.buttonItem = buttonItem
+ root.buttonName = buttonName || ""
+
+ setPosition()
+
+ PanelService.willOpenPanel(root)
+
+ backgroundClickEnabled = true
+ active = true
+ root.opened()
+ }
+
+ // -----------------------------------------
+ function close() {
+ dimmingOpacity = 0
+ scaleValue = originalScale
+ opacityValue = originalOpacity
+ root.closed()
+ active = false
+ useButtonPosition = false
+ backgroundClickEnabled = true
+ PanelService.closedPanel(root)
+ }
+
+ // -----------------------------------------
+ function setPosition() {
+ // If we have a button name, we are landing here from an IPC call.
+ // IPC calls have no idead on which screen they panel will spawn.
+ // Resolve the button name to a proper button item now that we have a screen.
+ if (buttonName !== "" && root.screen !== null) {
+ buttonItem = BarService.lookupWidget(buttonName, root.screen.name)
+ }
+
+ // Get the button position if provided
+ if (buttonItem !== undefined && buttonItem !== null) {
+ useButtonPosition = true
+ var itemPos = buttonItem.mapToItem(null, 0, 0)
+ buttonPosition = Qt.point(itemPos.x, itemPos.y)
+ buttonWidth = buttonItem.width
+ buttonHeight = buttonItem.height
+ } else {
+ useButtonPosition = false
+ }
+ }
+
+ // -----------------------------------------
+ sourceComponent: Component {
+ PanelWindow {
+ id: panelWindow
+
+ readonly property bool isVertical: false
+ readonly property bool barIsVisible: (screen !== null)
+ readonly property real verticalBarWidth: Math.round(Style.barHeight)
+
+ Component.onCompleted: {
+ Logger.log("NPanel", "Opened", root.objectName)
+ dimmingOpacity = Style.opacityHeavy
+ }
+
+ Connections {
+ target: panelWindow
+ function onScreenChanged() {
+ root.screen = screen
+
+ // If called from IPC always reposition if screen is updated
+ if (buttonName) {
+ setPosition()
+ }
+ // Logger.log("NPanel", "OnScreenChanged", root.screen.name)
+ }
+ }
+
+ visible: true
+ color: Qt.alpha(Color.mShadow, dimmingOpacity)
+
+ WlrLayershell.exclusionMode: ExclusionMode.Ignore
+ WlrLayershell.namespace: "noctalia-panel"
+ WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
+
+ mask: root.isMasked ? maskRegion : null
+
+ Region {
+ id: maskRegion
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationNormal
+ }
+ }
+
+ anchors.top: true
+ anchors.left: true
+ anchors.right: true
+ anchors.bottom: true
+
+ // Close any panel with Esc without requiring focus
+ Shortcut {
+ sequences: ["Escape"]
+ enabled: root.active
+ onActivated: root.close()
+ context: Qt.WindowShortcut
+ }
+
+ // Clicking outside of the rectangle to close
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.backgroundClickEnabled
+ onClicked: root.close()
+ }
+
+ // The actual panel's content
+ Rectangle {
+ id: panelBackground
+ color: panelBackgroundColor
+ radius: Style.radiusL
+ border.color: Color.mOutline
+ border.width: Math.max(1, Style.borderS )
+ // Dragging support
+ property bool draggable: root.draggable
+ property bool isDragged: false
+ property real manualX: 0
+ property real manualY: 0
+ width: {
+ var w
+ if (preferredWidthRatio !== undefined) {
+ w = Math.round(Math.max(screen?.width * preferredWidthRatio, preferredWidth) )
+ } else {
+ w = preferredWidth
+ }
+ // Clamp width so it is never bigger than the screen
+ return Math.min(w, screen?.width - Style.marginL * 2)
+ }
+ height: {
+ var h
+ if (preferredHeightRatio !== undefined) {
+ h = Math.round(Math.max(screen?.height * preferredHeightRatio, preferredHeight) )
+ } else {
+ h = preferredHeight
+ }
+
+ // Clamp width so it is never bigger than the screen
+ return Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2)
+ }
+
+ scale: root.scaleValue
+ opacity: root.isMasked ? 0 : root.opacityValue
+ x: isDragged ? manualX : calculatedX
+ y: isDragged ? manualY : calculatedY
+
+ // ---------------------------------------------
+ // Does not account for corners are they are negligible and helps keep the code clean.
+ // ---------------------------------------------
+ property real marginTop: {
+ if (!barIsVisible) {
+ return 0
+ }
+ return (Style.barHeight + Style.marginS)
+ }
+
+ property real marginBottom: {
+ if (!barIsVisible) {
+ return 0
+ }
+ return Style.marginS
+ }
+
+ property real marginLeft: {
+ if (!barIsVisible) {
+ return 0
+ }
+ return Style.marginS
+ }
+
+ property real marginRight: {
+ if (!barIsVisible) {
+ return 0
+ }
+ return Style.marginS
+ }
+
+ // ---------------------------------------------
+ property int calculatedX: {
+ // Priority to fixed anchoring
+ if (panelAnchorHorizontalCenter) {
+ // Center horizontally but respect bar margins
+ var centerX = Math.round((panelWindow.width - panelBackground.width) / 2)
+ var minX = marginLeft
+ var maxX = panelWindow.width - panelBackground.width - marginRight
+ return Math.round(Math.max(minX, Math.min(centerX, maxX)))
+ } else if (panelAnchorLeft) {
+ return marginLeft
+ } else if (panelAnchorRight) {
+ return Math.round(panelWindow.width - panelBackground.width - marginRight)
+ }
+
+ // No fixed anchoring
+ if (isVertical) {
+ // Vertical bar
+ if (barPosition === "right") {
+ // To the left of the right bar
+ return Math.round(panelWindow.width - panelBackground.width - marginRight)
+ } else {
+ // To the right of the left bar
+ return marginLeft
+ }
+ } else {
+ // Horizontal bar
+ if (root.useButtonPosition) {
+ // Position panel relative to button
+ var targetX = buttonPosition.x + (buttonWidth / 2) - (panelBackground.width / 2)
+ // Keep panel within screen bounds
+ var maxX = panelWindow.width - panelBackground.width - marginRight
+ var minX = marginLeft
+ return Math.round(Math.max(minX, Math.min(targetX, maxX)))
+ } else {
+ // Fallback to center horizontally
+ return Math.round((panelWindow.width - panelBackground.width) / 2)
+ }
+ }
+ }
+
+ // ---------------------------------------------
+ property int calculatedY: {
+ // Priority to fixed anchoring
+ if (panelAnchorVerticalCenter) {
+ // Center vertically but respect bar margins
+ var centerY = Math.round((panelWindow.height - panelBackground.height) / 2)
+ var minY = marginTop
+ var maxY = panelWindow.height - panelBackground.height - marginBottom
+ return Math.round(Math.max(minY, Math.min(centerY, maxY)))
+ } else if (panelAnchorTop) {
+ return marginTop
+ } else if (panelAnchorBottom) {
+ return Math.round(panelWindow.height - panelBackground.height - marginBottom)
+ }
+
+ // No fixed anchoring
+ if (isVertical) {
+ // Vertical bar
+ if (useButtonPosition) {
+ // Position panel relative to button
+ var targetY = buttonPosition.y + (buttonHeight / 2) - (panelBackground.height / 2)
+ // Keep panel within screen bounds
+ var maxY = panelWindow.height - panelBackground.height - marginBottom
+ var minY = marginTop
+ return Math.round(Math.max(minY, Math.min(targetY, maxY)))
+ } else {
+ // Fallback to center vertically
+ return Math.round((panelWindow.height - panelBackground.height) / 2)
+ }
+ } else {
+ return marginTop
+ }
+ }
+
+ // Animate in when component is completed
+ Component.onCompleted: {
+ root.scaleValue = 1.0
+ root.opacityValue = 1.0
+ }
+
+ // Reset drag position when panel closes
+ Connections {
+ target: root
+ function onClosed() {
+ panelBackground.isDragged = false
+ }
+ }
+
+ // Prevent closing when clicking in the panel bg
+ MouseArea {
+ anchors.fill: parent
+ }
+
+ // Animation behaviors
+ Behavior on scale {
+ NumberAnimation {
+ duration: Style.animationNormal
+ easing.type: Easing.OutExpo
+ }
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationNormal
+ easing.type: Easing.OutQuad
+ }
+ }
+
+ Loader {
+ id: panelContentLoader
+ anchors.fill: parent
+ sourceComponent: root.panelContent
+ }
+
+ // Handle drag move on the whole panel area
+ DragHandler {
+ id: dragHandler
+ target: null
+ enabled: panelBackground.draggable
+ property real dragStartX: 0
+ property real dragStartY: 0
+ onActiveChanged: {
+ if (active) {
+ // Capture current position into manual coordinates BEFORE toggling isDragged
+ panelBackground.manualX = panelBackground.x
+ panelBackground.manualY = panelBackground.y
+ dragStartX = panelBackground.x
+ dragStartY = panelBackground.y
+ panelBackground.isDragged = true
+ if (root.enableBackgroundClick)
+ root.disableBackgroundClick()
+ } else {
+ // Keep isDragged true so we continue using the manual x/y after release
+ if (root.enableBackgroundClick)
+ root.enableBackgroundClick()
+ }
+ }
+ onTranslationChanged: {
+ // Proposed new coordinates from fixed drag origin
+ var nx = dragStartX + translation.x
+ var ny = dragStartY + translation.y
+
+ // Calculate gaps so we never overlap the bar on any side
+ var baseGap = Style.marginS
+ var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL : 0
+ var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL : 0
+
+ var insetLeft = baseGap + ((barIsVisible && barPosition === "left") ? (Style.barHeight + floatExtraH) : 0)
+ var insetRight = baseGap + ((barIsVisible && barPosition === "right") ? (Style.barHeight + floatExtraH) : 0)
+ var insetTop = baseGap + ((barIsVisible && barPosition === "top") ? (Style.barHeight + floatExtraV) : 0)
+ var insetBottom = baseGap + ((barIsVisible && barPosition === "bottom") ? (Style.barHeight + floatExtraV) : 0)
+
+ // Clamp within screen bounds accounting for insets
+ var maxX = panelWindow.width - panelBackground.width - insetRight
+ var minX = insetLeft
+ var maxY = panelWindow.height - panelBackground.height - insetBottom
+ var minY = insetTop
+
+ panelBackground.manualX = Math.round(Math.max(minX, Math.min(nx, maxX)))
+ panelBackground.manualY = Math.round(Math.max(minY, Math.min(ny, maxY)))
+ }
+ }
+
+ // Drag indicator border
+ Rectangle {
+ anchors.fill: parent
+ anchors.margins: 0
+ color: Color.transparent
+ border.color: Color.mPrimary
+ border.width: Math.max(2, Style.borderL )
+ radius: parent.radius
+ visible: panelBackground.isDragged && dragHandler.active
+ opacity: 0.8
+ z: 3000
+
+ // Subtle glow effect
+ Rectangle {
+ anchors.fill: parent
+ anchors.margins: 0
+ color: Color.transparent
+ border.color: Color.mPrimary
+ border.width: Math.max(1, Style.borderS )
+ radius: parent.radius
+ opacity: 0.3
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/quickshell/Noctalia/NSlider.qml b/quickshell/Noctalia/NSlider.qml
new file mode 100644
index 0000000..45f8491
--- /dev/null
+++ b/quickshell/Noctalia/NSlider.qml
@@ -0,0 +1,152 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import qs.Constants
+import qs.Noctalia
+import qs.Services
+import qs.Utils
+
+Slider {
+ id: root
+
+ property var cutoutColor: Color.mSurface
+ property bool snapAlways: true
+ property real heightRatio: 0.7
+ readonly property real knobDiameter: Math.round((Style.baseWidgetSize * heightRatio) / 2) * 2
+ readonly property real trackHeight: Math.round((knobDiameter * 0.4) / 2) * 2
+ readonly property real cutoutExtra: Math.round((Style.baseWidgetSize * 0.1) / 2) * 2
+
+ padding: cutoutExtra / 2
+ snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease
+ implicitHeight: Math.max(trackHeight, knobDiameter)
+
+ background: Rectangle {
+ x: root.leftPadding
+ y: root.topPadding + root.availableHeight / 2 - height / 2
+ implicitWidth: Style.sliderWidth
+ implicitHeight: trackHeight
+ width: root.availableWidth
+ height: implicitHeight
+ radius: height / 2
+ color: Qt.alpha(Color.mSurface, 0.5)
+ border.color: Qt.alpha(Color.mOutline, 0.5)
+ border.width: Math.max(1, Style.borderS)
+
+ // A container composite shape that puts a semicircle on the end
+ Item {
+ id: activeTrackContainer
+
+ width: root.visualPosition * parent.width
+ height: parent.height
+
+ // The rounded end cap made from a rounded rectangle
+ Rectangle {
+ width: parent.height
+ height: parent.height
+ radius: width / 2
+ color: Qt.darker(Color.mPrimary, 1.2) //starting color of gradient
+ }
+
+ // The main rectangle
+ Rectangle {
+ x: parent.height / 2
+ width: parent.width - x // Fills the rest of the container
+ height: parent.height
+ radius: 0
+
+ // Animated gradient fill
+ gradient: Gradient {
+ orientation: Gradient.Horizontal
+
+ GradientStop {
+ position: 0
+ color: Qt.darker(Color.mPrimary, 1.2)
+
+ Behavior on color {
+ ColorAnimation {
+ duration: 300
+ }
+
+ }
+
+ }
+
+ GradientStop {
+ position: 0.5
+ color: Color.mPrimary
+
+ SequentialAnimation on position {
+ loops: Animation.Infinite
+
+ NumberAnimation {
+ from: 0.3
+ to: 0.7
+ duration: 2000
+ easing.type: Easing.InOutSine
+ }
+
+ NumberAnimation {
+ from: 0.7
+ to: 0.3
+ duration: 2000
+ easing.type: Easing.InOutSine
+ }
+
+ }
+
+ }
+
+ GradientStop {
+ position: 1
+ color: Qt.lighter(Color.mPrimary, 1.2)
+ }
+
+ }
+
+ }
+
+ }
+
+ // Circular cutout
+ Rectangle {
+ id: knobCutout
+
+ implicitWidth: knobDiameter + cutoutExtra
+ implicitHeight: knobDiameter + cutoutExtra
+ radius: width / 2
+ color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface
+ x: root.leftPadding + root.visualPosition * (root.availableWidth - root.knobDiameter) - cutoutExtra
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ }
+
+ handle: Item {
+ implicitWidth: knobDiameter
+ implicitHeight: knobDiameter
+ x: root.leftPadding + root.visualPosition * (root.availableWidth - width)
+ anchors.verticalCenter: parent.verticalCenter
+
+ Rectangle {
+ id: knob
+
+ implicitWidth: knobDiameter
+ implicitHeight: knobDiameter
+ radius: width / 2
+ color: root.pressed ? Color.mTertiary : Color.mSurface
+ border.color: Color.mPrimary
+ border.width: Math.max(1, Style.borderL)
+ anchors.centerIn: parent
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/quickshell/Noctalia/NText.qml b/quickshell/Noctalia/NText.qml
new file mode 100644
index 0000000..83ec766
--- /dev/null
+++ b/quickshell/Noctalia/NText.qml
@@ -0,0 +1,20 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Constants
+import qs.Noctalia
+
+Text {
+ id: root
+
+ property string family: Fonts.primary
+ property real pointSize: Style.fontSizeM
+ property real fontScale: 1
+
+ font.family: root.family
+ font.weight: Style.fontWeightMedium
+ font.pointSize: root.pointSize * fontScale
+ color: Color.mOnSurface
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ verticalAlignment: Text.AlignVCenter
+}
diff --git a/quickshell/Services/AudioService.qml b/quickshell/Services/AudioService.qml
index 5c7c55f..0b63944 100644
--- a/quickshell/Services/AudioService.qml
+++ b/quickshell/Services/AudioService.qml
@@ -4,6 +4,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
+import qs.Utils
Singleton {
id: root
@@ -60,6 +61,7 @@ Singleton {
function onMutedChanged() {
root._muted = (sink?.audio.muted ?? true)
+ Logger.log("AudioService", "OnMuteChanged:", root._muted)
}
}
@@ -76,6 +78,7 @@ Singleton {
function onMutedChanged() {
root._inputMuted = (source?.audio.muted ?? true)
+ Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted)
}
}
@@ -93,7 +96,7 @@ Singleton {
sink.audio.muted = false
sink.audio.volume = Math.max(0, Math.min(1.0, newVolume))
} else {
- console.warn("No sink available")
+ Logger.warn("AudioService", "No sink available")
}
}
@@ -101,7 +104,7 @@ Singleton {
if (sink?.ready && sink?.audio) {
sink.audio.muted = muted
} else {
- console.warn("No sink available")
+ Logger.warn("AudioService", "No sink available")
}
}
@@ -111,7 +114,7 @@ Singleton {
source.audio.muted = false
source.audio.volume = Math.max(0, Math.min(1.0, newVolume))
} else {
- console.warn("No source available")
+ Logger.warn("AudioService", "No source available")
}
}
@@ -119,7 +122,7 @@ Singleton {
if (source?.ready && source?.audio) {
source.audio.muted = muted
} else {
- console.warn("No source available")
+ Logger.warn("AudioService", "No source available")
}
}
diff --git a/quickshell/Services/BrightnessService.qml b/quickshell/Services/BrightnessService.qml
index 72fc110..1bb59da 100644
--- a/quickshell/Services/BrightnessService.qml
+++ b/quickshell/Services/BrightnessService.qml
@@ -3,6 +3,7 @@ pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
+import qs.Utils
Singleton {
id: root
@@ -44,6 +45,10 @@ Singleton {
reloadableId: "brightness"
+ Component.onCompleted: {
+ Logger.log("Brightness", "Service started")
+ }
+
onMonitorsChanged: {
ddcMonitors = []
ddcProc.running = true
@@ -80,7 +85,7 @@ Singleton {
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)
+ Logger.log("Brigthness", "Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel)
return {
"model": model,
"busNum": bus,
@@ -188,7 +193,7 @@ Singleton {
var val = parseInt(dataText)
if (!isNaN(val)) {
monitor.brightness = val / 101
- console.log("Apple display brightness:", monitor.brightness)
+ Logger.log("Brightness", "Apple display brightness:", monitor.brightness)
}
} else if (monitor.isDdc) {
var parts = dataText.split(" ")
@@ -197,7 +202,7 @@ Singleton {
var max = parseInt(parts[4])
if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.brightness = current / max
- console.log("DDC brightness:", monitor.brightness)
+ Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness)
}
}
} else {
@@ -213,8 +218,8 @@ Singleton {
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)
+ Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
+ Logger.log("Brightness", "Using backlight device:", monitor.backlightDevice)
}
}
}
diff --git a/quickshell/Services/Caffeine.qml b/quickshell/Services/Caffeine.qml
index 4dc9946..51e0eab 100644
--- a/quickshell/Services/Caffeine.qml
+++ b/quickshell/Services/Caffeine.qml
@@ -2,6 +2,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
+import qs.Utils
pragma Singleton
Singleton {
@@ -136,10 +137,10 @@ Singleton {
if (isInhibited)
isInhibited = false;
- console.log("Inhibitor process exited with code:", exitCode, "status:", exitStatus);
+ Logger.log("Caffeine", "Inhibitor process exited with code:", exitCode, "status:", exitStatus);
}
onStarted: function() {
- console.log("Inhibitor process started with strategy:", root.strategy);
+ Logger.log("Caffeine", "Inhibitor process started with PID:", inhibitorProcess.processId);
}
}
diff --git a/quickshell/Services/IPCService.qml b/quickshell/Services/IPCService.qml
new file mode 100644
index 0000000..66a5425
--- /dev/null
+++ b/quickshell/Services/IPCService.qml
@@ -0,0 +1,35 @@
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Constants
+
+Item {
+ IpcHandler {
+ function setPrimary(color: color) {
+ SettingsService.primaryColor = color;
+ }
+
+ target: "colors"
+ }
+
+ IpcHandler {
+ function toggleCalendar() {
+ calendarPanel.toggle();
+ }
+
+ function toggleControlCenter() {
+ controlCenterPanel.toggle();
+ }
+
+ target: "panels"
+ }
+
+ IpcHandler {
+ function toggleBarLyrics() {
+ SettingsService.showLyricsBar = !SettingsService.showLyricsBar;
+ }
+
+ target: "lyrics"
+ }
+
+}
diff --git a/quickshell/Services/IpService.qml b/quickshell/Services/IpService.qml
index 22b273f..cb5a775 100644
--- a/quickshell/Services/IpService.qml
+++ b/quickshell/Services/IpService.qml
@@ -1,6 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Io
+import qs.Utils
pragma Singleton
Singleton {
@@ -9,28 +10,93 @@ Singleton {
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 geoURL: "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;
+ const xhr = new XMLHttpRequest();
+ xhr.timeout = fetchTimeout * 1000;
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ try {
+ const response = JSON.parse(xhr.responseText);
+ if (response && response.ip) {
+ let newIP = response.ip;
+ Logger.log("IpService", "Fetched IP: " + newIP);
+ if (newIP !== ip) {
+ ip = newIP;
+ fetchGeoInfo(); // Fetch geo info only if IP has changed
+ }
+ } else {
+ ip = "N/A";
+ countryCode = "N/A";
+ Logger.error("IpService", "IP response does not contain 'ip' field");
+ }
+ } catch (e) {
+ ip = "N/A";
+ countryCode = "N/A";
+ Logger.error("IpService", "Failed to parse IP response: " + e);
+ }
+ } else {
+ ip = "N/A";
+ countryCode = "N/A";
+ Logger.error("IpService", "Failed to fetch IP, status: " + xhr.status);
+ }
+ }
+ };
+ xhr.ontimeout = function() {
+ ip = "N/A";
+ countryCode = "N/A";
+ Logger.error("IpService", "Fetch IP request timed out");
+ };
+ xhr.open("GET", ipURL);
+ xhr.send();
}
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;
+ const xhr = new XMLHttpRequest();
+ xhr.timeout = fetchTimeout * 1000;
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ try {
+ const response = JSON.parse(xhr.responseText);
+ if (response && response.country) {
+ let newCountryCode = response.country_code;
+ Logger.log("IpService", "Fetched country code: " + newCountryCode);
+ if (newCountryCode !== countryCode) {
+ countryCode = newCountryCode;
+ SendNotification.show("New IP", `IP: ${ip}\nCountry: ${newCountryCode}`);
+ }
+ } else {
+ countryCode = "N/A";
+ Logger.error("IpService", "Geo response does not contain 'country' field");
+ }
+ } catch (e) {
+ countryCode = "N/A";
+ Logger.error("IpService", "Failed to parse geo response: " + e);
+ }
+ } else {
+ countryCode = "N/A";
+ Logger.error("IpService", "Failed to fetch geo info, status: " + xhr.status);
+ }
+ }
+ };
+ xhr.ontimeout = function() {
+ countryCode = "N/A";
+ Logger.error("IpService", "Fetch geo info request timed out");
+ };
+ let url = geoURL + ip;
+ if (geoURLToken)
+ url += "?token=" + geoURLToken;
+
+ xhr.open("GET", url);
+ xhr.send();
}
function refresh() {
@@ -46,11 +112,11 @@ Singleton {
FileView {
id: tokenFile
- path: Qt.resolvedUrl("../Assets/Ip/token.txt")
+ path: Qt.resolvedUrl("../Assets/Config/GeoInfoToken.txt")
onLoaded: {
geoURLToken = tokenFile.text();
if (!geoURLToken)
- console.warn("No token found for geoIP service, assuming none is required");
+ Logger.warn("IpService", "No token found for geoIP service, assuming none is required");
fetchIP();
fetchTimer.start();
@@ -68,64 +134,4 @@ Singleton {
}
}
- 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";
- }
- }
-
- }
-
}
diff --git a/quickshell/Services/LocationService.qml b/quickshell/Services/LocationService.qml
new file mode 100644
index 0000000..8f5f70b
--- /dev/null
+++ b/quickshell/Services/LocationService.qml
@@ -0,0 +1,323 @@
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Constants
+import qs.Utils
+pragma Singleton
+
+// Weather logic and caching with stable UI properties
+Singleton {
+ //console.log(JSON.stringify(weatherData))
+
+ id: root
+
+ property string locationName: "Munich"
+ property string locationFile: Qt.resolvedUrl("../Assets/Config/Location.json")
+ property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds
+ property bool isFetchingWeather: false
+ readonly property alias data: adapter // Used to access via LocationService.data.xxx from outside, best to use "adapter" inside the service.
+ // Stable UI properties - only updated when location is fully resolved
+ property bool coordinatesReady: false
+ property string stableLatitude: ""
+ property string stableLongitude: ""
+ property string stableName: ""
+ // Helper property for UI components (outside JsonAdapter to avoid binding loops)
+ readonly property string displayCoordinates: {
+ if (!root.coordinatesReady || root.stableLatitude === "" || root.stableLongitude === "")
+ return "";
+
+ const lat = parseFloat(root.stableLatitude).toFixed(4);
+ const lon = parseFloat(root.stableLongitude).toFixed(4);
+ return `${lat}, ${lon}`;
+ }
+
+ // --------------------------------
+ function init() {
+ // does nothing but ensure the singleton is created
+ // do not remove
+ Logger.log("Location", "Service started");
+ }
+
+ // --------------------------------
+ function resetWeather() {
+ Logger.log("Location", "Resetting weather data");
+ // Mark as changing to prevent UI updates
+ root.coordinatesReady = false;
+ // Reset stable properties
+ root.stableLatitude = "";
+ root.stableLongitude = "";
+ root.stableName = "";
+ // Reset core data
+ adapter.latitude = "";
+ adapter.longitude = "";
+ adapter.name = "";
+ adapter.weatherLastFetch = 0;
+ adapter.weather = null;
+ // Try to fetch immediately
+ updateWeather();
+ }
+
+ // --------------------------------
+ function updateWeather() {
+ if (isFetchingWeather) {
+ Logger.warn("Location", "Weather is still fetching");
+ return ;
+ }
+ if ((adapter.weatherLastFetch === "") || (adapter.weather === null) || (adapter.latitude === "") || (adapter.longitude === "") || (adapter.name !== root.locationName) || (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency))
+ getFreshWeather();
+
+ }
+
+ // --------------------------------
+ function getFreshWeather() {
+ isFetchingWeather = true;
+ // Check if location name has changed
+ const locationChanged = data.name !== root.locationName;
+ if (locationChanged) {
+ root.coordinatesReady = false;
+ Logger.log("Location", "Location changed from", adapter.name, "to", root.locationName);
+ }
+ if ((adapter.latitude === "") || (adapter.longitude === "") || locationChanged)
+ _geocodeLocation(root.locationName, function(latitude, longitude, name, country) {
+ Logger.log("Location", "Geocoded", root.locationName, "to:", latitude, "/", longitude);
+ // Save location name
+ adapter.name = root.locationName;
+ // Save GPS coordinates
+ adapter.latitude = latitude.toString();
+ adapter.longitude = longitude.toString();
+ root.stableName = `${name}, ${country}`;
+ _fetchWeather(latitude, longitude, errorCallback);
+ }, errorCallback);
+ else
+ _fetchWeather(adapter.latitude, adapter.longitude, errorCallback);
+ }
+
+ // --------------------------------
+ function _geocodeLocation(locationName, callback, errorCallback) {
+ Logger.log("Location", "Geocoding location name");
+ var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent(locationName) + "&language=en&format=json";
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ try {
+ var geoData = JSON.parse(xhr.responseText);
+ if (geoData.lat != null)
+ callback(geoData.lat, geoData.lng, geoData.name, geoData.country);
+ else
+ errorCallback("Location", "could not resolve location name");
+ } catch (e) {
+ errorCallback("Location", "Failed to parse geocoding data: " + e);
+ }
+ } else {
+ errorCallback("Location", "Geocoding error: " + xhr.status);
+ }
+ }
+ };
+ xhr.open("GET", geoUrl);
+ xhr.send();
+ }
+
+ // --------------------------------
+ function _fetchWeather(latitude, longitude, errorCallback) {
+ Logger.log("Location", "Fetching weather from api.open-meteo.com");
+ var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto";
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ try {
+ var weatherData = JSON.parse(xhr.responseText);
+ // Save core data
+ data.weather = weatherData;
+ data.weatherLastFetch = Time.timestamp;
+ // Update stable display values only when complete and successful
+ root.stableLatitude = data.latitude = weatherData.latitude.toString();
+ root.stableLongitude = data.longitude = weatherData.longitude.toString();
+ root.coordinatesReady = true;
+ isFetchingWeather = false;
+ Logger.log("Location", "Cached weather to disk - stable coordinates updated");
+ } catch (e) {
+ errorCallback("Location", "Failed to parse weather data");
+ }
+ } else {
+ errorCallback("Location", "Weather fetch error: " + xhr.status);
+ }
+ }
+ };
+ xhr.open("GET", url);
+ xhr.send();
+ }
+
+ // --------------------------------
+ function errorCallback(module, message) {
+ Logger.error(module, message);
+ isFetchingWeather = false;
+ }
+
+ // --------------------------------
+ function weatherSymbolFromCode(code) {
+ if (code === 0)
+ return "weather-sun";
+
+ if (code === 1 || code === 2)
+ return "weather-cloud-sun";
+
+ if (code === 3)
+ return "weather-cloud";
+
+ if (code >= 45 && code <= 48)
+ return "weather-cloud-haze";
+
+ if (code >= 51 && code <= 67)
+ return "weather-cloud-rain";
+
+ if (code >= 71 && code <= 77)
+ return "weather-cloud-snow";
+
+ if (code >= 71 && code <= 77)
+ return "weather-cloud-snow";
+
+ if (code >= 85 && code <= 86)
+ return "weather-cloud-snow";
+
+ if (code >= 95 && code <= 99)
+ return "weather-cloud-lightning";
+
+ return "weather-cloud";
+ }
+
+ function weatherColorFromCode(code) {
+ // Clear sky - bright yellow
+ if (code === 0)
+ return Colors.yellow;
+
+ // Mainly clear/Partly cloudy - soft peach/rosewater tones
+ if (code === 1 || code === 2)
+ return Colors.peach;
+
+ // Overcast - neutral sky blue
+ if (code === 3)
+ return Colors.sky;
+
+ // Fog - soft lavender/muted tone
+ if (code >= 45 && code <= 48)
+ return Colors.lavender;
+
+ // Drizzle - light blue/sapphire
+ if (code >= 51 && code <= 67)
+ return Colors.sapphire;
+
+ // Snow - cool teal
+ if (code >= 71 && code <= 77)
+ return Colors.teal;
+
+ // Rain showers - deeper blue
+ if (code >= 80 && code <= 82)
+ return Colors.blue;
+
+ // Snow showers - teal
+ if (code >= 85 && code <= 86)
+ return Colors.teal;
+
+ // Thunderstorm - dramatic mauve/pink
+ if (code >= 95 && code <= 99)
+ return Colors.mauve;
+
+ // Default - sky blue
+ return Colors.sky;
+ }
+
+ // --------------------------------
+ function weatherDescriptionFromCode(code) {
+ if (code === 0)
+ return "Clear sky";
+
+ if (code === 1)
+ return "Mainly clear";
+
+ if (code === 2)
+ return "Partly cloudy";
+
+ if (code === 3)
+ return "Overcast";
+
+ if (code === 45 || code === 48)
+ return "Fog";
+
+ if (code >= 51 && code <= 67)
+ return "Drizzle";
+
+ if (code >= 71 && code <= 77)
+ return "Snow";
+
+ if (code >= 80 && code <= 82)
+ return "Rain showers";
+
+ if (code >= 95 && code <= 99)
+ return "Thunderstorm";
+
+ return "Unknown";
+ }
+
+ // --------------------------------
+ function celsiusToFahrenheit(celsius) {
+ return 32 + celsius * 1.8;
+ }
+
+ FileView {
+ id: locationFileView
+
+ path: locationFile
+ printErrors: false
+ onAdapterUpdated: saveTimer.start()
+ onLoaded: {
+ Logger.log("Location", "Loaded cached data");
+ // Initialize stable properties on load
+ if (adapter.latitude !== "" && adapter.longitude !== "" && adapter.weatherLastFetch > 0) {
+ root.stableLatitude = adapter.latitude;
+ root.stableLongitude = adapter.longitude;
+ root.stableName = adapter.name;
+ root.coordinatesReady = true;
+ Logger.log("Location", "Coordinates ready");
+ }
+ updateWeather();
+ }
+ onLoadFailed: function(error) {
+ updateWeather();
+ }
+
+ JsonAdapter {
+ id: adapter
+
+ // Core data properties
+ property string latitude: ""
+ property string longitude: ""
+ property string name: ""
+ property int weatherLastFetch: 0
+ property var weather: null
+ }
+
+ }
+
+ // Every 20s check if we need to fetch new weather
+ Timer {
+ id: updateTimer
+
+ interval: 20 * 1000
+ running: true
+ repeat: true
+ onTriggered: {
+ updateWeather();
+ }
+ }
+
+ Timer {
+ id: saveTimer
+
+ running: false
+ interval: 1000
+ onTriggered: locationFileView.writeAdapter()
+ }
+
+}
diff --git a/quickshell/Services/LyricsService.qml b/quickshell/Services/LyricsService.qml
new file mode 100644
index 0000000..e8d19ed
--- /dev/null
+++ b/quickshell/Services/LyricsService.qml
@@ -0,0 +1,144 @@
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Services
+import qs.Utils
+pragma Singleton
+
+Singleton {
+ property int linesCount: 3
+ property int linesAhead: linesCount / 2
+ property int currentIndex: linesCount - linesAhead - 1
+ property string offsetFile: Qt.resolvedUrl("../Assets/Config/LyricsOffset.txt")
+ property int offset: 0 // in ms
+ property int offsetStep: 500 // in ms
+ property int referenceCount: 0
+ // with linesCount=3 and linesAhead=1, lyrics will be like:
+ // line 1
+ // line 2 <- current line
+ // line 3
+ property var lyrics: Array(linesCount).fill(" ")
+
+ function startSyncing() {
+ referenceCount++;
+ Logger.log("LyricsService", "Reference count:", referenceCount);
+ if (referenceCount === 1) {
+ Logger.log("LyricsService", "Starting lyrics syncing");
+ // fill lyrics with empty lines
+ lyrics = Array(linesCount).fill(" ");
+ listenProcess.exec(["sh", "-c", `sl-wrap listen -l ${linesCount} -a ${linesAhead} -f ${offsetFile.slice(7)}`]);
+ }
+ }
+
+ function stopSyncing() {
+ referenceCount--;
+ Logger.log("LyricsService", "Reference count:", referenceCount);
+ if (referenceCount <= 0) {
+ Logger.log("LyricsService", "Stopping lyrics syncing");
+ // Execute again to stop
+ // kinda ugly but works, but meanwhile:
+ // listenProcess.signal(9)
+ // listenProcess.signal(15)
+ // listenProcess.running = false
+ // counts on exec() to terminate previous exec()
+ // all don't work
+ listenProcess.exec(["sh", "-c", `sl-wrap trackid`]);
+ }
+ }
+
+ function writeOffset() {
+ offsetFileView.setText(String(offset));
+ }
+
+ function increaseOffset() {
+ offset += offsetStep;
+ }
+
+ function decreaseOffset() {
+ offset -= offsetStep;
+ }
+
+ function resetOffset() {
+ offset = 0;
+ }
+
+ function clearCache() {
+ action.command = ["sh", "-c", "spotify-lyrics clear $(spotify-lyrics trackid)"];
+ action.startDetached();
+ }
+
+ function showLyricsText() {
+ action.command = ["sh", "-c", "ghostty -e sh -c 'spotify-lyrics fetch | less'"];
+ action.startDetached();
+ }
+
+ onOffsetChanged: {
+ if (SettingsService.showLyricsBar)
+ SendNotification.show("Lyrics Offset Changed", `Current offset: ${offset} ms`, 1000);
+
+ writeOffset();
+ }
+
+ Process {
+ id: listenProcess
+
+ running: false
+
+ stdout: SplitParser {
+ splitMarker: ""
+ onRead: (data) => {
+ lyrics = data.split("\n").slice(0, linesCount);
+ if (lyrics.length < linesCount) {
+ // fill with empty lines if not enough
+ for (let i = lyrics.length; i < linesCount; i++) {
+ lyrics[i] = " ";
+ }
+ }
+ }
+ }
+
+ }
+
+ Process {
+ id: action
+
+ running: false
+ }
+
+ FileView {
+ id: offsetFileView
+
+ path: offsetFile
+ watchChanges: false
+ onLoaded: {
+ try {
+ const fileContents = text();
+ if (fileContents.length > 0) {
+ const val = parseInt(fileContents);
+ if (!isNaN(val)) {
+ offset = val;
+ Logger.log("LyricsService", "Loaded offset:", offset);
+ } else {
+ offset = 0;
+ writeOffset();
+ }
+ } else {
+ offset = 0;
+ writeOffset();
+ }
+ } catch (e) {
+ Logger.log("LyricsService", "Error reading offset file:", e);
+ }
+ }
+ onLoadFailed: {
+ Logger.log("LyricsService", "Error loading offset file:", errorString);
+ }
+ onSaveFailed: {
+ Logger.log("LyricsService", "Error saving offset file:", errorString);
+ }
+ onSaved: {
+ Logger.log("LyricsService", "Offset file saved.");
+ }
+ }
+
+}
diff --git a/quickshell/Services/MusicManager.qml b/quickshell/Services/MusicManager.qml
index 4d87380..fcbe174 100644
--- a/quickshell/Services/MusicManager.qml
+++ b/quickshell/Services/MusicManager.qml
@@ -2,6 +2,7 @@ import QtQuick
import Quickshell
import Quickshell.Services.Mpris
import qs.Modules.Misc
+import qs.Utils
pragma Singleton
Singleton {
@@ -11,6 +12,7 @@ Singleton {
property var currentPlayer: null
property real currentPosition: 0
property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false
+ property int selectedPlayerIndex: -1
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
@@ -59,11 +61,25 @@ Singleton {
// Updates currentPlayer and currentPosition
function updateCurrentPlayer() {
+ // Use selected player if index is valid
+ if (selectedPlayerIndex >= 0) {
+ let availablePlayers = getAvailablePlayers();
+ if (selectedPlayerIndex < availablePlayers.length) {
+ currentPlayer = availablePlayers[selectedPlayerIndex];
+ currentPosition = currentPlayer.position;
+ Logger.log("MusicManager", "Current player set by index:", currentPlayer ? currentPlayer.identity : "None");
+ return ;
+ } else {
+ selectedPlayerIndex = -1; // Reset if index is out of range
+ }
+ }
+ // Otherwise, find active player
let newPlayer = findActivePlayer();
if (newPlayer !== currentPlayer) {
currentPlayer = newPlayer;
currentPosition = currentPlayer ? currentPlayer.position : 0;
}
+ Logger.log("MusicManager", "Current player updated:", currentPlayer ? currentPlayer.identity : "None");
}
// Player control functions
diff --git a/quickshell/Services/Niri.qml b/quickshell/Services/Niri.qml
index 363d27d..9a12197 100644
--- a/quickshell/Services/Niri.qml
+++ b/quickshell/Services/Niri.qml
@@ -1,6 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Io
+import qs.Utils
pragma Singleton
pragma ComponentBehavior: Bound
@@ -66,7 +67,7 @@ Singleton {
});
root.workspaces = workspacesList;
} catch (e) {
- console.error("Failed to parse workspaces:", e, line);
+ Logger.error("Niri", "Failed to parse workspaces:", e, line);
}
}
}
@@ -102,7 +103,7 @@ Singleton {
}
root.windows = windowsMap;
} catch (e) {
- console.error("Error parsing windows event:", e);
+ Logger.error("Niri", "Error parsing windows event:", e);
}
} else if (event.WorkspaceActivated) {
workspaceProcess.running = true;
@@ -120,13 +121,13 @@ Singleton {
root.focusedWindowId = -1;
}
} catch (e) {
- console.error("Error parsing window focus event:", e);
+ Logger.error("Niri", "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);
+ Logger.error("Niri", "Error parsing overview state:", e);
}
} else if (event.WindowOpenedOrChanged) {
try {
@@ -161,7 +162,7 @@ Singleton {
}
} catch (e) {
- console.error("Error parsing window opened/changed event:", e);
+ Logger.error("Niri", "Error parsing window opened/changed event:", e);
}
} else if (event.windowClosed) {
try {
@@ -170,11 +171,11 @@ Singleton {
delete root.windows[closedId];
}
} catch (e) {
- console.error("Error parsing window closed event:", e);
+ Logger.error("Niri", "Error parsing window closed event:", e);
}
}
} catch (e) {
- console.error("Error parsing event stream:", e, data);
+ Logger.error("Niri", "Error parsing event stream:", e, data);
}
}
}
diff --git a/quickshell/Services/PowerProfileService.qml b/quickshell/Services/PowerProfileService.qml
new file mode 100644
index 0000000..422a1d6
--- /dev/null
+++ b/quickshell/Services/PowerProfileService.qml
@@ -0,0 +1,88 @@
+import QtQuick
+import Quickshell
+import Quickshell.Services.UPower
+import qs.Services
+import qs.Utils
+pragma Singleton
+
+Singleton {
+ id: root
+
+ readonly property var powerProfiles: PowerProfiles
+ readonly property bool available: powerProfiles && powerProfiles.hasPerformanceProfile
+ property int profile: powerProfiles ? powerProfiles.profile : PowerProfile.Balanced
+
+ function getName(p) {
+ if (!available)
+ return "Unknown";
+
+ const prof = (p !== undefined) ? p : profile;
+ switch (prof) {
+ case PowerProfile.Performance:
+ return "Performance";
+ case PowerProfile.Balanced:
+ return "Balanced";
+ case PowerProfile.PowerSaver:
+ return "Power saver";
+ default:
+ return "Unknown";
+ }
+ }
+
+ function getIcon(p) {
+ if (!available)
+ return "balanced";
+
+ const prof = (p !== undefined) ? p : profile;
+ switch (prof) {
+ case PowerProfile.Performance:
+ return "performance";
+ case PowerProfile.Balanced:
+ return "balanced";
+ case PowerProfile.PowerSaver:
+ return "powersaver";
+ default:
+ return "balanced";
+ }
+ }
+
+ function setProfile(p) {
+ if (!available)
+ return ;
+
+ try {
+ powerProfiles.profile = p;
+ } catch (e) {
+ Logger.error("PowerProfileService", "Failed to set profile:", e);
+ }
+ }
+
+ function cycleProfile() {
+ if (!available)
+ return ;
+
+ const current = powerProfiles.profile;
+ if (current === PowerProfile.Performance)
+ setProfile(PowerProfile.PowerSaver);
+ else if (current === PowerProfile.Balanced)
+ setProfile(PowerProfile.Performance);
+ else if (current === PowerProfile.PowerSaver)
+ setProfile(PowerProfile.Balanced);
+ }
+
+ Connections {
+ function onProfileChanged() {
+ root.profile = powerProfiles.profile;
+ // Only show toast if we have a valid profile name (not "Unknown")
+ const profileName = root.getName();
+ if (profileName !== "Unknown")
+ ToastService.showNotice(I18n.tr("toast.power-profile.changed"), I18n.tr("toast.power-profile.profile-name", {
+ "profile": profileName
+ }));
+
+ }
+
+ target: powerProfiles
+ }
+
+}
diff --git a/quickshell/Services/SendNotification.qml b/quickshell/Services/SendNotification.qml
index 661a951..197fc0f 100644
--- a/quickshell/Services/SendNotification.qml
+++ b/quickshell/Services/SendNotification.qml
@@ -6,7 +6,7 @@ pragma Singleton
Singleton {
id: root
- function show(title, message, icon = "", urgency = "normal", timeout = 5000) {
+ function show(title, message, timeout = 5000, icon = "", urgency = "normal") {
if (icon)
action.command = ["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message];
else
diff --git a/quickshell/Services/SettingsService.qml b/quickshell/Services/SettingsService.qml
new file mode 100644
index 0000000..ed8ac6c
--- /dev/null
+++ b/quickshell/Services/SettingsService.qml
@@ -0,0 +1,35 @@
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Constants
+import qs.Services
+pragma Singleton
+
+Singleton {
+ property alias primaryColor: adapter.primaryColor
+ property alias showLyricsBar: adapter.showLyricsBar
+ property string settingsFilePath: Qt.resolvedUrl("../Assets/Config/Settings.json")
+
+ FileView {
+ id: settingsFile
+
+ path: settingsFilePath
+ watchChanges: true
+ onFileChanged: reload()
+
+ JsonAdapter {
+ id: adapter
+
+ property string primaryColor: "#89b4fa"
+ property bool showLyricsBar: false
+ }
+
+ }
+
+ Connections {
+ target: adapter
+ onPrimaryColorChanged: settingsFile.writeAdapter()
+ onShowLyricsBarChanged: settingsFile.writeAdapter()
+ }
+
+}
diff --git a/quickshell/Services/SystemStatService.qml b/quickshell/Services/SystemStatService.qml
index 4605baf..bc571df 100644
--- a/quickshell/Services/SystemStatService.qml
+++ b/quickshell/Services/SystemStatService.qml
@@ -2,6 +2,7 @@ import Qt.labs.folderlistmodel
import QtQuick
import Quickshell
import Quickshell.Io
+import qs.Utils
pragma Singleton
Singleton {
@@ -222,7 +223,7 @@ Singleton {
}
root.cpuTemp = Math.round(sum / root.intelTempValues.length);
} else {
- console.warn("No temperature sensors found for coretemp");
+ Logger.warn("SystemStatService", "No temperature sensors found for coretemp");
root.cpuTemp = 0;
}
return ;
@@ -328,7 +329,7 @@ Singleton {
function checkNext() {
if (currentIndex >= 16) {
// Check up to hwmon10
- console.warn("No supported temperature sensor found");
+ Logger.warn("SystemStatService", "No supported temperature sensor found");
return ;
}
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`;
@@ -341,7 +342,7 @@ Singleton {
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}`);
+ Logger.log("SystemStatService", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`);
} else {
currentIndex++;
Qt.callLater(() => {
@@ -370,7 +371,6 @@ Singleton {
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
diff --git a/quickshell/Services/WorkspaceManager.qml b/quickshell/Services/WorkspaceManager.qml
index 945c13e..7d34859 100644
--- a/quickshell/Services/WorkspaceManager.qml
+++ b/quickshell/Services/WorkspaceManager.qml
@@ -5,6 +5,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
+import qs.Utils
Singleton {
id: root
@@ -40,7 +41,7 @@ Singleton {
try {
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]);
} catch (e) {
- console.error("Error switching Niri workspace:", e);
+ Logger.error("WorkspaceManager", "Error switching Niri workspace:", e);
}
}
diff --git a/quickshell/Shaders/qsb/circled_image.frag.qsb b/quickshell/Shaders/qsb/circled_image.frag.qsb
new file mode 100644
index 0000000..37a99ef
Binary files /dev/null and b/quickshell/Shaders/qsb/circled_image.frag.qsb differ
diff --git a/quickshell/Shaders/qsb/rounded_image.frag.qsb b/quickshell/Shaders/qsb/rounded_image.frag.qsb
new file mode 100644
index 0000000..c404fc7
Binary files /dev/null and b/quickshell/Shaders/qsb/rounded_image.frag.qsb differ
diff --git a/quickshell/Modules/Misc/Cava.qml b/quickshell/Utils/Cava.qml
similarity index 70%
rename from quickshell/Modules/Misc/Cava.qml
rename to quickshell/Utils/Cava.qml
index f0c87d7..82e5491 100644
--- a/quickshell/Modules/Misc/Cava.qml
+++ b/quickshell/Utils/Cava.qml
@@ -23,7 +23,9 @@ Scope {
},
"output": {
"method": "raw",
- "bit_format": 8,
+ "data_format": "ascii",
+ "ascii_max_range": 100,
+ "bit_format": "8bit",
"channels": channels,
"mono_option": monoOption
}
@@ -33,14 +35,11 @@ Scope {
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: {
@@ -56,23 +55,14 @@ Scope {
}
}
stdinEnabled = false;
+ values = Array(count).fill(0);
}
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;
+ root.values = data.slice(0, -1).split(";").map((v) => {
+ return parseInt(v, 10) / 100;
+ });
}
}
diff --git a/quickshell/Modules/Misc/CavaColorList.qml b/quickshell/Utils/CavaColorList.qml
similarity index 100%
rename from quickshell/Modules/Misc/CavaColorList.qml
rename to quickshell/Utils/CavaColorList.qml
diff --git a/quickshell/Utils/Logger.qml b/quickshell/Utils/Logger.qml
new file mode 100644
index 0000000..063e488
--- /dev/null
+++ b/quickshell/Utils/Logger.qml
@@ -0,0 +1,58 @@
+pragma Singleton
+
+import Quickshell
+import qs.Utils
+
+Singleton {
+ id: root
+
+ function _formatMessage(...args) {
+ var t = Time.getFormattedTimestamp()
+ if (args.length > 1) {
+ const maxLength = 14
+ var module = args.shift().substring(0, maxLength).padStart(maxLength, " ")
+ return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ")
+ } else {
+ return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ")
+ }
+ }
+
+ function _getStackTrace() {
+ try {
+ throw new Error("Stack trace")
+ } catch (e) {
+ return e.stack
+ }
+ }
+
+ function log(...args) {
+ var msg = _formatMessage(...args)
+ console.log(msg)
+ }
+
+ function warn(...args) {
+ var msg = _formatMessage(...args)
+ console.warn(msg)
+ }
+
+ function error(...args) {
+ var msg = _formatMessage(...args)
+ console.error(msg)
+ }
+
+ function callStack() {
+ var stack = _getStackTrace()
+ Logger.log("Debug", "--------------------------")
+ Logger.log("Debug", "Current call stack")
+ // Split the stack into lines and log each one
+ var stackLines = stack.split('\n')
+ for (var i = 0; i < stackLines.length; i++) {
+ var line = stackLines[i].trim() // Remove leading/trailing whitespace
+ if (line.length > 0) {
+ // Only log non-empty lines
+ Logger.log("Debug", `- ${line}`)
+ }
+ }
+ Logger.log("Debug", "--------------------------")
+ }
+}
diff --git a/quickshell/Utils/Time.qml b/quickshell/Utils/Time.qml
new file mode 100644
index 0000000..1cd3d08
--- /dev/null
+++ b/quickshell/Utils/Time.qml
@@ -0,0 +1,84 @@
+import QtQuick
+import Quickshell
+pragma Singleton
+
+Singleton {
+ id: root
+
+ // Current date
+ property var date: new Date()
+ // Returns a Unix Timestamp (in seconds)
+ readonly property int timestamp: {
+ return Math.floor(date / 1000);
+ }
+
+ // Formats a Date object into a YYYYMMDD-HHMMSS string.
+ function getFormattedTimestamp(date) {
+ if (!date)
+ date = new Date();
+
+ const year = date.getFullYear();
+ // getMonth() is zero-based, so we add 1
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ const seconds = String(date.getSeconds()).padStart(2, '0');
+ return `${year}${month}${day}-${hours}${minutes}${seconds}`;
+ }
+
+ // Format an easy to read approximate duration ex: 4h32m
+ // Used to display the time remaining on the Battery widget, computer uptime, etc..
+ function formatVagueHumanReadableDuration(totalSeconds) {
+ if (typeof totalSeconds !== 'number' || totalSeconds < 0)
+ return '0s';
+
+ // Floor the input to handle decimal seconds
+ totalSeconds = Math.floor(totalSeconds);
+ const days = Math.floor(totalSeconds / 86400);
+ const hours = Math.floor((totalSeconds % 86400) / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const seconds = totalSeconds % 60;
+ const parts = [];
+ if (days)
+ parts.push(`${days}d`);
+
+ if (hours)
+ parts.push(`${hours}h`);
+
+ if (minutes)
+ parts.push(`${minutes}m`);
+
+ // Only show seconds if no hours and no minutes
+ if (!hours && !minutes)
+ parts.push(`${seconds}s`);
+
+ return parts.join('');
+ }
+
+ // Format a date into
+ function formatRelativeTime(date) {
+ if (!date)
+ return "";
+
+ const diff = Date.now() - date.getTime();
+ if (diff < 60000)
+ return "now";
+
+ if (diff < 3.6e+06)
+ return `${Math.floor(diff / 60000)}m ago`;
+
+ if (diff < 8.64e+07)
+ return `${Math.floor(diff / 3600000)}h ago`;
+
+ return `${Math.floor(diff / 86400000)}d ago`;
+ }
+
+ Timer {
+ interval: 1000
+ repeat: true
+ running: true
+ onTriggered: root.date = new Date()
+ }
+
+}
diff --git a/quickshell/shell.qml b/quickshell/shell.qml
index 4e24b51..f32dddf 100644
--- a/quickshell/shell.qml
+++ b/quickshell/shell.qml
@@ -4,10 +4,16 @@ import Quickshell.Widgets
import qs.Constants
import qs.Modules.Bar
import qs.Modules.Misc
+import qs.Modules.Panel
+import qs.Services
Scope {
id: root
+ IPCService {
+ id: ipcService
+ }
+
Bar {
id: bar
@@ -20,4 +26,16 @@ Scope {
shell: root
}
+ CalendarPanel {
+ id: calendarPanel
+
+ objectName: "calendarPanel"
+ }
+
+ ControlCenterPanel {
+ id: controlCenterPanel
+
+ objectName: "controlCenterPanel"
+ }
+
}
diff --git a/rofi/config.rasi b/rofi/config.rasi
index 555c795..9530c95 100644
--- a/rofi/config.rasi
+++ b/rofi/config.rasi
@@ -20,7 +20,7 @@
peach: #fab387;
search: rgba(49, 50, 68, 0.5); // alpha(@surface, 0.5)
- accent: #89b4fa;
+ primary: #89b4fa;
}
@@ -53,7 +53,7 @@ window {
padding: 0px;
border: 2px solid;
border-radius: 24px;
- border-color: @accent;
+ border-color: @primary;
cursor: "default";
background-color: @base;
}
@@ -126,7 +126,7 @@ listview {
}
scrollbar {
handle-width: 5px ;
- handle-color: @accent;
+ handle-color: @primary;
border-radius: 10px;
background-color: @mantle;
}
@@ -150,7 +150,7 @@ element alternate.normal {
element normal.urgent,
element alternate.urgent,
element selected.active {
- background-color: @accent;
+ background-color: @primary;
text-color: @base;
border-radius: 10px;
}
@@ -198,9 +198,9 @@ button {
}
button selected {
background-color: @surface;
- text-color: @accent;
+ text-color: @primary;
border-radius: 0 0 14px 14px;
- border-color: @accent;
+ border-color: @primary;
}
/*****----- Message -----*****/
diff --git a/rofi/config.rasi.template b/rofi/config.rasi.template
index fdf8229..60d6ae8 100644
--- a/rofi/config.rasi.template
+++ b/rofi/config.rasi.template
@@ -20,7 +20,7 @@
peach: #fab387;
search: rgba(49, 50, 68, 0.5); // alpha(@surface, 0.5)
- accent: #;
+ primary: #;
}
@@ -53,7 +53,7 @@ window {
padding: 0px;
border: 2px solid;
border-radius: 24px;
- border-color: @accent;
+ border-color: @primary;
cursor: "default";
background-color: @base;
}
@@ -126,7 +126,7 @@ listview {
}
scrollbar {
handle-width: 5px ;
- handle-color: @accent;
+ handle-color: @primary;
border-radius: 10px;
background-color: @mantle;
}
@@ -150,7 +150,7 @@ element alternate.normal {
element normal.urgent,
element alternate.urgent,
element selected.active {
- background-color: @accent;
+ background-color: @primary;
text-color: @base;
border-radius: 10px;
}
@@ -198,9 +198,9 @@ button {
}
button selected {
background-color: @surface;
- text-color: @accent;
+ text-color: @primary;
border-radius: 0 0 14px 14px;
- border-color: @accent;
+ border-color: @primary;
}
/*****----- Message -----*****/
diff --git a/waybar-niri/config.jsonc b/waybar-niri/config.jsonc
deleted file mode 100644
index 49ac15e..0000000
--- a/waybar-niri/config.jsonc
+++ /dev/null
@@ -1,232 +0,0 @@
-{
- // -------------------------------------------------------------------------
- // Global configuration
- // -------------------------------------------------------------------------
- "layer": "bottom",
- "position": "top",
- "margin-left": 0,
- "margin-bottom": 0,
- "margin-right": 0,
- "spacing": 2, // Gaps between modules (px)
- "modules-left": [
- "custom/rofi",
- "custom/separator",
- "group/workspaceactions",
- "custom/separator",
- "niri/window",
- "custom/mediaplayer"
- ],
- "modules-center": ["clock"],
- "modules-right": ["group/monitors", "custom/separator", "group/tray-expander", "idle_inhibitor", "custom/power"],
- // -------------------------------------------------------------------------
- // Modules
- // -------------------------------------------------------------------------
- // Separators
- "custom/separator": {
- "format": "|"
- },
- // Buttons
- "custom/power": {
- "format": "",
- "tooltip": false,
- "on-click": "wlogout",
- "min-length": 2,
- "max-length": 2
- },
- "custom/rofi": {
- "format": "",
- "tooltip": false,
- // "on-click-right": "fuzzel -l 0 -p '>> ' | xargs -r sh -c",
- // "on-click": "fuzzel",
- // "on-click-middle": "pkill -9 fuzzel",
- "on-click": "eww open main --toggle",
- "on-click-right": "pkill rofi || rofi -show drun",
- "min-length": 2,
- "max-length": 2
- },
- "idle_inhibitor": {
- "format": "{icon}",
- "format-icons": {
- "activated": "",
- "deactivated": ""
- },
- "min-length": 2,
- "max-length": 2
- },
- "custom/caffeine": {
- "format": "{icon}",
- "format-icons": {
- "active": "",
- "inactive": ""
- },
- "return-type": "json",
- "interval": "once",
- "exec": "$HOME/.config/waybar/modules/caffeine.sh",
- "on-click": "$HOME/.config/waybar/modules/caffeine.sh toggle && sleep 0.2",
- "exec-on-event": true,
- "tooltip": false,
- "min-length": 2,
- "max-length": 2
- },
- // Time and Date
- "clock": {
- "format": "{:%H:%M | %e %b}",
- "tooltip-format": "{:%Y %B}\n{calendar}",
- "today-format": "{}",
- "on-click": "niri msg action center-column",
- "on-scroll-up": "niri msg action set-column-width +10%",
- "on-scroll-down": "niri msg action set-column-width -10%",
- "on-click-middle": "niri msg action close-window"
- },
-
- // System monitors
- "group/monitors": {
- "modules": ["network#speed", "custom/publicip", "temperature", "memory", "cpu", "battery", "backlight", "wireplumber"],
- "orientation": "inherit"
- },
- "network#speed": {
- "interval": 1,
- "format": "{ifname}",
- "format-wifi": " {bandwidthDownBytes} {bandwidthUpBytes} ",
- "format-ethernet": " {bandwidthDownBytes} {bandwidthUpBytes} ",
- "format-disconnected": "",
- "tooltip-format": "{ipaddr}",
- "format-linked": " {ifname} (No IP)",
- "tooltip-format-wifi": "{essid} {signalStrength}%",
- "tooltip-format-ethernet": "{ifname} ",
- "tooltip-format-disconnected": " Disconnected",
- "min-length": 20
- },
- "custom/publicip": {
- "interval": 30,
- "return-type": "json",
- "format": " {text}",
- "tooltip-format": "{alt}",
- "max-length": 6,
- "min-length": 6,
- "exec": "$HOME/.config/waybar/modules/publicip.sh",
- "on-click": "rm -f $HOME/.config/waybar/modules/publicip.cache && sleep 0.1"
- },
- "temperature": {
- "interval": 5,
- "thermal-zone": 6,
- "hwmon-path": "/sys/class/hwmon/hwmon6/temp1_input",
- "critical-threshold": 80,
- // "format-critical": " {temperatureC}°C",
- "format-critical": " {temperatureC}°C",
- "format": "{icon} {temperatureC}°C",
- "format-icons": ["", "", ""],
- "max-length": 6,
- "min-length": 6
- },
- "memory": {
- "interval": 11,
- // "format": " {used:0.2f} / {total:0.0f} GB",
- "format": " {percentage}%",
- "on-click": "killall btop || ghostty -e btop",
- "max-length": 6,
- "min-length": 6
- },
- "cpu": {
- "interval": 3,
- //"format": " {}%", // Icon: microchip
- "format": " {usage}%",
- "max-length": 6,
- "min-length": 6,
- "on-click": "killall btop || ghostty -e btop"
- },
- "battery": {
- "interval": 30,
- "states": {
- "good": 95,
- "warning": 30,
- "critical": 15
- },
- "format": "{icon} {capacity}%",
- "format-charging": " {capacity}%",
- "format-plugged": " {capacity}%",
- "format-icons": ["", "", "", "", ""],
- "max-length": 6,
- "min-length": 6
- },
- "backlight": {
- "device": "$DISPLAY_DEVICE",
- "format": "{icon} {percent}%",
- "format-alt": "{percent}% {icon}",
- "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%",
- "max-length": 6,
- "min-length": 6
- },
- "wireplumber": {
- "on-click": "pavucontrol",
- //on-click: "${wpctl} set-mute @DEFAULT_AUDIO_SINK@ toggle";
- "on-scroll-down": "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 0.04-",
- "on-scroll-up": "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 0.04+",
- "format": "{icon} {volume}%",
- "format-muted": "",
- "format-source": "",
- "format-source-muted": "",
- //"format-muted": " ",
- //"format-icons": [ "" ]
- "format-icons": {
- "headphone": "",
- "phone": "",
- "portable": "",
- "car": "",
- "default": ["", "", "", "", "", ""]
- },
- "max-length": 6,
- "min-length": 6
- },
- // Niri
- "group/workspaceactions": {
- "modules": ["niri/workspaces", "custom/workspacenew"],
- "orientation": "inherit"
- },
- "niri/workspaces": {
- "all-outputs": true,
- "format": "{index}",
- "on-scroll-up": "niri msg action focus-workspace-up",
- "on-scroll-down": "niri msg action focus-workspace-down",
- "sort-by-number": true
- },
- "niri/window": {
- "format": "",
- "separate-outputs": true,
- "icon": true,
- "icon-size": 14
- },
- "custom/mediaplayer": {
- "format": "{text}",
- "return-type": "json",
- "max-length": 100,
- "escape": true,
- "exec": "$HOME/.config/waybar/modules/mediaplayer.py 2> /dev/null",
- "on-click": "playerctl play-pause",
- "on-click-right": "lyrics-widgets",
- "on-scroll-up": "playerctl next",
- "on-scroll-down": "playerctl previous"
- },
- "group/tray-expander": {
- "orientation": "inherit",
- "drawer": {
- "transition-duration": 600,
- "children-class": "tray-group-item"
- },
- "modules": ["custom/expand-icon", "tray", "custom/separator"]
- },
- "custom/expand-icon": {
- "format": "",
- "tooltip": false,
- "min-length": 2,
- "max-length": 2
- },
- "tray": {
- "icon-size": 15,
- "spacing": 5
- }
-}
diff --git a/waybar-niri/mocha.css b/waybar-niri/mocha.css
deleted file mode 100644
index 0eb6a82..0000000
--- a/waybar-niri/mocha.css
+++ /dev/null
@@ -1,26 +0,0 @@
-@define-color rosewater #f5e0dc;
-@define-color flamingo #f2cdcd;
-@define-color pink #f5c2e7;
-@define-color mauve #cba6f7;
-@define-color red #f38ba8;
-@define-color maroon #eba0ac;
-@define-color peach #fab387;
-@define-color yellow #f9e2af;
-@define-color green #a6e3a1;
-@define-color teal #94e2d5;
-@define-color sky #89dceb;
-@define-color sapphire #74c7ec;
-@define-color blue #89b4fa;
-@define-color lavender #b4befe;
-@define-color text #cdd6f4;
-@define-color subtext1 #bac2de;
-@define-color subtext0 #a6adc8;
-@define-color overlay2 #9399b2;
-@define-color overlay1 #7f849c;
-@define-color overlay0 #6c7086;
-@define-color surface2 #585b70;
-@define-color surface1 #45475a;
-@define-color surface0 #313244;
-@define-color base #1e1e2e;
-@define-color mantle #181825;
-@define-color crust #11111b;
diff --git a/waybar-niri/modules/.gitignore b/waybar-niri/modules/.gitignore
deleted file mode 100644
index 3a64743..0000000
--- a/waybar-niri/modules/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-publicip.conf
-publicip.cache
-publicip.log
\ No newline at end of file
diff --git a/waybar-niri/modules/caffeine.sh b/waybar-niri/modules/caffeine.sh
deleted file mode 100755
index f36e09b..0000000
--- a/waybar-niri/modules/caffeine.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/env bash
-
-function output() {
- jq -n --unbuffered --compact-output \
- --arg alt "$1" \
- --arg class "$2" \
- '{alt: $alt, class: [$class]}'
-}
-
-if [ "$1" = "toggle" ]; then
- pid=$(pgrep -x "swayidle")
-
- if [ -n "$pid" ]; then
- killall swayidle > /dev/null 2>&1
- notify-send "Caffeine enabled" "POWERRR!!!"
- else
- nohup niri-swayidle > /dev/null 2>&1 &
- notify-send "Caffeine disabled" "zzz..."
- fi
-
- exit 0
-fi
-
-# sleep 0.2 # Allow hypridle to start
-
-pid=$(pgrep -x "swayidle")
-if [ -n "$pid" ]; then
- output "inactive" "inactive"
-else
- output "active" "active"
-fi
\ No newline at end of file
diff --git a/waybar-niri/modules/mediaplayer.py b/waybar-niri/modules/mediaplayer.py
deleted file mode 100755
index 8edbe7b..0000000
--- a/waybar-niri/modules/mediaplayer.py
+++ /dev/null
@@ -1,221 +0,0 @@
-#!/usr/bin/env python3
-from gi.repository.Playerctl import Player
-from gi.repository import Playerctl, GLib
-from typing import List
-import os
-import json
-import signal
-import sys
-import logging
-import argparse
-
-import gi
-
-gi.require_version("Playerctl", "2.0")
-
-
-logger = logging.getLogger(__name__)
-
-
-def signal_handler(sig, frame):
- logger.info("Received signal to stop, exiting")
- sys.stdout.write("\n")
- sys.stdout.flush()
- # loop.quit()
- sys.exit(0)
-
-
-class PlayerManager:
- def __init__(self, selected_player=None, excluded_player=[]):
- self.manager = Playerctl.PlayerManager()
- self.loop = GLib.MainLoop()
- self.manager.connect(
- "name-appeared", lambda *args: self.on_player_appeared(*args)
- )
- self.manager.connect(
- "player-vanished", lambda *args: self.on_player_vanished(*args)
- )
-
- signal.signal(signal.SIGINT, signal_handler)
- signal.signal(signal.SIGTERM, signal_handler)
- signal.signal(signal.SIGPIPE, signal.SIG_DFL)
- self.selected_player = selected_player
- self.excluded_player = excluded_player.split(",") if excluded_player else []
-
- self.init_players()
-
- def init_players(self):
- for player in self.manager.props.player_names:
- if player.name in self.excluded_player:
- continue
- if self.selected_player is not None and self.selected_player != player.name:
- logger.debug(f"{player.name} is not the filtered player, skipping it")
- continue
- self.init_player(player)
-
- def run(self):
- logger.info("Starting main loop")
- self.loop.run()
-
- def init_player(self, player):
- logger.info(f"Initialize new player: {player.name}")
- player = Playerctl.Player.new_from_name(player)
- player.connect("playback-status", self.on_playback_status_changed, None)
- player.connect("metadata", self.on_metadata_changed, None)
- self.manager.manage_player(player)
- self.on_metadata_changed(player, player.props.metadata)
-
- def get_players(self) -> List[Player]:
- return self.manager.props.players
-
- def write_output(self, text, player):
- logger.debug(f"Writing output: {text}")
-
- output = {
- "text": text,
- "class": "custom-" + player.props.player_name,
- "alt": player.props.player_name,
- }
-
- sys.stdout.write(json.dumps(output) + "\n")
- sys.stdout.flush()
-
- def clear_output(self):
- sys.stdout.write("\n")
- sys.stdout.flush()
-
- def on_playback_status_changed(self, player, status, _=None):
- logger.debug(
- f"Playback status changed for player {player.props.player_name}: {status}"
- )
- self.on_metadata_changed(player, player.props.metadata)
-
- def get_first_playing_player(self):
- players = self.get_players()
- logger.debug(f"Getting first playing player from {len(players)} players")
- if len(players) > 0:
- # if any are playing, show the first one that is playing
- # reverse order, so that the most recently added ones are preferred
- for player in players[::-1]:
- if player.props.status == "Playing":
- return player
- # if none are playing, show the first one
- return players[0]
- else:
- logger.debug("No players found")
- return None
-
- def show_most_important_player(self):
- logger.debug("Showing most important player")
- # show the currently playing player
- # or else show the first paused player
- # or else show nothing
- current_player = self.get_first_playing_player()
- if current_player is not None:
- self.on_metadata_changed(current_player, current_player.props.metadata)
- else:
- self.clear_output()
-
- def on_metadata_changed(self, player, metadata, _=None):
- logger.debug(f"Metadata changed for player {player.props.player_name}")
- player_name = player.props.player_name
- artist = player.get_artist()
- title = player.get_title()
- title = title.replace("&", "&")
-
- track_info = ""
- if (
- player_name == "spotify"
- and "mpris:trackid" in metadata.keys()
- and ":ad:" in player.props.metadata["mpris:trackid"]
- ):
- track_info = "Advertisement"
- elif artist is not None and title is not None:
- track_info = f"{artist} - {title}"
- else:
- track_info = title
-
- if track_info:
- if player.props.status == "Playing":
- track_info = " " + track_info
- else:
- track_info = " " + track_info
- # only print output if no other player is playing
- current_playing = self.get_first_playing_player()
- if (
- current_playing is None
- or current_playing.props.player_name == player.props.player_name
- ):
- self.write_output(track_info, player)
- else:
- logger.debug(
- f"Other player {current_playing.props.player_name} is playing, skipping"
- )
-
- def on_player_appeared(self, _, player):
- logger.info(f"Player has appeared: {player.name}")
- if player.name in self.excluded_player:
- logger.debug(
- "New player appeared, but it's in exclude player list, skipping"
- )
- return
- if player is not None and (
- self.selected_player is None or player.name == self.selected_player
- ):
- self.init_player(player)
- else:
- logger.debug(
- "New player appeared, but it's not the selected player, skipping"
- )
-
- def on_player_vanished(self, _, player):
- logger.info(f"Player {player.props.player_name} has vanished")
- self.show_most_important_player()
-
-
-def parse_arguments():
- parser = argparse.ArgumentParser()
-
- # Increase verbosity with every occurrence of -v
- parser.add_argument("-v", "--verbose", action="count", default=0)
-
- parser.add_argument("-x", "--exclude", "- Comma-separated list of excluded player")
-
- # Define for which player we"re listening
- parser.add_argument("--player")
-
- parser.add_argument("--enable-logging", action="store_true")
-
- return parser.parse_args()
-
-
-def main():
- arguments = parse_arguments()
-
- # Initialize logging
- if arguments.enable_logging:
- logfile = os.path.join(
- os.path.dirname(os.path.realpath(__file__)), "media-player.log"
- )
- logging.basicConfig(
- filename=logfile,
- level=logging.DEBUG,
- format="%(asctime)s %(name)s %(levelname)s:%(lineno)d %(message)s",
- )
-
- # Logging is set by default to WARN and higher.
- # With every occurrence of -v it's lowered by one
- logger.setLevel(max((3 - arguments.verbose) * 10, 0))
-
- logger.info("Creating player manager")
- if arguments.player:
- logger.info(f"Filtering for player: {arguments.player}")
- if arguments.exclude:
- logger.info(f"Exclude player {arguments.exclude}")
-
- player = PlayerManager(arguments.player, arguments.exclude)
- player.run()
-
-
-if __name__ == "__main__":
- main()
diff --git a/waybar-niri/modules/publicip.sh b/waybar-niri/modules/publicip.sh
deleted file mode 100755
index 57f5485..0000000
--- a/waybar-niri/modules/publicip.sh
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/usr/bin/env bash
-# shellcheck disable=SC1091,SC1090
-
-# Entries in publicip.conf:
-# IP_QUERY_URL: URL to query public IP of the system.
-# return: JSON object with "ip" field.
-# note: This URL will be queried with short intervals (60s for example),
-# therefore it may not be a good idea to use a public API with
-# a limited number of calls.
-# IP_INFO_URL: URL to query IP location.
-# placeholder:
-# return: JSON object with "country_code" field.
-# note: This URL will only be quetried when public IP changes or when "force" is given as parameter.
-
-path="$(dirname "$(readlink -f "$0")")"
-cache_file="$path/publicip.cache"
-config_file="$path/publicip.conf"
-time_log="$path/publicip.log"
-
-[ -f "$config_file" ] || exit 1
-. "$config_file"
-[ -z "$IP_QUERY_URL" ] && exit 1
-[ -z "$IP_INFO_URL" ] && exit 1
-[ "$1" == "force" ] && rm -f "$cache_file"
-[ -f "$cache_file" ] && . "$cache_file"
-
-# Try to check network connectivity before querying
-if ! ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then
- # No network, return cached values if available
- [ -z "$CACHED_IP" ] && CACHED_IP="N/A"
- [ -z "$CACHED_CODE" ] && CACHED_CODE="N/A"
-
- echo "$(date +%Y-%m-%dT%H:%M:%S) - Waiting for network" >>"$time_log"
-
- jq -n --unbuffered --compact-output \
- --arg ip "$CACHED_IP" \
- --arg country "$CACHED_CODE" \
- '{alt: $ip, text: $country}'
- exit 0
-fi
-
-ip_current=$(curl -s -L -4 "$IP_QUERY_URL" | jq -r '.ip')
-[ -z "$ip_current" ] && exit 1
-
-if [ "$ip_current" != "$CACHED_IP" ]; then
- echo "$(date +%Y-%m-%dT%H:%M:%S) - IP changed: $CACHED_IP -> $ip_current" >>"$time_log"
- CACHED_IP="$ip_current"
-
- ip_info_url=${IP_INFO_URL///$ip_current}
- CACHED_CODE=$(curl -s -L "$ip_info_url" | jq -r '.country_code')
- [ -z "$CACHED_CODE" ] && CACHED_CODE="N/A"
-
- echo "CACHED_IP=$CACHED_IP" >"$cache_file"
- echo "CACHED_CODE=$CACHED_CODE" >>"$cache_file"
- notify-send "New Public IP detected" "New IP: $ip_current\nCountry: $CACHED_CODE"
-fi
-
-jq -n --unbuffered --compact-output \
- --arg ip "$CACHED_IP" \
- --arg country "$CACHED_CODE" \
- '{alt: $ip, text: $country}'
-
diff --git a/waybar-niri/style.css b/waybar-niri/style.css
deleted file mode 100644
index c849ae8..0000000
--- a/waybar-niri/style.css
+++ /dev/null
@@ -1,222 +0,0 @@
-@import 'mocha.css';
-
-@define-color flavor #89b4fa;
-/* @define-color archlinux #1793d1; */
-@define-color archlinux @sapphire;
-
-/* Font(s) */
-* {
- /* main font icons CJK fallback */
- font-family: 'Sour Gummy Light', 'Meslo LGM Nerd Font Mono', 'WenQuanYi Micro Hei', 'Noto Sans', sans-serif;
- font-size: 16px;
-}
-
-/* Reset all styles */
-* {
- border: none;
- border-radius: 14px;
- min-height: 0;
- margin: 2px 1px 2px 1px;
- padding: 0;
- transition-property: background-color;
- transition-duration: 0.5s;
-}
-
-/* The whole bar */
-#waybar {
- background: linear-gradient(to bottom, alpha(@base, 0.8), alpha(@base, 0));
- /* background: transparent; */
- color: @text;
- border-radius: 0px;
- margin: 0px;
-}
-
-tooltip {
- background: @base;
- border: 2px solid @overlay0;
-}
-
-#workspaceactions,
-#window,
-#clock,
-#monitors,
-#custom-mediaplayer,
-#custom-power-menu,
-#tray,
-#custom-rofi,
-#idle_inhibitor,
-#custom-caffeine,
-#custom-power {
- padding: 0px 10px;
-}
-
-#custom-separator {
- padding: 0px 5px;
- font-size: 20px;
-}
-
-#custom-rofi,
-#custom-caffeine
-#idle_inhibitor,
-#custom-power,
-#custom-expand-icon {
- padding: 0px 6px;
- font-size: 18px;
-}
-
-#custom-workspacenew,
-#workspaces button {
- padding: 0px;
- margin: 3px 3px;
- border-radius: 8px;
- color: @flavor;
- background-color: transparent;
- transition: all 0.3s ease-in-out;
-}
-
-#workspaces button:hover {
- background-color: alpha(@flavor, 0.3);
-}
-
-#workspaces button.active {
- color: @base;
- background: @flavor;
-}
-
-#workspaces button.urgent {
- color: @base;
- background-color: @red;
-}
-
-#workspaceactions {
- padding-left: 1px;
- padding-right: 1px;
-}
-
-#workspaces {
- padding: 0px;
- margin: 0px;
-}
-
-#window {
- transition: none; /* Disable background transition */
-}
-
-window#waybar.empty #window {
- background-color: transparent;
- padding: 0;
- margin: 0;
-}
-
-#custom-mediaplayer {
- color: @flavor;
-}
-
-#network.speed {
- color: @flavor;
-}
-
-#custom-publicip {
- color: @peach;
-}
-
-#temperature {
- color: @yellow;
-}
-
-#memory {
- color: @green;
-}
-
-#cpu {
- color: @teal;
-}
-
-#battery {
- color: @sapphire;
-}
-
-#battery.charging,
-#battery.full,
-#battery.plugged {
- color: @green;
-}
-
-#backlight {
- color: @blue;
-}
-
-#wireplumber {
- color: @lavender;
-}
-
-#custom-power {
- color: @maroon;
-}
-
-#custom-power:hover {
- background-color: alpha(@maroon, 0.3);
-}
-
-#custom-rofi {
- color: @archlinux;
-}
-
-#custom-rofi:hover {
- background-color: alpha(@archlinux, 0.3);
-}
-
-#custom-expand-icon {
- color: @green;
-}
-
-#custom-caffeine,
-#idle_inhibitor {
- color: @yellow;
-}
-
-#custom-caffeine:hover,
-#idle_inhibitor:hover {
- background-color: alpha(@yellow, 0.3);
-}
-
-#custom-caffeine.active,
-#idle_inhibitor.activated {
- color: @peach;
-}
-
-#clock {
- color: @flavor;
-}
-
-@keyframes blink {
- to {
- background-color: alpha(@red, 0.5);
- }
-}
-
-#battery.critical:not(.charging) {
- color: @red;
- animation-name: blink;
- animation-duration: 1s;
- animation-timing-function: linear;
- animation-iteration-count: infinite;
- animation-direction: alternate;
-}
-
-#network.disconnected {
- color: @red;
-}
-
-#temperature.critical {
- background-color: #eb4d4b;
-}
-
-#tray > .passive {
- -gtk-icon-effect: dim;
-}
-
-#tray > .needs-attention {
- -gtk-icon-effect: highlight;
- background-color: #eb4d4b;
-}
diff --git a/waybar-niri/style.css.template b/waybar-niri/style.css.template
deleted file mode 100644
index ea0218c..0000000
--- a/waybar-niri/style.css.template
+++ /dev/null
@@ -1,222 +0,0 @@
-@import 'mocha.css';
-
-@define-color flavor #;
-/* @define-color archlinux #1793d1; */
-@define-color archlinux @sapphire;
-
-/* Font(s) */
-* {
- /* main font icons CJK fallback */
- font-family: 'Sour Gummy Light', 'Meslo LGM Nerd Font Mono', 'WenQuanYi Micro Hei', 'Noto Sans', sans-serif;
- font-size: 16px;
-}
-
-/* Reset all styles */
-* {
- border: none;
- border-radius: 14px;
- min-height: 0;
- margin: 2px 1px 2px 1px;
- padding: 0;
- transition-property: background-color;
- transition-duration: 0.5s;
-}
-
-/* The whole bar */
-#waybar {
- background: linear-gradient(to bottom, alpha(@base, 0.8), alpha(@base, 0));
- /* background: transparent; */
- color: @text;
- border-radius: 0px;
- margin: 0px;
-}
-
-tooltip {
- background: @base;
- border: 2px solid @overlay0;
-}
-
-#workspaceactions,
-#window,
-#clock,
-#monitors,
-#custom-mediaplayer,
-#custom-power-menu,
-#tray,
-#custom-rofi,
-#idle_inhibitor,
-#custom-caffeine,
-#custom-power {
- padding: 0px 10px;
-}
-
-#custom-separator {
- padding: 0px 5px;
- font-size: 20px;
-}
-
-#custom-rofi,
-#custom-caffeine
-#idle_inhibitor,
-#custom-power,
-#custom-expand-icon {
- padding: 0px 6px;
- font-size: 18px;
-}
-
-#custom-workspacenew,
-#workspaces button {
- padding: 0px;
- margin: 3px 3px;
- border-radius: 8px;
- color: @flavor;
- background-color: transparent;
- transition: all 0.3s ease-in-out;
-}
-
-#workspaces button:hover {
- background-color: alpha(@flavor, 0.3);
-}
-
-#workspaces button.active {
- color: @base;
- background: @flavor;
-}
-
-#workspaces button.urgent {
- color: @base;
- background-color: @red;
-}
-
-#workspaceactions {
- padding-left: 1px;
- padding-right: 1px;
-}
-
-#workspaces {
- padding: 0px;
- margin: 0px;
-}
-
-#window {
- transition: none; /* Disable background transition */
-}
-
-window#waybar.empty #window {
- background-color: transparent;
- padding: 0;
- margin: 0;
-}
-
-#custom-mediaplayer {
- color: @flavor;
-}
-
-#network.speed {
- color: @flavor;
-}
-
-#custom-publicip {
- color: @peach;
-}
-
-#temperature {
- color: @yellow;
-}
-
-#memory {
- color: @green;
-}
-
-#cpu {
- color: @teal;
-}
-
-#battery {
- color: @sapphire;
-}
-
-#battery.charging,
-#battery.full,
-#battery.plugged {
- color: @green;
-}
-
-#backlight {
- color: @blue;
-}
-
-#wireplumber {
- color: @lavender;
-}
-
-#custom-power {
- color: @maroon;
-}
-
-#custom-power:hover {
- background-color: alpha(@maroon, 0.3);
-}
-
-#custom-rofi {
- color: @archlinux;
-}
-
-#custom-rofi:hover {
- background-color: alpha(@archlinux, 0.3);
-}
-
-#custom-expand-icon {
- color: @green;
-}
-
-#custom-caffeine,
-#idle_inhibitor {
- color: @yellow;
-}
-
-#custom-caffeine:hover,
-#idle_inhibitor:hover {
- background-color: alpha(@yellow, 0.3);
-}
-
-#custom-caffeine.active,
-#idle_inhibitor.activated {
- color: @peach;
-}
-
-#clock {
- color: @flavor;
-}
-
-@keyframes blink {
- to {
- background-color: alpha(@red, 0.5);
- }
-}
-
-#battery.critical:not(.charging) {
- color: @red;
- animation-name: blink;
- animation-duration: 1s;
- animation-timing-function: linear;
- animation-iteration-count: infinite;
- animation-direction: alternate;
-}
-
-#network.disconnected {
- color: @red;
-}
-
-#temperature.critical {
- background-color: #eb4d4b;
-}
-
-#tray > .passive {
- -gtk-icon-effect: dim;
-}
-
-#tray > .needs-attention {
- -gtk-icon-effect: highlight;
- background-color: #eb4d4b;
-}
diff --git a/waybar-niri/waybar.sh b/waybar-niri/waybar.sh
deleted file mode 100755
index d03b9eb..0000000
--- a/waybar-niri/waybar.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/env sh
-
-# Terminate already running bar instances
-killall -q waybar
-
-# Wait until the processes have been shut down
-while pgrep -x waybar >/dev/null; do sleep 1; done
-
-# Launch main
-waybar &