From ae9a80c1f012686c1f65fce7ce76fdd1b77b0b71 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Wed, 23 Sep 2026 21:08:35 +0200 Subject: ... --- accounts/gkleen@skadhi/shell/default.nix | 123 +++ .../shell/quickshell-plugins/CMakeLists.txt | 190 ++++ .../shell/quickshell-plugins/Chrono.cpp | 83 ++ .../shell/quickshell-plugins/Chrono.hpp | 55 + .../shell/quickshell-plugins/FileSelector.cpp | 102 ++ .../shell/quickshell-plugins/FileSelector.hpp | 52 + .../shell/quickshell-plugins/KeePassXC.cpp | 18 + .../shell/quickshell-plugins/KeePassXC.hpp | 21 + .../shell/quickshell-plugins/ListUnitsEntry.cpp | 41 + .../shell/quickshell-plugins/ListUnitsEntry.hpp | 26 + .../shell/quickshell-plugins/Systemd.cpp | 288 +++++ .../shell/quickshell-plugins/Systemd.hpp | 193 ++++ .../shell/quickshell-plugins/customplugin.h | 7 + .../shell/quickshell-plugins/default.nix | 32 + .../org.freedesktop.DBus.Properties.xml | 28 + .../org.freedesktop.login1.Manager.xml | 445 ++++++++ .../org.freedesktop.login1.Session.xml | 146 +++ .../org.freedesktop.systemd1.Manager.xml | 821 +++++++++++++++ .../org.freedesktop.systemd1.Service.xml | 1100 ++++++++++++++++++++ .../org.freedesktop.systemd1.Unit.xml | 359 +++++++ .../shell/quickshell/ActiveWindowDisplay.qml | 172 +++ accounts/gkleen@skadhi/shell/quickshell/Bar.qml | 142 +++ .../shell/quickshell/BatteryWidget.qml | 139 +++ .../shell/quickshell/BrightnessOSD.qml | 117 +++ .../shell/quickshell/BrightnessWidget.qml | 84 ++ accounts/gkleen@skadhi/shell/quickshell/Clock.qml | 295 ++++++ .../shell/quickshell/KeyboardLayout.qml | 112 ++ .../shell/quickshell/LidSwitchInhibitorWidget.qml | 47 + .../gkleen@skadhi/shell/quickshell/LockSurface.qml | 227 ++++ .../gkleen@skadhi/shell/quickshell/Lockscreen.qml | 128 +++ .../shell/quickshell/LockscreenInhibitorWidget.qml | 48 + .../shell/quickshell/MaterialDesignIcon.qml | 35 + .../gkleen@skadhi/shell/quickshell/NiriIdle.qml | 30 + .../shell/quickshell/NotificationDisplay.qml | 340 ++++++ .../quickshell/NotificationInhibitorWidget.qml | 266 +++++ .../shell/quickshell/PipewireWidget.qml | 483 +++++++++ .../shell/quickshell/PrivacyWidget.qml | 49 + .../shell/quickshell/Services/Brightness.qml | 107 ++ .../shell/quickshell/Services/GpgAgent.qml | 18 + .../shell/quickshell/Services/InhibitorState.qml | 23 + .../shell/quickshell/Services/MprisProxy.qml | 8 + .../shell/quickshell/Services/NiriService.qml | 217 ++++ .../quickshell/Services/NotificationManager.qml | 162 +++ .../shell/quickshell/Services/Privacy.qml | 63 ++ .../shell/quickshell/Services/ScreenRecord.qml | 63 ++ .../quickshell/Services/WallpaperSelector.qml | 8 + .../shell/quickshell/Services/Worktime.qml | 84 ++ .../gkleen@skadhi/shell/quickshell/SystemTray.qml | 201 ++++ .../gkleen@skadhi/shell/quickshell/UnixIPC.qml | 106 ++ .../gkleen@skadhi/shell/quickshell/VolumeOSD.qml | 163 +++ .../shell/quickshell/WallpaperBackground.qml | 89 ++ .../shell/quickshell/WaylandInhibitorWidget.qml | 56 + .../shell/quickshell/WorkspaceSwitcher.qml | 204 ++++ .../shell/quickshell/WorktimeWidget.qml | 212 ++++ .../gkleen@skadhi/shell/quickshell/YtDlpWidget.qml | 190 ++++ .../shell/quickshell/displaymanager.qml | 115 ++ accounts/gkleen@skadhi/shell/quickshell/shell.qml | 53 + 57 files changed, 8986 insertions(+) create mode 100644 accounts/gkleen@skadhi/shell/default.nix create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/CMakeLists.txt create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/Chrono.cpp create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/Chrono.hpp create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/FileSelector.cpp create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/FileSelector.hpp create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/KeePassXC.cpp create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/KeePassXC.hpp create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/ListUnitsEntry.cpp create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/ListUnitsEntry.hpp create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/Systemd.cpp create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/Systemd.hpp create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/customplugin.h create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/default.nix create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.DBus.Properties.xml create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.login1.Manager.xml create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.login1.Session.xml create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Manager.xml create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Service.xml create mode 100644 accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Unit.xml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/ActiveWindowDisplay.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Bar.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/BatteryWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/BrightnessOSD.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/BrightnessWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Clock.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/KeyboardLayout.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/LidSwitchInhibitorWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/LockSurface.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Lockscreen.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/LockscreenInhibitorWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/MaterialDesignIcon.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/NiriIdle.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/NotificationDisplay.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/NotificationInhibitorWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/PipewireWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/PrivacyWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/SystemTray.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/UnixIPC.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/VolumeOSD.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/WallpaperBackground.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/WaylandInhibitorWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/WorkspaceSwitcher.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/WorktimeWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/YtDlpWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/displaymanager.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/shell.qml (limited to 'accounts/gkleen@skadhi/shell') diff --git a/accounts/gkleen@skadhi/shell/default.nix b/accounts/gkleen@skadhi/shell/default.nix new file mode 100644 index 00000000..09eb6863 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/default.nix @@ -0,0 +1,123 @@ +{ config, pkgs, lib, ... }: + +{ + config = { + programs.quickshell = { + enable = true; + package = pkgs.symlinkJoin { + pname = pkgs.quickshell.pname + "-wrapped"; + inherit (pkgs.quickshell) version meta; + paths = [ pkgs.quickshell ]; + buildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + for binary in quickshell qs; do + wrapProgram $out/bin/$binary \ + --prefix QT_PLUGIN_PATH : ${pkgs.qt6.qtimageformats}/${pkgs.qt6.qtbase.qtPluginPrefix} \ + --prefix QML_IMPORT_PATH : ${pkgs.qt6Packages.callPackage ./quickshell-plugins {}}/${pkgs.qt6.qtbase.qtQmlPrefix} + done + ''; + }; + config = { + src = ./quickshell; + replacements = { + ignore_workspaces = builtins.toJSON (map ({ name, ... }: name) config.programs.niri.scratchspaces); + wallpapers = builtins.toJSON (pkgs.stdenvNoCC.mkDerivation { + name = "wallpapers"; + srcs = [ + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/publicationtiff10k/carinanebula3.tif"; + hash = "sha256-YxZEweDKJfvfrdxb/QFmgJhcZDEJYxotoHrG+RRn1tw="; + }) + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/original/pillarsofcreation_composite.tif"; + hash = "sha256-qRiODxR0lZWdxgYXna0fNRFFDErpBJDwOJuQl6sNjRc="; + }) + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2212a.tif"; + hash = "sha256-l2fqE/z//C1a0xkvZwsnwPbTSb+WuA11h+SUl3E1dhw="; + }) + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2415a.tif"; + hash = "sha256-onBy7cPoUpDuzQStbY2E+qmlGgSLXPwFCLX53ukAb4c="; + }) + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2330a.tif"; + hash = "sha256-nn0ZtjZIrPcpj3YcLTsrL7XiXvyh3QYgCSmdDMD+3OM="; + }) + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/original/weic2426a.tif"; + hash = "sha256-EDnfPn3GE9jt6XPqiGInP7E2F3Az7d25NqATSWltDv0="; + }) + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/original/weic2503a.tif"; + hash = "sha256-3/RX6RQp8naELcgReHPd5/zhJkoCjnA10w5BEnNo+qI="; + }) + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/original/weic2506a.tif"; + hash = "sha256-aDld0aoY1owRxDVf7Jcyw71TH42M1foYotxn2thyFYw="; + }) + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/original/weic2514a.tif"; + hash = "sha256-jTi1G1Ofo5xsF6ggrbtYJHxqLaCQ7edM5B3uORiVQtg="; + }) + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/original/weic2425c.tif"; + hash = "sha256-oaEOexSJHEGj090dJF3ct5HAoR+Y5gRiPrUlxdvnTRo="; + }) + (pkgs.fetchurl { + url = "https://esawebb.org/media/archives/images/original/weic2605c.tif"; + hash = "sha256-IwWfgkz84LFXkfk77anb5e0Mf0wWiIszum7UC3yZn0U="; + }) + ]; + + dontUnpack = true; + + buildInputs = [ pkgs.imagemagick ]; + buildPhase = '' + runHook preBuild + + typeset sources=($srcs) + + mkdir -p $out + magick ''${sources[0]} -crop 10000x5625+0+79 +repage -define webp:target-size=10000000 $out/carinanebula3.webp + magick ''${sources[1]} -crop 6716x3778+329+80 +repage -define webp:target-size=10000000 $out/pillarsofcreation_composite.webp + magick ''${sources[2]} -crop 10000x5625+0+79 +repage -define webp:target-size=10000000 $out/weic2212a.webp + magick ''${sources[3]} -crop 7650x4302+1166+389 +repage -define webp:target-size=10000000 $out/weic2415a.webp + magick ''${sources[4]} -crop 8732x4912+0+434 +repage -define webp:target-size=10000000 $out/weic2330a.webp + magick ''${sources[5]} -crop 5302x2982+636+0 +repage -define webp:target-size=10000000 $out/weic2426a.webp + magick ''${sources[6]} -crop 4328x2434+0+906 +repage -define webp:target-size=10000000 $out/weic2503a.webp + magick ''${sources[7]} -crop 4152x2335+0+666 +repage -define webp:target-size=10000000 $out/weic2506a.webp + magick ''${sources[8]} -crop 4320x2430+0+0 +repage -define webp:target-size=10000000 $out/weic2514a.webp + magick ''${sources[9]} -crop 5863x3298+0+477 +repage -define webp:target-size=10000000 $out/weic2425c.webp + magick ''${sources[10]} -crop 3990x2244+0+1379 +repage -define webp:target-size=10000000 $out/weic2605c.webp + + runHook postBuild + ''; + }); + niri_session = builtins.toJSON [ + (pkgs.writeShellScript "niri-session" '' + exec ${lib.getExe pkgs.dex} -w ${config.programs.niri.package}/share/wayland-sessions/niri.desktop &>/tmp/niri-session-$$.log + '') + # (lib.getExe pkgs.dex) + # "${config.programs.niri.package}/share/wayland-sessions/niri.desktop" + ]; + username = builtins.toJSON config.home.username; + mdi = builtins.toJSON (pkgs.fetchFromGitHub { + owner = "Templarian"; + repo = "MaterialDesign"; + rev = "2424e748e0cc63ab7b9c095a099b9fe239b737c0"; + hash = "sha256-QMGl7soAhErrrnY3aKOZpt49yebkSNzy10p/v5OaqQ0="; + }); + worktime = builtins.toJSON (lib.getExe pkgs.worktime); + slurp = builtins.toJSON (lib.getExe pkgs.slurp); + wf-recorder = builtins.toJSON (lib.getExe pkgs.wf-recorder); + }; + }; + }; + systemd.user.services.quickshell = { + Service = { + RuntimeDirectory = "quickshell"; + }; + }; + }; +} diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/CMakeLists.txt b/accounts/gkleen@skadhi/shell/quickshell-plugins/CMakeLists.txt new file mode 100644 index 00000000..6b4f0d48 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/CMakeLists.txt @@ -0,0 +1,190 @@ +set(INSTALL_QMLDIR "" CACHE STRING "QML install dir") +set(INSTALL_QML_PREFIX "" CACHE STRING "QML install prefix") + +# There doesn't seem to be a standard cross-distro qml install path. +if ("${INSTALL_QMLDIR}" STREQUAL "" AND "${INSTALL_QML_PREFIX}" STREQUAL "") + message(WARNING "Neither INSTALL_QMLDIR nor INSTALL_QML_PREFIX is set. QML modules will not be installed.") +else() + if ("${INSTALL_QMLDIR}" STREQUAL "") + set(QML_FULL_INSTALLDIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_QML_PREFIX}") + else() + set(QML_FULL_INSTALLDIR "${INSTALL_QMLDIR}") + endif() + + message(STATUS "QML install dir: ${QML_FULL_INSTALLDIR}") +endif() + +# Install a given target as a QML module. This is mostly pulled from ECM, as there does not seem +# to be an official way to do it. +# see https://github.com/KDE/extra-cmake-modules/blob/fe0f606bf7f222e36f7560fd7a2c33ef993e23bb/modules/ECMQmlModule6.cmake#L160 +function(install_qml_module arg_TARGET) + if (NOT DEFINED QML_FULL_INSTALLDIR) + return() + endif() + + qt_query_qml_module(${arg_TARGET} + URI module_uri + VERSION module_version + PLUGIN_TARGET module_plugin_target + TARGET_PATH module_target_path + QMLDIR module_qmldir + TYPEINFO module_typeinfo + QML_FILES module_qml_files + RESOURCES module_resources + ) + + set(module_dir "${QML_FULL_INSTALLDIR}/${module_target_path}") + + if (NOT TARGET "${module_plugin_target}") + message(FATAL_ERROR "install_qml_modules called for a target without a plugin") + endif() + + get_target_property(target_type "${arg_TARGET}" TYPE) + if (NOT "${target_type}" STREQUAL "STATIC_LIBRARY") + install( + TARGETS "${arg_TARGET}" + LIBRARY DESTINATION "${module_dir}" + RUNTIME DESTINATION "${module_dir}" + ) + + install( + TARGETS "${module_plugin_target}" + LIBRARY DESTINATION "${module_dir}" + RUNTIME DESTINATION "${module_dir}" + ) + endif() + + install(FILES "${module_qmldir}" DESTINATION "${module_dir}") + install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") + + # Install QML files + list(LENGTH module_qml_files num_files) + if (NOT "${module_qml_files}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0) + qt_query_qml_module(${arg_TARGET} QML_FILES_DEPLOY_PATHS qml_files_deploy_paths) + + math(EXPR last_index "${num_files} - 1") + foreach(i RANGE 0 ${last_index}) + list(GET module_qml_files ${i} src_file) + list(GET qml_files_deploy_paths ${i} deploy_path) + get_filename_component(dst_name "${deploy_path}" NAME) + get_filename_component(dest_dir "${deploy_path}" DIRECTORY) + install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}") + endforeach() + endif() + + # Install resources + list(LENGTH module_resources num_files) + if (NOT "${module_resources}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0) + qt_query_qml_module(${arg_TARGET} RESOURCES_DEPLOY_PATHS resources_deploy_paths) + + math(EXPR last_index "${num_files} - 1") + foreach(i RANGE 0 ${last_index}) + list(GET module_resources ${i} src_file) + list(GET resources_deploy_paths ${i} deploy_path) + get_filename_component(dst_name "${deploy_path}" NAME) + get_filename_component(dest_dir "${deploy_path}" DIRECTORY) + install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}") + endforeach() + endif() +endfunction() + + +cmake_minimum_required(VERSION 3.20) +project(custom LANGUAGES CXX) + +find_package(Qt6 REQUIRED COMPONENTS Core Qml DBus) + +qt_standard_project_setup(REQUIRES 6.6) + +qt6_policy(SET QTP0001 NEW) +qt6_add_qml_module(customplugin + URI "Custom" + PLUGIN_TARGET customplugin +) + +set_source_files_properties(org.keepassxc.KeePassXC.MainWindow.xml PROPERTIES + CLASSNAME DBusKeePassXC + NO_NAMESPACE TRUE +) +qt_add_dbus_interface(DBUS_INTERFACES + org.keepassxc.KeePassXC.MainWindow.xml + dbus_keepassxc +) + +set_source_files_properties(org.freedesktop.systemd1.Manager.xml PROPERTIES + CLASSNAME DBusSystemdManager + NO_NAMESPACE TRUE + INCLUDE ListUnitsEntry.hpp +) +qt_add_dbus_interface(DBUS_INTERFACES + org.freedesktop.systemd1.Manager.xml + dbus_systemd_manager +) + +set_source_files_properties(org.freedesktop.login1.Manager.xml PROPERTIES + CLASSNAME DBusLogindManager + NO_NAMESPACE TRUE +) +qt_add_dbus_interface(DBUS_INTERFACES + org.freedesktop.login1.Manager.xml + dbus_logind_manager +) + +set_source_files_properties(org.freedesktop.login1.Session.xml PROPERTIES + CLASSNAME DBusLogindSession + NO_NAMESPACE TRUE +) +qt_add_dbus_interface(DBUS_INTERFACES + org.freedesktop.login1.Session.xml + dbus_logind_session +) + +set_source_files_properties(org.freedesktop.systemd1.Unit.xml PROPERTIES + CLASSNAME DBusSystemdUnit + NO_NAMESPACE TRUE +) +qt_add_dbus_interface(DBUS_INTERFACES + org.freedesktop.systemd1.Unit.xml + dbus_systemd_unit +) + +set_source_files_properties(org.freedesktop.systemd1.Service.xml PROPERTIES + CLASSNAME DBusSystemdService + NO_NAMESPACE TRUE +) +qt_add_dbus_interface(DBUS_INTERFACES + org.freedesktop.systemd1.Service.xml + dbus_systemd_service +) + +set_source_files_properties(org.freedesktop.DBus.Properties.xml PROPERTIES + CLASSNAME DBusProperties + NO_NAMESPACE TRUE +) +qt_add_dbus_interface(DBUS_INTERFACES + org.freedesktop.DBus.Properties.xml + dbus_properties +) + +include_directories(${CMAKE_SOURCE_DIR}/build) +include_directories(${QUICKSHELL_SRC}/src) + +target_compile_features(customplugin PUBLIC cxx_std_26) + +target_link_libraries(customplugin PRIVATE + Qt6::Core + Qt6::Qml + Qt6::DBus +) + +target_sources(customplugin PRIVATE + Chrono.cpp Chrono.hpp + FileSelector.cpp FileSelector.hpp + KeePassXC.cpp KeePassXC.hpp + Systemd.cpp Systemd.hpp + ListUnitsEntry.cpp ListUnitsEntry.hpp + ${DBUS_INTERFACES} + ${QUICKSHELL_SRC}/src/core/model.cpp ${QUICKSHELL_SRC}/src/core/model.hpp +) + +install_qml_module(customplugin) diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/Chrono.cpp b/accounts/gkleen@skadhi/shell/quickshell-plugins/Chrono.cpp new file mode 100644 index 00000000..22b3469b --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/Chrono.cpp @@ -0,0 +1,83 @@ +#include "Chrono.hpp" + +#include +#include +#include + +Chrono::Chrono(QObject* parent): QObject(parent) { + QObject::connect(&this->timer, &QTimer::timeout, this, &Chrono::onTimeout); + this->update(); +} + +bool Chrono::enabled() const { return this->mEnabled; } + +void Chrono::setEnabled(bool enabled) { + if (enabled == this->mEnabled) return; + this->mEnabled = enabled; + emit this->enabledChanged(); + this->update(); +} + +Chrono::Precision Chrono::precision() const { return this->mPrecision; } + +void Chrono::setPrecision(Chrono::Precision precision) { + if (precision == this->mPrecision) return; + this->mPrecision = precision; + emit this->precisionChanged(); + this->update(); +} + +void Chrono::onTimeout() { + this->setTime(this->targetTime); + this->schedule(this->targetTime); +} + +void Chrono::update() { + if (this->mEnabled) { + this->setTime(std::chrono::time_point()); + this->schedule(std::chrono::time_point()); + } else { + this->timer.stop(); + } +} + +void Chrono::setTime(const std::chrono::time_point& targetTime) { + using namespace std::chrono_literals; + + auto currentTime = std::chrono::system_clock::now(); + auto offset = std::chrono::duration_cast(targetTime - currentTime); + this->currentTime = abs(offset) < 500ms ? targetTime : currentTime; + + switch (this->mPrecision) { + case Chrono::Hours: this->currentTime = std::chrono::time_point_cast(this->currentTime); + case Chrono::Minutes: this->currentTime = std::chrono::time_point_cast(this->currentTime); + case Chrono::Seconds: this->currentTime = std::chrono::time_point_cast(this->currentTime); + } + + emit this->dateChanged(); +} + +void Chrono::schedule(const std::chrono::time_point& targetTime) { + using namespace std::chrono_literals; + + auto currentTime = std::chrono::system_clock::now(); + auto offset = std::chrono::duration_cast(targetTime - currentTime); + auto nextTime = abs(offset) < 500ms ? targetTime : currentTime; + + switch (this->mPrecision) { + case Chrono::Hours: nextTime = std::chrono::time_point_cast(nextTime) + 1h; + case Chrono::Minutes: nextTime = std::chrono::time_point_cast(nextTime) + 1min; + case Chrono::Seconds: nextTime = std::chrono::time_point_cast(nextTime) + 1s; + } + + this->targetTime = nextTime; + this->timer.start(std::chrono::duration_cast(nextTime - currentTime)); +} + +QString Chrono::format(const QString& fmt) const { + return QString::fromStdString(std::format(std::runtime_format(fmt.toStdString()), std::chrono::zoned_time(std::chrono::current_zone(), std::chrono::time_point_cast(this->currentTime)))); +} + +QDateTime Chrono::date() const { + return QDateTime::fromStdTimePoint(std::chrono::time_point_cast(this->currentTime)); +} diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/Chrono.hpp b/accounts/gkleen@skadhi/shell/quickshell-plugins/Chrono.hpp new file mode 100644 index 00000000..04080187 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/Chrono.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include +#include +#include + +#include + +class Chrono : public QObject { + Q_OBJECT; + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); + Q_PROPERTY(Chrono::Precision precision READ precision WRITE setPrecision NOTIFY precisionChanged); + Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged) + QML_ELEMENT; + +public: + enum Precision : quint8 { + Hours = 1, + Minutes = 2, + Seconds = 3, + }; + Q_ENUM(Precision); + + explicit Chrono(QObject* parent = nullptr); + + bool enabled() const; + void setEnabled(bool enabled); + + Chrono::Precision precision() const; + void setPrecision(Chrono::Precision precision); + + Q_INVOKABLE QString format(const QString& fmt) const; + + QDateTime date() const; + +signals: + void enabledChanged(); + void precisionChanged(); + void dateChanged(); + +private slots: + void onTimeout(); + +private: + bool mEnabled = true; + Chrono::Precision mPrecision = Chrono::Seconds; + QTimer timer; + std::chrono::time_point currentTime, targetTime; + + void update(); + void setTime(const std::chrono::time_point& targetTime); + void schedule(const std::chrono::time_point& targetTime); +}; diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/FileSelector.cpp b/accounts/gkleen@skadhi/shell/quickshell-plugins/FileSelector.cpp new file mode 100644 index 00000000..d7051d2a --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/FileSelector.cpp @@ -0,0 +1,102 @@ +#include "FileSelector.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace fs = std::filesystem; + +FileSelector::FileSelector(QObject* parent): QObject(parent) { + QObject::connect(&this->timer, &QTimer::timeout, this, &FileSelector::onTimeout); + this->timer.setTimerType(Qt::PreciseTimer); +} + +QString FileSelector::directory() const { + return QString::fromStdString(this->mDirectory->string()); +} +void FileSelector::setDirectory(QString directory) { + this->mDirectory = directory.toStdString(); + if (this->mDirectory && this->mEpoch) + this->update(); + emit this->directoryChanged(); +} + +unsigned int FileSelector::epoch() const { + return std::chrono::duration_cast(*this->mEpoch).count(); +} +void FileSelector::setEpoch(unsigned int epoch) { + this->mEpoch = std::chrono::milliseconds{epoch}; + if (this->mDirectory && this->mEpoch) + this->update(); + emit this->epochChanged(); +} + +QString FileSelector::seed() const { + return this->mSeed; +} +void FileSelector::setSeed(QString seed) { + this->mSeed = seed; + emit this->seedChanged(); + if (this->mDirectory && this->mEpoch) + emit this->selectedChanged(); +} + +QString FileSelector::selected() const { + if (!this->mDirectory || !this->mEpoch) + return QString(); + + std::vector shuffled(this->mFiles.begin(), this->mFiles.end()); + std::sort(shuffled.begin(), shuffled.end()); + + auto currentTime = std::chrono::system_clock::now(); + uint64_t currentEpoch = currentTime.time_since_epoch() / *this->mEpoch; + std::chrono::milliseconds timeInEpoch = std::chrono::duration_cast(currentTime.time_since_epoch()) % *this->mEpoch; + + std::ostringstream seed; + seed << this->mSeed.size() << ";" << this->mSeed.toStdString() << ";"; + seed << *this->mEpoch << ";"; + seed << currentEpoch << ";"; + seed << this->mDirectory->string().size() << ";" << *this->mDirectory << ";"; + seed << this->mFiles.size() << ";"; + for (const fs::path& p: this->mFiles) + seed << p.string().size() << ";" << p << ";"; + + std::vector v; + v.reserve(seed.str().size()); + for (const char& c: seed.str()) + v.push_back(c); + + std::seed_seq engine_seed(v.begin(), v.end()); + std::mt19937 g(engine_seed); + std::shuffle(shuffled.begin(), shuffled.end(), g); + + std::vector::size_type ix = shuffled.size() * timeInEpoch / *this->mEpoch; + return QString::fromStdString((*this->mDirectory / shuffled[ix]).string()); +} + +void FileSelector::onTimeout() { + if (!this->mFiles.size()) + return; + + auto currentTime = std::chrono::system_clock::now(); + uint64_t currentMinorEpoch = currentTime.time_since_epoch() / (*this->mEpoch / this->mFiles.size()); + auto nextTime = std::chrono::time_point((currentMinorEpoch + 1) * (*this->mEpoch / this->mFiles.size())); + this->timer.start(std::chrono::duration_cast(nextTime - currentTime)); + + emit this->selectedChanged(); +} + +void FileSelector::update() { + this->mFiles = std::set{}; + for (const fs::directory_entry& entry: + fs::recursive_directory_iterator(*this->mDirectory, fs::directory_options::follow_directory_symlink)) + { + this->mFiles.insert(fs::relative(entry, *this->mDirectory)); + } + + this->onTimeout(); +} diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/FileSelector.hpp b/accounts/gkleen@skadhi/shell/quickshell-plugins/FileSelector.hpp new file mode 100644 index 00000000..72c4f2a7 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/FileSelector.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include + +class FileSelector : public QObject { + Q_OBJECT; + Q_PROPERTY(QString directory READ directory WRITE setDirectory NOTIFY directoryChanged REQUIRED); + Q_PROPERTY(unsigned int epoch READ epoch WRITE setEpoch NOTIFY epochChanged REQUIRED); + Q_PROPERTY(QString seed READ seed WRITE setSeed NOTIFY seedChanged); + Q_PROPERTY(QString selected READ selected NOTIFY selectedChanged); + QML_ELEMENT; + +public: + explicit FileSelector(QObject* parent = nullptr); + + QString directory() const; + void setDirectory(QString directory); + + unsigned int epoch() const; + void setEpoch(unsigned int epoch); + + QString seed() const; + void setSeed(QString seed); + + QString selected() const; + +signals: + void directoryChanged(); + void epochChanged(); + void seedChanged(); + void selectedChanged(); + +private slots: + void onTimeout(); + +private: + std::optional mDirectory; + std::optional mEpoch; + std::set mFiles; + QString mSeed; + QTimer timer; + + void update(); +}; diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/KeePassXC.cpp b/accounts/gkleen@skadhi/shell/quickshell-plugins/KeePassXC.cpp new file mode 100644 index 00000000..f6e4dd6e --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/KeePassXC.cpp @@ -0,0 +1,18 @@ +#include "KeePassXC.hpp" + +#include + +KeePassXC::KeePassXC() { + this->service = new DBusKeePassXC(DBusKeePassXC::staticInterfaceName(), "/keepassxc", QDBusConnection::sessionBus(), this); +} +KeePassXC::~KeePassXC() { + if (this->service) + delete this->service; +} + +void KeePassXC::lockAllDatabases() { + if (!this->service) + return; + + this->service->lockAllDatabases(); +} diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/KeePassXC.hpp b/accounts/gkleen@skadhi/shell/quickshell-plugins/KeePassXC.hpp new file mode 100644 index 00000000..c4cd71e0 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/KeePassXC.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "dbus_keepassxc.h" + +#include +#include + +class KeePassXC : public QObject { + Q_OBJECT; + QML_SINGLETON; + QML_ELEMENT; + +public: + explicit KeePassXC(); + ~KeePassXC(); + + Q_INVOKABLE void lockAllDatabases(); + +private: + DBusKeePassXC* service = nullptr; +}; diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/ListUnitsEntry.cpp b/accounts/gkleen@skadhi/shell/quickshell-plugins/ListUnitsEntry.cpp new file mode 100644 index 00000000..c1f9537c --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/ListUnitsEntry.cpp @@ -0,0 +1,41 @@ +#include "ListUnitsEntry.hpp" + +void ListUnitsEntry::registerMetaType() { + qRegisterMetaType("ListUnitsEntry"); + qDBusRegisterMetaType(); +} + +QDBusArgument& operator<<(QDBusArgument& argument, const ListUnitsEntry& entry) { + argument.beginStructure(); + argument + << entry.primaryName + << entry.description + << entry.loadState + << entry.activeState + << entry.subState + << entry.followingUnit + << entry.unitPath + << entry.jobId + << entry.jobType + << entry.jobPath; + argument.endStructure(); + + return argument; +} +const QDBusArgument& operator>>(const QDBusArgument& argument, ListUnitsEntry& entry) { + argument.beginStructure(); + argument + >> entry.primaryName + >> entry.description + >> entry.loadState + >> entry.activeState + >> entry.subState + >> entry.followingUnit + >> entry.unitPath + >> entry.jobId + >> entry.jobType + >> entry.jobPath; + argument.endStructure(); + + return argument; +} diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/ListUnitsEntry.hpp b/accounts/gkleen@skadhi/shell/quickshell-plugins/ListUnitsEntry.hpp new file mode 100644 index 00000000..15d789b9 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/ListUnitsEntry.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +struct ListUnitsEntry { + QString primaryName; + QString description; + QString loadState; + QString activeState; + QString subState; + QString followingUnit; + QDBusObjectPath unitPath; + uint jobId; + QString jobType; + QDBusObjectPath jobPath; + + friend QDBusArgument& operator<<(QDBusArgument& argument, const ListUnitsEntry& entry); + friend const QDBusArgument& operator>>(const QDBusArgument& argument, ListUnitsEntry& entry); + static void registerMetaType(); +}; + +Q_DECLARE_METATYPE(ListUnitsEntry) + +using ListUnitsResult = QList; +Q_DECLARE_METATYPE(ListUnitsResult) diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/Systemd.cpp b/accounts/gkleen@skadhi/shell/quickshell-plugins/Systemd.cpp new file mode 100644 index 00000000..6376b527 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/Systemd.cpp @@ -0,0 +1,288 @@ +#include "Systemd.hpp" + +#include + +#include +#include +#include +#include +#include + +#include + +Systemd::Systemd(QObject* parent) : QObject(parent) { + ListUnitsEntry::registerMetaType(); + qDBusRegisterMetaType(); + + this->systemdManager = new DBusSystemdManager( + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + QDBusConnection::sessionBus(), + this + ); + this->logindManager = new DBusLogindManager( + "org.freedesktop.login1", + "/org/freedesktop/login1", + QDBusConnection::systemBus(), + this + ); + + + QDBusReply sessionPath = this->logindManager->GetSession("auto"); + // qDebug() << sessionPath; + this->logindSession = new DBusLogindSession( + "org.freedesktop.login1", + sessionPath.value().path(), + QDBusConnection::systemBus(), + this + ); + this->logindSessionProperties = new DBusProperties( + "org.freedesktop.login1", + sessionPath.value().path(), + QDBusConnection::systemBus(), + this + ); + + QObject::connect(this->logindManager, &DBusLogindManager::PrepareForShutdown, this, &Systemd::shutdown); + QObject::connect(this->logindManager, &DBusLogindManager::PrepareForSleep, this, &Systemd::sleep); + QObject::connect(this->logindSession, &DBusLogindSession::Lock, this, &Systemd::lock); + QObject::connect(this->logindSession, &DBusLogindSession::Unlock, this, &Systemd::unlock); + QObject::connect(this->logindSessionProperties, &DBusProperties::PropertiesChanged, this, &Systemd::onLogindSessionPropertiesChanged); + this->systemdManager->Subscribe(); + QObject::connect(this->systemdManager, &DBusSystemdManager::UnitNew, this, &Systemd::onUserUnitNew); + QObject::connect(this->systemdManager, &DBusSystemdManager::UnitRemoved, this, &Systemd::onUserUnitRemoved); + + for (const auto& unit: this->systemdManager->ListUnits().value()) + this->onUserUnitNew(unit.primaryName, unit.unitPath); +} + +void Systemd::onLogindSessionPropertiesChanged(const QString& interface_name, const QVariantMap& changed_properties, const QStringList& invalidated_properties) { + if (changed_properties.contains("IdleHint") || invalidated_properties.contains("IdleHint")) + emit this->idleHintChanged(); + if (changed_properties.contains("LockedHint") || invalidated_properties.contains("LockedHint")) + emit this->lockedHintChanged(); +} +void Systemd::onUserUnitNew(const QString& id, const QDBusObjectPath& object_path) { + for (auto unit: this->mUserUnits.valueList()) + if (unit->path() == object_path) + return; + + for (const auto& unit: this->systemdManager->ListUnits().value()) + if (unit.unitPath == object_path) { + this->mUserUnits.insertObject(new SystemdUnit(this, object_path, id)); + return; + } +} +void Systemd::onUserUnitRemoved(const QString& id, const QDBusObjectPath& object_path) { + std::set toRemove; + for (auto unit: this->mUserUnits.valueList()) + if (unit->path() == object_path) + toRemove.insert(unit); + + for (auto unit: toRemove) { + this->mUserUnits.removeObject(unit); + delete unit; + } +} + +SystemdUnit::SystemdUnit(QObject* parent, const QDBusObjectPath& path, const QString& id): QObject(parent), mPath(path), mId(id) { + this->mUnitProperties = new DBusProperties( + "org.freedesktop.systemd1", + this->path().path(), + QDBusConnection::sessionBus(), + this + ); +} +std::unique_ptr SystemdUnit::systemdUnit() { + return std::make_unique( + "org.freedesktop.systemd1", + this->path().path(), + QDBusConnection::sessionBus(), + nullptr + ); +} +std::unique_ptr SystemdUnit::systemdService() { + if (!this->id().endsWith(".service")) + return std::unique_ptr(); + + return std::make_unique( + "org.freedesktop.systemd1", + this->path().path(), + QDBusConnection::sessionBus(), + nullptr + ); +} +QList SystemdUnit::runtimeDirectory() { + if (std::unique_ptr systemdService = this->systemdService()) { + if (!this->mRuntimeDirectory) + this->mRuntimeDirectory = systemdService->runtimeDirectory(); + return *this->mRuntimeDirectory; + } + + return QList{}; +} +QString SystemdUnit::activeState() { + if (!this->mActiveState) { + QObject::connect(this->mUnitProperties, &DBusProperties::PropertiesChanged, this, &SystemdUnit::onUnitPropertiesChanged); + this->mActiveState = this->systemdUnit()->activeState(); + } + + return *this->mActiveState; +} + +const QDBusObjectPath& SystemdUnit::path() const { return this->mPath; } +const QString& SystemdUnit::id() const { return this->mId; } +void SystemdUnit::onUnitPropertiesChanged(const QString& interface_name, const QVariantMap& changed_properties, const QStringList& invalidated_properties) { + if (changed_properties.contains("ActiveState") || invalidated_properties.contains("ActiveState")) { + this->mActiveState = this->systemdUnit()->activeState(); + emit this->activeStateChanged(); + } +} + +void Systemd::stopUserUnit(const QString& unit, const QString& mode) { this->systemdManager->StopUnit(unit, mode); } + +void Systemd::setBrightness(const QString& subsystem, const QString& name, quint32 brightness) { this->logindSession->SetBrightness(subsystem, name, brightness); } + +bool Systemd::idleHint() { return this->logindSession->idleHint(); } +void Systemd::setIdleHint(bool idle) { this->logindSession->SetIdleHint(idle); } +bool Systemd::lockedHint() { return this->logindSession->lockedHint(); } +void Systemd::setLockedHint(bool locked) { this->logindSession->SetLockedHint(locked); } +void Systemd::lockSession() { this->logindSession->call("Lock"); } +void Systemd::suspend() { this->logindManager->Suspend(true); } +void Systemd::hibernate() { this->logindManager->Hibernate(true); } + +ObjectModel* Systemd::userUnits() { return &this->mUserUnits; } + +std::string SystemdInhibitorParams::toString(SystemdInhibitorParams::WhatItem what) { + if (what == SystemdInhibitorParams::Shutdown) + return "shutdown"; + else if (what == SystemdInhibitorParams::Sleep) + return "sleep"; + else if (what == SystemdInhibitorParams::Idle) + return "idle"; + else if (what == SystemdInhibitorParams::HandlePowerKey) + return "handle-power-key"; + else if (what == SystemdInhibitorParams::HandleSuspendKey) + return "handle-suspend-key"; + else if (what == SystemdInhibitorParams::HandleHibernateKey) + return "handle-hibernate-key"; + else if (what == SystemdInhibitorParams::HandleLidSwitch) + return "handle-lid-switch"; + return ""; +} + +std::string SystemdInhibitorParams::toString(SystemdInhibitorParams::What what) { + std::ostringstream res; + bool first = true; + for (const WhatItem& item: SystemdInhibitorParams::allWhatItems) { + if (!(what & item)) + continue; + + if (!first) + res << ":"; + else + first = false; + res << SystemdInhibitorParams::toString(item); + } + return res.str(); +} + +std::string SystemdInhibitorParams::toString(SystemdInhibitorParams::Mode mode) { + if (mode == SystemdInhibitorParams::Block) + return "block"; + else if (mode == SystemdInhibitorParams::BlockWeak) + return "block-weak"; + else if (mode == SystemdInhibitorParams::Delay) + return "delay"; + return ""; +} + +bool SystemdInhibitor::enabled() const { return static_cast(this->activeInhibitor); } +void SystemdInhibitor::setEnabled(bool enabled) { + this->mEnabled = enabled; + + if (enabled) + this->update(); + else + this->release(); +} + +SystemdInhibitorParams::What SystemdInhibitor::what() const { return this->mWhat; } +void SystemdInhibitor::setWhat(SystemdInhibitorParams::What what) { + this->mWhat = what; + this->update(); +} + +QString SystemdInhibitor::who() const { return this->mWho; } +void SystemdInhibitor::setWho(QString who) { + this->mWho = who; + this->update(); +} + +QString SystemdInhibitor::why() const { return this->mWhy; } +void SystemdInhibitor::setWhy(QString why) { + this->mWhy = why; + this->update(); +} + +SystemdInhibitorParams::Mode SystemdInhibitor::mode() const { return this->mMode; } +void SystemdInhibitor::setMode(SystemdInhibitorParams::Mode mode) { + this->mMode = mode; + this->update(); +} + +SystemdInhibitor::ActiveSystemdInhibitor::ActiveSystemdInhibitor(SystemdInhibitorParams::What what_, QString who_, QString why_, SystemdInhibitorParams::Mode mode_): what(what_), who(who_), why(why_), mode(mode_) { + DBusLogindManager logindManager( + "org.freedesktop.login1", + "/org/freedesktop/login1", + QDBusConnection::systemBus() + ); + QDBusReply fd_ = logindManager.Inhibit(QString::fromStdString(SystemdInhibitorParams::toString(what)), who, why, QString::fromStdString(SystemdInhibitorParams::toString(mode))); + if (fd_.error().isValid()) + throw fd_.error(); + this->fd = ::dup(fd_.value().fileDescriptor()); +} +SystemdInhibitor::ActiveSystemdInhibitor::~ActiveSystemdInhibitor() { + if (this->fd != -1) + ::close(this->fd); +} + +void SystemdInhibitor::release() { + if (!this->activeInhibitor) + return; + + this->activeInhibitor.reset(); + emit this->enabledChanged(); +} + +void SystemdInhibitor::update() { + if (!this->mWhat || this->mWho.isEmpty() || this->mWhy.isEmpty() || !this->mMode || !this->mEnabled) + if (this->activeInhibitor) + this->release(); + else + return; + + if (this->activeInhibitor && this->mWhat == this->activeInhibitor->what && this->mWho == this->activeInhibitor->who && this->mWhy == this->activeInhibitor->why && this->mMode == this->activeInhibitor->mode) + return; + + std::unique_ptr otherInhibitor; + try { + otherInhibitor.reset(new SystemdInhibitor::ActiveSystemdInhibitor(this->mWhat, this->mWho, this->mWhy, this->mMode)); + } catch (const QDBusError& err) { + qCritical().noquote() + << err.name().toStdString() << " " << err.message().toStdString(); + return; + } + this->activeInhibitor.swap(otherInhibitor); + + if (!otherInhibitor && this->activeInhibitor) + emit this->enabledChanged(); + if (otherInhibitor && otherInhibitor->what != this->activeInhibitor->what) + emit this->whatChanged(); + if (otherInhibitor && otherInhibitor->who != this->activeInhibitor->who) + emit this->whoChanged(); + if (otherInhibitor && otherInhibitor->why != this->activeInhibitor->why) + emit this->whyChanged(); + if (otherInhibitor && otherInhibitor->mode != this->activeInhibitor->mode) + emit this->modeChanged(); +} diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/Systemd.hpp b/accounts/gkleen@skadhi/shell/quickshell-plugins/Systemd.hpp new file mode 100644 index 00000000..98f73d8f --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/Systemd.hpp @@ -0,0 +1,193 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include "dbus_systemd_manager.h" +#include "dbus_logind_manager.h" +#include "dbus_logind_session.h" +#include "dbus_systemd_unit.h" +#include "dbus_systemd_service.h" +#include "dbus_properties.h" + +#include "core/model.hpp" + +class SystemdUnit : public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("SystemdUnits cannot be created directly"); + +public: + explicit SystemdUnit(QObject* parent, const QDBusObjectPath& path, const QString& id); + + Q_PROPERTY(QDBusObjectPath path READ path CONSTANT); + Q_PROPERTY(QString id READ id CONSTANT); + Q_PROPERTY(QList runtimeDirectory READ runtimeDirectory CONSTANT); + Q_PROPERTY(QString activeState READ activeState NOTIFY activeStateChanged); + + const QDBusObjectPath& path() const; + const QString& id() const; + QList runtimeDirectory(); + QString activeState(); + +signals: + void activeStateChanged(); + +private slots: + void onUnitPropertiesChanged(const QString& interface_name, const QVariantMap& changed_properties, const QStringList& invalidated_properties); + +private: + std::unique_ptr systemdUnit(); + std::unique_ptr systemdService(); + + DBusProperties* mUnitProperties; + QDBusObjectPath mPath; + QString mId; + std::optional mActiveState; + std::optional> mRuntimeDirectory; +}; + +class Systemd : public QObject { + Q_OBJECT; + QML_SINGLETON; + QML_ELEMENT; + +public: + explicit Systemd(QObject* parent = nullptr); + + Q_PROPERTY(bool idleHint READ idleHint WRITE setIdleHint NOTIFY idleHintChanged) + Q_PROPERTY(bool lockedHint READ lockedHint WRITE setLockedHint NOTIFY lockedHintChanged) + Q_PROPERTY(UntypedObjectModel* userUnits READ userUnits CONSTANT) + + Q_INVOKABLE void stopUserUnit(const QString& unit, const QString& mode); + Q_INVOKABLE void setBrightness(const QString& subsystem, const QString& name, quint32 brightness); + Q_INVOKABLE void setIdleHint(bool idle); + Q_INVOKABLE void setLockedHint(bool locked); + Q_INVOKABLE void lockSession(); + Q_INVOKABLE void suspend(); + Q_INVOKABLE void hibernate(); + + bool idleHint(); + bool lockedHint(); + ObjectModel* userUnits(); + +signals: + void shutdown(bool before); + void sleep(bool before); + void lock(); + void unlock(); + void idleHintChanged(); + void lockedHintChanged(); + +private slots: + void onLogindSessionPropertiesChanged(const QString& interface_name, const QVariantMap& changed_properties, const QStringList& invalidated_properties); + void onUserUnitNew(const QString& id, const QDBusObjectPath& objectPath); + void onUserUnitRemoved(const QString& id, const QDBusObjectPath& objectPath); + +private: + DBusSystemdManager* systemdManager; + DBusLogindManager* logindManager; + DBusLogindSession* logindSession; + DBusProperties* logindSessionProperties; + + ObjectModel mUserUnits{this}; +}; + +class SystemdInhibitorParams : public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum WhatItem : uint8_t { + Shutdown = 0b1, + Sleep = 0b10, + Idle = 0b100, + HandlePowerKey = 0b1000, + HandleSuspendKey = 0b10000, + HandleHibernateKey = 0b100000, + HandleLidSwitch = 0b1000000, + }; + Q_ENUM(WhatItem); + Q_DECLARE_FLAGS(What, WhatItem); + + enum Mode : uint8_t { + Block = 1, + BlockWeak = 2, + Delay = 3, + }; + Q_ENUM(Mode); + + Q_INVOKABLE static std::string toString(WhatItem what); + Q_INVOKABLE static std::string toString(What what); + Q_INVOKABLE static std::string toString(Mode mode); + + static constexpr WhatItem allWhatItems[] = { Shutdown, Sleep, Idle, HandlePowerKey, HandleSuspendKey, HandleHibernateKey, HandleLidSwitch }; +}; +Q_DECLARE_OPERATORS_FOR_FLAGS(SystemdInhibitorParams::What) + +class SystemdInhibitor : public QObject { + Q_OBJECT; + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); + Q_PROPERTY(SystemdInhibitorParams::What what READ what WRITE setWhat NOTIFY whatChanged); + Q_PROPERTY(QString who READ who WRITE setWho NOTIFY whoChanged); + Q_PROPERTY(QString why READ why WRITE setWhy NOTIFY whyChanged); + Q_PROPERTY(SystemdInhibitorParams::Mode mode READ mode WRITE setMode NOTIFY modeChanged); + QML_ELEMENT; + +public: + explicit SystemdInhibitor(QObject* parent = nullptr): QObject(parent) {} + + bool enabled() const; + void setEnabled(bool enabled); + + SystemdInhibitorParams::What what() const; + void setWhat(SystemdInhibitorParams::What what); + + QString who() const; + void setWho(QString who); + + QString why() const; + void setWhy(QString why); + + SystemdInhibitorParams::Mode mode() const; + void setMode(SystemdInhibitorParams::Mode mode); + + Q_INVOKABLE void release(); + +signals: + void enabledChanged(); + void whatChanged(); + void whoChanged(); + void whyChanged(); + void modeChanged(); + +private: + class ActiveSystemdInhibitor { + public: + uint32_t fd = -1; + SystemdInhibitorParams::What what; + QString who; + QString why; + SystemdInhibitorParams::Mode mode; + + ActiveSystemdInhibitor(SystemdInhibitorParams::What what_, QString who_, QString why_, SystemdInhibitorParams::Mode mode_); + ~ActiveSystemdInhibitor(); + }; + + void update(); + + bool mEnabled = true; + std::unique_ptr activeInhibitor; + SystemdInhibitorParams::What mWhat = static_cast(0); + QString mWho; + QString mWhy; + SystemdInhibitorParams::Mode mMode = static_cast(0); +}; + diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/customplugin.h b/accounts/gkleen@skadhi/shell/quickshell-plugins/customplugin.h new file mode 100644 index 00000000..e66ba9e3 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/customplugin.h @@ -0,0 +1,7 @@ +#include + +class CustomPlugin : public QQmlEngineExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid) +}; diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/default.nix b/accounts/gkleen@skadhi/shell/quickshell-plugins/default.nix new file mode 100644 index 00000000..dfe93bca --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/default.nix @@ -0,0 +1,32 @@ +{ lib +, stdenv +, cmake +, qt6 +, fmt +, keepassxc +, systemd +, quickshell +}: + +stdenv.mkDerivation rec { + name = "quickshell-custom"; + + src = ./.; + + prePatch = '' + cp ${keepassxc.src}/src/gui/org.keepassxc.KeePassXC.MainWindow.xml . + ''; + + nativeBuildInputs = [ cmake qt6.wrapQtAppsHook ]; + buildInputs = [ + qt6.qtbase + qt6.qtdeclarative + ]; + + cmakeFlags = [ + (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix) + (lib.cmakeOptionType "string" "QUICKSHELL_SRC" (toString quickshell.src)) + ]; + + LC_ALL = "C.UTF-8"; +} diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.DBus.Properties.xml b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.DBus.Properties.xml new file mode 100644 index 00000000..7588e7a5 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.DBus.Properties.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.login1.Manager.xml b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.login1.Manager.xml new file mode 100644 index 00000000..120a06d9 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.login1.Manager.xml @@ -0,0 +1,445 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.login1.Session.xml b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.login1.Session.xml new file mode 100644 index 00000000..7d6fc8ee --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.login1.Session.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Manager.xml b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Manager.xml new file mode 100644 index 00000000..1dadbc55 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Manager.xml @@ -0,0 +1,821 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Service.xml b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Service.xml new file mode 100644 index 00000000..316dd080 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Service.xml @@ -0,0 +1,1100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Unit.xml b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Unit.xml new file mode 100644 index 00000000..a554f513 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell-plugins/org.freedesktop.systemd1.Unit.xml @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/accounts/gkleen@skadhi/shell/quickshell/ActiveWindowDisplay.qml b/accounts/gkleen@skadhi/shell/quickshell/ActiveWindowDisplay.qml new file mode 100644 index 00000000..ac1cbf3a --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/ActiveWindowDisplay.qml @@ -0,0 +1,172 @@ +import QtQuick +import qs.Services +import Quickshell +import Quickshell.Widgets + +Item { + id: activeWindowDisplay + + required property int maxWidth + required property var screen + + property var activeWindow: { + let currWindowId = Array.from(NiriService.workspaces).find(ws => { + return ws.output === screen?.name && ws.is_active; + })?.active_window_id; + + return currWindowId ? Array.from(NiriService.windows).find(win => win.id == currWindowId) : null; + } + property var windowEntry: activeWindow ? DesktopEntries.heuristicLookup(activeWindow.app_id) : null + + anchors.verticalCenter: parent.verticalCenter + width: activeWindowDisplayContent.width + height: parent.height + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + + Item { + anchors.fill: parent + + Row { + id: activeWindowDisplayContent + + width: childrenRect.width + height: parent.height + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + IconImage { + id: activeWindowIcon + + implicitSize: 14 + + anchors.verticalCenter: parent.verticalCenter + + source: { + let icon = activeWindowDisplay.windowEntry?.icon + if (typeof icon === 'string' || icon instanceof String) { + if (icon.includes("?path=")) { + const split = icon.split("?path=") + if (split.length !== 2) + return icon + const name = split[0] + const path = split[1] + const fileName = name.substring( + name.lastIndexOf("/") + 1) + return `file://${path}/${fileName}` + } else + icon = Quickshell.iconPath(icon); + return icon + } + return "" + } + asynchronous: true + smooth: true + mipmap: true + } + + Text { + id: windowTitle + + width: Math.min(implicitWidth, activeWindowDisplay.maxWidth - activeWindowIcon.width - activeWindowDisplayContent.spacing) + + property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" } + + elide: Text.ElideRight + maximumLineCount: 1 + color: "white" + anchors.verticalCenter: parent.verticalCenter + text: { + if (!activeWindowDisplay.activeWindow) + return ""; + + var title = activeWindowDisplay.activeWindow.title; + var appName = activeWindowDisplay.windowEntry?.name; + if (appAliases[appName]) + appName = appAliases[appName]; + if (appName && title.endsWith(appName)) { + const oldTitle = title; + title = title.substring(0, title.length - appName.length); + title = title.replace(/\s*(—|-)\s*$/, ""); + if (!title) + title = oldTitle; + } + return title; + } + } + } + } + } + + Loader { + id: tooltipLoader + + active: false + + Connections { + target: widgetMouseArea + function onContainsMouseChanged() { + if (widgetMouseArea.containsMouse) + tooltipLoader.active = true; + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: tooltip.visible ? 100 : 500 + onTriggered: { + tooltip.visible = tooltip.nextVisible; + if (!tooltip.visible) + tooltipLoader.active = false; + } + } + + implicitWidth: widgetTooltipText.contentWidth + 16 + implicitHeight: widgetTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: widgetTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Mono" + color: "white" + + text: JSON.stringify(Object.assign({}, activeWindowDisplay.activeWindow), null, 2) + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Bar.qml b/accounts/gkleen@skadhi/shell/quickshell/Bar.qml new file mode 100644 index 00000000..33909884 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Bar.qml @@ -0,0 +1,142 @@ +import Quickshell +import Quickshell.Wayland +import QtQuick +import qs.Services + +PanelWindow { + id: bar + + WlrLayershell.namespace: "bar" + + property bool haveMaximizedWindow: { + let currWindowId = Array.from(NiriService.workspaces).find(ws => { + return ws.output === bar.screen.name && ws.is_active; + })?.active_window_id; + let activeWindowTileSize = Array.from(NiriService.windows).find(win => win.id == currWindowId)?.layout?.tile_size; + + if (!activeWindowTileSize) + return false; + return activeWindowTileSize[0] >= bar.screen.width && activeWindowTileSize[1] >= bar.screen.height - bar.height; + } + + anchors { + top: true + left: true + right: true + } + margins { + left: bar.haveMaximizedWindow ? 0 : 26 + 8 + right: bar.haveMaximizedWindow ? 0 : 26 + 8 + } + + implicitHeight: 21 + color: "transparent" + + property bool haveScreenshare: Array.from(NiriService.casts).some(cast => cast.target.Output?.name == bar.screen.name) + + Rectangle { + color: { + if (bar.haveScreenshare) + return bar.haveMaximizedWindow ? Qt.rgba(0.2, 0, 0, 1) : Qt.rgba(0.2, 0, 0, 0.75); + return bar.haveMaximizedWindow ? "black" : Qt.rgba(0, 0, 0, 0.75); + } + anchors.fill: parent + // bottomLeftRadius: 8 + // bottomRightRadius: 8 + } + + Row { + id: left + + height: parent.height + width: childrenRect.width + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + WorkspaceSwitcher { + screen: bar.screen + } + } + + Row { + id: center + + height: parent.height + width: childrenRect.width + anchors.centerIn: parent + spacing: 5 + + ActiveWindowDisplay { + screen: bar.screen + maxWidth: bar.width - 2*Math.max(left.width, right.width) - 2*8 + } + } + + Row { + id: right + + height: parent.height + width: childrenRect.width + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + WorktimeWidget {} + + KeyboardLayout {} + + Item { + visible: privacy.visible || ytDlp.visible + height: parent.height + width: 8 - 4 + } + + PrivacyWidget { + id: privacy + } + + Item { + visible: privacy.visible && ytDlp.visible + height: parent.height + width: 8 - 4 + } + + YtDlpWidget { + id: ytDlp + } + + Item { + visible: privacy.visible || ytDlp.visible + height: parent.height + width: 8 - 4 + } + + SystemTray {} + + PipewireWidget {} + + BrightnessWidget {} + + BatteryWidget {} + + LockscreenInhibitorWidget {} + + WaylandInhibitorWidget { + window: bar + } + + LidSwitchInhibitorWidget {} + + NotificationInhibitorWidget {} + + Item { + height: parent.height + width: 8 - 4 + } + + Clock {} + } +} \ No newline at end of file diff --git a/accounts/gkleen@skadhi/shell/quickshell/BatteryWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/BatteryWidget.qml new file mode 100644 index 00000000..f465fa20 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/BatteryWidget.qml @@ -0,0 +1,139 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.UPower + +Item { + id: root + + height: parent.height + width: batteryIcon.width + 8 + anchors.verticalCenter: parent.verticalCenter + + property var batteryDevice: Array.from(UPower.devices.values).find(dev => dev.isLaptopBattery) + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: batteryIcon + + implicitSize: 14 + anchors.centerIn: parent + + icon: { + if (!root.batteryDevice?.ready) + return "battery-unknown"; + + if (root.batteryDevice.state == UPowerDeviceState.FullyCharged) + return "power-plug-battery"; + + const perdec = 10 * Math.max(1, Math.ceil(root.batteryDevice.percentage * 10)); + if (root.batteryDevice.state == UPowerDeviceState.Charging) + return `battery-charging-${perdec}`; + if (perdec == 100) + return "battery"; + return `battery-${perdec}`; + } + color: { + if (!root.batteryDevice?.ready) + return "#555"; + + if (root.batteryDevice.state != UPowerDeviceState.FullyCharged && root.batteryDevice.state != UPowerDeviceState.Charging && root.batteryDevice.timeToEmpty < 20 * 60) + return "#f2201f"; + if (root.batteryDevice.state != UPowerDeviceState.FullyCharged && root.batteryDevice.state != UPowerDeviceState.Charging && root.batteryDevice.timeToEmpty < 40 * 60) + return "#f28a21"; + if (root.batteryDevice.state != UPowerDeviceState.FullyCharged) + return "#fff"; + return "#555"; + } + } + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: widgetTooltipText.contentWidth + 16 + implicitHeight: widgetTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: widgetTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: { + if (!root.batteryDevice?.ready) + return "Not ready"; + + const stateStr = UPowerDeviceState.toString(root.batteryDevice.state); + var outStr = stateStr; + if (root.batteryDevice.state !== UPowerDeviceState.FullyCharged) + outStr += ` ${Math.round(root.batteryDevice.percentage * 100)}%`; + + function formatTime(t) { + var res = ""; + for (const unit of [{ "s": "h", "v": 3600 }, { "s": "m", "v": 60 }, { "s": "s", "v": 1 }]) { + if (t < unit.v) + continue; + res += Math.floor(t / unit.v) + unit.s; + t %= unit.v; + } + return res; + } + if (root.batteryDevice.timeToEmpty != 0) { + const tStr = formatTime(Math.floor(root.batteryDevice.timeToEmpty / 60) * 60); + if (tStr) + outStr += " " + tStr; + } else if (root.batteryDevice.timeToFull != 0) { + const tStr = formatTime(Math.ceil(root.batteryDevice.timeToFull / 60) * 60); + if (tStr) + outStr += " " + tStr; + } + + return outStr; + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/BrightnessOSD.qml b/accounts/gkleen@skadhi/shell/quickshell/BrightnessOSD.qml new file mode 100644 index 00000000..a432179e --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/BrightnessOSD.qml @@ -0,0 +1,117 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs.Services + +Scope { + id: root + + property bool show: false + property bool inhibited: true + + Connections { + target: Brightness + + function onCurrBrightnessChanged() { + root.show = true; + hideTimer.restart(); + } + } + + onShowChanged: { + if (show) + hideTimer.restart(); + } + + Timer { + id: hideTimer + interval: 1000 + onTriggered: root.show = false + } + + Timer { + id: startInhibit + interval: 100 + running: true + onTriggered: { + root.show = false; + root.inhibited = false; + } + } + + LazyLoader { + active: root.show && !root.inhibited + + Variants { + model: Quickshell.screens + + delegate: Scope { + id: screenScope + + required property var modelData + + PanelWindow { + id: window + + screen: screenScope.modelData + + anchors.top: true + margins.top: screen.height / 2 - 50 + 3.5 + exclusiveZone: 0 + exclusionMode: ExclusionMode.Ignore + + implicitWidth: 400 + implicitHeight: 50 + + mask: Region {} + + color: "transparent" + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.75) + } + + RowLayout { + id: layout + + anchors.centerIn: parent + + height: 50 - 8*2 + width: 400 - 8*2 + + MaterialDesignIcon { + id: volumeIcon + + implicitWidth: parent.height + implicitHeight: parent.height + + icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}` + } + + Rectangle { + Layout.fillWidth: true + + implicitHeight: 10 + + color: "#50ffffff" + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + color: "white" + + implicitWidth: parent.width * Brightness.currBrightness + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/BrightnessWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/BrightnessWidget.qml new file mode 100644 index 00000000..3bb5a80e --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/BrightnessWidget.qml @@ -0,0 +1,84 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Services + +Item { + height: parent.height + width: brightnessIcon.width + 8 + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + + property real sensitivity: (1 / 50) / 120 + onWheel: event => Brightness.currBrightness += event.angleDelta.y * sensitivity + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: brightnessIcon + + implicitSize: 14 + anchors.centerIn: parent + + icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}` + color: "#555" + } + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: widgetTooltipText.contentWidth + 16 + implicitHeight: widgetTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: widgetTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: `${Math.round(Brightness.currBrightness * 100)}%` + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Clock.qml b/accounts/gkleen@skadhi/shell/quickshell/Clock.qml new file mode 100644 index 00000000..58a80b8b --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Clock.qml @@ -0,0 +1,295 @@ +import QtQml +import QtQuick +import Quickshell +import Custom as Custom +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Widgets + +Item { + id: clockItem + + property bool calendarPopup: true + + width: clock.contentWidth + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: clockMouseArea + + anchors.fill: parent + hoverEnabled: true + enabled: clockItem.calendarPopup + + Item { + anchors.fill: parent + + Text { + id: clock + color: "white" + + anchors.verticalCenter: parent.verticalCenter + + Custom.Chrono { + id: chrono + + onDateChanged: clock.text = format("W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}") + } + + font.pointSize: 10 + font.family: "Fira Sans" + font.features: { "tnum": 1 } + } + } + } + + Loader { + id: tooltipLoader + + active: false + + Connections { + target: clockMouseArea + function onContainsMouseChanged() { + if (clockMouseArea.containsMouse) + tooltipLoader.active = true; + } + } + + sourceComponent: PopupWindow { + id: tooltip + + property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse + + anchor { + item: clockMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: { + tooltip.visible = tooltip.nextVisible; + if (!tooltip.visible) + tooltipLoader.active = false; + } + } + + implicitWidth: tooltipLayout.childrenRect.width + 16 + implicitHeight: tooltipLayout.childrenRect.height + 16 + color: "black" + + onVisibleChanged: { + yearCalendar.year = chrono.date.getFullYear(); + yearCalendar.angleRem = 0; + } + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + onWheel: event => yearCalendar.scrollYear(event) + + anchors.fill: parent + + Item { + id: clockTooltipContent + + anchors.fill: parent + + ColumnLayout { + id: tooltipLayout + + anchors { + left: parent.left + top: parent.top + leftMargin: 8 + topMargin: 8 + } + + Text { + id: yearLabel + + horizontalAlignment: Text.AlignHCenter + + font.pointSize: 14 + font.family: "Fira Sans" + font.features: { "tnum": 1 } + color: "white" + + text: yearCalendar.year + + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + GridLayout { + property int year: chrono.date.getFullYear() + + id: yearCalendar + + columns: 3 + columnSpacing: 16 + rowSpacing: 16 + + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: false + + property real angleRem: 0 + property real sensitivity: 1 / 120 + + function scrollYear(event) { + angleRem += event.angleDelta.y; + const d = Math.round(angleRem * sensitivity); + yearCalendar.year -= d; + angleRem -= d / sensitivity; + } + + Connections { + target: clockMouseArea + function onWheel(event) { yearCalendar.scrollYear(event); } + } + + Repeater { + model: 12 + + GridLayout { + columns: 2 + + required property int index + property int month: index + + id: monthCalendar + + Layout.alignment: Qt.AlignTop | Qt.AlignRight + Layout.fillWidth: false + + Text { + Layout.column: 1 + Layout.fillWidth: true + + horizontalAlignment: Text.AlignHCenter + + font.pointSize: 10 + font.family: "Fira Sans" + + text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM") + + color: "#ffead3" + } + + DayOfWeekRow { + locale: grid.locale + + Layout.row: 1 + Layout.column: 1 + Layout.fillWidth: true + + delegate: WrapperItem { + required property string shortName + + width: dowLabel.contentWidth + 6 + + Text { + id: dowLabel + + anchors.fill: parent + + font.pointSize: 10 + font.family: "Fira Sans" + + text: parent.shortName + color: "#ffcc66" + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + + WeekNumberColumn { + month: grid.month + year: grid.year + locale: grid.locale + + Layout.fillHeight: true + + delegate: Text { + required property int weekNumber + + opacity: { + const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7); + const dayOfWeek = simple.getDay(); + const isoWeekStart = simple; + + isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); + if (dayOfWeek > 4) { + isoWeekStart.setDate(isoWeekStart.getDate() + 7); + } + + for (let i = 0; i < 7; i++) { + const dayInWeek = new Date(isoWeekStart); + dayInWeek.setDate(dayInWeek.getDate() + i); + if (dayInWeek.getMonth() == monthCalendar.month) + return 1; + } + + return 0; + } + + font.pointSize: 10 + font.family: "Fira Sans" + font.features: { "tnum": 1 } + + text: weekNumber + color: "#99ffdd" + + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + } + } + + MonthGrid { + id: grid + + year: yearCalendar.year + month: monthCalendar.month + locale: Qt.locale("en_DK") + + Layout.fillWidth: true + Layout.fillHeight: true + + delegate: Text { + required property var model + + opacity: model.month === monthCalendar.month ? 1 : 0 + + font.pointSize: 10 + font.family: "Fira Sans" + font.features: { "tnum": 1 } + + property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day + + text: model.day + color: today ? "#ff6699" : "white" + + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + } + } + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/KeyboardLayout.qml b/accounts/gkleen@skadhi/shell/quickshell/KeyboardLayout.qml new file mode 100644 index 00000000..46302e54 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/KeyboardLayout.qml @@ -0,0 +1,112 @@ +import Quickshell +import QtQuick +import qs.Services +import Quickshell.Widgets + +Item { + width: kbdLabel.contentWidth + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: kbdMouseArea + + anchors.fill: parent + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: true + onClicked: { + NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": "Next" } } }, _ => {}) + } + onWheel: event => { + NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": event.angleDelta > 0 ? "Next" : "Prev" } } }, _ => {}) + } + + Rectangle { + id: kbdWidget + + property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" } + + anchors.fill: parent + color: { + if (kbdMouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Text { + id: kbdLabel + + font.pointSize: 10 + font.family: "Fira Sans" + color: { + if (NiriService.keyboardLayouts?.current_idx === 0) + return "#555"; + return "white"; + } + anchors.centerIn: parent + + text: { + const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; + if (!currentLayout) + return ""; + return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout; + } + } + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: kbdMouseArea.containsMouse || tooltipMouseArea.containsMouse + + anchor { + item: kbdMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: kbdTooltipText.contentWidth + 16 + implicitHeight: kbdTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: kbdTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: { + const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; + return currentLayout || ""; + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/LidSwitchInhibitorWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/LidSwitchInhibitorWidget.qml new file mode 100644 index 00000000..8410dcda --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/LidSwitchInhibitorWidget.qml @@ -0,0 +1,47 @@ +import Quickshell +import QtQuick +import Quickshell.Widgets +import qs.Services + +Item { + id: root + + width: icon.width + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: InhibitorState.lidSwitchInhibited = !InhibitorState.lidSwitchInhibited + + Rectangle { + anchors.fill: parent + color: { + if (widgetMouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: InhibitorState.lidSwitchInhibited ? "laptop-off" : "laptop" + color: InhibitorState.lidSwitchInhibited ? "#f28a21" : "#555" + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/LockSurface.qml b/accounts/gkleen@skadhi/shell/quickshell/LockSurface.qml new file mode 100644 index 00000000..f4f8f0cd --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/LockSurface.qml @@ -0,0 +1,227 @@ +import Quickshell.Widgets +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Fusion +import qs.Services +import QtQml + +Item { + id: lockSurface + + property var screen + property list messages: [] + property bool responseRequired: false + property bool responseVisible: false + property string currentText: "" + property bool authRunning: false + + signal response(string responseText) + + anchors.fill: parent + + Item { + id: background + + anchors.fill: parent + + property Img current: one + property string source: selector.selected + + WallpaperSelector { + id: selector + seed: lockSurface.screen?.name || "" + } + + onSourceChanged: { + if (!source) + current = null; + else if (current === one) + two.update() + else + one.update() + } + + Img { id: one } + Img { id: two } + + component Img: Item { + id: img + + property string source + + function update() { + source = background.source || "" + } + + anchors.fill: parent + + Image { + id: imageSource + + source: img.source + sourceSize: Qt.size(parent.width, parent.height) + fillMode: Image.PreserveAspectCrop + smooth: true + visible: false + asynchronous: true + cache: false + + onStatusChanged: { + if (status === Image.Ready) { + background.current = img + } + } + } + + MultiEffect { + id: imageEffect + + source: imageSource + anchors.fill: parent + blurEnabled: true + blur: 1 + blurMax: 64 + blurMultiplier: 2 + + opacity: 0 + + states: State { + name: "visible" + when: background.current === img + + PropertyChanges { + imageEffect.opacity: 1 + } + StateChangeScript { + name: "unloadOther" + script: { + if (img === one) + two.source = "" + if (img === two) + one.source = "" + } + } + } + + transitions: Transition { + SequentialAnimation { + NumberAnimation { + target: imageEffect + properties: "opacity" + duration: { + if (img === one && two.source == "" || img === two && one.source == "") + return 0; + return 5000; + } + easing.type: Easing.OutCubic + } + ScriptAction { + scriptName: "unloadOther" + } + } + } + } + } + } + + Item { + anchors { + top: lockSurface.top + left: lockSurface.left + right: lockSurface.right + } + + implicitWidth: lockSurface.width + implicitHeight: 21 + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.75) + } + + Clock { + anchors.centerIn: parent + calendarPopup: false + } + } + + WrapperRectangle { + id: unlockUi + + Keys.onPressed: event => { + if (!lockSurface.authRunning) { + event.accepted = true; + lockSurface.authRunning = true; + } + } + focus: !passwordBox.visible + + visible: lockSurface.authRunning + + color: Qt.rgba(0, 0, 0, 0.75) + margin: 8 + + anchors.centerIn: parent + + ColumnLayout { + spacing: 4 + + BusyIndicator { + visible: running + running: !Array.from(lockSurface.messages).length && !lockSurface.responseRequired + } + + Repeater { + model: lockSurface.messages + + Text { + required property var modelData + + font.pointSize: 10 + font.family: "Fira Sans" + color: modelData.error ? "#f28a21" : "#ffffff" + + text: String(modelData.text).trim() + + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + } + + TextField { + id: passwordBox + + visible: lockSurface.responseRequired + echoMode: lockSurface.responseVisible ? TextInput.Normal : TextInput.Password + inputMethodHints: Qt.ImhSensitiveData + + onTextChanged: lockSurface.currentText = passwordBox.text + onAccepted: { + passwordBox.readOnly = true; + lockSurface.response(lockSurface.currentText); + } + + Connections { + target: lockSurface + function onCurrentTextChanged() { + passwordBox.text = lockSurface.currentText + } + } + Connections { + target: lockSurface + function onResponseRequiredChanged() { + if (lockSurface.responseRequired) + passwordBox.readOnly = false; + passwordBox.focus = true; + passwordBox.selectAll(); + } + } + + Layout.topMargin: 4 + Layout.fillWidth: true + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Lockscreen.qml b/accounts/gkleen@skadhi/shell/quickshell/Lockscreen.qml new file mode 100644 index 00000000..0b4a68b2 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Lockscreen.qml @@ -0,0 +1,128 @@ +import Quickshell +import Quickshell.Wayland +import Quickshell.Io +import Quickshell.Services.Pam +import Quickshell.Services.Mpris +import Custom as Custom +import qs.Services +import QtQml + +Scope { + id: lockscreen + + property string currentText: "" + + PamContext { + id: pam + + property list messages: [] + + config: "quickshell" + onCompleted: result => { + if (result === PamResult.Success) { + lock.locked = false; + } + } + onPamMessage: { + messages = Array.from(messages).concat([{ "text": pam.message, "error": pam.messageIsError }]) + } + onActiveChanged: { + messages = []; + } + } + + Connections { + target: Custom.Systemd + function onSleep(before: bool) { + console.log(`received prepare for sleep ${before}`); + if (before) { + lock.locked = true; + if (pam.active) + pam.abort(); + } + } + function onLock() { lock.locked = true; } + function onUnlock() { lock.locked = false; } + } + + IdleMonitor { + id: idleMonitor + enabled: !lock.secure && !InhibitorState.lockscreenInhibited + timeout: 600 + respectInhibitors: true + + onIsIdleChanged: { + if (idleMonitor.isIdle) + lock.locked = true; + } + } + + Custom.SystemdInhibitor { + enabled: !lock.secure + + what: Custom.SystemdInhibitorParams.Sleep + who: "quickshell" + why: "Lock session" + mode: Custom.SystemdInhibitorParams.Delay + } + + Binding { + target: NotificationManager + property: "lockscreenActive" + value: lock.locked + } + + WlSessionLock { + id: lock + + onLockStateChanged: { + if (!locked && pam.active) + pam.abort(); + + if (locked) { + NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {}); + Custom.KeePassXC.lockAllDatabases(); + Array.from(MprisProxy.players).forEach(player => { + if (player.canPause && player.isPlaying) + player.pause(); + }); + // Custom.Systemd.stopUserUnit("gpg-agent.service", "replace"); + GpgAgent.reloadAgent(); + } + } + Component.onCompleted: { (_ => {})(MprisProxy.players); } + + onSecureStateChanged: Custom.Systemd.lockedHint = lock.secure + + WlSessionLockSurface { + id: lockSurface + + color: "black" + + LockSurface { + id: surfaceContent + + onResponse: responseText => pam.respond(responseText) + onAuthRunningChanged: { + if (authRunning) + pam.start(); + } + Connections { + target: pam + function onMessagesChanged() { surfaceContent.messages = pam.messages; } + function onResponseRequiredChanged() { surfaceContent.responseRequired = pam.responseRequired; } + function onActiveChanged() { surfaceContent.authRunning = pam.active; } + } + onCurrentTextChanged: lockscreen.currentText = currentText + Connections { + target: lockscreen + function onCurrentTextChanged() { surfaceContent.currentText = lockscreen.currentText; } + } + Connections { + target: lockSurface + function onScreenChanged() { surfaceContent.screen = lockSurface.screen; } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/LockscreenInhibitorWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/LockscreenInhibitorWidget.qml new file mode 100644 index 00000000..1cbf3f16 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/LockscreenInhibitorWidget.qml @@ -0,0 +1,48 @@ +import Quickshell +import QtQuick +import Quickshell.Widgets +import qs.Services + +Item { + id: root + + width: icon.width + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + // enabled: InhibitorState.lockscreenInhibited || !InhibitorState.waylandIdleInhibited + hoverEnabled: true + cursorShape: widgetMouseArea.enabled ? Qt.PointingHandCursor : undefined + + onClicked: InhibitorState.lockscreenInhibited = !InhibitorState.lockscreenInhibited + + Rectangle { + anchors.fill: parent + color: { + if (widgetMouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: InhibitorState.lockscreenInhibited || InhibitorState.waylandIdleInhibited ? "lock-off" : "lock" + color: InhibitorState.lockscreenInhibited ? "#f28a21" : "#555" + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/MaterialDesignIcon.qml b/accounts/gkleen@skadhi/shell/quickshell/MaterialDesignIcon.qml new file mode 100644 index 00000000..155a009e --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/MaterialDesignIcon.qml @@ -0,0 +1,35 @@ +import QtQuick +import QtQuick.Effects + +Item { + id: root + + required property string icon + property color color: "white" + + property real implicitSize: 0 + + readonly property real actualSize: Math.min(root.width, root.height) + + implicitWidth: root.implicitSize + implicitHeight: root.implicitSize + + Image { + id: sourceImage + source: "file://" + @mdi@ + "/svg/" + root.icon + ".svg" + anchors.fill: parent + fillMode: Image.PreserveAspectFit + + sourceSize.width: root.actualSize + sourceSize.height: root.actualSize + + layer.enabled: true + layer.effect: MultiEffect { + id: effect + + brightness: 1 + colorization: 1 + colorizationColor: root.color + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/NiriIdle.qml b/accounts/gkleen@skadhi/shell/quickshell/NiriIdle.qml new file mode 100644 index 00000000..beff205c --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/NiriIdle.qml @@ -0,0 +1,30 @@ +import QtQml +import Quickshell +import Quickshell.Wayland +import qs.Services +import Custom as Custom + +Scope { + IdleMonitor { + id: idleMonitor30 + timeout: 30 + + onIsIdleChanged: Custom.Systemd.setIdleHint(idleMonitor30.isIdle) + } + IdleMonitor { + id: idleMonitor540 + timeout: 540 + + onIsIdleChanged: { + if (idleMonitor540.isIdle) + NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {}); + } + } + Connections { + target: Custom.Systemd + function onSleep(before: bool) { + if (!before) + NiriService.sendCommand({ "Action": { "PowerOnMonitors": {} } }, _ => {}); + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/NotificationDisplay.qml b/accounts/gkleen@skadhi/shell/quickshell/NotificationDisplay.qml new file mode 100644 index 00000000..cc0e49b1 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/NotificationDisplay.qml @@ -0,0 +1,340 @@ +import QtQml +import QtQml.Models +import QtQuick +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import qs.Services +import QtQuick.Layouts +import Quickshell.Services.Notifications + +Scope { + id: root + + property var activeScreen: Array.from(Quickshell.screens).find(screen => screen.name === Array.from(NiriService.workspaces).find(ws => ws.is_focused)?.output) ?? null + + Instantiator { + id: notifsRepeater + + model: ScriptModel { + values: NotificationManager.groups + } + + delegate: PanelWindow { + id: notifWindow + + visible: NotificationManager.active + + screen: root.activeScreen + + WlrLayershell.namespace: "notifications" + + required property var modelData + required property var index + + property int activeIx: modelData.length - 1 + onModelDataChanged: { + notifWindow.activeIx = modelData.length - 1; + } + + property color textColor: { + if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Low) + return "#ff999999"; + return "white"; + } + property color backgroundColor: { + if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Critical) + return "#dd900000"; + return "black"; + } + + anchors { + right: true + top: true + } + + readonly property real spaceAbove: { + var res = 0; + for (let i = 0; i < notifWindow.index; i++) { + (_ => {})(notifsRepeater.objectAt(i).modelData); + res += notifsRepeater.objectAt(i).height + 8; + } + return res; + } + + margins { + right: 26 + 8 + top: 8 + spaceAbove + } + + color: "transparent" + + implicitHeight: Math.max(notifCount.visible ? notifCount.contentHeight : 0, notifSummary.contentHeight) + (notifBody.visible ? 8 + notifBody.contentHeight : 0) + (notifActions.visible ? 8 + notifActions.height : 0) + (notifTime.visible ? 8 + notifTime.contentHeight : 0) + 16 + implicitWidth: 400 + + WrapperMouseArea { + enabled: true + + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + for (const notif of notifWindow.modelData) + notif.dismiss(); + } + + property real angleRem: 0 + property real sensitivity: 1 / 120 + onWheel: event => { + angleRem += event.angleDelta.y; + const d = Math.round(angleRem * sensitivity); + angleRem -= d / sensitivity; + notifWindow.activeIx = ((notifWindow.modelData?.length ?? 1) + notifWindow.activeIx - d) % (notifWindow.modelData?.length ?? 1); + } + + Rectangle { + color: notifWindow.backgroundColor + anchors.fill: parent + border { + color: Qt.hsla(195/360, 1, 0.45, 1) + width: 2 + } + + GridLayout { + id: notifLayout + + width: 400 - 16 + anchors.fill: parent + anchors.margins: 8 + columnSpacing: 8 + rowSpacing: 8 + + columns: notifImage.visible ? 3 : 2 + rows: { + var res = 1; + if (notifBody.visible) + res += 1; + if (notifActions.visible) + res += 1; + if (notifTime.visible) + res += 1; + return res; + } + + Text { + id: notifCount + + visible: notifWindow.modelData?.length > 1 ?? false + text: `${notifWindow.activeIx + 1}/${notifWindow.modelData?.length ?? ""}` + + font.pointSize: 10 + font.family: "Fira Sans" + font.bold: true + font.features: { "tnum": 1 } + color: notifWindow.textColor + maximumLineCount: 1 + + Layout.fillWidth: false + Layout.row: 0 + Layout.column: notifImage.visible ? 1 : 0 + } + + Text { + id: notifSummary + + text: notifWindow.modelData?.[notifWindow.activeIx]?.summary ?? "" + + font.pointSize: 10 + font.family: "Fira Sans" + font.italic: true + color: notifWindow.textColor + maximumLineCount: 1 + elide: Text.ElideRight + + Layout.fillWidth: true + Layout.row: 0 + Layout.column: (notifCount.visible ? 1 : 0) + (notifImage.visible ? 1 : 0) + Layout.columnSpan: notifCount.visible ? 1 : 2 + } + + Image { + id: notifImage + + visible: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? false + + onStatusChanged: { + if (notifImage.status == Image.Error) + notifImage.visible = false; + } + + source: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? "" + fillMode: Image.PreserveAspectFit + asynchronous: true + smooth: true + mipmap: true + + Layout.maximumWidth: 50 + Layout.column: 0 + Layout.row: 0 + Layout.fillHeight: true + Layout.rowSpan: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) + } + + Text { + id: notifBody + + visible: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? false + text: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? "" + textFormat: Text.RichText + wrapMode: Text.Wrap + + font.pointSize: 10 + font.family: "Fira Sans" + color: notifWindow.textColor + + Layout.fillWidth: true + Layout.row: 1 + Layout.column: notifImage.visible ? 1 : 0 + Layout.columnSpan: notifCount.visible ? 2 : 1 + } + + Text { + id: notifTime + + Connections { + target: NotificationManager.clock + function onDateChanged() { + notifTime.text = NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime); + } + } + + visible: notifTime.text && notifTime.text !== "now" + text: NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime) + + font.pointSize: 8 + font.family: "Fira Sans" + font.italic: true + color: "#555" + maximumLineCount: 1 + horizontalAlignment: Text.AlignRight + + Layout.fillWidth: true + Layout.row: notifBody.visible ? 2 : 1 + Layout.column: notifImage.visible ? 1 : 0 + Layout.columnSpan: 2 + } + + RowLayout { + id: notifActions + + visible: notifWindow.modelData?.[notifWindow.activeIx]?.actions.length > 0 ?? false + + spacing: 8 + uniformCellSizes: true + + width: 400 - 16 + Layout.row: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) + Layout.column: 0 + Layout.columnSpan: 2 + (notifImage.visible ? 1 : 0) + + Repeater { + model: ScriptModel { + values: notifWindow.modelData?.[notifWindow.activeIx]?.actions + } + + delegate: WrapperMouseArea { + id: actionMouseArea + + required property var modelData + + height: actionLabelWrapper.implicitHeight + Layout.fillWidth: true + Layout.horizontalStretchFactor: 1 + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: actionMouseArea.modelData?.invoke() + + Rectangle { + anchors.fill: parent + + color: actionMouseArea.containsMouse ? "#20ffffff" : "transparent" + + border { + width: 2 + color: "#20ffffff" + } + + WrapperItem { + id: actionLabelWrapper + + margin: 8 + anchors.centerIn: parent + + RowLayout { + id: actionLabelLayout + + spacing: 8 + + IconImage { + id: actionIcon + + visible: notifWindow.modelData?.[notifWindow.activeIx]?.hasActionIcons + + onStatusChanged: { + if (actionIcon.status == Image.Error) + actionIcon.visible = false; + } + + implicitSize: 16 + source: { + if (!actionIcon.visible) + return ""; + + let icon = actionMouseArea.modelData?.identifier ?? "" + if (icon.includes("?path=")) { + const split = icon.split("?path=") + if (split.length !== 2) + return icon + const name = split[0] + const path = split[1] + const fileName = name.substring( + name.lastIndexOf("/") + 1) + return `file://${path}/${fileName}` + } + return icon + } + asynchronous: true + smooth: true + mipmap: true + + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + } + + Text { + id: actionLabel + + visible: actionMouseArea.modelData?.text ?? false + + text: actionMouseArea.modelData?.text ?? "" + + font.pointSize: 10 + font.family: "Fira Sans" + color: notifWindow.textColor + maximumLineCount: 1 + elide: Text.ElideRight + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/NotificationInhibitorWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/NotificationInhibitorWidget.qml new file mode 100644 index 00000000..80cbba19 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/NotificationInhibitorWidget.qml @@ -0,0 +1,266 @@ +import Quickshell +import QtQuick +import Quickshell.Widgets +import qs.Services +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes + +Item { + id: root + + width: icon.width + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: NotificationManager.displayInhibited = !NotificationManager.displayInhibited + + Rectangle { + anchors.fill: parent + color: { + if (widgetMouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: NotificationManager.active ? "message" : "message-off" + color: { + if (!NotificationManager.active && !NotificationManager.displayInhibited) + return "#f28a21"; + if (NotificationManager.displayInhibited) + return "white"; + return "#555"; + } + } + } + } + } + + Loader { + id: tooltipLoader + + active: false + + Connections { + target: widgetMouseArea + function onContainsMouseChanged() { + if (widgetMouseArea.containsMouse) + tooltipLoader.active = true; + } + } + + sourceComponent: PopupWindow { + id: tooltip + + property bool nextVisible: NotificationManager.active && NotificationManager.history.length > 0 && (widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse) + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: tooltip.visible ? 100 : 500 + onTriggered: { + tooltip.visible = tooltip.nextVisible; + if (!tooltip.visible) + tooltipLoader.active = false; + } + } + + implicitWidth: 400 + implicitHeight: Math.min(tooltip.screen.height * 0.66, Math.max(100, scroll.contentHeight + 16)) + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + WrapperItem { + margin: 8 + + ScrollView { + id: scroll + + contentWidth: availableWidth + // ScrollBar.vertical.policy: ScrollBar.AlwaysOn + + ColumnLayout { + id: historyLayout + anchors { + left: parent.left + right: parent.right + } + + spacing: 8 + + Repeater { + model: ScriptModel { + values: [...NotificationManager.history].reverse().map(o => o.notification) + } + + delegate: GridLayout { + id: notif + + Layout.fillWidth: true + Layout.preferredHeight: notifSummary.contentHeight + (notifBody.visible ? notifBody.contentHeight + 8 : 0) + (notifSep.visible ? notifSep.height + 8 : 0) + notifTime.contentHeight + 8 + + required property var modelData + required property int index + + columnSpacing: 8 + rowSpacing: 8 + + columns: notifImage.visible ? 2 : 1 + rows: { + var res = 2; + if (notifBody.visible) + res += 1; + if (notifSep.visible) + res += 1; + return res; + } + + Shape { + id: notifSep + + visible: notif.index != 0 + + height: 2 + width: 400 - 32 + + ShapePath { + strokeWidth: 2 + strokeColor: "#20ffffff" + startX: 0; startY: 0; + PathLine { x: 400 - 32; y: 0; } + } + + Layout.row: 0 + Layout.column: 0 + Layout.columnSpan: notifImage.visible ? 2 : 1 + Layout.alignment: Qt.AlignHCenter + } + + Text { + id: notifSummary + + text: notif.modelData?.summary ?? "" + + font.pointSize: 10 + font.family: "Fira Sans" + font.italic: true + color: "white" + maximumLineCount: 1 + elide: Text.ElideRight + + Layout.fillWidth: true + Layout.row: notifSep.visible ? 1 : 0 + Layout.column: notifImage.visible ? 1 : 0 + } + + Image { + id: notifImage + + visible: (notif.modelData?.image || notif.modelData?.appIcon) ?? false + + onStatusChanged: { + if (notifImage.status == Image.Error) + notifImage.visible = false; + } + + source: (notif.modelData?.image || notif.modelData?.appIcon) ?? "" + fillMode: Image.PreserveAspectFit + asynchronous: true + smooth: true + mipmap: true + + Layout.maximumWidth: 50 + Layout.column: 0 + Layout.row: notifSep.visible ? 1 : 0 + Layout.fillHeight: true + Layout.rowSpan: notifBody.visible ? 3 : 2 + } + + Text { + id: notifBody + + visible: notif.modelData?.body ?? false + text: notif.modelData?.body ?? "" + textFormat: Text.RichText + wrapMode: Text.Wrap + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + Layout.fillWidth: true + Layout.row: notifSep.visible ? 2 : 1 + Layout.column: notifImage.visible ? 1 : 0 + } + + Text { + id: notifTime + + Connections { + target: NotificationManager.clock + function onDateChanged() { + notifTime.text = NotificationManager.formatTime(notif.modelData?.receivedTime); + } + } + + text: NotificationManager.formatTime(notif.modelData?.receivedTime) + + font.pointSize: 8 + font.family: "Fira Sans" + font.italic: true + color: "#555" + maximumLineCount: 1 + horizontalAlignment: Text.AlignRight + + Layout.fillWidth: true + Layout.row: { + var res = 1; + if (notifSep.visible) + res += 1; + if (notifBody.visible) + res += 1; + return res; + } + Layout.column: notifImage.visible ? 1 : 0 + } + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/PipewireWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/PipewireWidget.qml new file mode 100644 index 00000000..9c6b65a4 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/PipewireWidget.qml @@ -0,0 +1,483 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Fusion +import Quickshell +import Quickshell.Services.Pipewire +import Quickshell.Widgets + +Item { + height: parent.height + width: volumeIcon.width + 8 + anchors.verticalCenter: parent.verticalCenter + + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (!Pipewire.defaultAudioSink) + return; + Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted; + } + + property real sensitivity: (1 / 40) / 120 + onWheel: event => { + if (!Pipewire.defaultAudioSink) + return; + Pipewire.defaultAudioSink.audio.volume += event.angleDelta.y * sensitivity; + } + + Rectangle { + id: volumeWidget + + anchors.fill: parent + color: { + if (widgetMouseArea.containsMouse) + return "#33808080"; + return "transparent"; + } + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: volumeIcon + + implicitSize: 14 + anchors.centerIn: parent + + icon: { + if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted) + return "volume-off"; + if (Pipewire.defaultAudioSink.audio.volume <= 0.33) + return "volume-low"; + if (Pipewire.defaultAudioSink.audio.volume <= 0.67) + return "volume-medium"; + return "volume-high"; + } + color: "#555" + } + } + } + } + + Loader { + id: tooltipLoader + + active: false + + Connections { + target: widgetMouseArea + function onContainsMouseChanged() { + if (widgetMouseArea.containsMouse) + tooltipLoader.active = true; + } + } + + PwObjectTracker { + objects: Pipewire.devices + } + PwObjectTracker { + objects: Pipewire.nodes + } + + sourceComponent: PopupWindow { + id: tooltip + + property bool openPopup: false + property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: { + tooltip.visible = tooltip.nextVisible; + if (!tooltip.visible) + tooltipLoader.active = false; + } + } + + implicitWidth: tooltipContent.width + implicitHeight: tooltipContent.height + color: "transparent" + + Rectangle { + width: tooltip.width + height: tooltipLayout.childrenRect.height + 16 + color: "black" + } + + WrapperItem { + id: tooltipContent + + bottomMargin: Math.max(0, 200 - tooltipLayout.implicitHeight) + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + WrapperItem { + margin: 8 + bottomMargin: 8 + + GridLayout { + id: tooltipLayout + + columns: 4 + + Repeater { + model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device") + + Item { + id: descItem + + required property var modelData + required property int index + + Layout.column: 0 + Layout.row: index + + implicitWidth: descText.contentWidth + implicitHeight: descText.contentHeight + + Text { + id: descText + + color: "white" + font.pointSize: 10 + font.family: "Fira Sans" + + text: descItem.modelData.description + } + } + } + + Repeater { + id: defaultSinkRepeater + + model: { + Array.from(Pipewire.devices.values) + .filter(dev => dev.type == "Audio/Device") + .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id )); + } + + Item { + id: defaultSinkItem + + required property var modelData + required property int index + + visible: Boolean(modelData) + + PwObjectTracker { + objects: [defaultSinkItem.modelData] + } + + Layout.column: 1 + Layout.row: index + + Layout.fillHeight: true + + implicitWidth: 16 + 8 + + WrapperMouseArea { + id: defaultSinkMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData + } + + onWheel: event => scrollVolume(event); + property real sensitivity: (1 / 40) / 120 + function scrollVolume(event) { + defaultSinkItem.modelData.audio.volume += event.angleDelta.y * sensitivity; + } + + Rectangle { + id: defaultSinkWidget + + anchors.fill: parent + color: { + if (defaultSinkMouseArea.containsMouse) + return "#33808080"; + return "transparent"; + } + + MaterialDesignIcon { + width: 16 + height: 16 + anchors.centerIn: parent + + icon: { + if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id) + return "speaker"; + return "speaker-off"; + } + color: icon == "speaker" ? "white" : "#555" + } + } + } + + PopupWindow { + id: volumeTooltip + + property bool nextVisible: defaultSinkMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse + + anchor { + item: defaultSinkMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: volumeHangTimer.restart() + + onVisibleChanged: tooltip.openPopup = volumeTooltip.visible + + Timer { + id: volumeHangTimer + interval: 100 + onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible + } + + implicitWidth: volumeTooltipText.contentWidth + 16 + implicitHeight: volumeTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: volumeTooltipMouseArea + + hoverEnabled: true + enabled: true + + onWheel: event => defaultSinkMouseArea.scrollVolume(event); + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: volumeTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: `${Math.round(defaultSinkItem.modelData?.audio?.volume * 100)}%` + } + } + } + } + } + } + + Repeater { + id: defaultSourceRepeater + + model: { + Array.from(Pipewire.devices.values) + .filter(dev => dev.type == "Audio/Device") + .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id )); + } + + Item { + id: defaultSourceItem + + required property var modelData + required property int index + + visible: Boolean(modelData) + + PwObjectTracker { + objects: [defaultSourceItem.modelData] + } + + Layout.column: 2 + Layout.row: index + + Layout.fillHeight: true + + implicitWidth: 16 + 8 + + WrapperMouseArea { + id: defaultSourceMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData + } + + onWheel: event => scrollVolume(event); + property real sensitivity: (1 / 40) / 120 + function scrollVolume(event) { + defaultSourceItem.modelData.audio.volume += event.angleDelta.y * sensitivity; + } + + Rectangle { + id: defaultSourceWidget + + anchors.fill: parent + color: { + if (defaultSourceMouseArea.containsMouse) + return "#33808080"; + return "transparent"; + } + + MaterialDesignIcon { + width: 16 + height: 16 + anchors.centerIn: parent + + icon: { + if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id) + return "microphone"; + return "microphone-off"; + } + color: icon == "microphone" ? "white" : "#555" + } + } + } + + PopupWindow { + id: volumeTooltip + + property bool nextVisible: defaultSourceMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse + + anchor { + item: defaultSourceMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: volumeHangTimer.restart() + + onVisibleChanged: tooltip.openPopup = volumeTooltip.visible + + Timer { + id: volumeHangTimer + interval: 100 + onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible + } + + implicitWidth: volumeTooltipText.contentWidth + 16 + implicitHeight: volumeTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: volumeTooltipMouseArea + + hoverEnabled: true + enabled: true + + onWheel: event => defaultSourceMouseArea.scrollVolume(event); + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: volumeTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: `${Math.round(defaultSourceItem.modelData?.audio?.volume * 100)}%` + } + } + } + } + } + } + + Repeater { + id: profileRepeater + + model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device") + + Item { + id: profileItem + + required property var modelData + required property int index + + PwObjectTracker { + objects: [profileItem.modelData] + } + + Layout.column: 3 + Layout.row: index + + Layout.fillWidth: true + + implicitWidth: Math.max(profileBox.implicitWidth, 300) + implicitHeight: profileBox.height + + ComboBox { + id: profileBox + + model: profileItem.modelData.profiles + + textRole: "description" + valueRole: "index" + onActivated: profileItem.modelData.setProfile(currentValue) + + anchors.fill: parent + + implicitContentWidthPolicy: ComboBox.WidestText + + Connections { + target: profileItem.modelData + function onCurrentProfileChanged() { + profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile); + } + } + Component.onCompleted: { + profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile); + } + + Connections { + target: profileBox.popup + function onVisibleChanged() { + tooltip.openPopup = profileBox.popup.visible + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/PrivacyWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/PrivacyWidget.qml new file mode 100644 index 00000000..d7ffadfe --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/PrivacyWidget.qml @@ -0,0 +1,49 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs.Services + +Item { + height: parent.height + width: layout.childrenRect.width + anchors.verticalCenter: parent.verticalCenter + + visible: Array.from(Privacy.activeItems).length > 0 + + RowLayout { + id: layout + + anchors.fill: parent + + spacing: 8 + + Repeater { + model: Privacy.activeItems + + Item { + id: privacyItem + + required property var modelData; + + height: parent.height + width: icon.width + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: { + if (privacyItem.modelData == Privacy.Item.Microphone) + return "microphone"; + if (privacyItem.modelData == Privacy.Item.Screensharing) + return "monitor-share"; + } + color: "#f2201f" + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml new file mode 100644 index 00000000..f3ae6804 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml @@ -0,0 +1,107 @@ +pragma Singleton + +import QtQml +import Quickshell +import Quickshell.Io +import Custom as Custom +import qs.Services + +Singleton { + id: root + + property string subsystem: "backlight" + property string device: "intel_backlight" + + property real currBrightness + property real exponent: 4 + + function calcCurrBrightness() { + if (!currFile.loaded || !maxFile.loaded) + return undefined; + const curr = Number(currFile.text()); + const max = Number(maxFile.text()); + const val = Math.pow(curr / max, 1 / root.exponent); + return val; + } + + Connections { + target: currFile + function onLoaded() { + const b = root.calcCurrBrightness(); + if (typeof b !== 'undefined') + root.currBrightness = b; + } + } + Connections { + target: maxFile + function onLoaded() { + const b = root.calcCurrBrightness(); + if (typeof b !== 'undefined') + root.currBrightness = b; + } + } + + onCurrBrightnessChanged: { + root.currBrightness = Math.max(0, Math.min(1, root.currBrightness)); + + const prev = root.calcCurrBrightness(); + if (typeof prev === 'undefined' || Math.abs(root.currBrightness - prev) < 0.01) + return; + + const max = Number(maxFile.text()); + const actual = Number(currFile.text()); + let curr = Math.max(0, Math.min(max, Math.pow(root.currBrightness, root.exponent) * max)); + if (Math.round(curr) == actual && curr < actual) + curr = Math.max(0, actual - 1); + else if (Math.round(curr) == actual && curr > actual) + curr = Math.min(max, actual + 1); + // root.currBrightness = Math.pow(curr / max, 1 / root.exponent); + Custom.Systemd.setBrightness(root.subsystem, root.device, Math.round(curr)); + } + + FileView { + id: currFile + path: `/sys/class/${root.subsystem}/${root.device}/brightness` + blockAllReads: true + watchChanges: true + onFileChanged: reload() + } + FileView { + id: maxFile + path: `/sys/class/${root.subsystem}/${root.device}/max_brightness` + blockAllReads: true + watchChanges: true + onFileChanged: reload() + } + + + Timer { + id: startupDelay + interval: 500 + running: true + repeat: false + } + property bool onlyInternal: true + Connections { + target: NiriService + function onOutputsChanged() { + root.onlyInternal = Object.keys(NiriService.outputs).every(oname => oname == "eDP-1"); + } + } + onOnlyInternalChanged: { + if (startupDelay.running) + return; + + if (!root.onlyInternal) + root.currBrightness = 1 + } + + + IpcHandler { + target: "Brightness" + + function set(brightness: real): void { + root.currBrightness = brightness; + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml new file mode 100644 index 00000000..3de69535 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml @@ -0,0 +1,18 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + Socket { + id: agentSocket + connected: true + path: `${Quickshell.env("XDG_RUNTIME_DIR")}/gnupg/S.gpg-agent` + } + + function reloadAgent() { + agentSocket.write("RELOADAGENT\n") + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml new file mode 100644 index 00000000..60202a29 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml @@ -0,0 +1,23 @@ +pragma Singleton + +import Quickshell +import Custom as Custom + +Singleton { + id: inhibitorState + + property bool waylandIdleInhibited: false + property alias lidSwitchInhibited: lidSwitchInhibitor.enabled + property bool lockscreenInhibited: false + + Custom.SystemdInhibitor { + id: lidSwitchInhibitor + + enabled: false + + what: Custom.SystemdInhibitorParams.HandleLidSwitch + who: "quickshell" + why: "User request" + mode: Custom.SystemdInhibitorParams.BlockWeak + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml new file mode 100644 index 00000000..e3ab9755 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml @@ -0,0 +1,8 @@ +pragma Singleton + +import Quickshell +import Quickshell.Services.Mpris + +Scope { + property list players: Mpris.players.values +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml new file mode 100644 index 00000000..e9e86f95 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml @@ -0,0 +1,217 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property var workspaces: [] + property var outputs: {} + property var keyboardLayouts: {} + property var windows: [] + property var casts: [] + readonly property string socketPath: Quickshell.env("NIRI_SOCKET") + + function refreshOutputs() { + commandSocket.sendCommand("Outputs", data => { + outputs = data.Ok.Outputs; + }); + } + + function sendCommand(command, callback) { + commandSocket.sendCommand(command, callback); + } + + Socket { + id: eventStreamSocket + path: root.socketPath + connected: true + + property bool acked: false + + onConnectionStateChanged: { + if (connected) { + acked = false; + write('"EventStream"\n'); + } + } + + parser: SplitParser { + onRead: line => { + try { + const event = JSON.parse(line) + + // console.log(JSON.stringify(event)) + + if (event.WorkspacesChanged) { + root.workspaces = event.WorkspacesChanged.workspaces + root.refreshOutputs(); + } else if (event.WorkspaceActivated) + eventWorkspaceActivated(event.WorkspaceActivated); + else if (event.WorkspaceUrgencyChanged) + eventWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged); + else if (event.WorkspaceActiveWindowChanged) + eventWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged); + else if (event.KeyboardLayoutsChanged) + root.keyboardLayouts = event.KeyboardLayoutsChanged.keyboard_layouts; + else if (event.KeyboardLayoutSwitched) + root.keyboardLayouts = Object.assign({}, root.keyboardLayouts, {"current_idx": event.KeyboardLayoutSwitched.idx }); + else if (event.WindowsChanged) + root.windows = event.WindowsChanged.windows + else if (event.WindowOpenedOrChanged) + eventWindowOpenedOrChanged(event.WindowOpenedOrChanged); + else if (event.WindowClosed) + eventWindowClosed(event.WindowClosed); + else if (event.WindowFocusChanged) + eventWindowFocusChanged(event.WindowFocusChanged); + else if (event.WindowUrgencyChanged) + eventWindowUrgencyChanged(event.WindowUrgencyChanged); + else if (event.WindowLayoutsChanged) + eventWindowLayoutsChanged(event.WindowLayoutsChanged); + else if (event.WindowFocusTimestampChanged) + eventWindowFocusTimestampChanged(event.WindowFocusTimestampChanged); + else if (event.CastsChanged) + root.casts = event.CastsChanged.casts + else if (event.CastStartedOrChanged) + eventCastStartedOrChanged(event.CastStartedOrChanged); + else if (event.CastStopped) + eventCastStopped(event.CastStopped); + else if (event.Ok && !eventStreamSocket.acked) { eventStreamSocket.acked = true; } + else if (event.OverviewOpenedOrClosed) {} + else if (event.ScreenshotCaptured) {} + else if (event.ConfigLoaded) {} + else + console.log(JSON.stringify(event)); + } catch (e) { + console.warn("NiriService: Failed to parse event:", line, e) + } + } + } + } + + Socket { + id: commandSocket + path: root.socketPath + connected: true + + property var awaitingAnswer: null + property var cmdQueue: [] + + parser: SplitParser { + onRead: line => { + if (commandSocket.awaitingAnswer === null) + return; + + try { + const response = JSON.parse(line); + commandSocket.awaitingAnswer.callback(response); + commandSocket.awaitingAnswer = null; + } catch (e) { + console.warn("NiriService: Failed to parse response:", line, e) + } + commandSocket._handleQueue(); + } + } + + onCmdQueueChanged: { + _handleQueue(); + } + onAwaitingAnswerChanged: { + _handleQueue(); + } + + function _handleQueue() { + if (cmdQueue.length <= 0 || awaitingAnswer !== null) + return; + + let localQueue = Array.from(cmdQueue); + awaitingAnswer = localQueue.shift(); + cmdQueue = localQueue; + write(JSON.stringify(awaitingAnswer.command) + '\n'); + } + + function sendCommand(command, callback) { + cmdQueue = Array.from(cmdQueue).concat([{ "command": command, "callback": callback }]) + } + } + + function eventWorkspaceActivated(data) { + let relevant_output = null; + Array.from(root.workspaces).forEach(ws => { + if (data.id === ws.id) + relevant_output = ws.output; + }); + root.workspaces = Array.from(root.workspaces).map(ws => { + if (data.focused) + ws.is_focused = false; + if (ws.output === relevant_output) + ws.is_active = false; + if (data.id === ws.id) { + ws.is_active = true; + ws.is_focused = data.focused; + } + return ws; + }); + } + function eventWorkspaceUrgencyChanged(data) { + root.workspaces = Array.from(root.workspaces).map(ws => { + if (data.id == ws.id) + ws.is_urgent = data.urgent; + return ws; + }); + } + function eventWorkspaceActiveWindowChanged(data) { + root.workspaces = Array.from(root.workspaces).map(ws => { + if (data.workspace_id === ws.id) + ws.active_window_id = data.active_window_id; + return ws; + }); + } + function eventWindowOpenedOrChanged(data) { + root.windows = Array.from(root.windows).map(win => { + if (data.window.is_focused) + win.is_focused = false; + return win; + }).filter(win => win.id !== data.window.id).concat([data.window]); + } + function eventWindowClosed(data) { + root.windows = Array.from(root.windows).filter(win => win.id !== data.id); + } + function eventWindowFocusChanged(data) { + root.windows = Array.from(root.windows).map(win => { + win.is_focused = win.id === data.id; + return win; + }); + } + function eventWindowUrgencyChanged(data) { + root.windows = Array.from(root.windows).map(win => { + if (win.id === data.id) + win.is_urgent = data.urgent; + return win; + }); + } + function eventWindowLayoutsChanged(data) { + root.windows = Array.from(root.windows).map(win => { + Array.from(data.changes).forEach(change => { + if (win.id === change[0]) + win.layout = change[1]; + }); + return win; + }); + } + function eventWindowFocusTimestampChanged(data) { + root.windows = Array.from(root.windows).map(win => { + if (win.id === data.id) + win.focus_timestamp = data.focus_timestamp; + return win; + }); + } + function eventCastStartedOrChanged(data) { + root.casts = [...Array.from(root.casts).filter(cast => cast.stream_id !== data.cast.stream_id), data.cast]; + } + function eventCastStopped(data) { + root.casts = Array.from(root.casts).filter(cast => cast.stream_id !== data.stream_id); + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml new file mode 100644 index 00000000..5f8ff419 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml @@ -0,0 +1,162 @@ +pragma Singleton + +import QtQml +import Quickshell +import Quickshell.Services.Notifications + +Singleton { + id: root + + readonly property bool active: !root.lockscreenActive && !root.displayInhibited + property bool lockscreenActive: false + property bool displayInhibited: false + property alias trackedNotifications: server.trackedNotifications + readonly property var groups: { + function matchesGroupKey(notif, groupKey) { + var matches = true; + for (const prop in groupKey.test) { + if (notif[prop] !== groupKey.test[prop]) { + matches = false; + break; + } + } + return matches; + } + + var groups = new Map(); + var notifs = new Array(); + for (const [ix, notif] of server.trackedNotifications.values.entries()) { + var didGroup = false; + for (const groupKey of root.groupKeys) { + if (!matchesGroupKey(notif, groupKey)) + continue; + + const key = JSON.stringify({ + "key": groupKey, + "values": Object.assign({}, ...(Array.from(groupKey["group-by"]).map(prop => { + var res = {}; + res[prop] = notif[prop]; + return res; + }))) + }); + if (!groups.has(key)) + groups.set(key, new Array()); + groups.get(key).push({ "ix": ix, "notif": notif }); + didGroup = true; + break; + } + + if (!didGroup) + notifs.push([{ "ix": ix, "notif": notif }]); + } + notifs.push(...groups.values()); + notifs.sort((as, bs) => Math.min(...(as.map(o => o.ix))) - Math.min(...(bs.map(o => o.ix)))); + return notifs.map(ns => ns.map(n => n.notif)); + } + + property var groupKeys: [ + { "test": { "appName": "Element" }, "group-by": [ "summary" ] } + ]; + + property int historyLimit: 100 + property var history: [] + + Component { + id: expirationTimer + + QtObject { + id: timer + + required property QtObject parent + required property int expirationTime + + property list data: [ + Timer { + running: root.active && !timer.expired + interval: timer.expirationTime + onTriggered: { + timer.parent.expirationTimer.destroy(); + timer.parent.expirationTimer = null; + timer.parent.expire(); + } + } + ] + } + } + + Component { + id: notificationLock + + RetainableLock {} + } + + readonly property SystemClock clock: SystemClock { + precision: SystemClock.Minutes + } + + function formatTime(time) { + const now = root.clock.date; + const diff = now - time; + const minutes = Math.ceil(diff / 60000); + const hours = Math.floor(minutes / 60); + + if (hours < 1) { + if (minutes < 1) + return "now"; + if (minutes == 1) + return "1 minute"; + return `${minutes} minutes`; + } + + const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate()) + const days = Math.floor((nowDate - timeDate) / (1000 * 86400)) + + const timeStr = time.toLocaleTimeString(Qt.locale(), "HH:mm"); + + if (days === 0) + return timeStr; + if (days === 1) + return `yesterday ${timeStr}`; + + const dateStr = time.toLocaleDateString(Qt.locale(), "YYYY-MM-DD"); + return `${dateStr} ${timeStr}`; + } + + NotificationServer { + id: server + + bodySupported: true + actionsSupported: true + actionIconsSupported: true + imageSupported: true + bodyMarkupSupported: true + bodyImagesSupported: true + + onNotification: notification => { + var timeout = notification.expireTimeout * 1000; + if (notification.appName == "poweralertd") + timeout = 2000; + if (timeout > 0) { + Object.defineProperty(notification, "expirationTimer", { configurable: true, enumerable: true, writable: true }); + notification.expirationTimer = expirationTimer.createObject(notification, { parent: notification, expirationTime: timeout }); + } + Object.defineProperty(notification, "receivedTime", { configurable: true, enumerable: true, writable: true }); + notification.receivedTime = root.clock.date; + notification.closed.connect((reason) => server.onNotificationClosed(notification, reason)); + notification.tracked = true; + } + + function onNotificationClosed(notification, reason) { + while (root.history.length >= root.historyLimit) { + root.history[0].lock.locked = false; + root.history.shift(); + } + + root.history.push({ + lock: notificationLock.createObject(root, { locked: true, object: notification }), + notification: notification + }); + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml new file mode 100644 index 00000000..9c813e49 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml @@ -0,0 +1,63 @@ +pragma Singleton + +import QtQml +import Quickshell +import Quickshell.Services.Pipewire + +Singleton { + id: root + + PwObjectTracker { + objects: Pipewire.nodes.values + } + + enum Item { + Microphone, + Screensharing + } + + readonly property list activeItems: { + var items = []; + if (microphoneActive) + items.push(Privacy.Item.Microphone); + if (screensharingActive) + items.push(Privacy.Item.Screensharing); + return items; + } + + readonly property bool microphoneActive: { + if (!Pipewire.ready || !Pipewire.nodes?.values) { + return false + } + + for (const node of Pipewire.nodes.values) { + if (!node || (node.type & PwNodeType.AudioInStream) != PwNodeType.AudioInStream) + continue; + + if (node.properties?.["stream.monitor"] === "true") + continue; + + if (node.audio?.muted) + continue; + + return true; + } + + return false; + } + + readonly property bool screensharingActive: { + if (!Pipewire.ready || !Pipewire.nodes?.values) { + return false + } + + for (const node of Pipewire.nodes.values) { + if (!node || (node.type & PwNodeType.VideoInStream) != PwNodeType.VideoInStream) + continue; + + return true; + } + + return false; + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml new file mode 100644 index 00000000..eb415452 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml @@ -0,0 +1,63 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io + +Singleton { + id: root + property bool active: false + property bool slurpSuccess: false + + onActiveChanged: { + if (!active) { + slurp.running = false; + screenRecorder.running = false; + } + if (active) + slurp.running = true; + } + + Process { + id: screenRecorder + running: false + onRunningChanged: { + console.log("wf-recorder running: ", screenRecorder.running); + + if (!screenRecorder.running && !slurp.running) + root.active = false; + } + stderr: SplitParser { + onRead: line => console.log("wf-recorder stderr: ", line) + } + stdout: SplitParser { + onRead: line => console.log("wf-recorder stdout: ", line) + } + } + + Process { + id: slurp + running: false + command: [ @slurp@, "-o", "-d" ] + stdout: StdioCollector {} + stderr: SplitParser { + onRead: line => console.log("slurp stderr: ", line) + } + onExited: exitCode => { + if (exitCode !== 0) { + console.log("slurp failed: ", exitCode); + root.active = false; + return; + } + console.log("slurp succeeded: ", slurp.stdout.text); + + const nowDate = new Date(); + + screenRecorder.command = [ + @wf-recorder@, + "-g", slurp.stdout.text, + "-f", `${Quickshell.env("HOME")}/screenshots/${nowDate.toLocaleString(Qt.locale(), "yyyy-MM-ddThh:mm:ss")}.mkv`, + ]; + screenRecorder.running = true; + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml new file mode 100644 index 00000000..c71a9cca --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml @@ -0,0 +1,8 @@ +import Custom as Custom + +Custom.FileSelector { + id: root + + directory: @wallpapers@ + epoch: 79200000 +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml new file mode 100644 index 00000000..d98378f1 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml @@ -0,0 +1,84 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import QtQml + +Singleton { + id: root + + property alias time: timeState + property alias today: todayState + + CommandState { + id: timeState + command: "time" + } + CommandState { + id: todayState + command: "today" + } + + IpcHandler { + target: "Worktime" + + function refresh(): void { + time.running = true; + today.running = true; + } + } + + component CommandState : Scope { + id: commandState + + required property string command + property var state: null + + property bool strikeout: !strikeoutTimer.running + property alias running: process.running + property alias updating: updateTimer.running + + Process { + id: process + running: true + command: [ @worktime@, commandState.command, "--waybar" ] + stdout: StdioCollector { + id: processCollector + onStreamFinished: { + try { + commandState.state = JSON.parse(processCollector.text); + strikeoutTimer.restart(); + } catch (e) { + console.warn("Worktime: Failed to parse output:", processCollector.text, e); + } + } + } + } + + Timer { + id: updateTimer + running: commandState.state?.class == "running" || commandState.state?.class == "over" + interval: 60000 + repeat: true + onTriggered: process.running = true + } + + Timer { + id: strikeoutTimer + running: false + interval: 5 * updateTimer.interval + repeat: false + } + } + + Timer { + running: Boolean(timeState.state) && Boolean(todayState.state) && timeState.strikeout && todayState.strikeout + interval: 1000 + repeat: false + onTriggered: { + timeState.state = null; + todayState.state = null; + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/SystemTray.qml b/accounts/gkleen@skadhi/shell/quickshell/SystemTray.qml new file mode 100644 index 00000000..f0c50632 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/SystemTray.qml @@ -0,0 +1,201 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.SystemTray + +Item { + anchors.verticalCenter: parent.verticalCenter + width: systemTrayRow.childrenRect.width + height: parent.height + clip: true + + Row { + id: systemTrayRow + anchors.centerIn: parent + width: childrenRect.width + height: parent.height + spacing: 0 + + Repeater { + model: ScriptModel { + values: { + var trayItems = Array.from(SystemTray.items.values).filter(item => item.status !== Status.Passive && !(["Fcitx"].includes(item.id))); + trayItems.sort((a, b) => a.category !== b.category ? b.category - a.category : a.id.localeCompare(b.id)) + return trayItems; + } + } + + delegate: Item { + id: trayItemWrapper + + required property var modelData + required property int index + + property var trayItem: modelData + property string iconSource: { + let icon = trayItem && trayItem.icon + if (typeof icon === 'string' || icon instanceof String) { + if (icon.includes("?path=")) { + const split = icon.split("?path=") + if (split.length !== 2) + return icon + const name = split[0] + const path = split[1] + const fileName = name.substring( + name.lastIndexOf("/") + 1) + return `file://${path}/${fileName}` + } + return icon + } + return "" + } + + width: icon.width + 6 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: trayItemArea + + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + cursorShape: trayItem.onlyMenu ? Qt.ArrowCursor : Qt.PointingHandCursor + onClicked: mouse => { + if (!trayItem) + return + + if (mouse.button === Qt.LeftButton + && !trayItem.onlyMenu) { + trayItem.activate() + return + } + + if (trayItem.hasMenu) { + var globalPos = mapToGlobal(0, 0) + var currentScreen = screen || Screen + var screenX = currentScreen.x || 0 + var relativeX = globalPos.x - screenX + menuAnchor.menu = trayItem.menu + menuAnchor.anchor.window = bar + menuAnchor.anchor.rect = Qt.rect( + relativeX, + 21, + parent.width, 1) + menuAnchor.open() + } + } + + Rectangle { + anchors.fill: parent + color: { + if (trayItemArea.containsMouse) + return "#33808080"; + return "transparent"; + } + + Item { + anchors.fill: parent + + layer.enabled: true + layer.effect: MultiEffect { + colorization: 1 + colorizationColor: "#555" + } + + IconImage { + id: icon + + anchors.centerIn: parent + implicitSize: 16 + source: trayItemWrapper.iconSource + asynchronous: true + smooth: true + mipmap: true + + layer.enabled: true + layer.effect: MultiEffect { + id: effect + + brightness: 1 + } + } + } + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: (trayItem.tooltipTitle || trayItem.tooltipDescription) && (trayItemArea.containsMouse || tooltipMouseArea.containsMouse) && !menuAnchor.visible + + anchor { + item: trayItemArea + edges: Edges.Bottom + } + + visible: false + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + color: "black" + + implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16 + implicitHeight: (trayItem.tooltipTitle ? tooltipTitle.contentHeight : 0) + (trayItem.tooltipDescription ? tooltipDescription.contentHeight : 0) + 16 + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + margin: 4 + + anchors.fill: parent + + Item { + anchors.fill: parent + + Column { + anchors.centerIn: parent + Text { + id: tooltipTitle + + enabled: trayItem.tooltipTitle + + font.pointSize: 10 + font.family: "Fira Sans" + font.bold: true + color: "white" + + text: trayItem.tooltipTitle + } + + Text { + id: tooltipDescription + + enabled: trayItem.tooltipDescription + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: trayItem.tooltipDescription + } + } + } + } + } + } + } + } + QsMenuAnchor { + id: menuAnchor + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/UnixIPC.qml b/accounts/gkleen@skadhi/shell/quickshell/UnixIPC.qml new file mode 100644 index 00000000..3d950031 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/UnixIPC.qml @@ -0,0 +1,106 @@ +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire +import Quickshell.Services.Mpris +import qs.Services +import Custom as Custom +import QtQml + +Scope { + id: root + + SocketServer { + active: true + path: `${Quickshell.env("XDG_RUNTIME_DIR")}/shell.sock` + handler: Socket { + parser: SplitParser { + onRead: line => { + const command = (() => { + try { + return JSON.parse(line); + } catch (e) { + console.warn("UnixIPC: Failed to parse command:", line, e); + } + })(); + if (!command) + return; + + if (command.Volume) + root.onCommandVolume(command.Volume); + else if (command.Brightness) + root.onCommandBrightness(command.Brightness); + else if (command.LockSession) + Custom.Systemd.lockSession(); + else if (command.Suspend) + Custom.Systemd.suspend(); + else if (command.Hibernate) + Custom.Systemd.hibernate(); + else if (command.Mpris) + root.onCommandMpris(command.Mpris); + else if (command.Notifications) + root.onCommandNotifications(command.Notifications); + else if (command.ScreenRecord) + root.onCommandScreenRecord(command.ScreenRecord); + else + console.warn("UnixIPC: Command not handled:", JSON.stringify(command)); + } + } + + onError: e => { + if (e == 1) + return; + console.warn("QLocalSocket::LocalSocketError", e); + } + } + } + + PwObjectTracker { + objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ] + } + function onCommandVolume(command) { + if (command.muted === "toggle") + Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted; + if (command.volume === "up") + Pipewire.defaultAudioSink.audio.volume += 0.02; + if (command.volume === "down") + Pipewire.defaultAudioSink.audio.volume -= 0.02; + + if (command["mic-muted"] === "toggle") + Pipewire.defaultAudioSource.audio.muted = !Pipewire.defaultAudioSource.audio.muted; + } + + function onCommandBrightness(command) { + if (command === "up") + Brightness.currBrightness += 0.02 + if (command === "down") + Brightness.currBrightness -= 0.02 + } + + function onCommandMpris(command) { + if (command.PauseAll) + Array.from(MprisProxy.players).forEach(player => { + if (player.canPause && player.isPlaying) + player.pause(); + }); + } + Component.onCompleted: { (_ => {})(MprisProxy.players); } + + function onCommandNotifications(command) { + if (command.DismissGroup && NotificationManager.active) { + if (NotificationManager.groups.length > 0) + for (const notif of [...NotificationManager.groups[0]]) + notif.dismiss(); + } + if (command.DismissAll && NotificationManager.active) { + for (const notif of [...NotificationManager.trackedNotifications.values]) + notif.dismiss(); + } + } + + function onCommandScreenRecord(command) { + if (command.Toggle) + ScreenRecord.active = !ScreenRecord.active; + else + console.warn("UnixIPC.ScreenRecord: Command not handled:", JSON.stringify(command)); + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/VolumeOSD.qml b/accounts/gkleen@skadhi/shell/quickshell/VolumeOSD.qml new file mode 100644 index 00000000..91dc9591 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/VolumeOSD.qml @@ -0,0 +1,163 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire +import Quickshell.Widgets + +Scope { + id: root + + property string show: "" + property bool inhibited: true + + PwObjectTracker { + objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ] + } + + Connections { + enabled: Pipewire.defaultAudioSink + target: Pipewire.defaultAudioSink?.audio || null + + function onVolumeChanged() { + root.show = "sink"; + hideTimer.restart(); + } + function onMutedChanged() { + root.show = "sink"; + hideTimer.restart(); + } + } + + Connections { + enabled: Pipewire.defaultAudioSource + target: Pipewire.defaultAudioSource?.audio || null + + function onVolumeChanged() { + root.show = "source"; + hideTimer.restart(); + } + function onMutedChanged() { + root.show = "source"; + hideTimer.restart(); + } + } + + onShowChanged: { + if (show) + hideTimer.restart(); + } + + Timer { + id: hideTimer + interval: 1000 + onTriggered: root.show = "" + } + + Timer { + id: startInhibit + interval: 100 + running: true + onTriggered: { + root.show = ""; + root.inhibited = false; + } + } + + LazyLoader { + active: root.show && !root.inhibited + + Variants { + model: Quickshell.screens + + delegate: Scope { + id: screenScope + + required property var modelData + + PanelWindow { + id: window + + screen: screenScope.modelData + + anchors.top: true + margins.top: screen.height / 2 - 50 + 3.5 + exclusiveZone: 0 + exclusionMode: ExclusionMode.Ignore + + implicitWidth: 400 + implicitHeight: 50 + + mask: Region {} + + color: "transparent" + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.75) + } + + RowLayout { + id: layout + + anchors.centerIn: parent + + height: 50 - 8*2 + width: 400 - 8*2 + + MaterialDesignIcon { + id: volumeIcon + + implicitWidth: parent.height + implicitHeight: parent.height + + icon: { + if (root.show == "sink") { + if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted) + return "volume-off"; + if (Pipewire.defaultAudioSink.audio.volume <= 0.33) + return "volume-low"; + if (Pipewire.defaultAudioSink.audio.volume <= 0.67) + return "volume-medium"; + return "volume-high"; + } else if (root.show == "source") { + if (!Pipewire.defaultAudioSource || Pipewire.defaultAudioSource.audio.muted) + return "microphone-off"; + if (Pipewire.defaultAudioSource.audio.volume > 1) + return "microphone-plus"; + return "microphone"; + } + return "volume-high"; + } + } + + Rectangle { + Layout.fillWidth: true + + implicitHeight: 10 + + color: "#50ffffff" + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + color: Pipewire.defaultAudioSink?.audio.muted ? "#70ffffff" : "white" + + implicitWidth: { + if (root.show == "sink") + return parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0); + else if (root.show == "source") + return parent.width * Math.min(1, (Pipewire.defaultAudioSource?.audio.volume ?? 0)); + return 0; + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/WallpaperBackground.qml b/accounts/gkleen@skadhi/shell/quickshell/WallpaperBackground.qml new file mode 100644 index 00000000..4f85a900 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/WallpaperBackground.qml @@ -0,0 +1,89 @@ +import QtQuick +import Quickshell +import qs.Services + +Item { + id: root + + anchors.fill: parent + + required property string screen + + property Img current: one + property string source: selector.selected + + WallpaperSelector { + id: selector + seed: screen + } + + onSourceChanged: { + if (!source) + current = null; + else if (current === one) + two.update() + else + one.update() + } + + Img { id: one } + Img { id: two } + + component Img: Image { + id: img + + function update() { + source = root.source || "" + } + + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + smooth: true + asynchronous: true + cache: false + + opacity: 0 + + onStatusChanged: { + if (status === Image.Ready) { + root.current = this + } + } + + states: State { + name: "visible" + when: root.current === img + + PropertyChanges { + img.opacity: 1 + } + StateChangeScript { + name: "unloadOther" + script: { + if (img === one) + two.source = "" + if (img === two) + one.source = "" + } + } + } + + transitions: Transition { + SequentialAnimation { + NumberAnimation { + target: img + properties: "opacity" + duration: { + if (img === one && two.source == "" || img === two && one.source == "") + return 0; + return 5000; + } + easing.type: Easing.OutCubic + } + ScriptAction { + scriptName: "unloadOther" + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/WaylandInhibitorWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/WaylandInhibitorWidget.qml new file mode 100644 index 00000000..0512ff51 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/WaylandInhibitorWidget.qml @@ -0,0 +1,56 @@ +import Quickshell +import QtQuick +import Quickshell.Widgets +import Quickshell.Wayland +import qs.Services + +Item { + id: root + + required property var window + + width: icon.width + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + IdleInhibitor { + id: inhibitor + enabled: InhibitorState.waylandIdleInhibited + window: root.window + } + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: InhibitorState.waylandIdleInhibited = !InhibitorState.waylandIdleInhibited + + Rectangle { + anchors.fill: parent + color: { + if (widgetMouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: inhibitor.enabled ? "eye" : "eye-off" + color: inhibitor.enabled ? "white" : "#555" + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/WorkspaceSwitcher.qml b/accounts/gkleen@skadhi/shell/quickshell/WorkspaceSwitcher.qml new file mode 100644 index 00000000..a893937a --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/WorkspaceSwitcher.qml @@ -0,0 +1,204 @@ +import Quickshell +import QtQuick +import qs.Services +import Quickshell.Widgets +import QtQuick.Layouts + +Row { + id: workspaces + + required property var screen + + property var ignoreWorkspaces: @ignore_workspaces@ + + height: parent.height + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + Repeater { + model: ScriptModel { + values: { + let currWorkspaces = NiriService.workspaces; + const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces); + currWorkspaces = currWorkspaces.filter(ws => ws.output == workspaces.screen?.name).filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name)); + currWorkspaces.sort((a, b) => { + if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x) + return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x + if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y) + return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y + return a.idx - b.idx; + }); + return currWorkspaces.map(o => Object.assign({}, o)); + } + } + + Item { + id: wsItem + + property var workspaceData: modelData + + width: wsLabel.contentWidth + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: mouseArea + + anchors.fill: parent + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: true + onClicked: { + NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {}); + } + + Rectangle { + anchors.fill: parent + + color: { + if (mouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Text { + id: wsLabel + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: { + if (workspaceData.is_active) + return "#23fd00"; + if (workspaceData.active_window_id === null) + return "#555"; + return "white"; + } + + text: workspaceData.name ? workspaceData.name : workspaceData.idx + } + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: (mouseArea.containsMouse || tooltipMouseArea.containsMouse) && [...windowsModel.values].length > 0 + + anchor { + item: mouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: tooltipContent.implicitWidth + implicitHeight: tooltipContent.implicitHeight + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + WrapperItem { + id: tooltipContent + + margin: 0 + + ColumnLayout { + spacing: 0 + + Repeater { + model: ScriptModel { + id: windowsModel + + values: { + let currWindows = NiriService.windows.filter(win => win.workspace_id == wsItem.workspaceData.id); + currWindows.sort((a, b) => { + if (a.is_floating !== b.is_floating) + return b.is_floating - a.is_floating; + if (a.layout.tile_pos_in_workspace_view?.[0] !== b.layout.tile_pos_in_workspace_view?.[0]) + return a.layout.tile_pos_in_workspace_view?.[0] - b.layout.tile_pos_in_workspace_view?.[0] + if (a.layout.tile_pos_in_workspace_view?.[1] !== b.layout.tile_pos_in_workspace_view?.[1]) + return a.layout.tile_pos_in_workspace_view?.[1] - b.layout.tile_pos_in_workspace_view?.[1] + if (a.layout.pos_in_scrolling_layout?.[0] !== b.layout.pos_in_scrolling_layout?.[0]) + return a.layout.pos_in_scrolling_layout?.[0] - b.layout.pos_in_scrolling_layout?.[0] + if (a.layout.pos_in_scrolling_layout?.[1] !== b.layout.pos_in_scrolling_layout?.[1]) + return a.layout.pos_in_scrolling_layout?.[1] - b.layout.pos_in_scrolling_layout?.[1] + if (a.app_id !== b.app_id) + return a.app_id?.localeCompare(b.app_id); + + return a.title.localeCompare(b.title); + }); + return currWindows.map(o => Object.assign({}, o)); + } + } + + WrapperMouseArea { + id: windowMouseArea + + required property int index + required property var modelData + property var windowData: modelData + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: true + + Layout.fillWidth: true + + onClicked: { + NiriService.sendCommand({ "Action": { "FocusWindow": { "id": windowData.id } } }, _ => {}) + } + + WrapperRectangle { + color: windowMouseArea.containsMouse ? "#33808080" : "transparent"; + + WrapperItem { + rightMargin: 8 + leftMargin: 8 + topMargin: windowMouseArea.index == 0 ? 8 : 4 + bottomMargin: windowMouseArea.index == windowsModel.values.length - 1 ? 8 : 4 + + Text { + id: windowLabel + + font.pointSize: 10 + font.family: "Fira Sans" + color: { + if (windowData.is_focused) + return "#23fd00"; + if (NiriService.workspaces.find(ws => ws.id == windowData.workspace_id)?.active_window_id == windowData.id) + return "white"; + return "#555"; + } + + text: windowData.title + + horizontalAlignment: Text.AlignLeft + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/WorktimeWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/WorktimeWidget.qml new file mode 100644 index 00000000..b7baee34 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/WorktimeWidget.qml @@ -0,0 +1,212 @@ +import QtQml +import Quickshell +import QtQuick +import Quickshell.Widgets +import qs.Services + +Item { + id: root + + height: parent.height + width: (timeWidget.visible ? timeWidget.label.contentWidth + 8 : 0) + (todayWidget.visible ? todayWidget.label.contentWidth + 8 : 0) + (icon.visible ? icon.implicitWidth + 8 : 0) + anchors.verticalCenter: parent.verticalCenter + + component TextWidget : Item { + id: textWidget + + visible: textWidget.state.state?.text ?? false + + required property var state + property alias label: label + property alias mouseArea: mouseArea + + anchors.verticalCenter: parent.verticalCenter + implicitWidth: label.contentWidth + 8 + height: parent.height + + WrapperMouseArea { + id: mouseArea + + anchors.fill: parent + + enabled: true + hoverEnabled: true + acceptedButtons: Qt.NoButton + cursorShape: Qt.PointingHandCursor + + Item { + anchors.fill: parent + + Text { + id: label + + anchors.centerIn: parent + + text: textWidget.state.state?.text ?? "" + + font.pointSize: 10 + font.family: "Fira Sans" + font.strikeout: textWidget.state.strikeout + color: { + if (textWidget.state.state?.class == "running") + return "white"; + if (textWidget.state.state?.class == "over") + return "#f28a21"; + return "#555"; + } + } + } + } + } + + WrapperMouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + + cursorShape: Qt.PointingHandCursor + onClicked: { + Worktime.time.running = true; + Worktime.today.running = true; + } + + Rectangle { + anchors.fill: parent + color: { + if (mouseArea.containsMouse) + return "#33808080"; + return "transparent"; + } + + Row { + height: parent.height + anchors.centerIn: parent + spacing: 0 + + TextWidget { + id: timeWidget + state: Worktime.time + } + + TextWidget { + id: todayWidget + state: Worktime.today + } + + MaterialDesignIcon { + id: icon + + anchors.verticalCenter: parent.verticalCenter + + implicitSize: 14 + + visible: !timeWidget.visible && !todayWidget.visible + + icon: (Worktime.time.running || Worktime.today.running) ? "update" : "timer-off" + color: "#555" + } + } + } + } + + component WorktimePopup : PopupWindow { + id: tooltip + + required property var state + required property var mouseArea + + property bool iconVisible: tooltip.state.running || !tooltip.state.updating + property bool tooltipVisible: tooltip.state.state?.tooltip ?? false + + property bool nextVisible: (tooltip.tooltipVisible || tooltip.iconVisible) && (tooltip.mouseArea.containsMouse || tooltipMouseArea.containsMouse) + + anchor { + item: tooltip.mouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: (tooltipIcon.visible ? tooltipIcon.implicitWidth : 0) + (tooltipIcon.visible && tooltipText.visible ? 8 : 0) + (tooltipText.visible ? tooltipText.implicitWidth : 0) + 16 + implicitHeight: tooltipText.implicitHeight + 16 + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + enabled: true + hoverEnabled: true + + anchors.fill: parent + + Item { + anchors.fill: parent + + Row { + id: tooltipLayout + + anchors { + left: parent.left + top: parent.top + leftMargin: 8 + topMargin: 8 + verticalCenter: parent.verticalCenter + } + + height: parent.height + width: childrenRect.width + + spacing: 0 + + MaterialDesignIcon { + id: tooltipIcon + + implicitSize: 14 + anchors.verticalCenter: parent.verticalCenter + + visible: tooltip.iconVisible + + icon: tooltip.state.running ? "update" : "timer-off" + } + + Item { + visible: tooltipIcon.visible && tooltipText.visible + height: parent.height + width: 8 + } + + Text { + id: tooltipText + + visible: tooltip.tooltipVisible + + anchors.verticalCenter: parent.verticalCenter + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: tooltip.state.state?.tooltip ?? "" + } + } + } + } + } + + WorktimePopup { + state: Worktime.time + mouseArea: timeWidget.mouseArea + } + WorktimePopup { + state: Worktime.today + mouseArea: todayWidget.mouseArea + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/YtDlpWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/YtDlpWidget.qml new file mode 100644 index 00000000..ff338d49 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/YtDlpWidget.qml @@ -0,0 +1,190 @@ +import QtQml.Models +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Material +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import Custom as Custom + +Item { + id: root + + height: parent.height + width: icon.width + anchors.verticalCenter: parent.verticalCenter + + visible: Boolean(unitsModel.values.length) + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + hoverEnabled: true + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: "tray-arrow-down" + color: "white" + } + } + } + + PopupWindow { + id: tooltip + + property bool openPopup: false + property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: tooltipContent.width + implicitHeight: tooltipContent.height + color: "transparent" + + Rectangle { + width: tooltip.width + height: tooltipLayout.childrenRect.height + 16 + color: "black" + } + + WrapperItem { + id: tooltipContent + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + WrapperItem { + margin: 8 + + child: GridLayout { + id: tooltipLayout + + columns: 2 + } + + Repeater { + model: ScriptModel { + id: unitsModel + + values: [...Custom.Systemd.userUnits.values].filter(unit => unit.id.match(/^yt-dlp@.+\.service$/) && unit.activeState === "active" && unit.runtimeDirectory) + } + + Item { + id: unitDelegate + + Component.onCompleted: { + while (children.length) { children[0].parent = tooltipLayout; } + } + + required property var modelData + required property int index + + property var state: null + + Socket { + id: socket + + path: Quickshell.env("XDG_RUNTIME_DIR") + "/" + unitDelegate.modelData.runtimeDirectory[0] + "/status.sock" + connected: true + onError: e => { + console.warn("YtDlpWidget: Error in Socket:", socket.path, e); + } + parser: SplitParser { + onRead: line => { + try { + unitDelegate.state = JSON.parse(line) + } catch (e) { + console.warn("YtDlpWidget: Failed to parse state:", line, e) + } + } + } + } + + WrapperItem { + Layout.column: 0 + Layout.row: unitDelegate.index + + implicitWidth: descText.contentWidth + implicitHeight: descText.contentHeight + + Text { + id: descText + + color: "white" + font.pointSize: 10 + font.family: "Fira Sans" + + text: unitDelegate.state?.title || unitDelegate.modelData.id + } + } + + /* + WrapperItem { + Layout.column: 1 + Layout.row: unitDelegate.index + + implicitWidth: fragText.contentWidth + implicitHeight: fragText.contentHeight + + visible: Boolean(unitDelegate.state) + + Text { + id: fragText + + color: "white" + font.pointSize: 10 + font.family: "Fira Sans" + + text: `${unitDelegate.state?.fragment_index}/${unitDelegate.state?.fragment_count}` + } + } + */ + + WrapperItem { + Layout.column: 1 + Layout.row: unitDelegate.index + + visible: Boolean(unitDelegate.state) + + ProgressBar { + id: progress + + Material.background: "black" + Material.accent: "white" + + from: 0 + to: unitDelegate.state?.total_bytes || unitDelegate.state?.total_bytes_estimate || 0 + value: unitDelegate.state?.downloaded_bytes || 0 + indeterminate: !Boolean(unitDelegate.state?.total_bytes || unitDelegate.state?.total_bytes_estimate) || unitDelegate.state?.status !== "downloading" + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/displaymanager.qml b/accounts/gkleen@skadhi/shell/quickshell/displaymanager.qml new file mode 100644 index 00000000..b452c03d --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/displaymanager.qml @@ -0,0 +1,115 @@ +//@ pragma UseQApplication + +import Quickshell +import Quickshell.Wayland +import Quickshell.Io +import Quickshell.Services.Greetd +import QtQml + + +ShellRoot { + id: displaymanager + + settings.watchFiles: false + + property string currentText: "" + property string username: @username@ + property list command: @niri_session@ + property list messages: [] + property bool responseRequired: false + property bool responseVisible: false + + signal startAuth() + + onStartAuth: { + if (Greetd.state !== GreetdState.Inactive) + Greetd.cancelSession(); + displaymanager.messages = []; + Greetd.createSession(displaymanager.username); + } + + Connections { + target: Greetd + function onStateChanged() { + console.log("greetd state: ", GreetdState.toString(Greetd.state)); + if (Greetd.state === GreetdState.ReadyToLaunch) + Greetd.launch(displaymanager.command); + } + function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) { + displaymanager.responseVisible = echoResponse; + displaymanager.responseRequired = responseRequired; + displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": error }]); + } + function onAuthFailure(message: string) { + displaymanager.responseRequired = false; + displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": true }]); + } + } + + Component.onCompleted: { + if (Greetd.state !== GreetdState.Inactive) + Greetd.cancelSession(); + } + + Variants { + model: Quickshell.screens + + delegate: Scope { + id: screenScope + + required property var modelData + + PanelWindow { + color: "black" + + screen: screenScope.modelData + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + + LockSurface { + id: surfaceContent + + screen: screenScope.modelData + + onCurrentTextChanged: displaymanager.currentText = currentText + Connections { + target: displaymanager + function onCurrentTextChanged() { surfaceContent.currentText = displaymanager.currentText; } + function onMessagesChanged() { surfaceContent.messages = Array.from(displaymanager.messages); } + function onResponseRequiredChanged() { surfaceContent.responseRequired = displaymanager.responseRequired; } + function onResponseVisibleChanged() { surfaceContent.responseVisible = displaymanager.responseVisible; } + } + + onResponse: responseText => Greetd.respond(responseText); + Connections { + target: Greetd + function onStateChanged() { + if (Greetd.state === GreetdState.Authenticating) { + surfaceContent.authRunning = true; + } else { + surfaceContent.authRunning = false; + } + } + } + + onAuthRunningChanged: { + if (surfaceContent.authRunning && Greetd.state !== GreetdState.Authenticating) + displaymanager.startAuth(); + } + Component.onCompleted: { + surfaceContent.authRunning = Greetd.state === GreetdState.Authenticating + surfaceContent.messages = Array.from(displaymanager.messages); + surfaceContent.responseVisible = displaymanager.responseVisible; + surfaceContent.responseRequired = displaymanager.responseRequired; + surfaceContent.currentText = displaymanager.currentText; + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/shell.qml b/accounts/gkleen@skadhi/shell/quickshell/shell.qml new file mode 100644 index 00000000..1a5875f0 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/shell.qml @@ -0,0 +1,53 @@ +//@ pragma UseQApplication + +import Quickshell +import Quickshell.Wayland + +ShellRoot { + settings.watchFiles: false + + Variants { + model: Quickshell.screens + + delegate: Scope { + id: screenScope + + required property var modelData + + PanelWindow { + id: bgWindow + + screen: screenScope.modelData + + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.namespace: "background" + exclusionMode: ExclusionMode.Ignore + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + + color: "black" + + WallpaperBackground { + screen: bgWindow.screen.name + } + } + + Bar { + screen: screenScope.modelData + } + } + } + + Lockscreen {} + NiriIdle {} + + VolumeOSD {} + BrightnessOSD {} + + NotificationDisplay {} + + UnixIPC {} +} -- cgit v1.2.3