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 &