├── README.md ├── assets ├── icons │ └── nix_rose-pine.png ├── profile.gif └── wallpapers │ ├── girl.jpg │ └── wallpaper.jpg ├── flake.lock ├── flake.nix ├── hosts └── default │ ├── configuration.nix │ ├── hardware-configuration.nix │ ├── home.nix │ └── packages.nix ├── modules ├── desktop │ ├── hyprland.nix │ ├── hyprlock.nix │ └── walker.nix ├── editors │ ├── nixvim.nix │ └── vscode.nix ├── programs │ ├── fastfetch.nix │ ├── firefox.nix │ ├── ghostty.nix │ ├── obs.nix │ ├── spicetify.nix │ ├── swiftfetch.nix │ └── vesktop.nix └── quickshell │ ├── qml │ ├── Bar │ │ ├── Bar.qml │ │ ├── CustomTrayMenu.qml │ │ └── modules │ │ │ ├── DateTimeDisplay.qml │ │ │ ├── LeftCornerShape.qml │ │ │ ├── RightCornerShape.qml │ │ │ ├── TopLeftCorner.qml │ │ │ ├── TopRightCorner.qml │ │ │ └── VolumeControl.qml │ ├── Core │ │ ├── NotificationService.qml │ │ ├── ScreenBorder.qml │ │ ├── ShellWindows.qml │ │ ├── SystemTimers.qml │ │ ├── Version.qml │ │ ├── VolumeOSD.qml │ │ ├── WeatherService.qml │ │ └── WorkspaceOSD.qml │ ├── Data │ │ ├── Colors.qml │ │ ├── ProcessManager.qml │ │ └── Settings.qml │ ├── HotCorner │ │ ├── HotCornerBar.qml │ │ └── modules │ │ │ ├── HotCornerTrigger.qml │ │ │ ├── PerformanceControls.qml │ │ │ ├── RecordingButton.qml │ │ │ ├── SlideBar.qml │ │ │ ├── SystemButton.qml │ │ │ ├── SystemControls.qml │ │ │ └── UserProfile.qml │ ├── Popup │ │ ├── Cliphist.qml │ │ ├── NotificationPopup.qml │ │ ├── PopupContent.qml │ │ └── modules │ │ │ ├── CalendarView.qml │ │ │ ├── CustomTrayMenu.qml │ │ │ ├── SystemTray.qml │ │ │ ├── SystemView.qml │ │ │ └── WeatherView.qml │ └── shell.qml │ └── quickshell.nix └── system ├── environment.nix ├── filesystems.nix ├── greeter └── greetd.nix ├── programs ├── lact.nix ├── steam.nix └── stylix.nix ├── shell └── zsh.nix └── xdg.nix /README.md: -------------------------------------------------------------------------------- 1 | ![NixOS Configuration](https://i.imgur.com/kG8o6iF.jpeg) 2 | 3 | > ⚠️ **Important:** This repository includes my personal `hardware-configuration.nix`, which is specific to my hardware setup. 4 | > You **must replace it** with one generated for your system using `nixos-generate-config` to avoid compatibility issues. 5 | 6 | # NixOS Configuration 7 | 8 | This repository contains my personal NixOS configuration for a customized desktop and development environment. 🎨💻 9 | 10 | ## Directory Structure 11 | 12 | - **`assets/`** 🎨 13 | Contains custom icons and wallpapers. 14 | 15 | - **`icons/`**: Custom icon set. 16 | - **`wallpapers/`**: Collection of wallpapers. 17 | 18 | - **`dev-shells/`** 🧑‍💻 19 | Development environments. 20 | 21 | - **`hosts/`** 🖥️ 22 | Host-specific configurations. 23 | 24 | - **`default/`**: Default host configuration including `hardware-configuration.nix`, `home.nix`, and `packages.nix`. 25 | 26 | - **`modules/`** ⚙️ 27 | Custom NixOS modules for desktop, editors, programs, and more. 28 | 29 | - **`desktop/`**: Configuration for Hyprland, Waybar, and related tools. 30 | - **`editors/`**: Neovim and VSCode configurations. 31 | - **`programs/`**: Additional program configurations (e.g., Fastfetch, Ghostty). 32 | - **`quickshell/`**: The current quickshell setup, thanks to [Rexcrazy804](https://github.com/Rexcrazy804) for creating it. 33 | 34 | - **`system/`** 🔧 35 | System-wide configurations. 36 | - **`environment.nix`**: Global environment settings. 37 | - **`greeter/`**: Greetd configuration for login. 38 | - **`shell/`**: Shell configurations for Bash and Fish. 39 | - **`xdg.nix`**: XDG settings. 40 | 41 | ## Getting Started 42 | 43 | Clone this repository and adjust the configurations based on your system. Modify the host-specific files and modules to suit your needs. 44 | 45 | Feel free to customize and contribute! 46 | -------------------------------------------------------------------------------- /assets/icons/nix_rose-pine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ly-sec/nixos/60577675f20fce5c558e9b031a3b3b5b9c60e18e/assets/icons/nix_rose-pine.png -------------------------------------------------------------------------------- /assets/profile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ly-sec/nixos/60577675f20fce5c558e9b031a3b3b5b9c60e18e/assets/profile.gif -------------------------------------------------------------------------------- /assets/wallpapers/girl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ly-sec/nixos/60577675f20fce5c558e9b031a3b3b5b9c60e18e/assets/wallpapers/girl.jpg -------------------------------------------------------------------------------- /assets/wallpapers/wallpaper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ly-sec/nixos/60577675f20fce5c558e9b031a3b3b5b9c60e18e/assets/wallpapers/wallpaper.jpg -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Nixos config flake"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 | chaotic.url = "github:chaotic-cx/nyx/nyxpkgs-unstable"; 7 | hyprpolkitagent.url = "github:hyprwm/hyprpolkitagent"; 8 | hyprland.url = "github:hyprwm/Hyprland"; 9 | nur.url = "github:nix-community/NUR"; 10 | home-manager.url = "github:nix-community/home-manager"; 11 | spicetify-nix.url = "github:Gerg-L/spicetify-nix"; 12 | nixvim.url = "github:nix-community/nixvim"; 13 | 14 | stylix = { 15 | url = "github:danth/stylix"; 16 | inputs.nixpkgs.follows = "nixpkgs"; 17 | }; 18 | 19 | quickshell = { 20 | url = "git+https://git.outfoxxed.me/outfoxxed/quickshell"; 21 | inputs.nixpkgs.follows = "nixpkgs"; 22 | }; 23 | }; 24 | 25 | outputs = { self, nixpkgs, home-manager, chaotic, nur, nixvim, quickshell, ... }@inputs: { 26 | 27 | # Expose NixOS configuration 28 | nixosConfigurations.default = nixpkgs.lib.nixosSystem { 29 | system = "x86_64-linux"; 30 | specialArgs = { inherit self inputs; }; 31 | modules = [ 32 | ./hosts/default/configuration.nix 33 | inputs.stylix.nixosModules.stylix 34 | inputs.home-manager.nixosModules.default 35 | inputs.spicetify-nix.nixosModules.default 36 | chaotic.nixosModules.default 37 | 38 | ({pkgs, ...}: { 39 | environment.systemPackages = [ 40 | (quickshell.packages.${pkgs.system}.default.override { 41 | withWayland = true; 42 | withHyprland = true; 43 | withQtSvg = true; 44 | }) 45 | ]; 46 | }) 47 | ]; 48 | }; 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /hosts/default/configuration.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, inputs, lib, self, ... }: 2 | 3 | { 4 | imports = [ 5 | ./hardware-configuration.nix 6 | 7 | "${self}/system/greeter/greetd.nix" 8 | "${self}/system/programs/steam.nix" 9 | "${self}/system/programs/lact.nix" 10 | "${self}/system/programs/stylix.nix" 11 | "${self}/system/xdg.nix" 12 | "${self}/system/environment.nix" 13 | "${self}/system/filesystems.nix" 14 | inputs.home-manager.nixosModules.default 15 | ]; 16 | 17 | # Add NUR overlay 18 | nixpkgs.overlays = [ 19 | (final: prev: { 20 | nur = import inputs.nur { 21 | nurpkgs = prev; 22 | pkgs = prev; 23 | }; 24 | }) 25 | ]; 26 | 27 | # Define the lysec user 28 | users.users.lysec = { 29 | isNormalUser = true; 30 | description = "lysec"; 31 | shell = pkgs.zsh; 32 | extraGroups = [ 33 | "networkmanager" 34 | "wheel" 35 | "video" 36 | "input" 37 | "plugdev" 38 | ]; 39 | }; 40 | 41 | # Home Manager configuration 42 | home-manager = { 43 | useGlobalPkgs = true; 44 | useUserPackages = true; 45 | extraSpecialArgs = { inherit inputs; }; 46 | users = { 47 | "lysec" = import ./home.nix; 48 | }; 49 | }; 50 | 51 | # Fonts 52 | fonts.packages = with pkgs; [ 53 | fira-sans 54 | roboto 55 | nerd-fonts._0xproto 56 | nerd-fonts.droid-sans-mono 57 | jetbrains-mono 58 | noto-fonts 59 | noto-fonts-emoji 60 | noto-fonts-cjk-sans 61 | noto-fonts-cjk-serif 62 | ]; 63 | 64 | # Bootloader 65 | boot.loader.systemd-boot.enable = true; 66 | boot.loader.efi.canTouchEfiVariables = true; 67 | 68 | # Enable flakes 69 | nix.settings.experimental-features = [ 70 | "nix-command" 71 | "flakes" 72 | ]; 73 | 74 | networking.hostName = "nixos"; 75 | 76 | # Enable networking 77 | networking.networkmanager.enable = true; 78 | 79 | # Set your time zone. 80 | time.timeZone = "Europe/Berlin"; 81 | 82 | # Select internationalisation properties. 83 | i18n.defaultLocale = "en_US.UTF-8"; 84 | 85 | # Enable ZSH 86 | programs.zsh.enable = true; 87 | 88 | i18n.extraLocaleSettings = { 89 | LC_ADDRESS = "de_DE.UTF-8"; 90 | LC_IDENTIFICATION = "de_DE.UTF-8"; 91 | LC_MEASUREMENT = "de_DE.UTF-8"; 92 | LC_MONETARY = "de_DE.UTF-8"; 93 | LC_NAME = "de_DE.UTF-8"; 94 | LC_NUMERIC = "de_DE.UTF-8"; 95 | LC_PAPER = "de_DE.UTF-8"; 96 | LC_TELEPHONE = "de_DE.UTF-8"; 97 | LC_TIME = "de_DE.UTF-8"; 98 | }; 99 | 100 | # Enable the X11 windowing system. 101 | services.xserver.enable = true; 102 | services.xserver.videoDrivers = [ "amdgpu" ]; 103 | 104 | services.power-profiles-daemon.enable = true; 105 | 106 | services.dbus.enable = true; 107 | xdg.portal.enable = true; 108 | 109 | # Configure keymap in X11 110 | services.xserver.xkb = { 111 | layout = "de"; 112 | variant = ""; 113 | }; 114 | 115 | # Configure console keymap 116 | console.keyMap = "de"; 117 | 118 | # Enable CUPS to print documents. 119 | services.printing.enable = true; 120 | 121 | # Enable sound with pipewire. 122 | security.rtkit.enable = true; 123 | services.pipewire = { 124 | enable = true; 125 | alsa.enable = true; 126 | alsa.support32Bit = true; 127 | pulse.enable = true; 128 | }; 129 | 130 | # v4l2loopback support (DroidCam) 131 | boot.kernelModules = [ "v4l2loopback" ]; 132 | 133 | boot.extraModprobeConfig = '' 134 | options v4l2loopback video_nr=0 card_label="DroidCam" exclusive_caps=1 135 | ''; 136 | boot.kernelPackages = pkgs.linuxPackages_cachyos; 137 | 138 | boot.extraModulePackages = with config.boot.kernelPackages; [ 139 | v4l2loopback 140 | ]; 141 | 142 | nix.settings.auto-optimise-store = true; 143 | nix.gc = { 144 | automatic = true; 145 | dates = "daily"; 146 | options = "--delete-older-than 5d"; 147 | }; 148 | 149 | programs.thunar.enable = true; 150 | services.gvfs.enable = true; 151 | services.tumbler.enable = true; 152 | 153 | services.sunshine = { 154 | enable = true; 155 | autoStart = false; 156 | capSysAdmin = true; 157 | openFirewall = true; 158 | }; 159 | 160 | nixpkgs.config.allowUnfree = true; 161 | 162 | home-manager.backupFileExtension = "backup"; 163 | 164 | system.stateVersion = "25.05"; 165 | 166 | system.activationScripts.logRebuildTime = { 167 | text = '' 168 | LOG_FILE="/var/log/nixos-rebuild-log.json" 169 | TIMESTAMP=$(date "+%d/%m") 170 | GENERATION=$(readlink /nix/var/nix/profiles/system | grep -o '[0-9]\+') 171 | 172 | echo "{\"last_rebuild\": \"$TIMESTAMP\", \"generation\": $GENERATION}" > "$LOG_FILE" 173 | chmod 644 "$LOG_FILE" 174 | ''; 175 | }; 176 | } 177 | -------------------------------------------------------------------------------- /hosts/default/hardware-configuration.nix: -------------------------------------------------------------------------------- 1 | # Do not modify this file! It was generated by ‘nixos-generate-config’ 2 | # and may be overwritten by future invocations. Please make changes 3 | # to /etc/nixos/configuration.nix instead. 4 | { config, lib, pkgs, modulesPath, ... }: 5 | 6 | { 7 | imports = 8 | [ (modulesPath + "/installer/scan/not-detected.nix") 9 | ]; 10 | 11 | boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" ]; 12 | boot.initrd.kernelModules = [ ]; 13 | boot.kernelModules = [ "kvm-amd" ]; 14 | boot.extraModulePackages = [ ]; 15 | 16 | fileSystems."/" = 17 | { device = "/dev/disk/by-uuid/bec032ac-9ac3-4aea-bb64-23f118fefd70"; 18 | fsType = "ext4"; 19 | }; 20 | 21 | fileSystems."/boot" = 22 | { device = "/dev/disk/by-uuid/63A3-88B2"; 23 | fsType = "vfat"; 24 | options = [ "fmask=0077" "dmask=0077" ]; 25 | }; 26 | 27 | swapDevices = 28 | [ { device = "/dev/disk/by-uuid/2832e55e-70e7-4556-a961-079624db0281"; } 29 | ]; 30 | 31 | # Enables DHCP on each ethernet and wireless interface. In case of scripted networking 32 | # (the default) this is the recommended approach. When using systemd-networkd it's 33 | # still possible to use this option, but it's recommended to use it in conjunction 34 | # with explicit per-interface declarations with `networking.interfaces..useDHCP`. 35 | networking.useDHCP = lib.mkDefault true; 36 | # networking.interfaces.enp14s0.useDHCP = lib.mkDefault true; 37 | # networking.interfaces.wlp15s0.useDHCP = lib.mkDefault true; 38 | 39 | nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; 40 | hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; 41 | } 42 | -------------------------------------------------------------------------------- /hosts/default/home.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | inputs, 5 | self, 6 | ... 7 | }: 8 | 9 | let 10 | allPackages = import ./packages.nix { inherit pkgs; }; 11 | in 12 | { 13 | home.username = "lysec"; 14 | home.homeDirectory = "/home/lysec"; 15 | 16 | imports = [ 17 | ../../modules/desktop/hyprland.nix 18 | ../../modules/quickshell/quickshell.nix 19 | ../../modules/desktop/hyprlock.nix 20 | ../../modules/desktop/walker.nix 21 | 22 | ../../modules/editors/vscode.nix 23 | ../../modules/editors/nixvim.nix 24 | 25 | ../../modules/programs/ghostty.nix 26 | ../../modules/programs/fastfetch.nix 27 | ../../modules/programs/spicetify.nix 28 | ../../modules/programs/obs.nix 29 | ../../modules/programs/vesktop.nix 30 | ../../modules/programs/firefox.nix 31 | 32 | ../../system/shell/zsh.nix 33 | 34 | inputs.hyprland.homeManagerModules.default 35 | inputs.spicetify-nix.homeManagerModules.default 36 | inputs.nixvim.homeManagerModules.nixvim 37 | ]; 38 | 39 | home.packages = allPackages; 40 | 41 | xdg.portal.enable = true; 42 | 43 | home.stateVersion = "24.11"; 44 | 45 | home.sessionVariables = { 46 | EDITOR = "nvim"; 47 | }; 48 | 49 | services.cliphist = { 50 | enable = true; 51 | allowImages = true; 52 | }; 53 | 54 | programs.home-manager.enable = true; 55 | } 56 | -------------------------------------------------------------------------------- /hosts/default/packages.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | ... 4 | }: 5 | 6 | with pkgs; 7 | [ 8 | # Applications 9 | protonplus 10 | lutris 11 | dolphin-emu 12 | prismlauncher 13 | heroic 14 | peazip 15 | 16 | # TUI 17 | btop 18 | yazi 19 | 20 | # Desktop 21 | hyprlock 22 | nwg-look 23 | walker 24 | 25 | # Development 26 | nodejs 27 | rustup 28 | gcc 29 | gh 30 | nixfmt-rfc-style 31 | nixpkgs-fmt 32 | black 33 | 34 | # Utilities 35 | jq 36 | socat 37 | tree 38 | libnotify 39 | nvd 40 | wl-clipboard 41 | pywalfox-native 42 | imagemagick 43 | amdvlk 44 | rar 45 | unzip 46 | droidcam 47 | gowall 48 | gruvbox-gtk-theme 49 | papirus-icon-theme 50 | qt6Packages.qt5compat 51 | grimblast 52 | gpu-screen-recorder 53 | mpv 54 | slop 55 | ] 56 | 57 | -------------------------------------------------------------------------------- /modules/desktop/hyprland.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | inputs, 5 | ... 6 | }: 7 | 8 | { 9 | home.packages = with pkgs; [ 10 | inputs.hyprpolkitagent.packages."${pkgs.system}".hyprpolkitagent 11 | ]; 12 | 13 | stylix.targets.hyprland.enable = false; 14 | 15 | wayland.windowManager.hyprland = { 16 | enable = true; 17 | systemd.enable = true; 18 | xwayland.enable = true; 19 | 20 | settings = { 21 | # Default Apps 22 | "$mainMod" = "SUPER"; 23 | "$terminal" = "ghostty"; 24 | "$browser" = "firefox"; 25 | "$menu" = "walker"; 26 | "$fileManager" = "thunar"; 27 | 28 | # Keybinds Start 29 | bind = [ 30 | "CTRL SHIFT, 1, exec, grimblast --freeze copy area" 31 | "CTRL SHIFT, 2, exec, grimblast --freeze copy window" 32 | "CTRL SHIFT, 3, exec, grimblast --freeze copy output" 33 | 34 | "$mainMod, RETURN, exec, $terminal" 35 | "$mainMod, B, exec, $browser" 36 | "$mainMod CTRL, RETURN, exec, $menu" 37 | "$mainMod CTRL, W, exec, waypaper" 38 | "$mainMod, E, exec, $fileManager" 39 | "$mainMod, T, togglefloating" 40 | "$mainMod, F, fullscreen" 41 | "$mainMod, J, togglesplit" 42 | "$mainMod, Q, killactive" 43 | 44 | "$mainMod, 1, workspace, 1" 45 | "$mainMod, 2, workspace, 2" 46 | "$mainMod, 3, workspace, 3" 47 | "$mainMod, 4, workspace, 4" 48 | "$mainMod, 5, workspace, 5" 49 | "$mainMod, 6, workspace, 6" 50 | "$mainMod, 7, workspace, 7" 51 | "$mainMod, 8, workspace, 8" 52 | "$mainMod, 9, workspace, 9" 53 | "$mainMod, 0, workspace, 10" 54 | 55 | "$mainMod SHIFT, 1, movetoworkspace, 1" 56 | "$mainMod SHIFT, 2, movetoworkspace, 2" 57 | "$mainMod SHIFT, 3, movetoworkspace, 3" 58 | "$mainMod SHIFT, 4, movetoworkspace, 4" 59 | "$mainMod SHIFT, 5, movetoworkspace, 5" 60 | "$mainMod SHIFT, 6, movetoworkspace, 6" 61 | "$mainMod SHIFT, 7, movetoworkspace, 7" 62 | "$mainMod SHIFT, 8, movetoworkspace, 8" 63 | "$mainMod SHIFT, 9, movetoworkspace, 9" 64 | "$mainMod SHIFT, 0, movetoworkspace, 10" 65 | ]; 66 | 67 | bindm = [ 68 | "$mainMod, mouse:272, movewindow" 69 | "$mainMod, mouse:273, resizewindow" 70 | ]; 71 | 72 | bindel = [ 73 | ", xf86audioraisevolume, exec, pactl set-sink-volume '@DEFAULT_SINK@' +5% && pactl get-sink-volume '@DEFAULT_SINK@' | grep -oP '\\d+(?=%)' | awk '{if($1>100) system(\"pactl set-sink-volume '@DEFAULT_SINK@' 100%\")}'" 74 | ", xf86audiolowervolume, exec, pactl set-sink-volume '@DEFAULT_SINK@' -5%" 75 | ]; 76 | 77 | input = { 78 | kb_layout = "de"; 79 | follow_mouse = 1; 80 | sensitivity = -0.2; 81 | }; 82 | 83 | binds = { 84 | "workspace_back_and_forth" = "true"; 85 | "allow_workspace_cycles" = "true"; 86 | "pass_mouse_when_bound" = "false"; 87 | }; 88 | # Keybinds End 89 | 90 | general = { 91 | gaps_in = 5; 92 | gaps_out = 14; 93 | border_size = 3; 94 | "col.active_border" = "rgb(eb6f92)"; 95 | "col.inactive_border" = "rgb(6e6a86)"; 96 | }; 97 | 98 | decoration = { 99 | active_opacity = 1.0; 100 | inactive_opacity = 0.95; 101 | rounding = 20; 102 | 103 | blur = { 104 | enabled = true; 105 | size = 6; 106 | passes = 2; 107 | new_optimizations = "on"; 108 | ignore_opacity = true; 109 | xray = true; 110 | }; 111 | }; 112 | 113 | animations = { 114 | enabled = true; 115 | 116 | bezier = [ "myBezier, 0.05, 0.9, 0.1, 1.05" ]; 117 | animation = [ 118 | "windows, 1, 7, myBezier" 119 | "windowsOut, 1, 7, default, popin 80%" 120 | "border, 1, 10, default" 121 | "borderangle, 1, 8, default" 122 | "fade, 1, 7, default" 123 | "workspaces, 1, 6, default" 124 | ]; 125 | }; 126 | 127 | dwindle = { 128 | pseudotile = true; 129 | preserve_split = true; 130 | }; 131 | 132 | master = { 133 | new_status = "master"; 134 | }; 135 | 136 | # Startup starts 137 | exec-once = [ 138 | "hyprctl setcursor theme_NotwaitaBlack 22" 139 | "systemctl --user start hyprpolkitagent" 140 | "[workspace 2] vesktop" 141 | "arrpc" 142 | "waypaper --restore" 143 | #"swaync" 144 | #"ironbar" 145 | "qs" # Run quickshell 146 | #"waybar -c ~/.config/waybar/config -s ~/.config/waybar/style.css" 147 | ]; 148 | # Startup ends 149 | 150 | # Env starts 151 | env = [ 152 | "XDG_SESSION_TYPE , wayland " 153 | "XDG_CURRENT_DESKTOP , Hyprland " 154 | "XDG_SESSION_DESKTOP , Hyprland " 155 | 156 | "DISABLE_QT5_COMPAT , 1 " 157 | "QT_QPA_PLATFORM , wayland " 158 | "QT_AUTO_SCREEN_SCALE_FACTOR , 1 " 159 | "QT_WAYLAND_DISABLE_WINDOWDECORATION , 1 " 160 | 161 | "MOZ_ENABLE_WAYLAND , 1 " 162 | "NIXOS_OZONE_WL , 1 " 163 | "ELECTRON_OZONE_PLATFORM_HINT , auto " 164 | 165 | "GTK_WAYLAND_DISABLE_WINDOWDECORATION, 1 " 166 | 167 | "GDK_SCALE , 1.25 " 168 | 169 | "XCURSOR_THEME ,Adwaita " 170 | "XCURSOR_SIZE ,24 " 171 | ]; 172 | # Env ends 173 | 174 | # Monitor starts 175 | monitor = [ 176 | "DP-1, 3440x1440@144, auto, auto" 177 | ]; 178 | # Monitor ends 179 | 180 | xwayland = { 181 | force_zero_scaling = true; 182 | }; 183 | 184 | }; 185 | }; 186 | home.file.".config/hypr/xdph.conf".text = '' 187 | screencopy { 188 | allow_token_by_default = true 189 | } 190 | ''; 191 | } 192 | -------------------------------------------------------------------------------- /modules/desktop/hyprlock.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | home.file.".config/hypr/hyprlock.conf".text = '' 5 | # BACKGROUND 6 | background { 7 | monitor = 8 | path = ${config.home.homeDirectory}/nixos/assets/wallpapers/girl.jpg 9 | #color = $background" 10 | blur_passes = 2 11 | contrast = 1 12 | brightness = 0.5 13 | vibrancy = 0.2 14 | vibrancy_darkness = 0.2 15 | } 16 | 17 | # GENERAL 18 | general { 19 | no_fade_in = true 20 | no_fade_out = true 21 | hide_cursor = false 22 | grace = 0 23 | disable_loading_bar = true 24 | } 25 | 26 | # INPUT FIELD 27 | input-field { 28 | monitor = 29 | size = 250, 60 30 | outline_thickness = 3 # changed from 2 to 3 31 | outline_color = rgb(235, 111, 146) # #eb6f92 pinkish rose pine accent 32 | dots_size = 0.2 33 | dots_spacing = 0.35 34 | dots_center = true 35 | outer_color = rgb(235, 111, 146) # rose pine surface (dark grayish) 36 | inner_color = rgba(68, 71, 90, 0.6) # rose pine base variant (darker) 37 | font_color = rgb(242, 234, 218) # rose pine text (light cream) 38 | fade_on_empty = false 39 | rounding = -1 40 | check_color = rgb(235, 111, 146) # same rose pine pink accent 41 | placeholder_text = Input Password... 42 | hide_input = false 43 | position = 0, -200 44 | halign = center 45 | valign = center 46 | } 47 | 48 | # DATE 49 | label { 50 | monitor = 51 | text = cmd[update:1000] LC_TIME=en_US.UTF-8 date +"%A, %B %d" 52 | color = rgba(242, 234, 218, 0.75) # rose pine text with transparency 53 | font_size = 22 54 | font_family = JetBrains Mono 55 | position = 0, 300 56 | halign = center 57 | valign = center 58 | } 59 | 60 | # TIME 61 | label { 62 | monitor = 63 | text = cmd[update:1000] echo "$(date +"%-I:%M")" 64 | color = rgba(242, 234, 218, 0.75) # rose pine text with transparency 65 | font_size = 95 66 | font_family = JetBrains Mono Extrabold 67 | position = 0, 200 68 | halign = center 69 | valign = center 70 | } 71 | 72 | ''; 73 | } 74 | -------------------------------------------------------------------------------- /modules/desktop/walker.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | 3 | { 4 | home.packages = with pkgs; [ 5 | walker 6 | ]; 7 | home.file.".config/walker/config.toml".text = '' 8 | app_launch_prefix = "" 9 | terminal_title_flag = "" 10 | locale = "" 11 | close_when_open = false 12 | theme = "rose-pine" 13 | monitor = "" 14 | hotreload_theme = true 15 | as_window = false 16 | timeout = 0 17 | disable_click_to_close = false 18 | force_keyboard_focus = false 19 | 20 | [keys] 21 | accept_typeahead = ["tab"] 22 | trigger_labels = "lalt" 23 | next = ["down"] 24 | prev = ["up"] 25 | close = ["esc"] 26 | remove_from_history = ["shift backspace"] 27 | resume_query = ["ctrl r"] 28 | toggle_exact_search = ["ctrl m"] 29 | 30 | [keys.activation_modifiers] 31 | keep_open = "shift" 32 | alternate = "alt" 33 | 34 | [keys.ai] 35 | clear_session = ["ctrl x"] 36 | copy_last_response = ["ctrl c"] 37 | resume_session = ["ctrl r"] 38 | run_last_response = ["ctrl e"] 39 | 40 | [events] 41 | on_activate = "" 42 | on_selection = "" 43 | on_exit = "" 44 | on_launch = "" 45 | on_query_change = "" 46 | 47 | [list] 48 | dynamic_sub = true 49 | keyboard_scroll_style = "emacs" 50 | max_entries = 50 51 | show_initial_entries = true 52 | single_click = true 53 | visibility_threshold = 20 54 | placeholder = "No Results" 55 | 56 | [search] 57 | argument_delimiter = "#" 58 | placeholder = "Search..." 59 | delay = 0 60 | resume_last_query = false 61 | 62 | [activation_mode] 63 | labels = "jkl;asdf" 64 | 65 | [builtins.applications] 66 | weight = 5 67 | name = "applications" 68 | placeholder = "Applications" 69 | prioritize_new = true 70 | hide_actions_with_empty_query = true 71 | context_aware = true 72 | refresh = true 73 | show_sub_when_single = true 74 | show_icon_when_single = true 75 | show_generic = true 76 | history = true 77 | 78 | [builtins.applications.actions] 79 | enabled = true 80 | hide_category = false 81 | hide_without_query = true 82 | 83 | [builtins.bookmarks] 84 | weight = 5 85 | placeholder = "Bookmarks" 86 | name = "bookmarks" 87 | icon = "bookmark" 88 | switcher_only = true 89 | 90 | [[builtins.bookmarks.entries]] 91 | label = "Walker" 92 | url = "https://github.com/abenz1267/walker" 93 | keywords = ["walker", "github"] 94 | 95 | [builtins.xdph_picker] 96 | hidden = true 97 | weight = 5 98 | placeholder = "Screen/Window Picker" 99 | show_sub_when_single = true 100 | name = "xdphpicker" 101 | switcher_only = true 102 | 103 | [builtins.ai] 104 | weight = 5 105 | placeholder = "AI" 106 | name = "ai" 107 | icon = "help-browser" 108 | switcher_only = true 109 | 110 | [[builtins.ai.anthropic.prompts]] 111 | model = "claude-3-5-sonnet-20241022" 112 | temperature = 1 113 | max_tokens = 1_000 114 | label = "General Assistant" 115 | prompt = "You are a helpful general assistant. Keep your answers short and precise." 116 | 117 | [builtins.calc] 118 | require_number = true 119 | weight = 5 120 | name = "calc" 121 | icon = "accessories-calculator" 122 | placeholder = "Calculator" 123 | min_chars = 4 124 | 125 | [builtins.windows] 126 | weight = 5 127 | icon = "view-restore" 128 | name = "windows" 129 | placeholder = "Windows" 130 | show_icon_when_single = true 131 | 132 | [builtins.clipboard] 133 | exec = "wl-copy" 134 | weight = 5 135 | name = "clipboard" 136 | avoid_line_breaks = true 137 | placeholder = "Clipboard" 138 | image_height = 300 139 | max_entries = 10 140 | switcher_only = true 141 | 142 | [builtins.commands] 143 | weight = 5 144 | icon = "utilities-terminal" 145 | switcher_only = true 146 | name = "commands" 147 | placeholder = "Commands" 148 | 149 | [builtins.custom_commands] 150 | weight = 5 151 | icon = "utilities-terminal" 152 | name = "custom_commands" 153 | placeholder = "Custom Commands" 154 | [builtins.emojis] 155 | exec = "wl-copy" 156 | weight = 5 157 | name = "emojis" 158 | placeholder = "Emojis" 159 | switcher_only = true 160 | history = true 161 | typeahead = true 162 | show_unqualified = false 163 | 164 | [builtins.symbols] 165 | after_copy = "" 166 | weight = 5 167 | name = "symbols" 168 | placeholder = "Symbols" 169 | switcher_only = true 170 | history = true 171 | typeahead = true 172 | 173 | [builtins.finder] 174 | use_fd = false 175 | weight = 5 176 | icon = "file" 177 | name = "finder" 178 | placeholder = "Finder" 179 | switcher_only = true 180 | ignore_gitignore = true 181 | refresh = true 182 | concurrency = 8 183 | show_icon_when_single = true 184 | 185 | [builtins.runner] 186 | weight = 5 187 | icon = "utilities-terminal" 188 | name = "runner" 189 | placeholder = "Runner" 190 | typeahead = true 191 | history = true 192 | generic_entry = false 193 | refresh = true 194 | 195 | [builtins.ssh] 196 | weight = 5 197 | icon = "preferences-system-network" 198 | name = "ssh" 199 | placeholder = "SSH" 200 | switcher_only = true 201 | history = true 202 | refresh = true 203 | 204 | [builtins.switcher] 205 | weight = 5 206 | name = "switcher" 207 | placeholder = "Switcher" 208 | prefix = "/" 209 | 210 | [builtins.websearch] 211 | weight = 5 212 | icon = "applications-internet" 213 | name = "websearch" 214 | placeholder = "Websearch" 215 | 216 | [[builtins.websearch.entries]] 217 | name = "Google" 218 | url = "https://www.google.com/search?q=%TERM%" 219 | 220 | [[builtins.websearch.entries]] 221 | name = "DuckDuckGo" 222 | url = "https://duckduckgo.com/?q=%TERM%" 223 | switcher_only = true 224 | 225 | [[builtins.websearch.entries]] 226 | name = "Ecosia" 227 | url = "https://www.ecosia.org/search?q=%TERM%" 228 | switcher_only = true 229 | 230 | [[builtins.websearch.entries]] 231 | name = "Yandex" 232 | url = "https://yandex.com/search/?text=%TERM%" 233 | switcher_only = true 234 | 235 | [builtins.dmenu] 236 | hidden = true 237 | weight = 5 238 | name = "dmenu" 239 | placeholder = "Dmenu" 240 | switcher_only = true 241 | ''; 242 | home.file.".config/walker/themes/rose-pine.css".text = '' 243 | @define-color foreground #e0def4; /* text */ 244 | @define-color foreground-light #f6c177; /* accent text */ 245 | @define-color background #191724; /* base */ 246 | @define-color cursor #524f67; /* subtle gray */ 247 | 248 | @define-color color0 #191724; /* base */ 249 | @define-color color1 #eb6f92; /* love */ 250 | @define-color color2 #9ccfd8; /* foam */ 251 | @define-color color3 #f6c177; /* gold */ 252 | @define-color color4 #31748f; /* iris */ 253 | @define-color color5 #c4a7e7; /* highlight */ 254 | @define-color color6 #ebbcba; /* rose */ 255 | @define-color color7 #e0def4; /* text */ 256 | 257 | @define-color color8 #6e6a86; /* surface2 */ 258 | @define-color color9 #eb6f92; /* love */ 259 | @define-color color10 #9ccfd8; /* foam */ 260 | @define-color color11 #f6c177; /* gold */ 261 | @define-color color12 #31748f; /* iris */ 262 | @define-color color13 #c4a7e7; /* highlight */ 263 | @define-color color14 #ebbcba; /* rose */ 264 | @define-color color15 #e0def4; /* text */ 265 | 266 | 267 | 268 | #window, 269 | #box, 270 | #aiScroll, 271 | #aiList, 272 | #search, 273 | #password, 274 | #input, 275 | #prompt, 276 | #clear, 277 | #typeahead, 278 | #list, 279 | child, 280 | scrollbar, 281 | slider, 282 | #item, 283 | #text, 284 | #label, 285 | #bar, 286 | #sub, 287 | #activationlabel { 288 | all: unset; 289 | } 290 | 291 | #cfgerr { 292 | background: rgba(255, 0, 0, 0.4); 293 | margin-top: 20px; 294 | padding: 8px; 295 | font-size: 1.2em; 296 | } 297 | 298 | #window { 299 | color: @foreground; 300 | } 301 | 302 | #box { 303 | border-radius: 20px; 304 | background: @background; 305 | padding: 32px; 306 | border: 3px solid @color1; 307 | box-shadow: 308 | 0 19px 38px rgba(0, 0, 0, 0.3), 309 | 0 15px 12px rgba(0, 0, 0, 0.22); 310 | } 311 | 312 | 313 | #search { 314 | box-shadow: 315 | 0 1px 3px rgba(0, 0, 0, 0.1), 316 | 0 1px 2px rgba(0, 0, 0, 0.22); 317 | background: lighter(@background); 318 | color: @foreground-light; 319 | padding: 8px; 320 | } 321 | 322 | #prompt { 323 | margin-left: 4px; 324 | margin-right: 12px; 325 | color: @foreground; 326 | opacity: 0.2; 327 | } 328 | 329 | #clear { 330 | color: @foreground; 331 | opacity: 0.8; 332 | } 333 | 334 | #password, 335 | #input, 336 | #typeahead { 337 | border-radius: 2px; 338 | } 339 | 340 | #input { 341 | background: none; 342 | } 343 | 344 | #password { 345 | } 346 | 347 | #spinner { 348 | padding: 8px; 349 | } 350 | 351 | #typeahead { 352 | color: @foreground; 353 | opacity: 0.8; 354 | } 355 | 356 | #input placeholder { 357 | opacity: 0.5; 358 | } 359 | 360 | #list { 361 | } 362 | 363 | child { 364 | padding: 8px; 365 | border-radius: 2px; 366 | color: @foreground-light; 367 | } 368 | 369 | child:selected, 370 | child:hover { 371 | color: @foreground; 372 | background: @color1; 373 | } 374 | 375 | #item { 376 | } 377 | 378 | #icon { 379 | margin-right: 8px; 380 | } 381 | 382 | #text { 383 | } 384 | 385 | #label { 386 | font-weight: 500; 387 | } 388 | 389 | #sub { 390 | opacity: 0.5; 391 | font-size: 0.8em; 392 | } 393 | 394 | #activationlabel { 395 | } 396 | 397 | #bar { 398 | } 399 | 400 | .barentry { 401 | } 402 | 403 | .activation #activationlabel { 404 | } 405 | 406 | .activation #text, 407 | .activation #icon, 408 | .activation #search { 409 | opacity: 0.5; 410 | } 411 | 412 | .aiItem { 413 | padding: 10px; 414 | border-radius: 2px; 415 | color: @foreground; 416 | background: @background; 417 | } 418 | 419 | .aiItem.user { 420 | padding-left: 0; 421 | padding-right: 0; 422 | } 423 | 424 | .aiItem.assistant { 425 | background: lighter(@background); 426 | } 427 | 428 | ''; 429 | home.file.".config/walker/themes/rose-pine.toml".text = '' 430 | 431 | [ui.anchors] 432 | bottom = true 433 | left = true 434 | right = true 435 | top = true 436 | 437 | [ui.window] 438 | h_align = "fill" 439 | v_align = "fill" 440 | 441 | [ui.window.box] 442 | h_align = "center" 443 | width = 450 444 | 445 | [ui.window.box.bar] 446 | orientation = "horizontal" 447 | position = "end" 448 | 449 | [ui.window.box.bar.entry] 450 | h_align = "fill" 451 | h_expand = true 452 | 453 | [ui.window.box.bar.entry.icon] 454 | h_align = "center" 455 | h_expand = true 456 | pixel_size = 24 457 | theme = "" 458 | 459 | [ui.window.box.margins] 460 | top = 200 461 | 462 | [ui.window.box.ai_scroll] 463 | name = "aiScroll" 464 | h_align = "fill" 465 | v_align = "fill" 466 | max_height = 300 467 | min_width = 400 468 | height = 300 469 | width = 400 470 | 471 | [ui.window.box.ai_scroll.margins] 472 | top = 8 473 | 474 | [ui.window.box.ai_scroll.list] 475 | name = "aiList" 476 | orientation = "vertical" 477 | width = 400 478 | spacing = 10 479 | 480 | [ui.window.box.ai_scroll.list.item] 481 | name = "aiItem" 482 | h_align = "fill" 483 | v_align = "fill" 484 | x_align = 0 485 | y_align = 0 486 | wrap = true 487 | 488 | [ui.window.box.scroll.list] 489 | max_height = 300 490 | max_width = 400 491 | min_width = 400 492 | width = 400 493 | 494 | [ui.window.box.scroll.list.item.activation_label] 495 | h_align = "fill" 496 | v_align = "fill" 497 | width = 20 498 | x_align = 0.5 499 | y_align = 0.5 500 | 501 | [ui.window.box.scroll.list.item.icon] 502 | pixel_size = 26 503 | theme = "" 504 | 505 | [ui.window.box.scroll.list.margins] 506 | top = 8 507 | 508 | [ui.window.box.search.prompt] 509 | name = "prompt" 510 | icon = "edit-find" 511 | theme = "" 512 | pixel_size = 18 513 | h_align = "center" 514 | v_align = "center" 515 | 516 | [ui.window.box.search.clear] 517 | name = "clear" 518 | icon = "edit-clear" 519 | theme = "" 520 | pixel_size = 18 521 | h_align = "center" 522 | v_align = "center" 523 | 524 | [ui.window.box.search.input] 525 | h_align = "fill" 526 | h_expand = true 527 | icons = true 528 | 529 | [ui.window.box.search.spinner] 530 | hide = true 531 | 532 | 533 | ''; 534 | } 535 | -------------------------------------------------------------------------------- /modules/editors/nixvim.nix: -------------------------------------------------------------------------------- 1 | 2 | { config, lib, pkgs, ... }: 3 | 4 | { 5 | programs.nixvim = { 6 | enable = true; 7 | viAlias = true; 8 | vimAlias = true; 9 | 10 | # Basic options 11 | opts = { 12 | number = true; 13 | relativenumber = true; 14 | shiftwidth = 2; 15 | tabstop = 2; 16 | expandtab = true; 17 | smartindent = true; 18 | wrap = false; 19 | swapfile = false; 20 | termguicolors = true; 21 | }; 22 | 23 | # Plugins 24 | plugins = { 25 | lualine = { 26 | enable = true; 27 | }; 28 | web-devicons = { 29 | enable = true; 30 | }; 31 | 32 | nvim-tree = { 33 | enable = true; 34 | openOnSetup = true; 35 | disableNetrw = true; 36 | hijackNetrw = true; 37 | updateFocusedFile.enable = true; 38 | view = { 39 | width = 30; 40 | side = "left"; 41 | }; 42 | renderer = { 43 | highlightGit = true; 44 | icons.show.file = true; 45 | icons.show.folder = true; 46 | }; 47 | }; 48 | 49 | telescope = { 50 | enable = true; 51 | }; 52 | 53 | treesitter = { 54 | enable = true; 55 | }; 56 | 57 | presence-nvim = { 58 | enable = true; 59 | enableLineNumber = true; 60 | autoUpdate = true; 61 | }; 62 | 63 | cmp.enable = true; 64 | comment.enable = true; 65 | 66 | vim-surround.enable = true; 67 | fugitive.enable = true; 68 | 69 | # LSP plugin and servers 70 | lsp = { 71 | enable = true; 72 | servers = { 73 | lua_ls = {}; 74 | pyright = {}; 75 | ts_ls = {}; 76 | nil_ls = { 77 | enable = true; 78 | settings = { 79 | formatting.command = ["nixpkgs-fmt"]; 80 | }; 81 | }; 82 | }; 83 | }; 84 | }; 85 | 86 | globals = { 87 | mapleader = " "; # Use space as leader 88 | maplocalleader = " "; # Optional: set local leader too 89 | }; 90 | 91 | # Key mappings 92 | keymaps = [ 93 | { 94 | key = "ff"; 95 | action = "Telescope find_files"; 96 | options.desc = "Find files"; 97 | } 98 | { 99 | key = "fg"; 100 | action = "Telescope live_grep"; 101 | options.desc = "Live grep"; 102 | } 103 | { 104 | key = "w"; 105 | action = "NvimTreeToggle"; 106 | options.desc = "Toggle file explorer"; 107 | } 108 | { 109 | key = "n"; 110 | action = "NvimTreeFindFile"; 111 | options.desc = "Find current file in explorer"; 112 | } 113 | ]; 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /modules/editors/vscode.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | 3 | { 4 | programs.vscode = { 5 | enable = true; 6 | profiles.default.extensions = with pkgs.vscode-extensions; [ 7 | jnoortheen.nix-ide 8 | esbenp.prettier-vscode 9 | ]; 10 | 11 | profiles.default.userSettings = { 12 | "editor.formatOnSave" = true; 13 | "editor.defaultFormatter" = "esbenp.prettier-vscode"; 14 | }; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /modules/programs/fastfetch.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | { 3 | programs.fastfetch = { 4 | enable = true; 5 | settings = { 6 | logo = { 7 | source = "~/nixos/assets/icons/nix_rose-pine.png"; 8 | padding = { 9 | top = 2; 10 | left = 3; 11 | }; 12 | width = 40; 13 | }; 14 | modules = [ 15 | "break" 16 | { 17 | type = "custom"; 18 | format = "──────────────────────Hardware──────────────────────"; 19 | } 20 | 21 | { 22 | type = "cpu"; 23 | key = " "; 24 | showPeCoreCount = true; 25 | keyColor = "33"; 26 | } 27 | { 28 | type = "gpu"; 29 | key = " 󰍛"; 30 | keyColor = "33"; 31 | } 32 | { 33 | type = "memory"; 34 | key = " "; 35 | keyColor = "33"; 36 | } 37 | { 38 | type = "custom"; 39 | format = "────────────────────────────────────────────────────"; 40 | } 41 | "break" 42 | { 43 | type = "custom"; 44 | format = "──────────────────────Software──────────────────────"; 45 | } 46 | { 47 | type = "os"; 48 | key = " 󱄅"; 49 | keyColor = "yellow"; 50 | } 51 | { 52 | type = "kernel"; 53 | key = " "; 54 | keyColor = "yellow"; 55 | } 56 | { 57 | type = "packages"; 58 | key = " "; 59 | keyColor = "yellow"; 60 | } 61 | { 62 | type = "wm"; 63 | key = " 󰇄"; 64 | keyColor = "33"; 65 | } 66 | { 67 | type = "lm"; 68 | key = " 󰍂"; 69 | keyColor = "33"; 70 | } 71 | { 72 | type = "terminal"; 73 | key = " "; 74 | keyColor = "33"; 75 | } 76 | { 77 | type = "shell"; 78 | key = " "; 79 | keyColor = "33"; 80 | } 81 | { 82 | type = "custom"; 83 | format = "────────────────────────────────────────────────────"; 84 | } 85 | "break" 86 | { 87 | type = "custom"; 88 | format = "────────────────────Uptime / Age────────────────────"; 89 | } 90 | { 91 | type = "command"; 92 | key = " OS Age "; 93 | keyColor = "33"; 94 | text = "birth_install=$(stat -c %W /); current=$(date +%s); time_progression=$((current - birth_install)); days_difference=$((time_progression / 86400)); echo $days_difference days"; 95 | } 96 | { 97 | type = "uptime"; 98 | key = " Uptime "; 99 | keyColor = "33"; 100 | } 101 | { 102 | type = "custom"; 103 | format = "────────────────────────────────────────────────────"; 104 | } 105 | "break" 106 | ]; 107 | }; 108 | }; 109 | } 110 | -------------------------------------------------------------------------------- /modules/programs/firefox.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | { 3 | programs.firefox = { 4 | enable = true; 5 | 6 | profiles = { 7 | lysec = { 8 | isDefault = true; 9 | 10 | extensions.packages = with pkgs.nur.repos.rycee.firefox-addons; [ 11 | bitwarden 12 | darkreader 13 | sponsorblock 14 | ublock-origin 15 | ]; 16 | 17 | settings = { 18 | # Show previous session on startup (2 = restore session) 19 | "browser.startup.page" = 2; 20 | 21 | # Remember zoom level per site 22 | "browser.zoom.siteSpecific" = true; 23 | 24 | # Cookie persistence behavior 25 | "network.cookie.lifetimePolicy" = 0; # Accept cookies normally 26 | "privacy.clearOnShutdown.cookies" = false; # Don't clear cookies on shutdown 27 | "privacy.clearOnShutdown.siteSettings" = false; # Keep site permissions/settings 28 | "privacy.sanitize.sanitizeOnShutdown" = false; # Disable general auto-cleanup 29 | 30 | # Needed for proper zoom and feature behavior — disables anti-fingerprinting zoom locking 31 | "privacy.resistFingerprinting" = false; 32 | 33 | # URL bar autocomplete behavior 34 | "browser.urlbar.autoFill" = true; # Autocomplete URLs as you type 35 | "browser.urlbar.dnsFirstForSingleWords" = true; # Resolve single words as domains if possible 36 | 37 | # Enable suggestions in the address bar 38 | "browser.urlbar.suggest.history" = true; # Suggest URLs from history 39 | "browser.urlbar.suggest.bookmark" = true; # Suggest bookmarks 40 | "browser.urlbar.suggest.openpage" = true; # Suggest open tabs 41 | "browser.urlbar.suggest.searches" = true; # Suggest search engine suggestions 42 | 43 | # Enable search suggestions in the search bar 44 | "browser.search.suggest.enabled" = true; 45 | 46 | # Disable password saving prompt 47 | "signon.rememberSignons" = false; 48 | "passwordmanager.enabled" = false; 49 | 50 | # Remove the "Firefox View" button/tab 51 | "browser.tabs.firefox-view" = false; 52 | 53 | # Disable Pocket integration (saves articles to Firefox Account) 54 | "extensions.pocket.enabled" = false; 55 | 56 | # Remove ads and sponsored content from new tab page 57 | "browser.newtabpage.activity-stream.showSponsored" = false; 58 | "browser.newtabpage.activity-stream.showSponsoredTopSites" = false; 59 | 60 | # Disable news/top stories from Mozilla on new tab page 61 | "browser.newtabpage.activity-stream.feeds.system.topstories" = false; 62 | 63 | # (Optional additions if you want a clean new tab page) 64 | # Disable highlights like recent history and bookmarks 65 | # "browser.newtabpage.activity-stream.feeds.section.highlights" = false; 66 | # "browser.newtabpage.activity-stream.section.highlights.includeVisited" = false; 67 | # "browser.newtabpage.activity-stream.section.highlights.includeBookmarks" = false; 68 | }; 69 | 70 | search = { 71 | force = true; 72 | default = "ddg"; 73 | order = [ "searxng" "nix-packages" "nixos-wiki" "ddg" ]; 74 | 75 | engines = { 76 | searxng = { 77 | urls = [ 78 | { template = "https://searx.org/search?q={searchTerms}"; } 79 | ]; 80 | icon = "https://searx.org/favicon.ico"; 81 | updateInterval = 86400000; # 24h 82 | definedAliases = [ "@searx" ]; 83 | suggestUrls = [ 84 | { template = "https://searx.org/autosuggest?q={searchTerms}"; } 85 | ]; 86 | }; 87 | 88 | "nix-packages" = { 89 | urls = [ 90 | { 91 | template = "https://search.nixos.org/packages?type=packages&query={searchTerms}"; 92 | params = [ 93 | { name = "type"; value = "packages"; } 94 | { name = "query"; value = "{searchTerms}"; } 95 | ]; 96 | } 97 | ]; 98 | icon = "https://nixos.wiki/favicon.png"; 99 | definedAliases = [ "@np" ]; 100 | }; 101 | 102 | "nixos-wiki" = { 103 | urls = [ 104 | { template = "https://nixos.wiki/index.php?search={searchTerms}"; } 105 | ]; 106 | icon = "https://nixos.wiki/favicon.png"; 107 | updateInterval = 86400000; 108 | definedAliases = [ "@nw" ]; 109 | }; 110 | 111 | ddg = { 112 | urls = [ 113 | { template = "https://duckduckgo.com/?q={searchTerms}"; } 114 | ]; 115 | definedAliases = [ "@ddg" ]; 116 | }; 117 | 118 | # Hide Bing from the UI 119 | bing.metaData.hidden = true; 120 | 121 | # Give Google an alias 122 | google.metaData.alias = "@g"; 123 | }; 124 | }; 125 | }; 126 | }; 127 | }; 128 | 129 | stylix.targets.firefox.profileNames = [ "lysec" ]; 130 | stylix.enableReleaseChecks = false; 131 | } 132 | -------------------------------------------------------------------------------- /modules/programs/ghostty.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, ... }: 2 | { 3 | programs.ghostty = { 4 | enable = true; 5 | settings = { 6 | font-size = 11; 7 | font-family = "JetBrainsMono Nerd Font"; 8 | 9 | window-decoration = false; 10 | 11 | # Disables ligatures 12 | font-feature = [ 13 | "-liga" 14 | "-dlig" 15 | "-calt" 16 | ]; 17 | }; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /modules/programs/obs.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | 3 | { 4 | home.packages = [ 5 | (pkgs.wrapOBS { 6 | plugins = with pkgs.obs-studio-plugins; [ 7 | obs-vkcapture 8 | obs-webkitgtk 9 | obs-vaapi 10 | obs-composite-blur 11 | ]; 12 | }) 13 | pkgs.libva 14 | pkgs.libva-utils 15 | pkgs.vaapiVdpau 16 | pkgs.libvdpau-va-gl 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /modules/programs/spicetify.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | inputs, 5 | ... 6 | }: 7 | 8 | let 9 | spicePkgs = inputs.spicetify-nix.legacyPackages.${pkgs.system}; 10 | in 11 | { 12 | programs.spicetify = { 13 | enable = true; 14 | enabledExtensions = with spicePkgs.extensions; [ 15 | hidePodcasts 16 | shuffle 17 | groupSession 18 | fullAppDisplay 19 | ]; 20 | enabledCustomApps = with spicePkgs.apps; [ 21 | marketplace 22 | ]; 23 | }; 24 | } -------------------------------------------------------------------------------- /modules/programs/swiftfetch.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | home.file.".config/swiftfetch/ascii.txt".text = '' 5 | ▗▄▄▄ ▗▄▄▄▄ ▄▄▄▖ 6 | ▜███▙ ▜███▙ ▟███▛ 7 | ▜███▙ ▜███▙▟███▛ 8 | ▜███▙ ▜██████▛ 9 | ▟█████████████████▙ ▜████▛ ▟▙ 10 | ▟███████████████████▙ ▜███▙ ▟██▙ 11 | ▄▄▄▄▖ ▜███▙ ▟███▛ 12 | ▟███▛ ▜██▛ ▟███▛ 13 | ▟███▛ ▜▛ ▟███▛ 14 | ▟███████████▛ ▟██████████▙ 15 | ▜██████████▛ ▟███████████▛ 16 | ▟███▛ ▟▙ ▟███▛ 17 | ▟███▛ ▟██▙ ▟███▛ 18 | ▟███▛ ▜███▙ ▝▀▀▀▀ 19 | ▜██▛ ▜███▙ ▜██████████████████▛ 20 | ▜▛ ▟████▙ ▜████████████████▛ 21 | ▟██████▙ ▜███▙ 22 | ▟███▛▜███▙ ▜███▙ 23 | ▟███▛ ▜███▙ ▜███▙ 24 | ▝▀▀▀ ▀▀▀▀▘ ▀▀▀▘ 25 | ''; 26 | 27 | home.file.".config/swiftfetch/config.toml".text = '' 28 | [display] 29 | separator = ": " 30 | ascii_path = "~/.config/swiftfetch/ascii.txt" 31 | ascii_color = "blue" 32 | 33 | [colors] 34 | green = "#A6CC70" 35 | blue = "#39BAE6" 36 | magenta = "#D2A6FF" 37 | yellow = "#39BAE6" # replaced with blue 38 | red = "#F07178" 39 | cyan = "#95E6CB" 40 | white = "#C7C7C7" 41 | 42 | [[display.items]] 43 | key = "" 44 | type = "text" 45 | value = "" 46 | 47 | [[display.items]] 48 | key = "user_info" 49 | type = "default" 50 | value = "user_info" 51 | value_color = "white" 52 | 53 | [[display.items]] 54 | key = "" 55 | type = "text" 56 | value = "┌────────────────── System Information ───────────────────┐" 57 | color = "blue" 58 | 59 | [[display.items]] 60 | key = " 󰣇 ‣ os" 61 | type = "default" 62 | value = "os" 63 | color = "blue" 64 | value_color = "white" 65 | 66 | [[display.items]] 67 | key = " 󰍛 ‣ kernel" 68 | type = "default" 69 | value = "kernel" 70 | color = "blue" 71 | value_color = "white" 72 | 73 | [[display.items]] 74 | key = "  ‣ wm" 75 | type = "default" 76 | value = "wm" 77 | color = "blue" 78 | value_color = "white" 79 | 80 | [[display.items]] 81 | key = "  ‣ editor" 82 | type = "default" 83 | value = "editor" 84 | color = "blue" 85 | value_color = "white" 86 | 87 | [[display.items]] 88 | key = "  ‣ shell" 89 | type = "default" 90 | value = "shell" 91 | color = "blue" 92 | value_color = "white" 93 | 94 | [[display.items]] 95 | key = "  ‣ term" 96 | type = "default" 97 | value = "terminal" 98 | color = "blue" 99 | value_color = "white" 100 | 101 | [[display.items]] 102 | key = "  ‣ pkgs" 103 | type = "default" 104 | value = "pkg_count" 105 | color = "blue" 106 | value_color = "white" 107 | 108 | [[display.items]] 109 | key = "  ‣ flat" 110 | type = "default" 111 | value = "flatpak_pkg_count" 112 | color = "blue" 113 | value_color = "white" 114 | 115 | [[display.items]] 116 | key = "" 117 | type = "text" 118 | value = "├───────────────── Hardware Information ─────────────────┤" 119 | color = "blue" 120 | 121 | [[display.items]] 122 | key = " 󰍛 ‣ cpu" 123 | type = "default" 124 | value = "cpu" 125 | color = "blue" 126 | value_color = "white" 127 | 128 | [[display.items]] 129 | key = " 󰓅 ‣ ram" 130 | type = "default" 131 | value = "memory" 132 | color = "blue" 133 | value_color = "white" 134 | 135 | [[display.items]] 136 | key = "" 137 | type = "text" 138 | value = "├───────────────── Uptime Information ───────────────────┤" 139 | color = "blue" 140 | 141 | [[display.items]] 142 | key = "  ‣ uptime" 143 | type = "default" 144 | value = "uptime_seconds" 145 | color = "blue" 146 | value_color = "white" 147 | 148 | [[display.items]] 149 | key = "  ‣ age" 150 | type = "default" 151 | value = "os_age" 152 | color = "blue" 153 | value_color = "white" 154 | 155 | [[display.items]] 156 | key = "" 157 | type = "text" 158 | value = "└────────────────────────────────────────────────────────┘" 159 | color = "blue" 160 | ''; 161 | } 162 | -------------------------------------------------------------------------------- /modules/programs/vesktop.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | 3 | { 4 | home.packages = with pkgs; [ 5 | vesktop 6 | ]; 7 | home.file.".config/vesktop/themes/system24-rosepine.theme.css".text = '' 8 | /** 9 | * @name system24 (rosé pine edit) 10 | * @description a tui-like discord theme with Rosé Pine colors. 11 | * @author refact0r 12 | */ 13 | 14 | @import url("https://refact0r.github.io/system24/build/system24.css"); 15 | 16 | body { 17 | --font: "JetBrains Mono"; 18 | --code-font: "JetBrains Mono"; 19 | font-weight: 300; 20 | letter-spacing: -0.05ch; 21 | 22 | --gap: 12px; 23 | --divider-thickness: 4px; 24 | --border-thickness: 2px; 25 | --border-hover-transition: 0.2s ease; 26 | 27 | --animations: on; 28 | --list-item-transition: 0.2s ease; 29 | --dms-icon-svg-transition: 0.4s ease; 30 | 31 | --top-bar-height: var(--gap); 32 | --top-bar-button-position: titlebar; 33 | --top-bar-title-position: off; 34 | --subtle-top-bar-title: off; 35 | 36 | --custom-window-controls: off; 37 | --window-control-size: 14px; 38 | 39 | --custom-dms-icon: off; 40 | --dms-icon-svg-url: url(""); 41 | --dms-icon-svg-size: 90%; 42 | --dms-icon-color-before: var(--icon-secondary); 43 | --dms-icon-color-after: var(--text-1); 44 | --custom-dms-background: off; 45 | --dms-background-image-url: url(""); 46 | --dms-background-image-size: cover; 47 | --dms-background-color: linear-gradient( 48 | 70deg, 49 | var(--rose), 50 | var(--iris), 51 | var(--love) 52 | ); 53 | 54 | --background-image: off; 55 | --background-image-url: url(""); 56 | 57 | --transparency-tweaks: off; 58 | --remove-bg-layer: off; 59 | --panel-blur: off; 60 | --blur-amount: 12px; 61 | --bg-floating: var(--bg-3); 62 | 63 | --small-user-panel: on; 64 | --unrounding: on; 65 | 66 | --custom-spotify-bar: on; 67 | --ascii-titles: on; 68 | --ascii-loader: system24; 69 | 70 | --panel-labels: on; 71 | --label-color: var(--text-muted); 72 | --label-font-weight: 500; 73 | } 74 | 75 | :root { 76 | --colors: on; 77 | 78 | /* Rosé Pine Palette */ 79 | --base: #191724; 80 | --surface: #1f1d2e; 81 | --overlay: #26233a; 82 | --muted: #7e7a94; /* slightly brighter for better contrast */ 83 | --subtle: #a8a4c2; /* more readable than original */ 84 | --text: #f6f3ff; /* brighter than #e0def4 */ 85 | --love: #eb6f92; 86 | --gold: #f6c177; 87 | --rose: #ebbcba; 88 | --pine: #31748f; 89 | --foam: #9ccfd8; 90 | --iris: #c4a7e7; 91 | --highlight-low: #21202e; 92 | --highlight-med: #403d52; 93 | --highlight-high: #524f67; 94 | 95 | /* Text colors */ 96 | --text-0: #000000; 97 | --text-1: var(--text); 98 | --text-2: var(--subtle); 99 | --text-3: var(--muted); 100 | --text-4: #5e5a70; 101 | --text-5: #4a4658; 102 | 103 | /* Backgrounds */ 104 | --bg-1: var(--overlay); 105 | --bg-2: var(--surface); 106 | --bg-3: var(--base); 107 | --bg-4: var(--highlight-low); 108 | --hover: rgba(235, 111, 146, 0.08); 109 | --active: rgba(235, 111, 146, 0.15); 110 | --active-2: rgba(235, 111, 146, 0.2); 111 | --message-hover: var(--hover); 112 | 113 | 114 | /* Accent colors */ 115 | --accent-1: var(--foam); 116 | --accent-2: var(--pine); 117 | --accent-3: var(--iris); 118 | --accent-4: var(--rose); 119 | --accent-5: var(--gold); 120 | 121 | --mention: linear-gradient(to right, rgba(246, 193, 119, 0.1) 40%, transparent); 122 | --mention-hover: linear-gradient(to right, rgba(246, 193, 119, 0.2) 40%, transparent); 123 | --reply: linear-gradient(to right, rgba(235, 111, 146, 0.08) 40%, transparent); 124 | --reply-hover: linear-gradient(to right, rgba(235, 111, 146, 0.15) 40%, transparent); 125 | 126 | /* Status indicators */ 127 | --online: #9ccfd8; 128 | --dnd: #eb6f92; 129 | --idle: #f6c177; 130 | --streaming: #c4a7e7; 131 | --offline: #6e6a86; 132 | 133 | /* Borders (now using Rosé Pine `love`) */ 134 | --border-light: #eb6f92; 135 | --border: #eb6f92; 136 | --border-hover: #eb6f92; 137 | --button-border: #eb6f92; 138 | 139 | /* Red (from love) */ 140 | --red-1: #f08399; 141 | --red-2: #eb6f92; 142 | --red-3: #d45b7a; 143 | --red-4: #be4966; 144 | --red-5: #a93552; 145 | 146 | /* Green (from foam) */ 147 | --green-1: #b1e3ed; 148 | --green-2: #9ccfd8; 149 | --green-3: #85bac3; 150 | --green-4: #6ea5ae; 151 | --green-5: #598f99; 152 | 153 | /* Blue (from pine) */ 154 | --blue-1: #5c93ab; 155 | --blue-2: #4d8198; 156 | --blue-3: #3e6f85; 157 | --blue-4: #2f5d72; 158 | --blue-5: #204b5f; 159 | 160 | /* Purple (from iris) */ 161 | --purple-1: #d9c2f0; 162 | --purple-2: #c4a7e7; 163 | --purple-3: #ae8cd3; 164 | --purple-4: #9971be; 165 | --purple-5: #8356aa; 166 | 167 | /* Yellow (from gold) */ 168 | --yellow-1: #f8d3a6; 169 | --yellow-2: #f6c177; 170 | --yellow-3: #d6a05f; 171 | --yellow-4: #b68047; 172 | --yellow-5: #965f2f; 173 | } 174 | 175 | ''; 176 | } 177 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Bar/Bar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | import Quickshell 5 | import Quickshell.Hyprland 6 | import Quickshell.Wayland 7 | import Quickshell.Io 8 | import QtQuick.Shapes 9 | import "root:/Data" as Data 10 | import "./modules" as BarModules 11 | import "root:/Core/" as Core 12 | Item { 13 | id: root 14 | required property var shell 15 | required property var popup 16 | required property var bar 17 | readonly property alias barRect: barRect 18 | width: 120 19 | height: 42 20 | property Process pavucontrol: null 21 | 22 | function createPavucontrol() { 23 | if (!pavucontrol) { 24 | pavucontrol = pavucontrolComponent.createObject(root) 25 | } 26 | return pavucontrol 27 | } 28 | 29 | Component { 30 | id: pavucontrolComponent 31 | Process { 32 | command: ["pavucontrol"] 33 | } 34 | } 35 | 36 | Rectangle { 37 | id: barRect 38 | anchors.top: parent.top 39 | anchors.left: parent.left 40 | anchors.right: parent.right 41 | height: 42 42 | color: shell.bgColor 43 | bottomLeftRadius: 20 44 | bottomRightRadius: 20 45 | opacity: 1 46 | 47 | Item { 48 | anchors.fill: parent 49 | anchors.leftMargin: 16 50 | anchors.rightMargin: 16 51 | 52 | RowLayout { 53 | anchors.fill: parent 54 | spacing: 12 55 | 56 | Item { Layout.fillWidth: true } 57 | 58 | BarModules.DateTimeDisplay { 59 | shell: root.shell 60 | } 61 | 62 | Item { Layout.fillWidth: true } 63 | 64 | 65 | } 66 | } 67 | } 68 | 69 | 70 | BarModules.RightCornerShape { 71 | shell: root.shell 72 | popup: root.popup 73 | barRect: barRect 74 | } 75 | 76 | BarModules.LeftCornerShape { 77 | shell: root.shell 78 | popup: root.popup 79 | barRect: barRect 80 | } 81 | 82 | BarModules.TopLeftCorner { 83 | shell: root.shell 84 | popup: root.popup 85 | barRect: barRect 86 | } 87 | 88 | BarModules.TopRightCorner { 89 | shell: root.shell 90 | popup: root.popup 91 | barRect: barRect 92 | } 93 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/Bar/CustomTrayMenu.qml: -------------------------------------------------------------------------------- 1 | pragma ComponentBehavior: Bound 2 | import QtQuick 3 | import QtQuick.Controls 4 | import QtQuick.Layouts 5 | import Quickshell 6 | import "root:/Data/" as Data 7 | 8 | Rectangle { 9 | id: trayMenu 10 | width: 180 11 | height: Math.max(40, listView.contentHeight + 12) 12 | clip: true 13 | color: Data.Colors.bgColor 14 | border.color: Data.Colors.accentColor 15 | border.width: 3 16 | radius: 20 17 | visible: false 18 | 19 | // Only accept mouse events when visible, disables interaction when hidden 20 | enabled: visible 21 | 22 | property QsMenuHandle menu 23 | property point triggerPoint: Qt.point(0, 0) 24 | property Item originalParent 25 | 26 | // Toggle visibility of the menu 27 | function toggle() { 28 | if (visible) { 29 | hide() 30 | } else { 31 | show(triggerPoint, originalParent) 32 | } 33 | } 34 | 35 | // Hide menu on right-click outside 36 | function closeOnRightClick() { 37 | if (visible) { 38 | hide() 39 | } 40 | } 41 | 42 | QsMenuOpener { 43 | id: opener 44 | menu: trayMenu.menu 45 | } 46 | 47 | // Overlay covers entire screen, catches clicks outside menu to close it 48 | // Only active when menu is visible to avoid unnecessary event capturing 49 | Rectangle { 50 | id: overlay 51 | x: -trayMenu.x 52 | y: -trayMenu.y 53 | width: Screen.width 54 | height: Screen.height 55 | color: "transparent" 56 | visible: trayMenu.visible // Overlay visible only when menu is visible 57 | z: -1 // Behind menu 58 | 59 | MouseArea { 60 | anchors.fill: parent 61 | enabled: trayMenu.visible // Only enabled when menu is visible 62 | acceptedButtons: Qt.AllButtons 63 | onPressed: { 64 | trayMenu.hide() // Close menu on any click outside 65 | } 66 | } 67 | } 68 | 69 | ListView { 70 | id: listView 71 | anchors.fill: parent 72 | anchors.margins: 6 73 | spacing: 2 74 | interactive: false 75 | enabled: trayMenu.visible // Enable interaction only when menu is visible 76 | 77 | model: ScriptModel { 78 | values: opener.children ? [...opener.children.values] : [] 79 | } 80 | 81 | delegate: Rectangle { 82 | id: entry 83 | required property var modelData 84 | 85 | width: listView.width 86 | height: (modelData?.isSeparator) ? 8 : 28 87 | color: "transparent" 88 | radius: 4 89 | 90 | // Separator line if needed 91 | Rectangle { 92 | anchors.centerIn: parent 93 | width: parent.width - 20 94 | height: 1 95 | color: Qt.darker(Data.Colors.bgColor, 1.4) 96 | visible: modelData?.isSeparator ?? false 97 | } 98 | 99 | // Background highlight on hover for non-separators 100 | Rectangle { 101 | anchors.fill: parent 102 | color: mouseArea.containsMouse ? Data.Colors.highlightBg : "transparent" 103 | radius: 4 104 | visible: !(modelData?.isSeparator ?? false) 105 | 106 | RowLayout { 107 | anchors.fill: parent 108 | anchors.leftMargin: 10 109 | anchors.rightMargin: 10 110 | spacing: 8 111 | 112 | Text { 113 | Layout.fillWidth: true 114 | color: (modelData?.enabled ?? true) ? Data.Colors.fgColor : Qt.darker(Data.Colors.fgColor, 1.8) 115 | text: modelData?.text ?? "" 116 | font.pixelSize: 13 117 | verticalAlignment: Text.AlignVCenter 118 | elide: Text.ElideRight 119 | } 120 | 121 | Image { 122 | Layout.preferredWidth: 16 123 | Layout.preferredHeight: 16 124 | source: modelData?.icon ?? "" 125 | visible: (modelData?.icon ?? "") !== "" 126 | fillMode: Image.PreserveAspectFit 127 | } 128 | } 129 | 130 | MouseArea { 131 | id: mouseArea 132 | anchors.fill: parent 133 | hoverEnabled: true 134 | enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible 135 | 136 | onClicked: { 137 | if (modelData && !modelData.isSeparator) { 138 | modelData.triggered() 139 | trayMenu.hide() 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | // Show menu at given point and reparent if needed, with boundary correction 148 | function show(point, parentItem) { 149 | 150 | if (parentItem) { 151 | originalParent = parent 152 | parent = parentItem 153 | x = point.x 154 | y = point.y 155 | 156 | // Adjust position to keep menu fully visible on screen 157 | const globalPos = mapToGlobal(0, 0) 158 | if (globalPos.x + width > Screen.width) { 159 | x = point.x - width + 24 160 | } 161 | if (globalPos.y + height > Screen.height) { 162 | y = point.y - height - 5 163 | } 164 | } else { 165 | parent = null 166 | x = point.x 167 | y = point.y 168 | } 169 | 170 | visible = true 171 | } 172 | 173 | // Hide menu 174 | function hide() { 175 | visible = false 176 | } 177 | 178 | Keys.onEscapePressed: hide() 179 | 180 | // When menu becomes visible, grab focus for keyboard events 181 | onVisibleChanged: { 182 | if (visible) { 183 | forceActiveFocus() 184 | } 185 | } 186 | 187 | // Restore original parent on destruction if reparented 188 | Component.onDestruction: { 189 | if (originalParent) parent = originalParent 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Bar/modules/DateTimeDisplay.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | 5 | Column { 6 | required property var shell // Shell object providing time, date, colors 7 | 8 | width: 80 9 | spacing: 2 10 | Layout.alignment: Qt.AlignVCenter // Center vertically in layout 11 | 12 | Label { 13 | id: timeLabel 14 | width: parent.width 15 | text: shell.time // Current time string from shell 16 | color: shell.accentColor // Accent color from shell 17 | font.family: "FiraCode Nerd Font" 18 | font.pixelSize: 14 19 | font.bold: true 20 | horizontalAlignment: Text.AlignHCenter 21 | verticalAlignment: Text.AlignVCenter 22 | 23 | // Subtle pulsing scale animation synced with time update (1 minute cycle) 24 | SequentialAnimation on scale { 25 | loops: Animation.Infinite 26 | NumberAnimation { 27 | to: 1.02 28 | duration: 60000 // Pulse expands slowly over 1 minute 29 | easing.type: Easing.InOutSine 30 | } 31 | NumberAnimation { 32 | to: 1.0 33 | duration: 200 // Snap back quickly 34 | easing.type: Easing.OutCubic 35 | } 36 | PauseAnimation { duration: 59800 } // Pause before next pulse 37 | } 38 | } 39 | 40 | Label { 41 | width: parent.width 42 | text: shell.date // Current date string from shell 43 | color: Qt.lighter(shell.fgColor, 1.2) // Slightly lighter than foreground color 44 | font.family: "FiraCode Nerd Font" 45 | font.pixelSize: 10 46 | horizontalAlignment: Text.AlignHCenter 47 | verticalAlignment: Text.AlignVCenter 48 | 49 | // Fade-in effect on component load 50 | opacity: 0 51 | Component.onCompleted: opacity = 1 52 | Behavior on opacity { 53 | NumberAnimation { 54 | duration: 800 55 | easing.type: Easing.OutCubic 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Bar/modules/LeftCornerShape.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Shapes 4 | 5 | Shape { 6 | id: leftCornerShape 7 | required property var shell 8 | required property var popup 9 | required property var barRect 10 | 11 | width: 60 12 | height: barRect.height 13 | y: barRect.y 14 | x: barRect.x - width + 34 15 | preferredRendererType: Shape.CurveRenderer 16 | 17 | opacity: 1 18 | 19 | ShapePath { 20 | strokeWidth: 0 21 | fillColor: shell.accentColor 22 | strokeColor: "transparent" 23 | startX: 0 24 | startY: 0 25 | PathLine { x: leftCornerShape.width - 15; y: 0 } 26 | PathLine { x: leftCornerShape.width - 15; y: 20 } 27 | PathArc { 28 | x: leftCornerShape.width - 5; y: 40 29 | radiusX: 20; radiusY: 20 30 | useLargeArc: false 31 | direction: PathArc.Counterclockwise 32 | } 33 | PathLine { x: leftCornerShape.width; y: 42 } 34 | PathLine { x: 20; y: leftCornerShape.height } 35 | PathArc { 36 | x: 0; y: leftCornerShape.height - 20 37 | radiusX: 20; radiusY: 20 38 | useLargeArc: false 39 | direction: PathArc.Clockwise 40 | } 41 | PathLine { x: 0; y: 0 } 42 | } 43 | 44 | Rectangle { 45 | id: popupButtonsLeft 46 | width: 24 47 | height: 24 48 | radius: 20 49 | color: popup.visible ? Qt.darker(shell.accentColor, 1.3) : shell.bgColor 50 | anchors.verticalCenter: parent.verticalCenter 51 | x: width - 13 52 | 53 | state: popup.visible ? "active" : (leftPopupMouseArea.containsMouse ? "hovered" : "normal") 54 | 55 | states: [ 56 | State { 57 | name: "normal" 58 | PropertyChanges { target: popupButtonsLeft; scale: 1.0 } 59 | PropertyChanges { target: iconLabel; scale: 1.0 } 60 | }, 61 | State { 62 | name: "hovered" 63 | PropertyChanges { target: popupButtonsLeft; scale: 1.05 } 64 | PropertyChanges { target: iconLabel; scale: 1.1 } 65 | }, 66 | State { 67 | name: "active" 68 | PropertyChanges { target: popupButtonsLeft; scale: 1.1 } 69 | PropertyChanges { target: iconLabel; scale: 1.1 } 70 | } 71 | ] 72 | 73 | transitions: [ 74 | Transition { 75 | NumberAnimation { 76 | properties: "scale" 77 | duration: 150 78 | easing.type: Easing.OutCubic 79 | } 80 | ColorAnimation { 81 | properties: "color" 82 | duration: 200 83 | easing.type: Easing.OutCubic 84 | } 85 | } 86 | ] 87 | 88 | Label { 89 | id: iconLabel 90 | anchors.centerIn: parent 91 | text: "󰘖" 92 | font.family: "FiraCode Nerd Font" 93 | font.pixelSize: 14 94 | color: shell.fgColor 95 | } 96 | 97 | MouseArea { 98 | id: leftPopupMouseArea 99 | anchors.fill: parent 100 | hoverEnabled: true 101 | onClicked: { 102 | // Close the clipboard window if it's open 103 | if (shell.cliphistWindow && shell.cliphistWindow.visible) { 104 | shell.cliphistWindow.visible = false 105 | } 106 | 107 | // Toggle main popup 108 | popup.visible = !popup.visible 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/Bar/modules/RightCornerShape.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Shapes 4 | 5 | Shape { 6 | id: rightCornerShape 7 | required property var shell 8 | required property var popup 9 | required property var barRect 10 | width: 60 11 | height: barRect.height 12 | y: barRect.y 13 | x: barRect.x + barRect.width - 16 14 | preferredRendererType: Shape.CurveRenderer 15 | 16 | // Entrance animation 17 | opacity: 0 18 | Component.onCompleted: opacity = 1 19 | Behavior on opacity { 20 | NumberAnimation { 21 | duration: 600 22 | easing.type: Easing.OutCubic 23 | } 24 | } 25 | 26 | ShapePath { 27 | strokeWidth: 0 28 | fillColor: shell.accentColor 29 | strokeColor: "transparent" 30 | 31 | startX: rightCornerShape.width 32 | startY: 0 33 | 34 | PathLine { x: 15; y: 0 } 35 | PathLine { x: 15; y: 20 } 36 | 37 | PathArc { 38 | x: 5; y: 40 39 | radiusX: 20; radiusY: 20 40 | useLargeArc: false 41 | direction: PathArc.Clockwise 42 | } 43 | 44 | PathLine { x: 0; y: 42 } 45 | PathLine { x: rightCornerShape.width - 20; y: rightCornerShape.height } 46 | 47 | PathArc { 48 | x: rightCornerShape.width; y: rightCornerShape.height - 20 49 | radiusX: 20; radiusY: 20 50 | useLargeArc: false 51 | direction: PathArc.Counterclockwise 52 | } 53 | 54 | PathLine { x: rightCornerShape.width; y: 0 } 55 | } 56 | 57 | Rectangle { 58 | id: popupButtons 59 | width: 24 60 | height: 24 61 | radius: 20 62 | color: (shell.cliphistWindow && shell.cliphistWindow.visible) ? Qt.darker(shell.accentColor, 1.3) : shell.bgColor 63 | anchors.verticalCenter: parent.verticalCenter 64 | x: 23 65 | 66 | property bool isHovered: rightPopupMouseArea.containsMouse 67 | property bool isCliphistActive: shell.cliphistWindow && shell.cliphistWindow.visible 68 | 69 | // Smooth color transitions 70 | Behavior on color { 71 | ColorAnimation { 72 | duration: 200 73 | easing.type: Easing.OutCubic 74 | } 75 | } 76 | 77 | // Hover and active state animations 78 | scale: isCliphistActive ? 1.1 : (isHovered ? 1.05 : 1.0) 79 | Behavior on scale { 80 | NumberAnimation { 81 | duration: 150 82 | easing.type: Easing.OutCubic 83 | } 84 | } 85 | 86 | Label { 87 | anchors.centerIn: parent 88 | text: "" 89 | font.family: "FiraCode Nerd Font" 90 | font.pixelSize: 14 91 | color: shell.fgColor 92 | 93 | // Clipboard icon bounce animation 94 | scale: parent.isHovered ? 1.1 : 1.0 95 | Behavior on scale { 96 | NumberAnimation { 97 | duration: 150 98 | easing.type: Easing.OutBack 99 | } 100 | } 101 | } 102 | 103 | MouseArea { 104 | id: rightPopupMouseArea 105 | anchors.fill: parent 106 | hoverEnabled: true 107 | onClicked: { 108 | console.log("Right button clicked - toggling cliphistWindow") 109 | if (shell.cliphistWindow) { 110 | // Close the main popup if it's open 111 | if (popup.visible) { 112 | popup.visible = false 113 | } 114 | 115 | // Toggle clipboard window 116 | if (shell.cliphistWindow.visible) { 117 | shell.cliphistWindow.visible = false 118 | } else { 119 | shell.cliphistWindow.visible = true 120 | } 121 | } else { 122 | console.log("cliphistWindow not found") 123 | } 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/Bar/modules/TopLeftCorner.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Shapes 4 | 5 | Shape { 6 | id: topLeftCorner 7 | required property var shell 8 | required property var popup 9 | required property var barRect 10 | width: 20 11 | height: 32 12 | y: barRect.y 13 | x: barRect.x + barRect.width - 164 14 | preferredRendererType: Shape.CurveRenderer 15 | 16 | opacity: 0 17 | Component.onCompleted: opacity = 1 18 | Behavior on opacity { 19 | NumberAnimation { 20 | duration: 600 21 | easing.type: Easing.OutCubic 22 | } 23 | } 24 | 25 | ShapePath { 26 | strokeWidth: 0 27 | fillColor: shell.accentColor 28 | strokeColor: "transparent" 29 | 30 | // Start at middle height LEFT edge (mirrored from right edge) 31 | startX: 0 32 | startY: 5 33 | 34 | // Concave arc from middle left to bottom right corner (mirrored horizontally) 35 | PathArc { 36 | x: width 37 | y: height 38 | radiusX: 40 39 | radiusY: 40 40 | useLargeArc: false 41 | direction: PathArc.Clockwise // flipped direction for concavity 42 | } 43 | 44 | // Line up the right edge 45 | PathLine { x: width; y: 0 } 46 | 47 | // Line along top edge back to left top corner 48 | PathLine { x: 0; y: 0 } 49 | 50 | // Close the path back to start 51 | PathLine { x: 0; y: 5 } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Bar/modules/TopRightCorner.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Shapes 4 | 5 | Shape { 6 | id: topRightCorner 7 | required property var shell 8 | required property var popup 9 | required property var barRect 10 | width: 20 11 | height: 32 12 | y: barRect.y 13 | x: barRect.x + barRect.width + 42 14 | preferredRendererType: Shape.CurveRenderer 15 | 16 | opacity: 0 17 | Component.onCompleted: opacity = 1 18 | Behavior on opacity { 19 | NumberAnimation { 20 | duration: 600 21 | easing.type: Easing.OutCubic 22 | } 23 | } 24 | 25 | ShapePath { 26 | strokeWidth: 0 27 | fillColor: shell.accentColor 28 | strokeColor: "transparent" 29 | 30 | // Start at middle height right edge (vertically flipped) 31 | startX: width 32 | startY: 5 33 | 34 | // Concave arc from middle right to bottom left corner (vertically flipped) 35 | PathArc { 36 | x: 0 37 | y: height 38 | radiusX: 40 39 | radiusY: 40 40 | useLargeArc: false 41 | direction: PathArc.Counterclockwise // changed to keep concave shape 42 | } 43 | 44 | // Line up the left edge 45 | PathLine { x: 0; y: 0 } 46 | 47 | // Line along top edge back to right top corner 48 | PathLine { x: 20; y: 0 } 49 | 50 | // Close the path back to start 51 | PathLine { x: width; y: height / 2 } 52 | } 53 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/Bar/modules/VolumeControl.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | import Quickshell 5 | import "root:/Data" as Data 6 | 7 | Rectangle { 8 | id: volumeRect 9 | required property var shell 10 | required property var pavucontrol 11 | 12 | width: 48 13 | height: 24 14 | radius: 20 15 | color: shell.accentColor 16 | Layout.alignment: Qt.AlignVCenter 17 | 18 | // Scale up on hover 19 | state: volumeMouseArea.containsMouse ? "hovered" : "normal" 20 | 21 | states: [ 22 | State { 23 | name: "normal" 24 | PropertyChanges { target: volumeRect; scale: 1.0 } 25 | }, 26 | State { 27 | name: "hovered" 28 | PropertyChanges { target: volumeRect; scale: 1.05 } 29 | } 30 | ] 31 | 32 | transitions: [ 33 | Transition { 34 | NumberAnimation { 35 | properties: "scale" 36 | duration: 150 37 | easing.type: Easing.OutCubic 38 | } 39 | ColorAnimation { 40 | properties: "color" 41 | duration: 150 42 | easing.type: Easing.OutCubic 43 | } 44 | } 45 | ] 46 | 47 | Label { 48 | id: volumeLabel 49 | anchors.centerIn: parent 50 | text: shell.volume + "%" 51 | verticalAlignment: Text.AlignVCenter 52 | color: shell.bgColor 53 | font.pixelSize: 12 54 | font.bold: true 55 | font.family: "FiraCode Nerd Font" 56 | 57 | property string lastVolumeText: "" 58 | 59 | // Animate label scale when volume changes 60 | onTextChanged: { 61 | if (text !== lastVolumeText && lastVolumeText !== "") { 62 | volumeChangeAnimation.start() 63 | } 64 | lastVolumeText = text 65 | } 66 | 67 | SequentialAnimation { 68 | id: volumeChangeAnimation 69 | NumberAnimation { 70 | target: volumeLabel 71 | property: "scale" 72 | to: 1.1 73 | duration: 100 74 | easing.type: Easing.OutCubic 75 | } 76 | NumberAnimation { 77 | target: volumeLabel 78 | property: "scale" 79 | to: 1.0 80 | duration: 100 81 | easing.type: Easing.OutCubic 82 | } 83 | } 84 | } 85 | 86 | MouseArea { 87 | id: volumeMouseArea 88 | anchors.fill: parent 89 | hoverEnabled: true 90 | // Open volume control on click 91 | onClicked: Data.ProcessManager.openPavuControl() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Core/NotificationService.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Quickshell.Services.Notifications 3 | import "root:/Data/" as Data 4 | 5 | Item { 6 | id: service 7 | 8 | property var shell 9 | property alias notificationServer: notificationServer 10 | 11 | property int maxHistorySize: 50 12 | 13 | NotificationServer { 14 | id: notificationServer 15 | actionsSupported: true 16 | bodyMarkupSupported: true 17 | imageSupported: true 18 | keepOnReload: false 19 | persistenceSupported: true 20 | 21 | Component.onCompleted: { 22 | // Notify when the notification server is ready (debug only) 23 | if (Qt.application.arguments.includes("--debug")) { 24 | console.log("Notification server initialized") 25 | } 26 | } 27 | 28 | onNotification: (notification) => { 29 | // Filter out notifications with no meaningful content 30 | if (!notification.appName && !notification.summary && !notification.body) { 31 | if (typeof notification.dismiss === 'function') { 32 | notification.dismiss() 33 | } 34 | return 35 | } 36 | 37 | // Ignore notifications from apps in ignoredApps list 38 | if (Data.Settings.ignoredApps.includes(notification.appName)) { 39 | if (typeof notification.dismiss === 'function') { 40 | notification.dismiss() 41 | } 42 | return 43 | } 44 | 45 | // Log notification details in debug mode 46 | if (Qt.application.arguments.includes("--debug")) { 47 | console.log("[NOTIFICATION]", notification.appName, notification.summary) 48 | } 49 | 50 | // Add to shell notification history, capped at maxHistorySize 51 | shell.addToNotificationHistory(notification, maxHistorySize) 52 | 53 | // Show notification window if hidden 54 | if (!shell.notificationWindow.visible) { 55 | shell.notificationWindow.visible = true 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Core/ScreenBorder.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Shapes 4 | import Quickshell 5 | import "root:/Data/" as Data 6 | 7 | PanelWindow { 8 | id: borderWindow 9 | implicitWidth: Screen.width 10 | implicitHeight: Screen.height 11 | visible: true 12 | color: "transparent" 13 | mask: Region{} 14 | 15 | 16 | Shape { 17 | anchors.fill: parent 18 | layer.enabled: true 19 | layer.samples: 4 20 | preferredRendererType: Shape.GeometryRenderer 21 | 22 | ShapePath { 23 | fillColor: Data.Colors.accentColor 24 | strokeWidth: 0 25 | fillRule: ShapePath.OddEvenFill 26 | 27 | // Outer Rectangle (Full Screen) 28 | PathMove { x: 0; y: 0 } 29 | PathLine { x: width; y: 0 } 30 | PathLine { x: width; y: height } 31 | PathLine { x: 0; y: height } 32 | PathLine { x: 0; y: 0 } 33 | 34 | // Inner Cutout (using properties) 35 | PathMove { 36 | x: Data.Settings.borderWidth 37 | y: Data.Settings.borderWidth + Data.Settings.cornerRadius 38 | } 39 | 40 | // Top-left concave corner 41 | PathArc { 42 | x: Data.Settings.borderWidth + Data.Settings.cornerRadius 43 | y: Data.Settings.borderWidth 44 | radiusX: Data.Settings.cornerRadius 45 | radiusY: Data.Settings.cornerRadius 46 | direction: PathArc.Clockwise 47 | } 48 | 49 | // Top edge 50 | PathLine { 51 | x: width - Data.Settings.borderWidth - Data.Settings.cornerRadius 52 | y: Data.Settings.borderWidth 53 | } 54 | 55 | // Top-right concave corner 56 | PathArc { 57 | x: width - Data.Settings.borderWidth 58 | y: Data.Settings.borderWidth + Data.Settings.cornerRadius 59 | radiusX: Data.Settings.cornerRadius 60 | radiusY: Data.Settings.cornerRadius 61 | direction: PathArc.Clockwise 62 | } 63 | 64 | // Right edge 65 | PathLine { 66 | x: width - Data.Settings.borderWidth 67 | y: height - Data.Settings.borderWidth - Data.Settings.cornerRadius 68 | } 69 | 70 | // Bottom-right concave corner 71 | PathArc { 72 | x: width - Data.Settings.borderWidth - Data.Settings.cornerRadius 73 | y: height - Data.Settings.borderWidth 74 | radiusX: Data.Settings.cornerRadius 75 | radiusY: Data.Settings.cornerRadius 76 | direction: PathArc.Clockwise 77 | } 78 | 79 | // Bottom edge 80 | PathLine { 81 | x: Data.Settings.borderWidth + Data.Settings.cornerRadius 82 | y: height - Data.Settings.borderWidth 83 | } 84 | 85 | // Bottom-left concave corner 86 | PathArc { 87 | x: Data.Settings.borderWidth 88 | y: height - Data.Settings.borderWidth - Data.Settings.cornerRadius 89 | radiusX: Data.Settings.cornerRadius 90 | radiusY: Data.Settings.cornerRadius 91 | direction: PathArc.Clockwise 92 | } 93 | 94 | // Close path 95 | PathLine { 96 | x: Data.Settings.borderWidth 97 | y: Data.Settings.borderWidth + Data.Settings.cornerRadius 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/Core/ShellWindows.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Quickshell 3 | import Quickshell.Services.Mpris 4 | import "root:/Bar" as Bar 5 | import "root:/HotCorner" as HotCorner 6 | import "root:/Popup" as Popup 7 | import "root:/Core" as Core 8 | 9 | Item { 10 | id: shellWindows 11 | 12 | property var shell 13 | property var notificationService 14 | 15 | // Expose windows for external access 16 | readonly property alias hotCornerWindow: hotCornerWindow 17 | readonly property alias mainWindow: mainWindow 18 | readonly property alias notificationWindow: notificationWindow 19 | readonly property alias popupWindow: popupWindow 20 | readonly property alias cliphistWindow: cliphistWindow 21 | 22 | // Hot corner panel on top-right 23 | PanelWindow { 24 | id: hotCornerWindow 25 | anchors.top: true 26 | anchors.right: true 27 | margins.top: -36 28 | implicitWidth: slideBarVisible ? 370 : 48 29 | implicitHeight: slideBarVisible ? Screen.height / 2 + 10: 48 30 | color: "transparent" 31 | exclusiveZone: 0 32 | 33 | property bool slideBarVisible: false 34 | 35 | Item { 36 | anchors.fill: parent 37 | MouseArea { 38 | anchors.fill: parent 39 | acceptedButtons: Qt.AllButtons 40 | propagateComposedEvents: true 41 | // Let events propagate, do not grab mouse 42 | onPressed: mouse.accepted = false 43 | onPositionChanged: mouse.accepted = false 44 | onReleased: mouse.accepted = false 45 | } 46 | } 47 | 48 | HotCorner.HotCornerBar { 49 | id: hotCornerContent 50 | shell: shellWindows.shell 51 | anchors.fill: parent 52 | onSlideBarVisibilityChanged: function(visible) { 53 | hotCornerWindow.slideBarVisible = visible 54 | } 55 | } 56 | } 57 | 58 | // Main status bar at top-center 59 | PanelWindow { 60 | id: mainWindow 61 | implicitWidth: 390 62 | implicitHeight: 46 63 | anchors.top: true 64 | color: "transparent" 65 | exclusiveZone: 36 66 | 67 | Bar.Bar { 68 | id: bar 69 | shell: shellWindows.shell 70 | popup: popupWindow 71 | bar: mainWindow 72 | anchors.horizontalCenter: parent.horizontalCenter 73 | implicitWidth: 260 74 | } 75 | } 76 | 77 | // Notification popup at top-right 78 | PanelWindow { 79 | id: notificationWindow 80 | anchors.top: true 81 | anchors.right: true 82 | margins.right: 12 83 | margins.top: 14 84 | implicitWidth: 420 85 | implicitHeight: notificationPopup.calculatedHeight 86 | color: "transparent" 87 | visible: false 88 | 89 | Popup.NotificationPopup { 90 | id: notificationPopup 91 | anchors.fill: parent 92 | shell: shellWindows.shell 93 | notificationServer: shellWindows.notificationService.notificationServer 94 | } 95 | 96 | Connections { 97 | target: notificationPopup 98 | // Hide window when notification queue is empty 99 | function onNotificationQueueChanged() { 100 | if (notificationPopup.notificationQueue.length === 0) { 101 | notificationWindow.visible = false 102 | } 103 | } 104 | } 105 | } 106 | 107 | // Main popup window (centered below main bar) 108 | PopupWindow { 109 | id: popupWindow 110 | anchor.window: mainWindow 111 | anchor.rect.x: mainWindow.implicitWidth / 2 - implicitWidth / 2 112 | anchor.rect.y: mainWindow.exclusiveZone + 12 // offset below bar 113 | implicitWidth: 500 114 | implicitHeight: dynamicHeight 115 | visible: false 116 | color: "transparent" 117 | 118 | property int dynamicHeight: 340 119 | property int minHeight: 340 120 | property int maxHeight: 600 121 | 122 | Behavior on implicitHeight { 123 | NumberAnimation { 124 | duration: 200 125 | easing.type: Easing.OutCubic 126 | } 127 | } 128 | 129 | Rectangle { 130 | anchors.fill: parent 131 | border.color: shellWindows.shell.accentColor 132 | border.width: 3 133 | color: shellWindows.shell.bgColor 134 | radius: 20 135 | 136 | Popup.PopupContent { 137 | id: popupContent 138 | shell: shellWindows.shell 139 | anchors.fill: parent 140 | anchors.margins: 12 141 | // Adjust height dynamically, clamped to min/max 142 | onContentHeightChanged: function(newHeight) { 143 | let clampedHeight = Math.max(popupWindow.minHeight, Math.min(popupWindow.maxHeight, newHeight)) 144 | popupWindow.dynamicHeight = clampedHeight + 30 145 | } 146 | } 147 | } 148 | } 149 | 150 | // Clipboard history popup (centered below main bar) 151 | PopupWindow { 152 | id: cliphistWindow 153 | anchor.window: mainWindow 154 | anchor.rect.x: mainWindow.implicitWidth / 2 - implicitWidth / 2 155 | anchor.rect.y: mainWindow.exclusiveZone + 12 156 | implicitWidth: 500 157 | implicitHeight: 320 158 | visible: false 159 | color: "transparent" 160 | 161 | function toggle() { 162 | if (visible) hide() 163 | else show() 164 | } 165 | 166 | Rectangle { 167 | anchors.fill: parent 168 | border.color: shellWindows.shell.accentColor 169 | border.width: 3 170 | color: shellWindows.shell.bgColor 171 | radius: 20 172 | 173 | Popup.Cliphist { 174 | shell: shellWindows.shell 175 | anchors.fill: parent 176 | anchors.margins: 12 177 | } 178 | } 179 | } 180 | 181 | // Fullscreen screen border 182 | Core.ScreenBorder { 183 | id: screenborderWindow 184 | visible: true 185 | implicitWidth: Screen.width 186 | implicitHeight: Screen.height 187 | anchors.top: true 188 | margins.top: -36 189 | exclusiveZone: 0 190 | } 191 | 192 | // Volume OSD shown on volume changes 193 | Core.VolumeOSD { 194 | shell: shellWindows.shell 195 | } 196 | 197 | // Workspace OSD shown on workspace changes 198 | Core.WorkspaceOSD { 199 | shell: shellWindows.shell 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Core/SystemTimers.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Quickshell.Services.SystemTray 3 | import Quickshell.Hyprland 4 | 5 | Item { 6 | id: service 7 | 8 | property var shell 9 | property var weatherService 10 | 11 | // Main timer - updates every second 12 | Timer { 13 | interval: 1000 14 | running: true 15 | repeat: true 16 | onTriggered: { 17 | shell.time = Qt.formatDateTime(new Date(), "hh:mm AP") 18 | shell.date = Qt.formatDateTime(new Date(), "ddd MMM d") 19 | if (Hyprland.refreshWorkspaces) Hyprland.refreshWorkspaces() 20 | shell.trayItems = SystemTray.items 21 | } 22 | } 23 | 24 | // Weather update timer - updates every 10 minutes 25 | Timer { 26 | interval: 600000 27 | running: true 28 | repeat: true 29 | onTriggered: weatherService.loadWeather() 30 | } 31 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/Core/Version.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import Quickshell 4 | import Quickshell.Wayland 5 | import Quickshell.Io 6 | 7 | // System info watermark component 8 | PanelWindow { 9 | id: systemVersion 10 | 11 | anchors { 12 | right: true 13 | bottom: true 14 | } 15 | 16 | margins { 17 | right: 60 18 | bottom: 60 19 | } 20 | visible: false 21 | 22 | implicitWidth: systemInfoContent.width 23 | implicitHeight: systemInfoContent.height 24 | 25 | color: "transparent" 26 | 27 | mask: Region {} 28 | 29 | WlrLayershell.layer: WlrLayer.Background 30 | WlrLayershell.exclusiveZone: 0 31 | WlrLayershell.keyboardFocus: WlrKeyboardFocus.None 32 | 33 | Timer { 34 | id: startupTimer 35 | interval: 1500 36 | running: true 37 | onTriggered: { 38 | visible = true 39 | console.log("SystemVersion window now visible after delay") 40 | } 41 | } 42 | 43 | // System Info Data 44 | component Details: QtObject { 45 | property string version 46 | property string commit 47 | } 48 | 49 | property QtObject os: QtObject { 50 | property string name: "Loading..." 51 | property Details details: Details { 52 | property string generation: "?" 53 | } 54 | } 55 | 56 | property QtObject wm: QtObject { 57 | property string name: "Loading..." 58 | property Details details: Details {} 59 | } 60 | 61 | // Component initialization 62 | Component.onCompleted: { 63 | console.log("SystemVersion component loaded"); 64 | osFile.reload(); 65 | genProcess.running = true; 66 | wmProcess.running = true; 67 | hlProcess.running = true; 68 | } 69 | 70 | Timer { 71 | running: true 72 | interval: 60000 73 | repeat: true 74 | onTriggered: { 75 | osFile.reload(); 76 | genProcess.running = true; 77 | wmProcess.running = true; 78 | hlProcess.running = true; 79 | } 80 | } 81 | 82 | FileView { 83 | id: osFile 84 | path: "/etc/os-release" 85 | 86 | onLoaded: { 87 | const data = text().trim().split("\n"); 88 | 89 | const nameLine = data.find((str) => str.match(/^NAME=/)); 90 | const versionLine = data.find((str) => str.match(/^VERSION_ID=/)); 91 | const buildLine = data.find((str) => str.match(/^BUILD_ID=/)); 92 | 93 | if (nameLine) { 94 | systemVersion.os.name = nameLine.split("=")[1].replace(/"/g, ""); 95 | } 96 | if (versionLine) { 97 | systemVersion.os.details.version = versionLine.split("=")[1].replace(/"/g, ""); 98 | } 99 | if (buildLine) { 100 | const commit = buildLine.split("=")[1].split(".")[3]; 101 | if (commit) { 102 | systemVersion.os.details.commit = commit.replace(/"/g, "").toUpperCase(); 103 | } 104 | } 105 | } 106 | } 107 | 108 | Process { 109 | id: genProcess 110 | running: true 111 | command: ["sh", "-c", "nixos-rebuild list-generations"] 112 | 113 | stdout: SplitParser { 114 | splitMarker: "" 115 | onRead: (data) => { 116 | const line = data.trim().split("\n").find((str) => str.match(/current/)); 117 | if (line) { 118 | const current = line.split(" ")[0]; 119 | systemVersion.os.details.generation = current; 120 | } 121 | } 122 | } 123 | } 124 | 125 | Process { 126 | id: wmProcess 127 | running: true 128 | command: ["sh", "-c", "echo $XDG_CURRENT_DESKTOP"] 129 | 130 | stdout: SplitParser { 131 | splitMarker: "" 132 | onRead: (data) => { 133 | const result = data.trim(); 134 | if (result && result !== "") { 135 | systemVersion.wm.name = result; 136 | } 137 | } 138 | } 139 | } 140 | 141 | Process { 142 | id: hlProcess 143 | running: true 144 | command: ["sh", "-c", "hyprctl version"] 145 | 146 | stdout: SplitParser { 147 | splitMarker: "" 148 | onRead: (data) => { 149 | const output = data.trim(); 150 | 151 | // Extract version 152 | const versionMatch = output.match(/Tag: (v\d+\.\d+\.\d+)/); 153 | if (versionMatch && versionMatch[1]) { 154 | systemVersion.wm.details.version = versionMatch[1]; 155 | } 156 | 157 | // Extract commit hash 158 | const commitMatch = output.match(/at commit (\w+)/); 159 | if (commitMatch && commitMatch[1]) { 160 | systemVersion.wm.details.commit = commitMatch[1].slice(0, 7).toUpperCase(); 161 | } 162 | } 163 | } 164 | } 165 | 166 | ColumnLayout { 167 | id: systemInfoContent 168 | spacing: 6 169 | 170 | // Main system info row 171 | RowLayout { 172 | spacing: 16 173 | Layout.alignment: Qt.AlignRight 174 | 175 | // OS Section 176 | ColumnLayout { 177 | spacing: 2 178 | Layout.alignment: Qt.AlignRight 179 | 180 | Text { 181 | text: systemVersion.os.name 182 | color: "#70ffffff" 183 | font.family: "SF Pro Display, -apple-system, system-ui, sans-serif" 184 | font.pointSize: 16 185 | font.weight: Font.DemiBold 186 | font.letterSpacing: -0.4 187 | Layout.alignment: Qt.AlignRight 188 | } 189 | 190 | Text { 191 | text: { 192 | let details = []; 193 | if (systemVersion.os.details.version) { 194 | details.push(systemVersion.os.details.version); 195 | } 196 | if (systemVersion.os.details.commit) { 197 | details.push("(" + systemVersion.os.details.commit + ")"); 198 | } 199 | if (systemVersion.os.details.generation && systemVersion.os.details.generation !== "?") { 200 | details.push("Gen " + systemVersion.os.details.generation); 201 | } 202 | return details.join(" "); 203 | } 204 | color: "#50ffffff" 205 | font.family: "SF Mono, Consolas, Monaco, monospace" 206 | font.pointSize: 10 207 | font.weight: Font.Medium 208 | visible: text.length > 0 209 | Layout.alignment: Qt.AlignRight 210 | } 211 | } 212 | 213 | Text { 214 | text: "│" 215 | color: "#40ffffff" 216 | font.family: "SF Pro Display, -apple-system, system-ui, sans-serif" 217 | font.pointSize: 14 218 | font.weight: Font.Light 219 | Layout.alignment: Qt.AlignCenter 220 | } 221 | 222 | // WM Section 223 | ColumnLayout { 224 | spacing: 2 225 | Layout.alignment: Qt.AlignRight 226 | 227 | Text { 228 | text: systemVersion.wm.name 229 | color: "#70ffffff" 230 | font.family: "SF Pro Display, -apple-system, system-ui, sans-serif" 231 | font.pointSize: 16 232 | font.weight: Font.DemiBold 233 | font.letterSpacing: -0.4 234 | Layout.alignment: Qt.AlignRight 235 | } 236 | 237 | Text { 238 | text: { 239 | let details = []; 240 | if (systemVersion.wm.details.version) { 241 | details.push(systemVersion.wm.details.version); 242 | } 243 | if (systemVersion.wm.details.commit) { 244 | details.push("(" + systemVersion.wm.details.commit + ")"); 245 | } 246 | return details.join(" "); 247 | } 248 | color: "#50ffffff" 249 | font.family: "SF Mono, Consolas, Monaco, monospace" 250 | font.pointSize: 10 251 | font.weight: Font.Medium 252 | visible: text.length > 0 253 | Layout.alignment: Qt.AlignRight 254 | } 255 | } 256 | } 257 | } 258 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/Core/VolumeOSD.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Quickshell 3 | import QtQuick.Layouts 4 | import QtQuick.Shapes 5 | import "root:/Data/" as Data 6 | 7 | PanelWindow { 8 | id: volumeOsd 9 | property var shell 10 | 11 | // Position on the right edge of the screen 12 | anchors.right: true 13 | 14 | implicitWidth: 45 15 | implicitHeight: 250 16 | visible: false 17 | color: "transparent" 18 | exclusiveZone: 0 19 | 20 | // Timer to auto-hide the OSD after inactivity 21 | Timer { 22 | id: hideTimer 23 | interval: 2500 24 | onTriggered: hideOsd() 25 | } 26 | 27 | property int lastVolume: -1 28 | 29 | // React to volume changes from the shell 30 | Connections { 31 | target: shell 32 | function onVolumeChanged() { 33 | if (shell.volume !== lastVolume && lastVolume !== -1) { 34 | showOsd() 35 | } 36 | lastVolume = shell.volume 37 | } 38 | } 39 | 40 | Component.onCompleted: { 41 | // Initialize lastVolume on startup 42 | if (shell && shell.volume !== undefined) { 43 | lastVolume = shell.volume 44 | } 45 | } 46 | 47 | // Show OSD with slide-in animation 48 | function showOsd() { 49 | if (!volumeOsd.visible) { 50 | volumeOsd.visible = true 51 | slideInAnimation.start() 52 | } 53 | hideTimer.restart() 54 | } 55 | 56 | // Start slide-out animation to hide OSD 57 | function hideOsd() { 58 | slideOutAnimation.start() 59 | } 60 | 61 | // Slide in from right edge 62 | NumberAnimation { 63 | id: slideInAnimation 64 | target: osdContent 65 | property: "x" 66 | from: volumeOsd.width 67 | to: 0 68 | duration: 300 69 | easing.type: Easing.OutCubic 70 | } 71 | 72 | // Slide out to right edge 73 | NumberAnimation { 74 | id: slideOutAnimation 75 | target: osdContent 76 | property: "x" 77 | from: 0 78 | to: volumeOsd.width 79 | duration: 250 80 | easing.type: Easing.InCubic 81 | onFinished: { 82 | volumeOsd.visible = false 83 | osdContent.x = 0 // Reset position for next show 84 | } 85 | } 86 | 87 | Rectangle { 88 | id: osdContent 89 | width: parent.width 90 | height: parent.height 91 | color: Data.Colors.accentColor 92 | topLeftRadius: 20 93 | bottomLeftRadius: 20 94 | 95 | Column { 96 | anchors.fill: parent 97 | anchors.margins: 16 98 | spacing: 12 99 | 100 | // Volume icon with animated scaling on change 101 | Text { 102 | id: volumeIcon 103 | font.family: "JetBrainsMono Nerd Font" 104 | font.pixelSize: 16 105 | color: Data.Colors.bgColor 106 | text: { 107 | if (!shell || shell.volume === undefined) return "󰝟" 108 | var vol = shell.volume 109 | if (vol === 0) return "󰝟" 110 | else if (vol < 33) return "󰕿" 111 | else if (vol < 66) return "󰖀" 112 | else return "󰕾" 113 | } 114 | anchors.horizontalCenter: parent.horizontalCenter 115 | 116 | Behavior on text { 117 | SequentialAnimation { 118 | PropertyAnimation { target: volumeIcon; property: "scale"; to: 1.2; duration: 100 } 119 | PropertyAnimation { target: volumeIcon; property: "scale"; to: 1.0; duration: 100 } 120 | } 121 | } 122 | } 123 | 124 | // Vertical volume bar background 125 | Rectangle { 126 | width: 10 127 | height: parent.height - volumeIcon.height - volumeLabel.height - 36 128 | radius: 5 129 | color: Qt.darker(Data.Colors.accentColor, 1.5) 130 | border.color: Qt.darker(Data.Colors.accentColor, 2.0) 131 | border.width: 1 132 | anchors.horizontalCenter: parent.horizontalCenter 133 | 134 | // Volume fill bar, scaled by current volume 135 | Rectangle { 136 | id: volumeFill 137 | width: parent.width - 2 138 | radius: parent.radius - 1 139 | x: 1 140 | color: Data.Colors.bgColor 141 | anchors.bottom: parent.bottom 142 | anchors.bottomMargin: 1 143 | height: { 144 | if (!shell || shell.volume === undefined) return 0 145 | var maxHeight = parent.height - 2 146 | return maxHeight * Math.max(0, Math.min(1, shell.volume / 100)) 147 | } 148 | Behavior on height { 149 | NumberAnimation { duration: 250; easing.type: Easing.OutCubic } 150 | } 151 | } 152 | } 153 | 154 | // Numeric volume percentage label 155 | Text { 156 | id: volumeLabel 157 | text: (shell && shell.volume !== undefined ? shell.volume + "%" : "0%") 158 | font.pixelSize: 10 159 | font.weight: Font.Bold 160 | color: Data.Colors.bgColor 161 | anchors.horizontalCenter: parent.horizontalCenter 162 | 163 | Behavior on text { 164 | PropertyAnimation { target: volumeLabel; property: "opacity"; from: 0.7; to: 1.0; duration: 150 } 165 | } 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Core/WeatherService.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | Item { 4 | id: service 5 | 6 | property var shell 7 | 8 | // Track active requests to limit concurrency 9 | property int activeRequests: 0 10 | property int maxConcurrentRequests: 2 11 | 12 | // Cache weather data and track last fetch time 13 | property var cachedWeatherData: null 14 | property var lastFetchTime: 0 15 | property int cacheTimeoutMs: 600000 // 10 minutes 16 | 17 | // Maps weather codes to human-readable strings 18 | function mapWeatherCode(code) { 19 | const weatherCodes = { 20 | 0: "Clear sky", 21 | 1: "Mainly clear", 22 | 2: "Partly cloudy", 23 | 3: "Overcast", 24 | 45: "Fog", 25 | 48: "Depositing rime fog", 26 | 51: "Light drizzle", 27 | 53: "Moderate drizzle", 28 | 55: "Dense drizzle", 29 | 56: "Light freezing drizzle", 30 | 57: "Dense freezing drizzle", 31 | 61: "Slight rain", 32 | 63: "Moderate rain", 33 | 65: "Heavy rain", 34 | 66: "Light freezing rain", 35 | 67: "Heavy freezing rain", 36 | 71: "Slight snow fall", 37 | 73: "Moderate snow fall", 38 | 75: "Heavy snow fall", 39 | 77: "Snow grains", 40 | 80: "Slight rain showers", 41 | 81: "Moderate rain showers", 42 | 82: "Violent rain showers", 43 | 85: "Slight snow showers", 44 | 86: "Heavy snow showers", 45 | 95: "Thunderstorm", 46 | 96: "Thunderstorm with slight hail", 47 | 99: "Thunderstorm with heavy hail" 48 | } 49 | return weatherCodes[code] || "Unknown" 50 | } 51 | 52 | // Loads weather data; uses cache if valid, otherwise fetches fresh data 53 | function loadWeather() { 54 | const now = Date.now() 55 | 56 | // Use cached data if still valid 57 | if (cachedWeatherData && (now - lastFetchTime) < cacheTimeoutMs) { 58 | shell.weatherData = cachedWeatherData 59 | shell.weatherLoading = false 60 | return 61 | } 62 | 63 | // Prevent excessive concurrent requests 64 | if (activeRequests >= maxConcurrentRequests) { 65 | return 66 | } 67 | 68 | shell.weatherLoading = true 69 | activeRequests++ 70 | 71 | // Geocoding request to get lat/lon from location name 72 | const geocodeXhr = new XMLHttpRequest() 73 | const geocodeUrl = "https://nominatim.openstreetmap.org/search?format=json&limit=1&q=" + encodeURIComponent(shell.weatherLocation) 74 | 75 | geocodeXhr.timeout = 10000 // 10 seconds timeout 76 | 77 | geocodeXhr.onreadystatechange = function() { 78 | if (geocodeXhr.readyState === XMLHttpRequest.DONE) { 79 | activeRequests-- 80 | if (geocodeXhr.status === 200) { 81 | try { 82 | const geoData = JSON.parse(geocodeXhr.responseText) 83 | if (geoData.length > 0) { 84 | const latitude = parseFloat(geoData[0].lat) 85 | const longitude = parseFloat(geoData[0].lon) 86 | fetchWeather(latitude, longitude) 87 | } else { 88 | fallbackWeatherData("City not found") 89 | } 90 | } catch (e) { 91 | fallbackWeatherData("Geocode parse error") 92 | } 93 | } else { 94 | fallbackWeatherData("Geocode service unavailable") 95 | } 96 | } 97 | } 98 | 99 | geocodeXhr.ontimeout = function() { 100 | activeRequests-- 101 | fallbackWeatherData("Request timeout") 102 | } 103 | 104 | geocodeXhr.onerror = function() { 105 | activeRequests-- 106 | fallbackWeatherData("Network error") 107 | } 108 | 109 | geocodeXhr.open("GET", geocodeUrl) 110 | geocodeXhr.setRequestHeader("User-Agent", "StatusBar_Ly-sec/1.0") 111 | geocodeXhr.send() 112 | } 113 | 114 | // Fetch weather data given latitude and longitude 115 | function fetchWeather(latitude, longitude) { 116 | if (activeRequests >= maxConcurrentRequests) { 117 | fallbackWeatherData("Too many requests") 118 | return 119 | } 120 | 121 | activeRequests++ 122 | 123 | const xhr = new XMLHttpRequest() 124 | const url = "https://api.open-meteo.com/v1/forecast?" + 125 | "latitude=" + latitude + 126 | "&longitude=" + longitude + 127 | "¤t_weather=true" + 128 | "&daily=temperature_2m_max,temperature_2m_min,weathercode" + 129 | "&forecast_days=3" + 130 | "&timezone=auto" 131 | 132 | xhr.timeout = 15000 // 15 seconds timeout 133 | 134 | xhr.onreadystatechange = function() { 135 | if (xhr.readyState === XMLHttpRequest.DONE) { 136 | activeRequests-- 137 | shell.weatherLoading = false 138 | 139 | if (xhr.status === 200) { 140 | try { 141 | const data = JSON.parse(xhr.responseText) 142 | const current = data.current_weather 143 | const daily = data.daily 144 | 145 | const currentTempFormatted = Math.round(parseFloat(current.temperature)) + "°C" 146 | 147 | // Prepare 3-day forecast 148 | const forecast = new Array(3) 149 | const today = new Date() 150 | 151 | for (let i = 0; i < 3; i++) { 152 | let dayName 153 | if (i === 0) { 154 | dayName = "Today" 155 | } else if (i === 1) { 156 | dayName = "Tomorrow" 157 | } else { 158 | const futureDate = new Date(today) 159 | futureDate.setDate(today.getDate() + i) 160 | dayName = Qt.formatDate(futureDate, "ddd MMM d") 161 | } 162 | 163 | forecast[i] = { 164 | dayName: dayName, 165 | condition: mapWeatherCode(daily.weathercode[i]), 166 | minTemp: Math.round(parseFloat(daily.temperature_2m_min[i])), 167 | maxTemp: Math.round(parseFloat(daily.temperature_2m_max[i])) 168 | } 169 | } 170 | 171 | // Construct weather data object for UI binding 172 | const weatherData = { 173 | location: shell.weatherLocation || "Current Location", 174 | currentTemp: currentTempFormatted, 175 | currentCondition: mapWeatherCode(current.weathercode), 176 | details: [ 177 | "Wind: " + current.windspeed + " km/h", 178 | "Direction: " + current.winddirection + "°" 179 | ], 180 | forecast: forecast 181 | } 182 | 183 | // Cache and update shell property 184 | cachedWeatherData = weatherData 185 | lastFetchTime = Date.now() 186 | shell.weatherData = weatherData 187 | 188 | } catch (e) { 189 | fallbackWeatherData("Weather data error") 190 | } 191 | } else { 192 | fallbackWeatherData("Weather service unavailable") 193 | } 194 | } 195 | } 196 | 197 | xhr.ontimeout = function() { 198 | activeRequests-- 199 | shell.weatherLoading = false 200 | fallbackWeatherData("Request timeout") 201 | } 202 | 203 | xhr.onerror = function() { 204 | activeRequests-- 205 | shell.weatherLoading = false 206 | fallbackWeatherData("Network error") 207 | } 208 | 209 | xhr.open("GET", url) 210 | xhr.setRequestHeader("User-Agent", "StatusBar_Ly-sec/1.0") 211 | xhr.send() 212 | } 213 | 214 | // Provide fallback weather data in case of errors 215 | function fallbackWeatherData(message) { 216 | shell.weatherLoading = false 217 | 218 | const fallbackData = { 219 | location: message, 220 | currentTemp: "?", 221 | currentCondition: "?", 222 | details: [], 223 | forecast: [ 224 | {dayName: "Today", condition: "?", minTemp: "?", maxTemp: "?"}, 225 | {dayName: "Tomorrow", condition: "?", minTemp: "?", maxTemp: "?"}, 226 | {dayName: "?", condition: "?", minTemp: "?", maxTemp: "?"} 227 | ] 228 | } 229 | 230 | shell.weatherData = fallbackData 231 | } 232 | 233 | // Clear cache on component destruction 234 | Component.onDestruction: { 235 | cachedWeatherData = null 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Core/WorkspaceOSD.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Quickshell 3 | import QtQuick.Layouts 4 | import QtQuick.Controls 5 | import Quickshell.Hyprland 6 | import "root:/Data/" as Data 7 | 8 | PanelWindow { 9 | id: workspaceOsd 10 | property var shell 11 | 12 | anchors.left: true 13 | implicitWidth: 45 14 | // Height depends on workspace count, minimum 100px 15 | implicitHeight: Math.max(100, activeWorkspaceCount * 32 + 32) 16 | visible: true 17 | color: "transparent" 18 | exclusiveZone: 0 19 | 20 | property int activeWorkspaceCount: 1 21 | property int lastWorkspace: -1 22 | property bool isAnimating: false 23 | 24 | // Animate height changes unless animating slide in/out 25 | Behavior on height { 26 | enabled: !isAnimating 27 | NumberAnimation { 28 | duration: 300 29 | easing.type: Easing.OutCubic 30 | } 31 | } 32 | 33 | // Update active workspace count safely 34 | function updateWorkspaceCount() { 35 | if (!shell?.workspaces) { 36 | activeWorkspaceCount = 1 37 | return 38 | } 39 | 40 | let count = 0 41 | if (typeof shell.workspaces.rowCount === 'function') { 42 | count = shell.workspaces.rowCount() 43 | } else if (workspaceRepeater.count > 0) { 44 | count = workspaceRepeater.count 45 | } 46 | 47 | const newCount = Math.max(1, count) 48 | if (newCount !== activeWorkspaceCount) { 49 | activeWorkspaceCount = newCount 50 | } 51 | } 52 | 53 | // Hide OSD after delay 54 | Timer { 55 | id: hideTimer 56 | interval: 2500 57 | onTriggered: hideOsd() 58 | } 59 | 60 | // Delay showing OSD on hover 61 | Timer { 62 | id: hoverTimer 63 | interval: 200 64 | onTriggered: showOsd() 65 | } 66 | 67 | // Periodic check to update workspace count if model changes 68 | Timer { 69 | id: checkTimer 70 | interval: 2000 71 | repeat: true 72 | running: shell && shell.workspaces 73 | onTriggered: updateWorkspaceCount() 74 | } 75 | 76 | // Listen for workspace focus changes 77 | Connections { 78 | target: shell 79 | enabled: shell !== null 80 | 81 | function onFocusedWorkspaceChanged() { 82 | if (!shell.focusedWorkspace) return 83 | 84 | const currentId = shell.focusedWorkspace.id 85 | // Show OSD only if workspace changed and not initial load 86 | if (currentId !== lastWorkspace && lastWorkspace !== -1) { 87 | showOsd() 88 | } 89 | lastWorkspace = currentId 90 | } 91 | } 92 | 93 | // Listen for changes in the workspace model and update count accordingly 94 | Connections { 95 | target: shell?.workspaces ?? null 96 | enabled: target !== null 97 | 98 | // Using Qt.callLater to ensure updates happen after model changes settle 99 | function onModelReset() { Qt.callLater(updateWorkspaceCount) } 100 | function onRowsInserted() { Qt.callLater(updateWorkspaceCount) } 101 | function onRowsRemoved() { Qt.callLater(updateWorkspaceCount) } 102 | function onDataChanged() { Qt.callLater(updateWorkspaceCount) } 103 | } 104 | 105 | // Initialize lastWorkspace and workspace count on startup 106 | Component.onCompleted: { 107 | if (shell?.focusedWorkspace?.id !== undefined) { 108 | lastWorkspace = shell.focusedWorkspace.id 109 | } 110 | Qt.callLater(updateWorkspaceCount) 111 | } 112 | 113 | // Show the OSD panel with animation and reset hide timer 114 | function showOsd() { 115 | if (osdContent.visible) { 116 | hideTimer.restart() 117 | return 118 | } 119 | 120 | isAnimating = true 121 | osdContent.visible = true 122 | slideInAnimation.start() 123 | hideTimer.restart() 124 | } 125 | 126 | // Hide the OSD panel with animation 127 | function hideOsd() { 128 | if (!osdContent.visible) return 129 | isAnimating = true 130 | slideOutAnimation.start() 131 | } 132 | 133 | // Slide-in animation for OSD content 134 | NumberAnimation { 135 | id: slideInAnimation 136 | target: osdContent 137 | property: "x" 138 | from: -workspaceOsd.width 139 | to: 0 140 | duration: 300 141 | easing.type: Easing.OutCubic 142 | onFinished: isAnimating = false 143 | } 144 | 145 | // Slide-out animation for OSD content 146 | NumberAnimation { 147 | id: slideOutAnimation 148 | target: osdContent 149 | property: "x" 150 | from: 0 151 | to: -workspaceOsd.width 152 | duration: 250 153 | easing.type: Easing.InCubic 154 | onFinished: { 155 | osdContent.visible = false 156 | osdContent.x = 0 157 | isAnimating = false 158 | } 159 | } 160 | 161 | // OSD content container with rounded right corners and accent color 162 | Rectangle { 163 | id: osdContent 164 | width: parent.width 165 | height: parent.height 166 | color: Data.Colors.accentColor 167 | topRightRadius: 20 168 | bottomRightRadius: 20 169 | visible: false 170 | 171 | Column { 172 | anchors.centerIn: parent 173 | spacing: 8 174 | 175 | // Workspace indicators 176 | Repeater { 177 | id: workspaceRepeater 178 | model: shell?.workspaces ?? [] 179 | 180 | delegate: Rectangle { 181 | id: workspaceIndicator 182 | width: 24 183 | height: 24 184 | radius: 20 185 | 186 | // Highlight active workspace and scale it up 187 | property bool isActive: shell?.focusedWorkspace && 188 | modelData && 189 | modelData.id === shell.focusedWorkspace.id 190 | 191 | color: isActive ? Data.Colors.bgColor : Qt.darker(Data.Colors.accentColor, 1.5) 192 | scale: isActive ? 1.2 : 1.0 193 | 194 | Behavior on scale { 195 | NumberAnimation { duration: 200; easing.type: Easing.OutCubic } 196 | } 197 | Behavior on color { 198 | ColorAnimation { duration: 200; easing.type: Easing.OutCubic } 199 | } 200 | 201 | Text { 202 | text: modelData?.id ?? "?" 203 | anchors.centerIn: parent 204 | color: parent.isActive ? Data.Colors.accentColor : Data.Colors.bgColor 205 | font.pixelSize: 10 206 | font.weight: Font.Bold 207 | 208 | Behavior on color { 209 | ColorAnimation { duration: 200; easing.type: Easing.OutCubic } 210 | } 211 | } 212 | } 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Data/Colors.qml: -------------------------------------------------------------------------------- 1 | pragma Singleton 2 | import QtQuick 3 | import Quickshell 4 | 5 | Singleton { 6 | // Rosé Pine Colors 7 | id: colors 8 | readonly property color bgColor: "#1f1d2e" // Base background 9 | readonly property color fgColor: "#e0def4" // Base foreground (text) 10 | readonly property color accentColor: "#eb6f92" // Pink accent ("love") 11 | readonly property color highlightBg: "#26233a" // Highlight background 12 | } 13 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Data/ProcessManager.qml: -------------------------------------------------------------------------------- 1 | pragma Singleton 2 | import QtQuick 3 | import Quickshell.Io 4 | 5 | QtObject { 6 | id: root 7 | 8 | // System monitoring properties 9 | property real cpuUsage: 0 10 | property real ramUsage: 0 11 | property real totalRam: 0 12 | property real usedRam: 0 13 | 14 | // Individual process objects as properties 15 | property Process shutdownProcess: Process { 16 | command: ["shutdown", "-h", "now"] 17 | } 18 | 19 | property Process rebootProcess: Process { 20 | command: ["reboot"] 21 | } 22 | 23 | property Process lockProcess: Process { 24 | command: ["hyprlock"] 25 | } 26 | 27 | property Process logoutProcess: Process { 28 | command: ["loginctl", "terminate-user", "$USER"] 29 | } 30 | 31 | property Process pavucontrolProcess: Process { 32 | command: ["pavucontrol"] 33 | } 34 | 35 | // System monitoring processes 36 | property Process cpuProcess: Process { 37 | command: ["sh", "-c", "grep '^cpu ' /proc/stat | awk '{usage=($2+$3+$4)*100/($2+$3+$4+$5)} END {print usage}'"] 38 | stdout: SplitParser { 39 | onRead: data => { 40 | root.cpuUsage = parseFloat(data) 41 | } 42 | } 43 | } 44 | 45 | property Process ramProcess: Process { 46 | command: ["sh", "-c", "free -b | awk '/Mem:/ {print $2\" \"$3\" \"$3/$2*100}'"] 47 | stdout: SplitParser { 48 | onRead: data => { 49 | var parts = data.trim().split(/\s+/) 50 | if (parts.length >= 3) { 51 | root.totalRam = parseFloat(parts[0]) / (1024 * 1024 * 1024) 52 | root.usedRam = parseFloat(parts[1]) / (1024 * 1024 * 1024) 53 | root.ramUsage = parseFloat(parts[2]) 54 | } 55 | } 56 | } 57 | } 58 | 59 | // Monitoring timers 60 | property Timer cpuTimer: Timer { 61 | interval: 2000 // 2 seconds default, can be overridden 62 | running: true 63 | repeat: true 64 | onTriggered: { 65 | // Restart the process to get fresh data 66 | cpuProcess.running = false 67 | cpuProcess.running = true 68 | } 69 | } 70 | 71 | property Timer ramTimer: Timer { 72 | interval: 2000 // 2 seconds default, can be overridden 73 | running: true 74 | repeat: true 75 | onTriggered: { 76 | // Restart the process to get fresh data 77 | ramProcess.running = false 78 | ramProcess.running = true 79 | } 80 | } 81 | 82 | // Convenience methods for common system commands 83 | function shutdown() { 84 | console.log("Executing shutdown command") 85 | shutdownProcess.running = true 86 | } 87 | 88 | function reboot() { 89 | console.log("Executing reboot command") 90 | rebootProcess.running = true 91 | } 92 | 93 | function lock() { 94 | console.log("Executing lock command") 95 | lockProcess.running = true 96 | } 97 | 98 | function logout() { 99 | console.log("Executing logout command") 100 | logoutProcess.running = true 101 | } 102 | 103 | function openPavuControl() { 104 | console.log("Opening PavuControl") 105 | pavucontrolProcess.running = true 106 | } 107 | 108 | // System monitoring control methods 109 | function startMonitoring() { 110 | console.log("Starting system monitoring") 111 | cpuTimer.running = true 112 | ramTimer.running = true 113 | } 114 | 115 | function stopMonitoring() { 116 | console.log("Stopping system monitoring") 117 | cpuTimer.running = false 118 | ramTimer.running = false 119 | } 120 | 121 | function setMonitoringInterval(intervalMs) { 122 | console.log("Setting monitoring interval to", intervalMs, "ms") 123 | cpuTimer.interval = intervalMs 124 | ramTimer.interval = intervalMs 125 | } 126 | 127 | function refreshSystemStats() { 128 | console.log("Manually refreshing system stats") 129 | // Restart processes to get fresh data 130 | cpuProcess.running = false 131 | cpuProcess.running = true 132 | ramProcess.running = false 133 | ramProcess.running = true 134 | } 135 | 136 | // Check if a process is running 137 | function isShutdownRunning() { return shutdownProcess.running } 138 | function isRebootRunning() { return rebootProcess.running } 139 | function isLockRunning() { return lockProcess.running } 140 | function isLogoutRunning() { return logoutProcess.running } 141 | function isPavuControlRunning() { return pavucontrolProcess.running } 142 | function isMonitoringActive() { return cpuTimer.running && ramTimer.running } 143 | 144 | // Stop processes (mainly useful for long-running ones) 145 | function stopPavuControl() { 146 | pavucontrolProcess.running = false 147 | } 148 | 149 | // System stats getters with formatted output 150 | function getCpuUsageFormatted() { 151 | return Math.round(cpuUsage) + "%" 152 | } 153 | 154 | function getRamUsageFormatted() { 155 | return Math.round(ramUsage) + "% (" + usedRam.toFixed(1) + "GB/" + totalRam.toFixed(1) + "GB)" 156 | } 157 | 158 | function getRamUsageSimple() { 159 | return Math.round(ramUsage) + "%" 160 | } 161 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/Data/Settings.qml: -------------------------------------------------------------------------------- 1 | pragma Singleton 2 | import Quickshell 3 | 4 | Singleton { 5 | 6 | // Panel 7 | // - avatarSource can be either a local file or a link 8 | property string avatarSource: "https://cdn.discordapp.com/avatars/158005126638993408/de403b05fd7f74bb17e01a9b066a30fa?size=1024" 9 | 10 | // Weather 11 | property string weatherLocation: "Dinslaken" 12 | 13 | // System Monitor 14 | property int cpuRefreshInterval: 5000 15 | property int ramRefreshInterval: 10000 16 | 17 | // Notification blacklist 18 | readonly property var ignoredApps: ["some-app", "another-app"] 19 | 20 | // Screen Border 21 | property real borderWidth: 9 22 | property real cornerRadius: 20 23 | 24 | // Recording location 25 | property string videoPath: "~/Videos/" 26 | 27 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/HotCorner/HotCornerBar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | import QtQuick.Effects 5 | import Quickshell 6 | import Quickshell.Io 7 | import "root:/Data" as Data 8 | import "root:/HotCorner/modules" as Modules 9 | 10 | Item { 11 | required property var shell 12 | 13 | anchors.fill: parent 14 | 15 | // Recording state management 16 | property bool isRecording: false 17 | property var recordingProcess: null 18 | property string lastError: "" 19 | 20 | // Signal to communicate SlideBar visibility changes 21 | signal slideBarVisibilityChanged(bool visible) 22 | 23 | // Function to trigger hot corner (can be called externally) 24 | function triggerHotCorner() { 25 | slideBar.show() 26 | } 27 | 28 | // Hot corner trigger - always in the corner 29 | Modules.HotCornerTrigger { 30 | id: hotCorner 31 | onTriggered: triggerHotCorner() 32 | } 33 | 34 | // The main sliding bar - positioned relative to screen edge, not parent 35 | Modules.SlideBar { 36 | id: slideBar 37 | shell: parent.shell 38 | isRecording: parent.isRecording 39 | 40 | // Position relative to the parent (hot corner window) 41 | anchors.top: parent.top 42 | anchors.right: parent.right 43 | anchors.topMargin: 8 44 | anchors.rightMargin: 8 45 | 46 | // Monitor visibility changes 47 | onVisibleChanged: { 48 | slideBarVisibilityChanged(visible) 49 | } 50 | 51 | onRecordingRequested: startRecording() 52 | onStopRecordingRequested: { 53 | stopRecording() 54 | slideBar.hide() 55 | } 56 | onSystemActionRequested: function(action) { 57 | performSystemAction(action) 58 | slideBar.hide() 59 | } 60 | onPerformanceActionRequested: function(action) { 61 | performPerformanceAction(action) 62 | slideBar.hide() 63 | } 64 | } 65 | 66 | // Recording management functions 67 | function startRecording() { 68 | var currentDate = new Date() 69 | var hours = String(currentDate.getHours()).padStart(2, '0') 70 | var minutes = String(currentDate.getMinutes()).padStart(2, '0') 71 | var day = String(currentDate.getDate()).padStart(2, '0') 72 | var month = String(currentDate.getMonth() + 1).padStart(2, '0') 73 | var year = currentDate.getFullYear() 74 | 75 | var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4" 76 | var outputPath = Data.Settings.videoPath + filename 77 | var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath 78 | 79 | var qmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "' + command + '"]; running: true; onExited: function(exitCode, exitStatus) { console.log("Recording process exited with code:", exitCode) } }' 80 | 81 | recordingProcess = Qt.createQmlObject(qmlString, parent) 82 | isRecording = true 83 | } 84 | 85 | function stopRecording() { 86 | if (recordingProcess && isRecording) { 87 | console.log("Stopping recording") 88 | 89 | var stopQmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -SIGINT -f \'gpu-screen-recorder.*portal\'"]; running: true; onExited: function(exitCode, exitStatus) { console.log("Stop signal sent, exit code:", exitCode); destroy() } }' 90 | 91 | var stopProcess = Qt.createQmlObject(stopQmlString, parent) 92 | 93 | var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', parent) 94 | cleanupTimer.triggered.connect(function() { 95 | console.log("Cleaning up recording process") 96 | if (recordingProcess) { 97 | recordingProcess.running = false 98 | recordingProcess.destroy() 99 | recordingProcess = null 100 | } 101 | 102 | var forceKillQml = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -9 -f \'gpu-screen-recorder.*portal\' 2>/dev/null || true"]; running: true; onExited: function() { destroy() } }' 103 | var forceKillProcess = Qt.createQmlObject(forceKillQml, parent) 104 | 105 | cleanupTimer.destroy() 106 | }) 107 | } 108 | isRecording = false 109 | } 110 | 111 | function performSystemAction(action) { 112 | switch(action) { 113 | case "lock": 114 | Data.ProcessManager.lock() 115 | break 116 | case "reboot": 117 | Data.ProcessManager.reboot() 118 | break 119 | case "shutdown": 120 | Data.ProcessManager.shutdown() 121 | break 122 | } 123 | } 124 | 125 | function performPerformanceAction(action) { 126 | // Add performance action handling here if needed 127 | console.log("Performance action requested:", action) 128 | } 129 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/HotCorner/modules/HotCornerTrigger.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | Rectangle { 4 | id: root 5 | width: 48 6 | height: 48 7 | color: "transparent" 8 | anchors.right: parent.right 9 | anchors.top: parent.top 10 | z: 999 11 | 12 | signal triggered() 13 | 14 | MouseArea { 15 | id: mouseArea 16 | anchors.fill: parent 17 | anchors.margins: -8 18 | hoverEnabled: true 19 | 20 | property bool isHovered: containsMouse 21 | 22 | onIsHoveredChanged: { 23 | if (isHovered) { 24 | showTimer.start() 25 | hideTimer.stop() 26 | } else { 27 | hideTimer.start() 28 | showTimer.stop() 29 | } 30 | } 31 | 32 | onEntered: hideTimer.stop() 33 | } 34 | 35 | // Smooth show/hide timers 36 | Timer { 37 | id: showTimer 38 | interval: 200 39 | onTriggered: root.triggered() 40 | } 41 | 42 | Timer { 43 | id: hideTimer 44 | interval: 500 45 | // Note: This timer is managed by the parent SlideBar component 46 | } 47 | 48 | // Expose properties and functions for parent components 49 | readonly property alias containsMouse: mouseArea.containsMouse 50 | function stopHideTimer() { hideTimer.stop() } 51 | function startHideTimer() { hideTimer.start() } 52 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/HotCorner/modules/PerformanceControls.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import Quickshell.Services.UPower 4 | 5 | Column { 6 | id: root 7 | required property var shell 8 | 9 | spacing: 8 10 | signal performanceActionRequested(string action) 11 | signal mouseChanged(bool containsMouse) 12 | 13 | readonly property bool containsMouse: performanceButton.containsMouse || 14 | balancedButton.containsMouse || 15 | powerSaverButton.containsMouse 16 | 17 | // Safe property access with fallbacks 18 | readonly property bool upowerReady: typeof PowerProfiles !== 'undefined' && PowerProfiles 19 | readonly property int currentProfile: upowerReady ? PowerProfiles.profile : 0 20 | 21 | onContainsMouseChanged: root.mouseChanged(containsMouse) 22 | 23 | opacity: visible ? 1 : 0 24 | 25 | Behavior on opacity { 26 | NumberAnimation { 27 | duration: 300 28 | easing.type: Easing.OutCubic 29 | } 30 | } 31 | 32 | Row { 33 | spacing: 8 34 | width: parent.width 35 | 36 | // Performance Profile Button 37 | SystemButton { 38 | id: performanceButton 39 | width: (parent.width - parent.spacing * 2) / 3 40 | height: 52 41 | 42 | shell: root.shell 43 | iconText: "󰓅" 44 | 45 | isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ? 46 | root.currentProfile === PowerProfile.Performance : false 47 | 48 | onClicked: { 49 | if (root.upowerReady && typeof PowerProfile !== 'undefined') { 50 | PowerProfiles.profile = PowerProfile.Performance 51 | root.performanceActionRequested("performance") 52 | } else { 53 | console.warn("PowerProfiles not available") 54 | } 55 | } 56 | onMouseChanged: function(containsMouse) { 57 | if (!containsMouse && !root.containsMouse) { 58 | root.mouseChanged(false) 59 | } 60 | } 61 | } 62 | 63 | // Balanced Profile Button 64 | SystemButton { 65 | id: balancedButton 66 | width: (parent.width - parent.spacing * 2) / 3 67 | height: 52 68 | 69 | shell: root.shell 70 | iconText: "󰾅" 71 | 72 | isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ? 73 | root.currentProfile === PowerProfile.Balanced : false 74 | 75 | onClicked: { 76 | if (root.upowerReady && typeof PowerProfile !== 'undefined') { 77 | PowerProfiles.profile = PowerProfile.Balanced 78 | root.performanceActionRequested("balanced") 79 | } else { 80 | console.warn("PowerProfiles not available") 81 | } 82 | } 83 | onMouseChanged: function(containsMouse) { 84 | if (!containsMouse && !root.containsMouse) { 85 | root.mouseChanged(false) 86 | } 87 | } 88 | } 89 | 90 | // Power Saver Profile Button 91 | SystemButton { 92 | id: powerSaverButton 93 | width: (parent.width - parent.spacing * 2) / 3 94 | height: 52 95 | 96 | shell: root.shell 97 | iconText: "󰌪" 98 | 99 | isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ? 100 | root.currentProfile === PowerProfile.PowerSaver : false 101 | 102 | onClicked: { 103 | if (root.upowerReady && typeof PowerProfile !== 'undefined') { 104 | PowerProfiles.profile = PowerProfile.PowerSaver 105 | root.performanceActionRequested("powersaver") 106 | } else { 107 | console.warn("PowerProfiles not available") 108 | } 109 | } 110 | onMouseChanged: function(containsMouse) { 111 | if (!containsMouse && !root.containsMouse) { 112 | root.mouseChanged(false) 113 | } 114 | } 115 | } 116 | } 117 | 118 | // Optional: Add a small delay to ensure services are ready 119 | Component.onCompleted: { 120 | // Small delay to ensure UPower service is fully initialized 121 | Qt.callLater(function() { 122 | if (!root.upowerReady) { 123 | console.warn("UPower service not ready - performance controls may not work correctly") 124 | } 125 | }) 126 | } 127 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/HotCorner/modules/RecordingButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | 5 | Rectangle { 6 | id: root 7 | required property var shell 8 | required property bool isRecording 9 | radius: 12 10 | 11 | signal recordingRequested() 12 | signal stopRecordingRequested() 13 | signal mouseChanged(bool containsMouse) 14 | 15 | // Enhanced gradient background 16 | gradient: Gradient { 17 | GradientStop { 18 | position: 0.0 19 | color: isRecording ? 20 | (mouseArea.containsMouse ? Qt.lighter("#e53e3e", 1.1) : "#e53e3e") : 21 | (mouseArea.containsMouse ? Qt.lighter(shell.accentColor, 1.1) : shell.accentColor) 22 | } 23 | GradientStop { 24 | position: 1.0 25 | color: isRecording ? 26 | (mouseArea.containsMouse ? Qt.darker("#e53e3e", 1.1) : Qt.darker("#e53e3e", 1.05)) : 27 | (mouseArea.containsMouse ? Qt.darker(shell.accentColor, 1.1) : Qt.darker(shell.accentColor, 1.05)) 28 | } 29 | } 30 | 31 | property bool isHovered: mouseArea.containsMouse 32 | readonly property alias containsMouse: mouseArea.containsMouse 33 | 34 | Behavior on scale { 35 | NumberAnimation { 36 | duration: 200 37 | easing.type: Easing.OutCubic 38 | } 39 | } 40 | scale: isHovered ? 1.03 : 1.0 41 | 42 | // Subtle pulsing when recording 43 | SequentialAnimation { 44 | running: isRecording 45 | loops: Animation.Infinite 46 | 47 | PropertyAnimation { 48 | target: root 49 | property: "opacity" 50 | to: 0.8 51 | duration: 1200 52 | easing.type: Easing.InOutSine 53 | } 54 | PropertyAnimation { 55 | target: root 56 | property: "opacity" 57 | to: 1.0 58 | duration: 1200 59 | easing.type: Easing.InOutSine 60 | } 61 | } 62 | 63 | RowLayout { 64 | anchors.centerIn: parent 65 | spacing: 10 66 | 67 | Text { 68 | text: isRecording ? "\uf04d" : "\uf111" 69 | font.family: "NerdFont" 70 | font.pixelSize: 16 71 | color: "#ffffff" 72 | 73 | Layout.alignment: Qt.AlignVCenter 74 | 75 | scale: root.isHovered ? 1.1 : 1.0 76 | Behavior on scale { 77 | NumberAnimation { 78 | duration: 200 79 | easing.type: Easing.OutBack 80 | } 81 | } 82 | } 83 | 84 | Label { 85 | text: isRecording ? "Stop Recording" : "Start Recording" 86 | font.pixelSize: 13 87 | font.weight: Font.Medium 88 | color: "#ffffff" 89 | 90 | // Remove anchors.verticalCenter, use Layout.alignment instead 91 | Layout.alignment: Qt.AlignVCenter 92 | } 93 | } 94 | 95 | MouseArea { 96 | id: mouseArea 97 | anchors.fill: parent 98 | hoverEnabled: true 99 | 100 | onContainsMouseChanged: root.mouseChanged(containsMouse) 101 | 102 | onClicked: { 103 | if (isRecording) { 104 | root.stopRecordingRequested() 105 | } else { 106 | root.recordingRequested() 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/HotCorner/modules/SlideBar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | 4 | Item { 5 | id: root 6 | required property var shell 7 | required property bool isRecording 8 | 9 | width: 360 10 | height: isRecording ? (userProfile.height + recordingButton.height + 36) : Screen.height / 2 11 | visible: false 12 | 13 | anchors.top: parent.top 14 | anchors.right: parent.right 15 | anchors.topMargin: 8 16 | x: visible ? 0 : width 17 | opacity: 1 18 | 19 | signal recordingRequested() 20 | signal stopRecordingRequested() 21 | signal systemActionRequested(string action) 22 | signal performanceActionRequested(string action) 23 | 24 | Rectangle { 25 | id: mainContainer 26 | anchors.fill: parent 27 | radius: 18 28 | color: shell.bgColor 29 | border.width: 3 30 | border.color: shell.accentColor 31 | 32 | Rectangle { 33 | anchors.fill: parent 34 | anchors.margins: -3 35 | radius: parent.radius + 3 36 | color: Qt.darker(shell.bgColor, 1.05) 37 | z: -1 38 | opacity: 0.3 39 | } 40 | } 41 | 42 | property bool isHovered: slideBarMouseArea.containsMouse || 43 | recordingButton.containsMouse || 44 | systemControls.containsMouse || 45 | performanceControls.containsMouse || 46 | userProfile.isHovered 47 | 48 | onIsHoveredChanged: { 49 | if (isHovered) { 50 | hideTimer.stop() 51 | } else { 52 | hideTimer.start() 53 | } 54 | } 55 | 56 | MouseArea { 57 | id: slideBarMouseArea 58 | anchors.fill: parent 59 | hoverEnabled: true 60 | propagateComposedEvents: true 61 | } 62 | 63 | Timer { 64 | id: hideTimer 65 | interval: 300 66 | repeat: false 67 | onTriggered: { 68 | if (root.x !== width) { 69 | slideOutAnimation.start() 70 | } else { 71 | root.visible = false 72 | } 73 | } 74 | } 75 | 76 | // Using regular Column here for more precise control 77 | Column { 78 | anchors.fill: parent 79 | anchors.margins: 18 80 | spacing: root.isRecording ? 0 : 28 // Adjust spacing as needed 81 | 82 | UserProfile { 83 | id: userProfile 84 | width: parent.width 85 | height: 80 86 | shell: root.shell 87 | } 88 | 89 | RecordingButton { 90 | id: recordingButton 91 | width: parent.width 92 | height: 48 93 | shell: root.shell 94 | isRecording: root.isRecording 95 | 96 | onRecordingRequested: root.recordingRequested() 97 | onStopRecordingRequested: root.stopRecordingRequested() 98 | } 99 | 100 | Text { 101 | id: performanceLabel 102 | width: parent.width 103 | text: "PERFORMANCE" 104 | color: shell.accentColor 105 | font.pixelSize: 11 106 | font.weight: Font.DemiBold 107 | font.letterSpacing: 1.2 108 | horizontalAlignment: Text.AlignCenter 109 | visible: !root.isRecording 110 | } 111 | 112 | PerformanceControls { 113 | id: performanceControls 114 | width: parent.width 115 | height: 52 116 | visible: !root.isRecording 117 | shell: root.shell 118 | onPerformanceActionRequested: function(action) { root.performanceActionRequested(action) } 119 | } 120 | 121 | Text { 122 | id: systemLabel 123 | width: parent.width 124 | text: "SYSTEM" 125 | color: shell.accentColor 126 | font.pixelSize: 11 127 | font.weight: Font.DemiBold 128 | font.letterSpacing: 1.2 129 | horizontalAlignment: Text.AlignCenter 130 | visible: !root.isRecording 131 | } 132 | 133 | SystemControls { 134 | id: systemControls 135 | width: parent.width 136 | height: 52 137 | visible: !root.isRecording 138 | shell: root.shell 139 | onSystemActionRequested: function(action) { root.systemActionRequested(action) } 140 | } 141 | } 142 | 143 | function show() { 144 | x = width 145 | opacity = 1 146 | visible = true 147 | slideInAnimation.start() 148 | hideTimer.stop() 149 | } 150 | 151 | function hide() { 152 | if (visible && x === 0) { 153 | slideOutAnimation.start() 154 | } 155 | } 156 | 157 | PropertyAnimation { 158 | id: slideInAnimation 159 | target: root 160 | property: "x" 161 | from: width 162 | to: 0 163 | duration: 350 164 | easing.type: Easing.OutCubic 165 | onStarted: root.opacity = 1 166 | } 167 | 168 | ParallelAnimation { 169 | id: slideOutAnimation 170 | 171 | PropertyAnimation { 172 | target: root 173 | property: "x" 174 | to: width 175 | duration: 300 176 | easing.type: Easing.InCubic 177 | } 178 | 179 | PropertyAnimation { 180 | target: root 181 | property: "opacity" 182 | to: 0 183 | duration: 300 184 | easing.type: Easing.InCubic 185 | } 186 | 187 | onFinished: { 188 | root.visible = false 189 | root.opacity = 1 190 | } 191 | } 192 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/HotCorner/modules/SystemButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | 4 | Rectangle { 5 | id: root 6 | required property var shell 7 | required property string iconText 8 | required property string labelText 9 | 10 | // Add active state property 11 | property bool isActive: false 12 | 13 | radius: 10 14 | 15 | // Modified color logic to handle active state 16 | color: { 17 | if (isActive) { 18 | return mouseArea.containsMouse ? 19 | Qt.lighter(shell.accentColor, 1.1) : 20 | Qt.rgba(shell.accentColor.r, shell.accentColor.g, shell.accentColor.b, 0.3) 21 | } else { 22 | return mouseArea.containsMouse ? 23 | Qt.lighter(shell.accentColor, 1.2) : 24 | Qt.lighter(shell.bgColor, 1.15) 25 | } 26 | } 27 | 28 | border.width: isActive ? 2 : 1 29 | border.color: isActive ? shell.accentColor : Qt.lighter(shell.bgColor, 1.3) 30 | 31 | signal clicked() 32 | signal mouseChanged(bool containsMouse) 33 | property bool isHovered: mouseArea.containsMouse 34 | readonly property alias containsMouse: mouseArea.containsMouse 35 | 36 | Behavior on color { 37 | ColorAnimation { 38 | duration: 200 39 | easing.type: Easing.OutCubic 40 | } 41 | } 42 | 43 | Behavior on border.color { 44 | ColorAnimation { 45 | duration: 200 46 | easing.type: Easing.OutCubic 47 | } 48 | } 49 | 50 | scale: isHovered ? 1.05 : 1.0 51 | Behavior on scale { 52 | NumberAnimation { 53 | duration: 200 54 | easing.type: Easing.OutCubic 55 | } 56 | } 57 | 58 | Column { 59 | anchors.centerIn: parent 60 | spacing: 2 61 | 62 | Text { 63 | text: root.iconText 64 | font.family: "NerdFont" 65 | font.pixelSize: 16 66 | anchors.horizontalCenter: parent.horizontalCenter 67 | color: { 68 | if (root.isActive) { 69 | return root.isHovered ? "#ffffff" : shell.accentColor 70 | } else { 71 | return root.isHovered ? "#ffffff" : shell.accentColor 72 | } 73 | } 74 | 75 | Behavior on color { 76 | ColorAnimation { 77 | duration: 200 78 | easing.type: Easing.OutCubic 79 | } 80 | } 81 | } 82 | 83 | Label { 84 | text: root.labelText 85 | font.pixelSize: 8 86 | color: { 87 | if (root.isActive) { 88 | return root.isHovered ? "#ffffff" : shell.accentColor 89 | } else { 90 | return root.isHovered ? "#ffffff" : shell.accentColor 91 | } 92 | } 93 | anchors.horizontalCenter: parent.horizontalCenter 94 | font.weight: root.isActive ? Font.Bold : Font.Medium 95 | 96 | Behavior on color { 97 | ColorAnimation { 98 | duration: 200 99 | easing.type: Easing.OutCubic 100 | } 101 | } 102 | } 103 | } 104 | 105 | MouseArea { 106 | id: mouseArea 107 | anchors.fill: parent 108 | hoverEnabled: true 109 | 110 | onContainsMouseChanged: root.mouseChanged(containsMouse) 111 | onClicked: root.clicked() 112 | } 113 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/HotCorner/modules/SystemControls.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | 5 | RowLayout { 6 | id: root 7 | required property var shell 8 | 9 | spacing: 8 10 | signal systemActionRequested(string action) 11 | signal mouseChanged(bool containsMouse) 12 | 13 | readonly property bool containsMouse: lockButton.containsMouse || 14 | rebootButton.containsMouse || 15 | shutdownButton.containsMouse 16 | 17 | onContainsMouseChanged: root.mouseChanged(containsMouse) 18 | 19 | opacity: visible ? 1 : 0 20 | 21 | Behavior on opacity { 22 | NumberAnimation { 23 | duration: 300 24 | easing.type: Easing.OutCubic 25 | } 26 | } 27 | 28 | // Lock Button 29 | SystemButton { 30 | id: lockButton 31 | Layout.fillHeight: true 32 | Layout.fillWidth: true 33 | 34 | shell: root.shell 35 | iconText: "\uf023" 36 | 37 | onClicked: root.systemActionRequested("lock") 38 | onMouseChanged: function(containsMouse) { 39 | if (!containsMouse && !root.containsMouse) { 40 | root.mouseChanged(false) 41 | } 42 | } 43 | } 44 | 45 | // Reboot Button 46 | SystemButton { 47 | id: rebootButton 48 | Layout.fillHeight: true 49 | Layout.fillWidth: true 50 | 51 | shell: root.shell 52 | iconText: "\uf2f1" 53 | 54 | onClicked: root.systemActionRequested("reboot") 55 | onMouseChanged: function(containsMouse) { 56 | if (!containsMouse && !root.containsMouse) { 57 | root.mouseChanged(false) 58 | } 59 | } 60 | } 61 | 62 | // Shutdown Button 63 | SystemButton { 64 | id: shutdownButton 65 | Layout.fillHeight: true 66 | Layout.fillWidth: true 67 | 68 | shell: root.shell 69 | iconText: "\uf011" 70 | 71 | onClicked: root.systemActionRequested("shutdown") 72 | onMouseChanged: function(containsMouse) { 73 | if (!containsMouse && !root.containsMouse) { 74 | root.mouseChanged(false) 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /modules/quickshell/qml/HotCorner/modules/UserProfile.qml: -------------------------------------------------------------------------------- 1 | import Quickshell.Io 2 | import QtQuick 3 | import QtQuick.Controls 4 | import "root:/Data/" as Data 5 | 6 | Rectangle { 7 | id: root 8 | required property var shell 9 | required property url avatarSource 10 | property string userName: "" // will be set by process output 11 | property string userInfo: "" // will hold uptime string 12 | 13 | property bool isActive: false 14 | property bool isHovered: false // track hover state 15 | 16 | radius: 12 17 | width: 220 18 | height: 80 19 | 20 | color: { 21 | if (isActive) { 22 | return isHovered ? 23 | Qt.lighter(shell.accentColor, 1.1) : 24 | Qt.rgba(shell.accentColor.r, shell.accentColor.g, shell.accentColor.b, 0.3) 25 | } else { 26 | return isHovered ? 27 | Qt.lighter(shell.accentColor, 1.2) : 28 | Qt.lighter(shell.bgColor, 1.15) 29 | } 30 | } 31 | 32 | border.width: isActive ? 2 : 1 33 | border.color: isActive ? shell.accentColor : Qt.lighter(shell.bgColor, 1.3) 34 | 35 | Behavior on color { ColorAnimation { duration: 200; easing.type: Easing.OutCubic } } 36 | Behavior on border.color { ColorAnimation { duration: 200; easing.type: Easing.OutCubic } } 37 | scale: isHovered ? 1.05 : 1.0 38 | Behavior on scale { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } 39 | 40 | Row { 41 | anchors.fill: parent 42 | anchors.margins: 14 43 | spacing: 12 44 | anchors.verticalCenter: parent.verticalCenter 45 | 46 | Rectangle { 47 | id: avatarCircle 48 | width: 52 49 | height: 52 50 | //radius: width / 2 51 | clip: true 52 | border.color: shell.accentColor 53 | border.width: 3 54 | 55 | Image { 56 | anchors.fill: parent 57 | anchors.margins: 2 58 | source: Data.Settings.avatarSource 59 | fillMode: Image.PreserveAspectCrop 60 | cache: false 61 | } 62 | } 63 | 64 | Column { 65 | spacing: 4 66 | anchors.verticalCenter: parent.verticalCenter 67 | 68 | Text { 69 | text: root.userName === "" ? "Loading..." : root.userName 70 | font.pixelSize: 16 71 | font.bold: true 72 | color: isHovered || root.isActive ? "#ffffff" : shell.accentColor 73 | elide: Text.ElideRight 74 | maximumLineCount: 1 75 | } 76 | 77 | Text { 78 | text: root.userInfo === "" ? "Loading uptime..." : root.userInfo 79 | font.pixelSize: 11 80 | font.bold: true 81 | color: isHovered || root.isActive ? "#cccccc" : Qt.lighter(shell.accentColor, 1.6) 82 | elide: Text.ElideRight 83 | maximumLineCount: 1 84 | } 85 | } 86 | } 87 | 88 | MouseArea { 89 | id: mouseArea 90 | anchors.fill: parent 91 | hoverEnabled: true 92 | cursorShape: Qt.PointingHandCursor 93 | onEntered: root.isHovered = true 94 | onExited: root.isHovered = false 95 | } 96 | 97 | Process { 98 | id: usernameProcess 99 | running: true 100 | command: ["sh", "-c", "whoami"] 101 | 102 | stdout: SplitParser { 103 | splitMarker: "\n" 104 | onRead: (data) => { 105 | const line = data.trim(); 106 | if (line.length > 0) { 107 | root.userName = line.charAt(0).toUpperCase() + line.slice(1); 108 | } 109 | } 110 | } 111 | } 112 | 113 | Process { 114 | id: uptimeProcess 115 | running: false 116 | command: ["sh", "-c", "uptime"] 117 | 118 | stdout: SplitParser { 119 | splitMarker: "\n" 120 | onRead: (data) => { 121 | const match = data.match(/up\s+(.*),\s+\d+\s+users/); 122 | if (match && match[1]) { 123 | root.userInfo = "Uptime: " + match[1]; // e.g. "Up: 5:54" 124 | } else { 125 | root.userInfo = "Uptime unknown"; 126 | } 127 | } 128 | } 129 | } 130 | 131 | 132 | Timer { 133 | id: uptimeTimer 134 | interval: 60000 // 60 seconds 135 | running: true 136 | repeat: true 137 | onTriggered: { 138 | uptimeProcess.running = false 139 | uptimeProcess.running = true 140 | } 141 | } 142 | 143 | Component.onCompleted: { 144 | uptimeProcess.running = true 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Popup/NotificationPopup.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Layouts 4 | import Quickshell.Services.Notifications 5 | import "root:/Data/" as Data 6 | 7 | Item { 8 | id: root 9 | required property var shell 10 | required property var notificationServer 11 | 12 | property var notificationQueue: [] 13 | property var activeTimers: ({}) 14 | property int lastNotificationTime: 0 15 | property int displayTime: 10000 16 | property int maxWidth: 400 17 | property int maxNotifications: 5 18 | property int notificationSpacing: 12 19 | property int baseNotificationHeight: 70 20 | 21 | property int calculatedHeight: { 22 | let total = 0; 23 | let count = Math.min(notificationQueue.length, maxNotifications); 24 | for (let i = 0; i < count; i++) { 25 | total += calculateIndividualHeight(notificationQueue[i]); 26 | if (i < count - 1) total += notificationSpacing; 27 | } 28 | return Math.max(total + 20, 0); 29 | } 30 | 31 | function calculateIndividualHeight(notification) { 32 | let h = 60; 33 | h += 18; 34 | if (notification?.summary?.trim()) h += 20; 35 | if (notification?.body?.trim()) { 36 | let body = notification.body.trim(); 37 | let lines = Math.max((body.match(/\n/g) || []).length + 1, Math.ceil(body.length / 60)); 38 | h += Math.min(lines * 18, 80); 39 | } 40 | if (notification?.actions?.length > 0) h += 35; 41 | return h; 42 | } 43 | 44 | Component { 45 | id: expiryTimerComponent 46 | Timer { 47 | property var targetNotification 48 | interval: displayTime 49 | running: true 50 | onTriggered: { 51 | if (targetNotification?.tracked) 52 | dismissNotification(targetNotification) 53 | destroy() 54 | } 55 | } 56 | } 57 | 58 | Connections { 59 | target: notificationServer 60 | 61 | function onNotification(notification) { 62 | if (!notification) return 63 | 64 | notification.tracked = true 65 | notification.arrivalTime = Date.now() 66 | lastNotificationTime = notification.arrivalTime 67 | 68 | notificationQueue.unshift(notification) 69 | notificationQueueChanged() 70 | 71 | let timer = expiryTimerComponent.createObject(root, { 72 | "targetNotification": notification 73 | }); 74 | if (timer && notification.id) 75 | activeTimers[notification.id] = timer; 76 | 77 | if (notification.closed) { 78 | notification.closed.connect(function(reason) { 79 | removeFromQueue(notification.id) 80 | }) 81 | } 82 | 83 | if (root.parent?.visible !== undefined) 84 | root.parent.visible = true 85 | } 86 | } 87 | 88 | function removeFromQueue(notificationId) { 89 | if (activeTimers[notificationId]) { 90 | activeTimers[notificationId].destroy(); 91 | delete activeTimers[notificationId]; 92 | } 93 | 94 | for (let i = 0; i < notificationQueue.length; i++) { 95 | if (notificationQueue[i]?.id === notificationId) { 96 | notificationQueue.splice(i, 1) 97 | notificationQueueChanged() 98 | break 99 | } 100 | } 101 | 102 | if (notificationQueue.length === 0 && root.parent?.visible !== undefined) 103 | root.parent.visible = false 104 | } 105 | 106 | function dismissNotification(notification) { 107 | if (!notification) return 108 | 109 | if (notification.id && activeTimers[notification.id]) { 110 | activeTimers[notification.id].destroy() 111 | delete activeTimers[notification.id] 112 | } 113 | 114 | removeFromQueue(notification.id) 115 | 116 | try { 117 | if (typeof notification.dismiss === 'function') notification.dismiss() 118 | else if (typeof notification.expire === 'function') notification.expire() 119 | } catch (e) {} 120 | } 121 | 122 | Component.onDestruction: { 123 | for (let id in activeTimers) { 124 | activeTimers[id]?.destroy() 125 | } 126 | activeTimers = {} 127 | } 128 | 129 | Column { 130 | id: notificationColumn 131 | width: maxWidth 132 | spacing: notificationSpacing 133 | anchors.top: parent.top 134 | anchors.right: parent.right 135 | anchors.rightMargin: 2 136 | 137 | Repeater { 138 | model: notificationQueue.length > 0 ? Math.min(notificationQueue.length, maxNotifications) : 0 139 | 140 | delegate: Rectangle { 141 | id: notificationContainer 142 | property var notification: index < notificationQueue.length ? notificationQueue[index] : null 143 | property bool isNewest: notification?.arrivalTime === lastNotificationTime 144 | visible: notification !== null 145 | 146 | width: maxWidth 147 | height: notification ? calculateIndividualHeight(notification) : 0 148 | radius: 20 149 | color: Data.Colors.bgColor 150 | border.width: 3 151 | border.color: Data.Colors.accentColor 152 | 153 | Rectangle { 154 | anchors.fill: parent 155 | anchors.margins: parent.border.width 156 | radius: parent.radius - parent.border.width 157 | gradient: Gradient { 158 | GradientStop { position: 0.0; color: Qt.rgba(255, 255, 255, 0.08) } 159 | GradientStop { position: 1.0; color: Qt.rgba(255, 255, 255, 0.02) } 160 | } 161 | } 162 | 163 | opacity: isNewest ? 0 : 1 164 | scale: isNewest ? 0.95 : 1 165 | 166 | Component.onCompleted: { 167 | if (isNewest) slideInAnimation.start() 168 | } 169 | 170 | ParallelAnimation { 171 | id: slideInAnimation 172 | NumberAnimation { target: notificationContainer; property: "opacity"; from: 0; to: 0.92; duration: 300; easing.type: Easing.OutCubic } 173 | NumberAnimation { target: notificationContainer; property: "scale"; from: 0.95; to: 1; duration: 300; easing.type: Easing.OutBack; easing.overshoot: 1.1 } 174 | } 175 | 176 | MouseArea { 177 | id: hoverArea 178 | anchors.fill: parent 179 | hoverEnabled: true 180 | onClicked: { if (notification) dismissNotification(notification) } 181 | z: -1 182 | onEntered: hoverAnimation.start() 183 | onExited: unhoverAnimation.start() 184 | } 185 | 186 | NumberAnimation { id: hoverAnimation; target: notificationContainer; property: "scale"; to: 1.02; duration: 150; easing.type: Easing.OutCubic } 187 | NumberAnimation { id: unhoverAnimation; target: notificationContainer; property: "scale"; to: 1.0; duration: 150; easing.type: Easing.OutCubic } 188 | 189 | Item { 190 | id: contentArea 191 | anchors.fill: parent 192 | anchors.margins: 12 193 | anchors.leftMargin: 16 194 | anchors.rightMargin: 16 195 | clip: true 196 | 197 | ColumnLayout { 198 | anchors.fill: parent 199 | spacing: 6 200 | 201 | RowLayout { 202 | Layout.fillWidth: true 203 | spacing: 12 204 | 205 | Rectangle { 206 | width: 28; height: 28; radius: 14 207 | color: Qt.rgba(255, 255, 255, 0.05) 208 | border.width: 1; border.color: Data.Colors.accentColor 209 | Layout.alignment: Qt.AlignTop; Layout.topMargin: 6 210 | 211 | Image { 212 | id: appImage 213 | source: notification ? (notification.image || notification.appIcon || "") : "" 214 | anchors.fill: parent 215 | anchors.margins: 2 216 | fillMode: Image.PreserveAspectFit 217 | visible: source.toString() !== "" 218 | } 219 | 220 | Text { 221 | anchors.centerIn: parent 222 | text: notification?.appName?.charAt(0).toUpperCase() || "!" 223 | color: Data.Colors.accentColor 224 | font.pixelSize: 12 225 | font.bold: true 226 | visible: !appImage.visible 227 | } 228 | } 229 | 230 | ColumnLayout { 231 | Layout.fillWidth: true 232 | spacing: 2 233 | 234 | RowLayout { 235 | Layout.fillWidth: true 236 | 237 | Text { 238 | text: notification?.appName || "Notification" 239 | color: Data.Colors.accentColor 240 | font.bold: true 241 | font.pixelSize: 13 242 | elide: Text.ElideRight 243 | Layout.fillWidth: true 244 | } 245 | 246 | Text { 247 | text: Qt.formatDateTime(new Date(), "hh:mm") 248 | color: Qt.lighter(Data.Colors.fgColor, 1.6) 249 | font.pixelSize: 10 250 | opacity: 0.8 251 | } 252 | 253 | Button { 254 | width: 18; height: 18; flat: true 255 | onClicked: { if (notification) dismissNotification(notification) } 256 | background: Rectangle { 257 | radius: 9 258 | color: parent.pressed ? Qt.rgba(255, 255, 255, 0.15) : 259 | parent.hovered ? Qt.rgba(255, 255, 255, 0.1) : 260 | Qt.rgba(255, 255, 255, 0.05) 261 | border.width: 1 262 | border.color: Qt.rgba(255, 255, 255, 0.08) 263 | } 264 | contentItem: Text { 265 | text: "×" 266 | color: Data.Colors.fgColor 267 | font.pixelSize: 11 268 | horizontalAlignment: Text.AlignHCenter 269 | verticalAlignment: Text.AlignVCenter 270 | opacity: 0.8 271 | } 272 | } 273 | } 274 | 275 | Text { 276 | text: notification?.summary || "" 277 | color: Data.Colors.fgColor 278 | font.bold: true 279 | font.pixelSize: 12 280 | wrapMode: Text.Wrap 281 | Layout.fillWidth: true 282 | visible: text.trim() !== "" 283 | maximumLineCount: 2 284 | elide: Text.ElideRight 285 | } 286 | } 287 | } 288 | 289 | Text { 290 | text: notification?.body || "" 291 | color: Qt.lighter(Data.Colors.fgColor, 1.2) 292 | font.pixelSize: 14 293 | wrapMode: Text.Wrap 294 | Layout.fillWidth: true 295 | maximumLineCount: 4 296 | elide: Text.ElideRight 297 | visible: text.trim() !== "" 298 | lineHeight: 1.2 299 | Layout.preferredHeight: visible ? implicitHeight : 0 300 | } 301 | } 302 | } 303 | } 304 | } 305 | } 306 | 307 | Rectangle { 308 | visible: notificationQueue.length > maxNotifications 309 | anchors.bottom: notificationColumn.bottom 310 | anchors.right: notificationColumn.right 311 | anchors.margins: 12 312 | width: 70; height: 28; radius: 20 313 | border.width: 1; border.color: Data.Colors.accentColor 314 | 315 | Rectangle { 316 | anchors.fill: parent 317 | radius: parent.radius 318 | color: Data.Colors.accentColor 319 | } 320 | 321 | Text { 322 | anchors.centerIn: parent 323 | text: "+" + (notificationQueue.length - maxNotifications) 324 | color: Data.Colors.bgColor 325 | font.pixelSize: 12 326 | font.bold: true 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Popup/PopupContent.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Layouts 4 | import QtQuick.Shapes 5 | import "./modules" as PopupModules 6 | 7 | Item { 8 | required property var shell 9 | property string selectedWidget: "calendar" 10 | signal contentHeightChanged(int newHeight) 11 | 12 | // Tracks current height of the system view 13 | property int systemViewHeight: 280 14 | 15 | // Calculates the appropriate content height for each view 16 | property int calculatedContentHeight: { 17 | switch(selectedWidget) { 18 | case "calendar": return 200 19 | case "weather": return 200 20 | case "system": { 21 | var base = 200 22 | var tray = systemView.trayMenu?.visible ? (systemView.trayMenu.calculatedHeight + 40) : 0 23 | var total = base + tray 24 | if (tray > 300) total += 40 25 | else if (tray > 150) total += 20 26 | return total 27 | } 28 | default: return 280 29 | } 30 | } 31 | 32 | // Bind root height to calculated content height + margins 33 | implicitHeight: calculatedContentHeight + 100 34 | 35 | onCalculatedContentHeightChanged: { 36 | var height = calculatedContentHeight + 100 37 | if (selectedWidget === "system") height += 40 38 | contentHeightChanged(height) 39 | } 40 | 41 | onSelectedWidgetChanged: { 42 | contentHeightChanged(calculatedContentHeight + 60) 43 | } 44 | 45 | ColumnLayout { 46 | anchors.fill: parent 47 | spacing: 12 48 | 49 | Item { 50 | Layout.fillWidth: true 51 | Layout.fillHeight: true 52 | 53 | StackLayout { 54 | anchors.fill: parent 55 | 56 | currentIndex: { 57 | switch(selectedWidget) { 58 | case "calendar": return 0 59 | case "weather": return 1 60 | case "system": return 2 61 | default: return 0 62 | } 63 | } 64 | 65 | // Calendar 66 | PopupModules.CalendarView { 67 | readonly property var shell: parent.shell 68 | // Make sure CalendarView has implicit sizes inside its QML file 69 | } 70 | 71 | // Weather 72 | PopupModules.WeatherView { 73 | readonly property var shell: parent.shell 74 | // Make sure WeatherView has implicit sizes inside its QML file 75 | } 76 | 77 | // System monitor 78 | PopupModules.SystemView { 79 | id: systemView 80 | readonly property var shell: parent.shell 81 | 82 | onHeightChanged: systemViewHeight = height 83 | onContentHeightChanged: systemViewHeight = contentHeight 84 | 85 | Component.onCompleted: { 86 | systemViewHeight = contentHeight !== undefined ? contentHeight : height 87 | } 88 | } 89 | } 90 | } 91 | 92 | RowLayout { 93 | Layout.fillWidth: true 94 | spacing: 10 95 | Layout.alignment: Qt.AlignHCenter 96 | 97 | Repeater { 98 | model: ["calendar", "weather", "system"] 99 | delegate: Button { 100 | checkable: true 101 | checked: selectedWidget === modelData 102 | onClicked: selectedWidget = modelData 103 | 104 | implicitWidth: 30 105 | implicitHeight: 30 106 | 107 | background: Rectangle { 108 | radius: 20 109 | color: parent.down ? Qt.darker(shell.accentColor, 1.2) : 110 | parent.hovered ? Qt.lighter(shell.highlightBg, 1.1) : shell.highlightBg 111 | } 112 | 113 | contentItem: Label { 114 | text: parent.checked ? "●" : "○" 115 | font.family: "FiraCode Nerd Font" 116 | font.pixelSize: parent.checked ? 14 : 18 117 | color: parent.checked ? shell.accentColor : shell.fgColor 118 | horizontalAlignment: Text.AlignHCenter 119 | verticalAlignment: Text.AlignVCenter 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Popup/modules/CalendarView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | import "root:/Data" as Data 5 | 6 | Rectangle { 7 | property var shell 8 | 9 | radius: 20 10 | color: Qt.lighter(Data.Colors.bgColor, 1.2) 11 | 12 | readonly property date currentDate: new Date() 13 | readonly property int currentMonth: currentDate.getMonth() 14 | readonly property int currentYear: currentDate.getFullYear() 15 | readonly property int currentDay: currentDate.getDate() 16 | 17 | ColumnLayout { 18 | anchors.fill: parent 19 | anchors.margins: 12 20 | spacing: 12 21 | 22 | RowLayout { 23 | Layout.fillWidth: true 24 | spacing: 8 25 | 26 | // Button component for navigation arrows 27 | component NavButton: AbstractButton { 28 | property alias buttonText: buttonLabel.text 29 | implicitWidth: 30 30 | implicitHeight: 30 31 | 32 | background: Rectangle { 33 | radius: 15 34 | color: parent.down ? Qt.darker(Data.Colors.accentColor, 1.2) : 35 | parent.hovered ? Qt.lighter(Data.Colors.highlightBg, 1.1) : Data.Colors.highlightBg 36 | } 37 | 38 | Text { 39 | id: buttonLabel 40 | anchors.centerIn: parent 41 | color: Data.Colors.fgColor 42 | } 43 | } 44 | 45 | // Previous month button 46 | NavButton { 47 | buttonText: "‹" 48 | onClicked: { 49 | if (calendar.month === 0) { 50 | calendar.month = 11 51 | calendar.year -= 1 52 | } else { 53 | calendar.month -= 1 54 | } 55 | } 56 | } 57 | 58 | // Display current month and year 59 | Text { 60 | text: Qt.locale("en_US").monthName(calendar.month) + " " + calendar.year 61 | color: Data.Colors.accentColor 62 | font.bold: true 63 | Layout.fillWidth: true 64 | horizontalAlignment: Text.AlignHCenter 65 | } 66 | 67 | // Next month button 68 | NavButton { 69 | buttonText: "›" 70 | onClicked: { 71 | if (calendar.month === 11) { 72 | calendar.month = 0 73 | calendar.year += 1 74 | } else { 75 | calendar.month += 1 76 | } 77 | } 78 | } 79 | } 80 | 81 | Grid { 82 | columns: 7 83 | rowSpacing: 4 84 | columnSpacing: 0 85 | Layout.leftMargin: 2 86 | Layout.fillWidth: true 87 | 88 | // Weekday headers 89 | Text { text: "M"; color: Data.Colors.fgColor; font.bold: true; horizontalAlignment: Text.AlignHCenter; width: parent.width / 7 } 90 | Text { text: "T"; color: Data.Colors.fgColor; font.bold: true; horizontalAlignment: Text.AlignHCenter; width: parent.width / 7 } 91 | Text { text: "W"; color: Data.Colors.fgColor; font.bold: true; horizontalAlignment: Text.AlignHCenter; width: parent.width / 7 } 92 | Text { text: "T"; color: Data.Colors.fgColor; font.bold: true; horizontalAlignment: Text.AlignHCenter; width: parent.width / 7 } 93 | Text { text: "F"; color: Data.Colors.fgColor; font.bold: true; horizontalAlignment: Text.AlignHCenter; width: parent.width / 7 } 94 | Text { text: "S"; color: Data.Colors.fgColor; font.bold: true; horizontalAlignment: Text.AlignHCenter; width: parent.width / 7 } 95 | Text { text: "S"; color: Data.Colors.fgColor; font.bold: true; horizontalAlignment: Text.AlignHCenter; width: parent.width / 7 } 96 | } 97 | 98 | // Main calendar grid for days 99 | MonthGrid { 100 | id: calendar 101 | month: currentMonth 102 | year: currentYear 103 | Layout.fillWidth: true 104 | Layout.fillHeight: true 105 | spacing: 4 106 | leftPadding: 0 107 | rightPadding: 0 108 | locale: Qt.locale("de_DE") 109 | implicitHeight: 400 110 | 111 | delegate: Rectangle { 112 | width: 30 113 | height: 30 114 | radius: 15 115 | 116 | readonly property bool isCurrentMonth: model.month === calendar.month 117 | readonly property bool isToday: model.day === currentDay 118 | && model.month === currentMonth 119 | && calendar.year === currentYear 120 | && isCurrentMonth 121 | 122 | // Highlight today, current month, or muted for others 123 | color: isToday ? Data.Colors.accentColor : 124 | isCurrentMonth ? Data.Colors.highlightBg : Qt.darker(Data.Colors.highlightBg, 1.2) 125 | 126 | Text { 127 | text: model.day 128 | anchors.centerIn: parent 129 | color: parent.isToday ? Data.Colors.bgColor : 130 | parent.isCurrentMonth ? Data.Colors.fgColor : Qt.darker(Data.Colors.fgColor, 1.5) 131 | font.bold: parent.isToday 132 | } 133 | } 134 | } 135 | 136 | // Button to reset calendar to today 137 | AbstractButton { 138 | Layout.fillWidth: true 139 | Layout.preferredHeight: 36 140 | 141 | onClicked: { 142 | calendar.month = currentMonth 143 | calendar.year = currentYear 144 | } 145 | 146 | background: Rectangle { 147 | radius: 18 148 | color: parent.down ? Qt.darker(Data.Colors.accentColor, 1.2) : 149 | parent.hovered ? Qt.lighter(Data.Colors.highlightBg, 1.1) : Data.Colors.highlightBg 150 | } 151 | 152 | Text { 153 | text: "Today" 154 | anchors.centerIn: parent 155 | color: Data.Colors.fgColor 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Popup/modules/CustomTrayMenu.qml: -------------------------------------------------------------------------------- 1 | pragma ComponentBehavior: Bound 2 | import QtQuick 3 | import QtQuick.Controls 4 | import QtQuick.Layouts 5 | import Quickshell 6 | import "root:/Data/" as Data 7 | 8 | Rectangle { 9 | id: trayMenu 10 | implicitWidth: 360 11 | implicitHeight: Math.max(40, listView.contentHeight + 12 + 16) 12 | clip: true 13 | color: Data.Colors.bgColor 14 | border.color: Data.Colors.accentColor 15 | border.width: 3 16 | radius: 20 17 | visible: false 18 | enabled: visible 19 | 20 | property QsMenuHandle menu 21 | property point triggerPoint: Qt.point(0, 0) 22 | property Item originalParent 23 | 24 | QsMenuOpener { 25 | id: opener 26 | menu: trayMenu.menu 27 | } 28 | 29 | Rectangle { 30 | id: overlay 31 | x: -trayMenu.x 32 | y: -trayMenu.y 33 | width: Screen.width 34 | height: Screen.height 35 | color: "transparent" 36 | visible: trayMenu.visible 37 | z: -1 38 | 39 | MouseArea { 40 | anchors.fill: parent 41 | enabled: trayMenu.visible 42 | acceptedButtons: Qt.AllButtons 43 | onPressed: { 44 | trayMenu.hide() 45 | } 46 | } 47 | } 48 | 49 | function flattenMenuItems(menuHandle) { 50 | var result = []; 51 | if (!menuHandle || !menuHandle.children) { 52 | return result; 53 | } 54 | 55 | var childrenArray = []; 56 | for (var i = 0; i < menuHandle.children.length; i++) { 57 | childrenArray.push(menuHandle.children[i]); 58 | } 59 | 60 | for (var i = 0; i < childrenArray.length; i++) { 61 | var item = childrenArray[i]; 62 | 63 | if (item.isSeparator) { 64 | result.push(item); 65 | } else if (item.menu) { 66 | result.push(item); 67 | var submenuItems = flattenMenuItems(item.menu); 68 | result = result.concat(submenuItems); 69 | } else { 70 | result.push(item); 71 | } 72 | } 73 | return result; 74 | } 75 | 76 | ListView { 77 | id: listView 78 | anchors.fill: parent 79 | anchors.margins: 6 80 | anchors.topMargin: 3 81 | anchors.bottomMargin: 9 82 | model: ScriptModel { 83 | values: flattenMenuItems(opener.menu) 84 | } 85 | interactive: false 86 | 87 | delegate: Rectangle { 88 | id: entry 89 | required property var modelData 90 | 91 | width: listView.width - 12 92 | height: modelData.isSeparator ? 10 : 28 93 | color: modelData.isSeparator ? Data.Colors.bgColor : (mouseArea.containsMouse ? Data.Colors.highlightBg : "transparent") 94 | radius: modelData.isSeparator ? 0 : 4 95 | 96 | Item { 97 | anchors.fill: parent 98 | visible: modelData.isSeparator 99 | 100 | Rectangle { 101 | anchors.horizontalCenter: parent.horizontalCenter 102 | anchors.verticalCenter: parent.verticalCenter 103 | width: parent.width * 0.85 104 | height: 1 105 | color: Data.Colors.accentColor 106 | opacity: 0.3 107 | } 108 | } 109 | 110 | RowLayout { 111 | anchors.fill: parent 112 | anchors.leftMargin: 8 113 | anchors.rightMargin: 8 114 | spacing: 6 115 | visible: !modelData.isSeparator 116 | 117 | Text { 118 | Layout.fillWidth: true 119 | color: (modelData?.enabled ?? true) ? Data.Colors.fgColor : Qt.darker(Data.Colors.fgColor, 1.8) 120 | text: modelData?.text ?? "" 121 | font.pixelSize: 12 122 | font.family: "FiraCode Nerd Font" 123 | verticalAlignment: Text.AlignVCenter 124 | elide: Text.ElideRight 125 | maximumLineCount: 1 126 | } 127 | 128 | Image { 129 | Layout.preferredWidth: 14 130 | Layout.preferredHeight: 14 131 | source: modelData?.icon ?? "" 132 | visible: (modelData?.icon ?? "") !== "" 133 | fillMode: Image.PreserveAspectFit 134 | } 135 | } 136 | 137 | MouseArea { 138 | id: mouseArea 139 | anchors.fill: parent 140 | hoverEnabled: true 141 | enabled: (modelData?.enabled ?? true) && trayMenu.visible && !modelData.isSeparator 142 | 143 | onClicked: { 144 | if (modelData) { 145 | modelData.triggered() 146 | trayMenu.hide() 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Popup/modules/SystemTray.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | import Quickshell 5 | import Quickshell.Services.SystemTray 6 | 7 | Row { 8 | property var bar 9 | property var shell 10 | property var trayMenu 11 | spacing: 8 12 | Layout.alignment: Qt.AlignVCenter 13 | 14 | property var systemTray: SystemTray 15 | 16 | Repeater { 17 | model: systemTray.items 18 | delegate: Item { 19 | width: 24 20 | height: 24 21 | property bool isHovered: trayMouseArea.containsMouse 22 | 23 | scale: isHovered ? 1.15 : 1.0 24 | Behavior on scale { 25 | NumberAnimation { 26 | duration: 150 27 | easing.type: Easing.OutCubic 28 | } 29 | } 30 | 31 | rotation: isHovered ? 5 : 0 32 | Behavior on rotation { 33 | NumberAnimation { 34 | duration: 200 35 | easing.type: Easing.OutCubic 36 | } 37 | } 38 | 39 | Image { 40 | anchors.centerIn: parent 41 | width: 18 42 | height: 18 43 | source: { 44 | let icon = modelData?.icon || ""; 45 | if (icon.includes("?path=")) { 46 | const [name, path] = icon.split("?path="); 47 | const fileName = name.substring(name.lastIndexOf("/") + 1); 48 | return `file://${path}/${fileName}`; 49 | } 50 | return icon; 51 | } 52 | opacity: 0 53 | Component.onCompleted: opacity = 1 54 | Behavior on opacity { 55 | NumberAnimation { 56 | duration: 300 57 | easing.type: Easing.OutCubic 58 | } 59 | } 60 | } 61 | 62 | MouseArea { 63 | id: trayMouseArea 64 | anchors.fill: parent 65 | hoverEnabled: true 66 | acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton 67 | onClicked: (mouse) => { 68 | if (!modelData) return; 69 | 70 | if (mouse.button === Qt.LeftButton) { 71 | if (trayMenu && trayMenu.visible) { 72 | trayMenu.hide() 73 | } 74 | if (!modelData.onlyMenu) { 75 | modelData.activate() 76 | } 77 | } else if (mouse.button === Qt.MiddleButton) { 78 | if (trayMenu && trayMenu.visible) { 79 | trayMenu.hide() 80 | } 81 | modelData.secondaryActivate && modelData.secondaryActivate() 82 | } else if (mouse.button === Qt.RightButton) { 83 | if (trayMenu && trayMenu.visible) { 84 | trayMenu.hide() 85 | return 86 | } 87 | if (modelData.hasMenu && modelData.menu && trayMenu) { 88 | trayMenu.menu = modelData.menu 89 | if (parent.parent.parent.showTrayMenu) { 90 | const iconCenter = Qt.point(width / 2, height) 91 | parent.parent.parent.showTrayMenu(iconCenter, this) 92 | } else { 93 | const iconPos = mapToItem(trayMenu.parent, 0, 0) 94 | const menuX = iconPos.x - (trayMenu.width / 2) + (width / 2) 95 | const menuY = iconPos.y + height + 15 96 | trayMenu.show(Qt.point(menuX, menuY), trayMenu.parent) 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /modules/quickshell/qml/Popup/modules/WeatherView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | import "root:/Data" as Data 5 | 6 | Rectangle { 7 | color: Qt.lighter(Data.Colors.bgColor, 1.2) 8 | radius: 20 9 | 10 | readonly property var weatherEmojiMap: ({ 11 | "clear": "☀️", 12 | "mainly clear": "🌤️", 13 | "partly cloudy": "⛅", 14 | "cloud": "☁️", 15 | "overcast": "☁️", 16 | "fog": "🌫️", 17 | "mist": "🌫️", 18 | "drizzle": "🌦️", 19 | "rain": "🌧️", 20 | "showers": "🌧️", 21 | "freezing rain": "🌧️❄️", 22 | "snow": "❄️", 23 | "snow grains": "❄️", 24 | "snow showers": "❄️", 25 | "thunderstorm": "⛈️", 26 | "wind": "🌬️" 27 | }) 28 | 29 | function getWeatherEmoji(condition) { 30 | if (!condition) return "❓" 31 | const lowerCondition = condition.toLowerCase() 32 | // Exact match first for efficiency 33 | if (weatherEmojiMap[lowerCondition]) return weatherEmojiMap[lowerCondition] 34 | // Partial match fallback 35 | for (const key in weatherEmojiMap) { 36 | if (lowerCondition.includes(key)) return weatherEmojiMap[key] 37 | } 38 | return "❓" 39 | } 40 | 41 | ColumnLayout { 42 | anchors.fill: parent 43 | anchors.margins: 12 44 | spacing: 8 45 | 46 | Label { 47 | text: weatherLoading ? "Loading weather..." : "Weather" 48 | color: Data.Colors.accentColor 49 | font { 50 | pixelSize: 18 51 | bold: true 52 | family: "FiraCode Nerd Font" 53 | } 54 | horizontalAlignment: Text.AlignHCenter 55 | Layout.alignment: Qt.AlignHCenter 56 | } 57 | 58 | ColumnLayout { 59 | spacing: 8 60 | Layout.alignment: Qt.AlignHCenter 61 | 62 | RowLayout { 63 | spacing: 16 64 | Layout.alignment: Qt.AlignHCenter 65 | 66 | Label { 67 | text: weatherLoading ? "⏳" : getWeatherEmoji((weatherData && weatherData.currentCondition) || "?") 68 | font.pixelSize: 48 69 | color: Data.Colors.fgColor 70 | } 71 | 72 | Label { 73 | text: weatherLoading ? "..." : ((weatherData && weatherData.currentTemp) || "?") 74 | font { 75 | pixelSize: 24 76 | bold: true 77 | family: "FiraCode Nerd Font" 78 | } 79 | color: Data.Colors.fgColor 80 | } 81 | } 82 | 83 | GridLayout { 84 | columns: 2 85 | columnSpacing: 16 86 | rowSpacing: 8 87 | Layout.alignment: Qt.AlignHCenter 88 | visible: Boolean(!weatherLoading && weatherData && weatherData.details && weatherData.details.length > 0) 89 | 90 | Repeater { 91 | model: weatherLoading ? 0 : (weatherData && weatherData.details ? weatherData.details.length : 0) 92 | delegate: RowLayout { 93 | spacing: 8 94 | 95 | property var detailItem: weatherData && weatherData.details ? weatherData.details[index] : "" 96 | property var detailParts: detailItem ? detailItem.split(":") : ["", ""] 97 | 98 | Label { 99 | text: detailParts[0] + ":" 100 | color: Qt.lighter(Data.Colors.fgColor, 1.2) 101 | font { 102 | pixelSize: 12 103 | bold: true 104 | } 105 | } 106 | Label { 107 | text: detailParts[1] || "" 108 | color: Data.Colors.fgColor 109 | font.pixelSize: 12 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | ColumnLayout { 117 | Layout.alignment: Qt.AlignHCenter 118 | spacing: 4 119 | visible: !weatherLoading 120 | 121 | Label { 122 | text: "3-Day Forecast" 123 | color: Data.Colors.accentColor 124 | font { 125 | pixelSize: 14 126 | bold: true 127 | family: "FiraCode Nerd Font" 128 | } 129 | horizontalAlignment: Text.AlignHCenter 130 | Layout.alignment: Qt.AlignHCenter 131 | } 132 | 133 | GridLayout { 134 | columns: 3 135 | columnSpacing: 70 136 | Layout.alignment: Qt.AlignHCenter 137 | 138 | Repeater { 139 | model: weatherLoading ? 0 : (weatherData && weatherData.forecast ? Math.min(3, weatherData.forecast.length) : 0) 140 | delegate: ColumnLayout { 141 | spacing: 4 142 | Layout.alignment: Qt.AlignHCenter 143 | 144 | property var forecastItem: weatherData && weatherData.forecast ? weatherData.forecast[index] : null 145 | 146 | Label { 147 | text: forecastItem ? forecastItem.dayName : "?" 148 | color: Data.Colors.fgColor 149 | font.pixelSize: 12 150 | font.bold: true 151 | horizontalAlignment: Text.AlignHCenter 152 | Layout.alignment: Qt.AlignHCenter 153 | } 154 | 155 | Label { 156 | text: forecastItem ? getWeatherEmoji(forecastItem.condition) : "?" 157 | font.pixelSize: 32 158 | color: Data.Colors.fgColor 159 | horizontalAlignment: Text.AlignHCenter 160 | Layout.alignment: Qt.AlignHCenter 161 | } 162 | 163 | Label { 164 | text: { 165 | if (!forecastItem) return "?" 166 | if (forecastItem.temp !== undefined) return forecastItem.temp + "°C" 167 | if (forecastItem.minTemp !== undefined && forecastItem.maxTemp !== undefined) return forecastItem.minTemp + "°C / " + forecastItem.maxTemp + "°C" 168 | return "?" 169 | } 170 | font.pixelSize: 12 171 | color: Data.Colors.fgColor 172 | horizontalAlignment: Text.AlignHCenter 173 | Layout.alignment: Qt.AlignHCenter 174 | } 175 | } 176 | } 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /modules/quickshell/qml/shell.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | import QtQuick.Shapes 5 | import Quickshell 6 | import Quickshell.Io 7 | import Quickshell.Wayland 8 | import Quickshell.Services.SystemTray 9 | import Quickshell.Hyprland 10 | import Quickshell.Wayland 11 | import Quickshell.Services.Pipewire 12 | import Quickshell.Services.Notifications 13 | import "Data" as Dat 14 | import "Bar" as Bar 15 | import "Core" as Core 16 | 17 | ShellRoot { 18 | id: root 19 | 20 | // Expose windows for external access 21 | property alias bar: shellWindows.mainWindow 22 | property alias popupWindow: shellWindows.popupWindow 23 | property alias notificationWindow: shellWindows.notificationWindow 24 | property alias notificationServer: notificationService.notificationServer 25 | property alias cliphistWindow: shellWindows.cliphistWindow 26 | 27 | // Notification history 28 | property var notificationHistory: [] 29 | property int maxHistoryItems: 50 30 | 31 | // Global theme properties from Data/Colors.qml 32 | readonly property color bgColor: Dat.Colors.bgColor 33 | readonly property color fgColor: Dat.Colors.fgColor 34 | readonly property color accentColor: Dat.Colors.accentColor 35 | readonly property color highlightBg: Dat.Colors.highlightBg 36 | readonly property color borderColor: Qt.darker(bgColor, 1.1) 37 | readonly property color errorColor: "#ff5555" 38 | 39 | // Time and date properties 40 | property string time: Qt.formatDateTime(new Date(), "hh:mm AP") 41 | property string date: Qt.formatDateTime(new Date(), "ddd MMM d") 42 | 43 | // System state properties 44 | property string active_window: ToplevelManager.activeToplevel ? ToplevelManager.activeToplevel.title : "" 45 | property var workspaces: Hyprland.workspaces || [] 46 | property var focusedWorkspace: Hyprland.focusedWorkspace 47 | property var trayItems: SystemTray.items.values || [] 48 | 49 | // Audio properties 50 | property var defaultAudioSink: Pipewire.defaultAudioSink 51 | property int volume: defaultAudioSink && defaultAudioSink.audio ? Math.round(defaultAudioSink.audio.volume * 100) : 0 52 | 53 | // Weather properties 54 | property string weatherLocation: Dat.Settings.weatherLocation 55 | property var weatherData: Dat.Settings.weatherData 56 | property bool weatherLoading: false 57 | 58 | // Utility function 59 | function copyToClipboard(text) { 60 | Clipboard.copy(text); 61 | } 62 | 63 | function addToNotificationHistory(notification) { 64 | notificationHistory.unshift({ 65 | appName: notification.appName, 66 | summary: notification.summary, 67 | body: notification.body, 68 | timestamp: new Date(), 69 | icon: notification.appIcon 70 | }) 71 | 72 | if (notificationHistory.length > maxHistoryItems) { 73 | notificationHistory.pop() 74 | } 75 | notificationHistoryChanged() 76 | } 77 | 78 | // Initialize services and components 79 | Component.onCompleted: { 80 | if (Hyprland.refreshWorkspaces) Hyprland.refreshWorkspaces() 81 | weatherService.loadWeather() 82 | } 83 | 84 | // Core services 85 | Core.NotificationService { 86 | id: notificationService 87 | shell: root 88 | } 89 | 90 | Core.WeatherService { 91 | id: weatherService 92 | shell: root 93 | } 94 | 95 | Core.SystemTimers { 96 | id: systemTimers 97 | shell: root 98 | weatherService: weatherService 99 | } 100 | 101 | Core.ShellWindows { 102 | id: shellWindows 103 | shell: root 104 | notificationService: notificationService 105 | } 106 | 107 | PwObjectTracker { 108 | objects: [Pipewire.defaultAudioSink] 109 | } 110 | 111 | // System info watermark 112 | Core.Version {} 113 | 114 | } -------------------------------------------------------------------------------- /modules/quickshell/quickshell.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | let 4 | homeDir = config.home.homeDirectory; 5 | quickshellDir = "${homeDir}/nixos/modules/quickshell/qml"; 6 | quickshellTarget = "${homeDir}/.config/quickshell"; 7 | faceIconSource = "${homeDir}/nixos/assets/profile.gif"; 8 | faceIconTarget = "${homeDir}/.face.icon"; 9 | in { 10 | home.activation.symlinkQuickshellAndFaceIcon = lib.hm.dag.entryAfter [ "writeBoundary" ] '' 11 | ln -sfn "${quickshellDir}" "${quickshellTarget}" 12 | ln -sfn "${faceIconSource}" "${faceIconTarget}" 13 | ''; 14 | } 15 | -------------------------------------------------------------------------------- /system/environment.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | inputs, 5 | ... 6 | }: 7 | 8 | { 9 | 10 | environment.systemPackages = with pkgs; [ 11 | wget 12 | unzip 13 | git 14 | pavucontrol 15 | pulseaudio 16 | waypaper 17 | pywal16 18 | pywalfox-native 19 | arrpc 20 | swww 21 | adwaita-icon-theme 22 | gnome-themes-extra 23 | nodePackages.prettier 24 | xwayland 25 | spicetify-cli 26 | alvr 27 | ffmpeg 28 | mesa 29 | libva 30 | libva-utils 31 | playerctl 32 | libayatana-appindicator 33 | ]; 34 | 35 | environment.variables = { 36 | XCURSOR_SIZE = "24"; 37 | QT_QPA_PLATFORM = "wayland"; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /system/filesystems.nix: -------------------------------------------------------------------------------- 1 | { ... }: 2 | 3 | { 4 | fileSystems."/mnt/storage" = { 5 | device = "UUID=74697b82-1266-4372-96cc-aac599abfb72"; 6 | fsType = "ext4"; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /system/greeter/greetd.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | inputs, 5 | ... 6 | }: 7 | 8 | { 9 | services.greetd = { 10 | enable = true; 11 | settings = { 12 | default_session = { 13 | command = "${pkgs.greetd.tuigreet}/bin/tuigreet --remember --asterisks --container-padding 2 --no-xsession-wrapper --cmd Hyprland"; 14 | user = "greeter"; 15 | }; 16 | }; 17 | }; 18 | 19 | # this is a life saver. 20 | # literally no documentation about this anywhere. 21 | # might be good to write about this... 22 | # https://www.reddit.com/r/NixOS/comments/u0cdpi/tuigreet_with_xmonad_how/ 23 | 24 | systemd = { 25 | # To prevent getting stuck at shutdown 26 | extraConfig = "DefaultTimeoutStopSec=10s"; 27 | services.greetd.serviceConfig = { 28 | Type = "idle"; 29 | StandardInput = "tty"; 30 | StandardOutput = "tty"; 31 | StandardError = "journal"; 32 | TTYReset = true; 33 | TTYVHangup = true; 34 | TTYVTDisallocate = true; 35 | }; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /system/programs/lact.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | 8 | { 9 | environment.systemPackages = with pkgs; [ 10 | lact 11 | ]; 12 | 13 | systemd.services.lact = { 14 | description = "AMDGPU Control Daemon"; 15 | after = [ "multi-user.target" ]; 16 | wantedBy = [ "multi-user.target" ]; 17 | serviceConfig = { 18 | ExecStart = "${pkgs.lact}/bin/lact daemon"; 19 | }; 20 | enable = true; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /system/programs/steam.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | { 3 | hardware.steam-hardware.enable = true; 4 | programs.steam = { 5 | enable = true; 6 | remotePlay.openFirewall = true; # Open ports in the firewall for Steam Remote Play 7 | dedicatedServer.openFirewall = true; # Open ports in the firewall for Source Dedicated Server 8 | localNetworkGameTransfers.openFirewall = true; # Open ports in the firewall for Steam Local Network Game Transfers 9 | }; 10 | 11 | # OpenGL and Vulkan configuration 12 | hardware.graphics.enable = true; 13 | hardware.graphics.extraPackages = with pkgs; [ 14 | vulkan-loader 15 | vulkan-tools 16 | ]; 17 | 18 | # Add system packages for VR support 19 | environment.systemPackages = with pkgs; [ 20 | openvr # Required for SteamVR 21 | libusb1 # Used for VR devices 22 | usbutils 23 | pkgs.libsndfile 24 | pkgs.xwayland 25 | gamescope 26 | ]; 27 | 28 | # Udev rules for VR devices 29 | services.udev.packages = with pkgs; [ 30 | openvr 31 | ]; 32 | 33 | services.udev.extraRules = '' 34 | # HTC Vive 35 | SUBSYSTEM=="usb", ATTR{idVendor}=="0bb4", ATTR{idProduct}=="2c87", MODE="0666", GROUP="plugdev" 36 | SUBSYSTEM=="usb", ATTR{idVendor}=="28de", ATTR{idProduct}=="2101", MODE="0666", GROUP="plugdev" 37 | SUBSYSTEM=="usb", ATTR{idVendor}=="28de", ATTR{idProduct}=="2000", MODE="0666", GROUP="plugdev" 38 | ''; 39 | } 40 | -------------------------------------------------------------------------------- /system/programs/stylix.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, self, ... }: 2 | { 3 | stylix.enable = true; 4 | stylix.autoEnable = true; 5 | #stylix.image = "${self}/assets/wallpapers/city.jpg"; 6 | #stylix.base16Scheme = "${pkgs.base16-schemes}/share/themes/ayu-dark.yaml"; 7 | stylix.base16Scheme = "${pkgs.base16-schemes}/share/themes/rose-pine.yaml"; 8 | 9 | stylix.enableReleaseChecks = false; 10 | } 11 | -------------------------------------------------------------------------------- /system/shell/zsh.nix: -------------------------------------------------------------------------------- 1 | # fish.nix or configuration.nix 2 | { 3 | pkgs, 4 | config, 5 | lib, 6 | ... 7 | }: 8 | 9 | { 10 | programs.zsh = { 11 | enable = true; 12 | 13 | enableCompletion = true; 14 | autosuggestion.enable = true; 15 | syntaxHighlighting.enable = true; 16 | 17 | oh-my-zsh = { 18 | enable = true; 19 | theme = "bira"; 20 | plugins = [ "git" "sudo" ]; 21 | }; 22 | 23 | initContent = '' 24 | if [[ $- == *i* ]]; then 25 | fastfetch 26 | fi 27 | ''; 28 | }; 29 | } -------------------------------------------------------------------------------- /system/xdg.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | { 3 | xdg.portal = { 4 | enable = true; 5 | config = { 6 | common.default = "*"; 7 | }; 8 | extraPortals = with pkgs; [ 9 | xdg-desktop-portal 10 | xdg-desktop-portal-hyprland 11 | xdg-desktop-portal-gtk 12 | ]; 13 | }; 14 | } 15 | --------------------------------------------------------------------------------