diff --git a/alacritty/.config/alacritty/alacritty.toml b/alacritty/.config/alacritty/alacritty.toml new file mode 100644 index 0000000..77dea2b --- /dev/null +++ b/alacritty/.config/alacritty/alacritty.toml @@ -0,0 +1,2 @@ +[colors.normal] +black = "#3d3d3d" \ No newline at end of file diff --git a/emacs/.config/emacs/config.el b/emacs/.config/emacs/config.el new file mode 100644 index 0000000..4db734d --- /dev/null +++ b/emacs/.config/emacs/config.el @@ -0,0 +1,1075 @@ +(recentf-mode t) + +(use-package golden-ratio + :ensure t + :init + (setq golden-ratio-auto-scale t) + (golden-ratio-mode 1)) + +(defun kylekrein/duplicate-line() + "Duplicate current line and move cursor to it" + (interactive) + (let ((column (- (point) (point-at-bol))) + (line (let ((s (thing-at-point 'line t))) + (if s (string-remove-suffix "\n" s) "")))) + (move-end-of-line 1) + (newline) + (insert line) + (move-beginning-of-line 1) + (forward-char column))) + +(global-set-key [remap list-buffers] 'ibuffer) +(global-set-key (kbd "M-o") 'other-window) +(global-set-key (kbd "C-c o t") 'vterm-toggle) +(global-set-key (kbd "C-c o a") 'org-agenda) +(global-set-key (kbd "C-c o m") 'magit) + +(global-set-key (kbd "C-.") 'kylekrein/duplicate-line) +;;(windmove-default-keybindings) ;; move between windows with S-, S-, S-, S- + +(defun split-and-follow-horizontally () + (interactive) + (split-window-below) + (balance-windows)) + + (defun split-and-follow-vertically () + (interactive) + (split-window-right) + (balance-windows)) + + (use-package emacs + :bind (:map ctl-x-map + ("2" . split-and-follow-horizontally) + ("3" . split-and-follow-vertically)) + :custom + (info-lookup-other-window-flag t) + (help-window-select t "Switch to help buffers automatically")) +;; Auto-select new Info buffer window when it’s created. + (advice-add 'info-lookup :after + (lambda (&rest _) + (when-let (window (get-buffer-window "*info*")) + (select-window window)))) + + ;; Auto-select new window after splitting. Splitting commands almost + ;;,all use `split-window’, so advice the function for auto selection. + (advice-add 'split-window :after + (lambda (&rest _) (select-window (get-lru-window)))) + +(defun git-package (url) + (let* ((pkg-name (file-name-base (directory-file-name url))) + (pkg-sym (intern pkg-name))) + (eval + `(use-package ,pkg-sym + :vc (:url ,url :rev :newest) + :ensure nil)))) + +;;(setq select-enable-primary t) +(defun kylekrein/copy-to-clipboard (text) + (with-temp-buffer + (insert text) + (copy-region-as-kill (point-min) (point-max)) + (clipboard-kill-region (point-min) (point-max)))) + +(defun kylekrein/detect-wsl () + (and (eq system-type 'gnu/linux) + (file-exists-p "/proc/sys/fs/binfmt_misc/WSLInterop"))) + +(use-package alert + :ensure t + ) + +(use-package alert-toast :ensure t :after alert) + +(setq alert-default-style + (cond + ((kylekrein/detect-wsl) 'toast) + (t 'libnotify))) + +(unless (file-exists-p "~/.cache/emacs/tildafiles") + (make-directory "~/.cache/emacs/tildafiles")) +(setq backup-directory-alist '((".*" . "~/.cache/emacs/tildafiles"))) + +(use-package diminish :ensure t) + +(defun kylekrein/copy-emoji-to-clipboard() + (interactive) + (let ((emoji (emoji--read-emoji))) + (when emoji + (kylekrein/copy-to-clipboard emoji) + (message "Copied: %s" (current-kill 0 t))))) + +(setq ediff-split-window-function 'split-window-horizontally) +;;(setq ediff-window-setup-function 'ediff-setup-windows-plain) + +(set-face-attribute 'variable-pitch nil + :family "DejaVu Sans";;"ET Bembo" + :height 160 + :weight 'normal) +(set-face-attribute 'default nil + :family "Iosevka" + :height 150 + :weight 'medium) + +(set-face-attribute 'fixed-pitch nil + :family "Iosevka" + :height 150 + :weight 'medium) + + ;; Makes commented text and keywords italics. + ;; This is working in emacsclient but not emacs. + ;; Your font must have an italic face available. + (set-face-attribute 'font-lock-comment-face nil + :slant 'italic) + (set-face-attribute 'font-lock-keyword-face nil + :slant 'italic) + + ;; This sets the default font on all graphical frames created after restarting Emacs. + ;; Does the same thing as 'set-face-attribute default' above, but emacsclient fonts + ;; are not right unless I also add this method of setting the default font. + ;;(add-to-list 'default-frame-alist '(font . "Iosevka Mono-20")) + + ;; Uncomment the following line if line spacing needs adjusting. + (setq-default line-spacing 0.12) + + + (add-hook 'text-mode-hook #'variable-pitch-mode) + ;; Enable variable-pitch-mode in Org + (add-hook 'org-mode-hook #'variable-pitch-mode) + + ;; Ensure code blocks, tables, and special elements remain fixed-pitch + (custom-set-faces + ;; Keep code blocks, src, and tables fixed-pitch (Iosevka) + '(org-block ((t (:inherit fixed-pitch)))) + '(org-block-begin-line ((t (:inherit fixed-pitch)))) + '(org-block-end-line ((t (:inherit fixed-pitch)))) + '(org-table ((t (:inherit fixed-pitch)))) + '(org-code ((t (:inherit fixed-pitch)))) + '(org-verbatim ((t (:inherit fixed-pitch)))) + '(org-meta-line ((t (:inherit fixed-pitch)))) + '(org-checkbox ((t (:inherit fixed-pitch)))) + ) + +(electric-indent-mode -1) ;; Turn off the weird indenting that Emacs does by default. +(electric-pair-mode 1) ;; Turns on automatic parens pairing +;; The following prevents <> from auto-pairing when electric-pair-mode is on. +;; Otherwise, org-tempo is broken when you try to ) and Redo (C-c ) for windows +(setq sentence-end-double-space t) ;; Single space doesn't end a sentence + +(save-place-mode t) ;; Restore cursor place in file + +(use-package nov :ensure t) +(add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode)) + +(use-package magit + :ensure t) + +(use-package doom-modeline + :ensure t + :init (doom-modeline-mode 1) + :config + (setq doom-modeline-height 35 ;; sets modeline height + doom-modeline-bar-width 5 ;; sets right bar width + doom-modeline-persp-name nil ;; adds perspective name to modeline + doom-modeline-time t ;; shows time + doom-modeline-persp-icon nil)) ;; adds folder icon next to persp name + +(use-package rainbow-delimiters + :ensure t + :hook ((emacs-lisp-mode . rainbow-delimiters-mode) + (clojure-mode . rainbow-delimiters-mode))) + +(setq calendar-date-style "european") +(setq calendar-week-start-day 1) + +;;Line truncation +(defun kylekrein/truncate-calendar-hook () + "Turn line truncation on." + (toggle-truncate-lines 1)) + +(add-hook 'calendar-mode-hook #'kylekrein/truncate-calendar-hook) + +;;Current month is the first +(add-hook 'calendar-initial-window-hook #'calendar-scroll-left) + +;;Calendar in org agenda +(setq org-agenda-include-diary t) + +(defadvice revert-buffer (after refresh-org-agenda-on-revert activate) +(if (member (buffer-file-name (current-buffer)) org-agenda-files) + (org-agenda-redo-all t))) + +(org-babel-do-load-languages + 'org-babel-load-languages + '((shell . t) + (C . t) + (python . t))) + +(use-package org + :config + (org-link-set-parameters + "copy" + :follow (lambda (link) (kill-new link)) + :export (lambda (_ desc &rest _) desc))) + +;;;; Better Looking Bullets +(add-hook 'org-mode-hook 'org-indent-mode) +(use-package org-bullets :ensure t) +(add-hook 'org-mode-hook (lambda () (org-bullets-mode 1))) + +(custom-set-faces + '(org-level-1 ((t (:inherit outline-1 :height 1.45)))) + '(org-level-2 ((t (:inherit outline-2 :height 1.35)))) + '(org-level-3 ((t (:inherit outline-3 :height 1.30)))) + '(org-level-4 ((t (:inherit outline-4 :height 1.25)))) + '(org-level-5 ((t (:inherit outline-5 :height 1.20)))) + '(org-level-6 ((t (:inherit outline-5 :height 1.15)))) + '(org-level-7 ((t (:inherit outline-5 :height 1.10))))) + +(require 'org-tempo) + +(defun org-update-table-by-name (name) + "Update the named table." + (org-table-map-tables + (lambda () + (let ((table_name (org-element-property :name (org-element-at-point)))) + (if (and table_name (string-match-p name table_name)) + (org-table-recalculate)))))) + +(defun org-update-and-realign-tables () + (interactive) + (org-map-dblocks 'org-update-dblock) + (redisplay) + (org-table-map-tables 'org-table-recalculate) + (org-table-map-tables 'org-table-align)) + +(global-set-key (kbd "C-c n u") 'org-update-and-realign-tables) + +(use-package org-transclusion :ensure t) +(custom-set-faces + '(org-transclusion-fringe + ((t + (:background "green")))) + '(org-transclusion-source-fringe + ((t + (:background "blue"))))) + +(use-package org-roam + :ensure t + :init + (setq org-roam-v2-ack t) + (when (file-exists-p "~/Документы/org") + (setq org-roam-directory "~/Документы/org")) + (when (file-exists-p "~/Documents/org") + (setq org-roam-directory "~/Documents/org")) + :custom + (org-roam-completion-everywhere t) + (org-roam-capture-templates + '(("d" "default" plain + "%?" + :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+category: ${title}\n") + :unnarrowed t) + ("p" "project" plain "* Goals\n\n%?\n\n* Tasks\n\n** TODO Add initial tasks\n\n* Dates\n\n" + :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+category: ${title}\n#+filetags: Project") + :unnarrowed t)) + ) + (org-roam-dailies-capture-templates + '(("d" "default" entry "* %<%I:%M %p>: %?" + :if-new (file+head "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n")))) + :bind (("C-c n l" . org-roam-buffer-toggle) + ("C-c n f" . org-roam-node-find) + ("C-c n i" . org-roam-node-insert) + :map org-mode-map + ("C-M-i" . completion-at-point)) + :bind-keymap + ("C-c n d" . org-roam-dailies-map) + :config + (require 'org-roam-dailies) ;; Ensure the keymap is available + (org-roam-db-autosync-mode) + (org-roam-setup)) + +(defun kylekrein/org-roam-ripgrep () + (interactive) + (require 'consult) + (require 'org-roam) + (let ((consult-ripgrep-command "rg --null --ignore-case --type org --line-buffered --color=always --max-columns=500 --no-heading --line-number . -e ARG OPTS")) + (consult-ripgrep org-roam-directory))) +(global-set-key (kbd "C-c n r") #'kylekrein/org-roam-ripgrep) + +(defun kylekrein/org-roam-capture-inbox () + (interactive) + (org-roam-capture- :node (org-roam-node-create) + :templates '(("i" "inbox" plain "* %?" + :if-new (file+head "Inbox.org" "#+title: Inbox\n#+category: Inbox\n#+filetags: Project"))))) +(global-set-key (kbd "C-c n b") #'kylekrein/org-roam-capture-inbox) + +(defun org-agenda-refresh () + "Refresh all `org-agenda' buffers." + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (derived-mode-p 'org-agenda-mode) + (org-agenda-maybe-redo))))) + +(defadvice org-schedule (after refresh-agenda activate) + "Refresh org-agenda." + (org-agenda-refresh)) + +(require 'org-roam-node) +(defun kylekrein/org-roam-filter-by-tag (tag-name) + (lambda (node) + (member tag-name (org-roam-node-tags node)))) + +(defun kylekrein/org-roam-list-notes-by-tag (tag-name) + (mapcar #'org-roam-node-file + (seq-filter + (kylekrein/org-roam-filter-by-tag tag-name) + (org-roam-node-list)))) + +(defun kylekrein/org-roam-refresh-agenda-list () + (interactive) + (setq org-agenda-files (kylekrein/org-roam-list-notes-by-tag "Project"))) + + +(setq org-agenda-files nil + org-roam-node-display-template "${title} ${tags}" + org-agenda-start-on-weekday 1 ;; Week starts on Monday instead of Sunday + ) +;; Build the agenda list the first time for the session +(kylekrein/org-roam-refresh-agenda-list) + +;; Log time a task was set to DONE. +(setq org-log-done (quote time)) + +;; Don't log the time a task was rescheduled or redeadlined. +(setq org-log-redeadline nil) +(setq org-log-reschedule nil) + +;; Prefer rescheduling to future dates and times +(setq org-read-date-prefer-future 'time) + +(use-package emacs + :config + ;; start warning 60 minutes before the appointment + (setq appt-message-warning-time 60) + + ;; warn me every 5 minutes + (setq appt-display-interval 15) + (setq appt-disp-window-function + (lambda (remaining new-time msg) + (alert (format "In %s minutes" remaining) + :title msg + :severity 'moderate + :category 'org-agenda + :id (intern msg)))) + + (advice-add 'appt-check + :before + (lambda (&rest args) + (org-agenda-to-appt t))) + + (appt-activate t)) +(setq alert-fade-time 50) + +(use-package org-upcoming-modeline + :ensure t + :after org + :config + (setq appt-display-mode-line nil) + (org-upcoming-modeline-mode)) + +(use-package rainbow-mode + :ensure t + :hook + ((org-mode prog-mode) . rainbow-mode)) + +(use-package gptel + :ensure t + :bind + ("C-c a c" . gptel) + ("C-c a r" . gptel-rewrite) + ("C-c a s" . gptel-send) + ("C-c a f" . gptel-add-file)) + (setq + gptel-model 'llama3.1 + gptel-backend (gptel-make-ollama "Ollama" + :host "localhost:11434" + :stream t + :models '(llama3.1 qwen2.5-coder:7b)) + gptel-track-media t + gptel-default-mode 'org-mode) +(add-hook 'gptel-post-stream-hook 'gptel-auto-scroll) +(add-hook 'gptel-post-response-functions 'gptel-end-of-response) + +(use-package eshell-syntax-highlighting + :ensure t + :after esh-mode + :config + (eshell-syntax-highlighting-global-mode +1)) + +(use-package vterm + :ensure t +) + +(use-package vterm-toggle + :ensure t + :after vterm + :config + (setq vterm-toggle-fullscreen-p nil) + (setq vterm-toggle-scope 'project) + (add-to-list 'display-buffer-alist + '((lambda (buffer-or-name _) + (let ((buffer (get-buffer buffer-or-name))) + (with-current-buffer buffer + (or (equal major-mode 'vterm-mode) + (string-prefix-p vterm-buffer-name (buffer-name buffer)))))) + (display-buffer-reuse-window display-buffer-at-bottom) + ;;(display-buffer-reuse-window display-buffer-in-direction) + ;;display-buffer-in-direction/direction/dedicated is added in emacs27 + ;;(direction . bottom) + ;;(dedicated . t) ;dedicated is supported in emacs27 + (reusable-frames . visible) + (window-height . 0.3)))) + +(git-package "https://github.com/darcamo/cmake-integration.git") +(use-package cmake-integration + :commands (cmake-integration-transient) + :custom + (cmake-integration-generator "Ninja") + (cmake-integration-use-separated-compilation-buffer-for-each-target t)) + +(defun is-cmake-project? () + "Determine if the current directory is a CMake project." + (interactive) + (if-let* ((project (project-current)) + (project-root (project-root project)) + (cmakelist-path (expand-file-name "CMakeLists.txt" project-root))) + (file-exists-p cmakelist-path))) + + +(defun cmake-integration-keybindings-mode-turn-on-in-cmake-projects () + "Turn on `cmake-integration-keybindings-mode' in CMake projects." + (when (is-cmake-project?) + (cmake-integration-keybindings-mode 1))) + + +(define-minor-mode cmake-integration-keybindings-mode + "A minor-mode for adding keybindings to compile C++ code using cmake-integration package." + nil + "cmake" + '( + ([f5] . cmake-integration-transient) ;; Open main transient menu + ([M-f9] . cmake-integration-save-and-compile) ;; Ask for the target name and compile it + ([f9] . cmake-integration-save-and-compile-last-target) ;; Recompile the last target + ([C-f9] . cmake-integration-run-ctest) ;; Run CTest + ([f7] . cmake-integration-run-last-target) ;; Run the target (using any previously set command line parameters) + ([S-f7] . kill-compilation) + ([C-f7] . cmake-integration-debug-last-target) ;; Debug the target (using any previously set command line parameters) + ([M-f7] . cmake-integration-run-last-target-with-arguments) ;; Ask for command line parameters to run the target + ([M-f8] . cmake-integration-select-configure-preset) ;; Ask for a preset name and call CMake to configure the project + ([f8] . cmake-integration-cmake-reconfigure) ;; Call CMake to configure the project using the last chosen preset + ) + ) + +(define-globalized-minor-mode global-cmake-integration-keybindings-mode + cmake-integration-keybindings-mode cmake-integration-keybindings-mode-turn-on-in-cmake-projects) + + +(global-cmake-integration-keybindings-mode) + +;; Extend project.el to recognize local projects based on a .project file +(cl-defmethod project-root ((project (head local))) + (cdr project)) + +(defun mu--project-files-in-directory (dir) + "Use `fd' to list files in DIR." + (let* ((default-directory dir) + (localdir (file-local-name (expand-file-name dir))) + (command (format "fd -t f -0 . %s" localdir))) + (project--remote-file-names + (sort (split-string (shell-command-to-string command) "\0" t) + #'string<)))) + +(cl-defmethod project-files ((project (head local)) &optional dirs) + "Override `project-files' to use `fd' in local projects." + (mapcan #'mu--project-files-in-directory + (or dirs (list (project-root project))))) + +(defun mu-project-try-local (dir) + "Determine if DIR is a non-Git project. +DIR must include a .project file to be considered a project." + (let ((root (locate-dominating-file dir ".project"))) + (and root (cons 'local root)))) + +(use-package project + :defer t + :config + (add-to-list 'project-find-functions 'mu-project-try-local) + ) + +(use-package direnv + :ensure t + :config + (direnv-mode)) + +(defun kylekrein/project-enable-direnv-flake () + "Add `use flake` to .envrc and run `direnv allow` in the project root." + (interactive) + (let* ((project (project-current t)) + (root (project-root project)) + (envrc-path (expand-file-name ".envrc" root))) + (unless (file-exists-p envrc-path) + (with-temp-buffer + (insert "use flake\n") + (write-file envrc-path))) + (unless (string-match-p "use flake" (with-temp-buffer + (insert-file-contents envrc-path) + (buffer-string))) + (with-temp-buffer + (insert-file-contents envrc-path) + (goto-char (point-max)) + (insert "\nuse flake\n") + (write-file envrc-path))) + (let ((default-directory root)) + (direnv-allow)) + (message "Added 'use flake' to .envrc and ran direnv allow in %s" root))) + +(use-package glsl-mode + :ensure t) + +(add-to-list 'auto-mode-alist '("\\.rml\\'" . html-ts-mode)) +(add-to-list 'auto-mode-alist '("\\.rcss\\'" . css-ts-mode)) + +(add-to-list 'auto-mode-alist '("CMakeLists\\.txt\\'" . cmake-ts-mode)) +(add-to-list 'auto-mode-alist '("\\.cmake\\'" . cmake-ts-mode)) + +(use-package zig-mode + :ensure t) + +(autoload 'zig-mode "zig-mode" nil t) +(add-to-list 'auto-mode-alist '("\\.\\(zig\\|zon\\)\\'" . zig-mode)) + +(use-package treesit-auto + :ensure t + :demand t + :config + (global-treesit-auto-mode)) + +(use-package eldoc + :init + (global-eldoc-mode)) + + (use-package eglot + :hook (prog-mode . eglot-ensure) + ;;:init + ;;(setq eglot-stay-out-of '(flymake)) + :bind (:map + eglot-mode-map + ("C-c c a" . eglot-code-actions) + ;;("C-c c o" . eglot-code-actions-organize-imports) + ("C-c c r" . eglot-rename) + ("C-c c f" . eglot-format))) + + (use-package flymake + :hook (prog-mode . flymake-mode) + :bind (:map flymake-mode-map + ("C-c ! n" . flymake-goto-next-error) + ("C-c ! p" . flymake-goto-prev-error) + ("C-c ! l" . flymake-show-buffer-diagnostics))) + +(with-eval-after-load 'eglot + (add-to-list 'eglot-server-programs + '((c-ts-mode c++-ts-mode) + . ("clangd" + "-j=8" + "--log=error" + "--malloc-trim" + "--background-index" + "--clang-tidy" + "--cross-file-rename" + "--completion-style=detailed" + "--pch-storage=memory" + "--header-insertion=never" + "--header-insertion-decorators=0"))) + (add-hook 'c-ts-mode-hook #'eglot-ensure) + (add-hook 'c++-ts-mode-hook #'eglot-ensure)) + +(with-eval-after-load 'eglot + (add-to-list 'eglot-server-programs + '(zig-mode . ( + ;; Use `zls` if it is in your PATH + "zls" + ;; There are two ways to set config options: + ;; - edit your `zls.json` that applies to any editor that uses ZLS + ;; - set in-editor config options with the `initializationOptions` field below. + ;; + ;; Further information on how to configure ZLS: + ;; https://zigtools.org/zls/configure/ + :initializationOptions + (;; Whether to enable build-on-save diagnostics + ;; + ;; Further information about build-on save: + ;; https://zigtools.org/zls/guides/build-on-save/ + ;;enable_build_on_save t + + ;; omit the following line if `zig` is in your PATH + ;:zig_exe_path "/path/to/zig_executable" + )))) + (add-hook 'zig-mode-hook #'eglot-ensure)) + +(with-eval-after-load 'eglot + (add-to-list 'eglot-server-programs + '(csharp-ts-mode + . ("csharp-ls"))) + (add-hook 'csharp-ts-mode-hook #'eglot-ensure)) + +(with-eval-after-load 'eglot + (add-to-list 'eglot-server-programs + '(python-ts-mode + . ("ty"))) + (add-hook 'python-ts-mode-hook #'eglot-ensure)) + +(use-package nerd-icons + :ensure t + ;; :custom + ;; The Nerd Font you want to use in GUI + ;; "Symbols Nerd Font Mono" is the default and is recommended + ;; but you can use any other Nerd Font if you want + ;; (nerd-icons-font-family "Symbols Nerd Font Mono") + ) + +(use-package nerd-icons-completion + :ensure t + :after marginalia + :config + (nerd-icons-completion-mode) + (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup)) + +(use-package persist-state + :ensure t + :after server + :if server-process + :config + (persist-state-mode)) + +(use-package multiple-cursors +:ensure t +:bind ( +("C-S-c C-S-c" . mc/edit-lines) +("C->" . mc/mark-next-like-this) +("C-<" . mc/mark-previous-like-this) +("C-C C-<" . mc/mark-all-like-this) +("C-\"" . mc/skip-to-next-like-this) +("C-:" . mc/skip-to-previous-like-this) +("C-C C->" . mc/mark-more-like-this-extended) +("C-S-" . mc/add-cursor-on-click) +)) + +(require 'windmove) + +;;;###autoload +(defun buf-move-up () + "Swap the current buffer and the buffer above the split. +If there is no split, ie now window above the current one, an +error is signaled." +;; "Switches between the current buffer, and the buffer above the +;; split, if possible." + (interactive) + (let* ((other-win (windmove-find-other-window 'up)) + (buf-this-buf (window-buffer (selected-window)))) + (if (null other-win) + (error "No window above this one") + ;; swap top with this one + (set-window-buffer (selected-window) (window-buffer other-win)) + ;; move this one to top + (set-window-buffer other-win buf-this-buf) + (select-window other-win)))) + +;;;###autoload +(defun buf-move-down () +"Swap the current buffer and the buffer under the split. +If there is no split, ie now window under the current one, an +error is signaled." + (interactive) + (let* ((other-win (windmove-find-other-window 'down)) + (buf-this-buf (window-buffer (selected-window)))) + (if (or (null other-win) + (string-match "^ \\*Minibuf" (buffer-name (window-buffer other-win)))) + (error "No window under this one") + ;; swap top with this one + (set-window-buffer (selected-window) (window-buffer other-win)) + ;; move this one to top + (set-window-buffer other-win buf-this-buf) + (select-window other-win)))) + +;;;###autoload +(defun buf-move-left () +"Swap the current buffer and the buffer on the left of the split. +If there is no split, ie now window on the left of the current +one, an error is signaled." + (interactive) + (let* ((other-win (windmove-find-other-window 'left)) + (buf-this-buf (window-buffer (selected-window)))) + (if (null other-win) + (error "No left split") + ;; swap top with this one + (set-window-buffer (selected-window) (window-buffer other-win)) + ;; move this one to top + (set-window-buffer other-win buf-this-buf) + (select-window other-win)))) + +;;;###autoload +(defun buf-move-right () +"Swap the current buffer and the buffer on the right of the split. +If there is no split, ie now window on the right of the current +one, an error is signaled." + (interactive) + (let* ((other-win (windmove-find-other-window 'right)) + (buf-this-buf (window-buffer (selected-window)))) + (if (null other-win) + (error "No right split") + ;; swap top with this one + (set-window-buffer (selected-window) (window-buffer other-win)) + ;; move this one to top + (set-window-buffer other-win buf-this-buf) + (select-window other-win)))) + +(use-package windmove + :bind + (("" . windmove-up) + ("" . windmove-down) + ("" . windmove-left) + ("" . windmove-right) + ("" . buf-move-up) + ("" . buf-move-down) + ("" . buf-move-left) + ("" . buf-move-right))) + +(use-package corfu + :ensure t + ;; Optional customizations + :custom + (corfu-cycle t) ;; Enable cycling for `corfu-next/previous' + (corfu-auto t) + (corfu-auto-prefix 2) + (corfu-quit-at-boundary 'separator) + (corfu-echo-documentation 0.25) + (corfu-preselect-first nil) + (corfu-popupinfo-delay '(1.0 . 0.3)) ;;default '(2.0 . 1.0) + ;; (corfu-quit-no-match nil) ;; Never quit, even if there is no match + ;; (corfu-preview-current nil) ;; Disable current candidate preview + ;; (corfu-preselect 'prompt) ;; Preselect the prompt + ;; (corfu-on-exact-match nil) ;; Configure handling of exact matches + + ;; Enable Corfu only for certain modes. See also `global-corfu-modes'. + ;; :hook ((prog-mode . corfu-mode) + ;; (shell-mode . corfu-mode) + ;; (eshell-mode . corfu-mode)) + :bind (:map corfu-map + ("M-SPC" . corfu-insert-separator) + ("RET" . nil) + ("TAB" . corfu-next) + ([tab] . corfu-next) + ("SHIFT-TAB" . corfu-previous) + ([backtab] . corfu-previous) + ("S-" . corfu-insert)) + + ;; Recommended: Enable Corfu globally. This is recommended since Dabbrev can + ;; be used globally (M-/). See also the customization variable + ;; `global-corfu-modes' to exclude certain modes. + :init + (global-corfu-mode) + (corfu-history-mode) + (corfu-popupinfo-mode)) + +;; A few more useful configurations... +(use-package emacs + :custom + ;; TAB cycle if there are only few candidates + ;; (completion-cycle-threshold 3) + + ;; Enable indentation+completion using the TAB key. + ;; `completion-at-point' is often bound to M-TAB. + (tab-always-indent 'complete) + + ;; Emacs 30 and newer: Disable Ispell completion function. + ;; Try `cape-dict' as an alternative. + (text-mode-ispell-word-completion nil) + + ;; Hide commands in M-x which do not apply to the current mode. Corfu + ;; commands are hidden, since they are not used via M-x. This setting is + ;; useful beyond Corfu. + (read-extended-command-predicate #'command-completion-default-include-p)) + +(use-package cape + :ensure t + :defer 10 + :init +(add-to-list 'completion-at-point-functions #'cape-file)) + +;; Enable vertico + (use-package vertico + :ensure t + :custom + ;; (vertico-scroll-margin 0) ;; Different scroll margin + ;; (vertico-count 20) ;; Show more candidates + ;; (vertico-resize t) ;; Grow and shrink the Vertico minibuffer + (vertico-cycle t) ;; Enable cycling for `vertico-next/previous' + :init + (vertico-mode)) + +(vertico-mode t) ;; enable vertico for all buffers + ;; Persist history over Emacs restarts. Vertico sorts by history position. + (use-package savehist + :init + (savehist-mode)) + + ;; A few more useful configurations... + (use-package emacs + :custom + ;; Support opening new minibuffers from inside existing minibuffers. + (enable-recursive-minibuffers t) + ;; Hide commands in M-x which do not work in the current mode. Vertico + ;; commands are hidden in normal buffers. This setting is useful beyond + ;; Vertico. + (read-extended-command-predicate #'command-completion-default-include-p) + :init + ;; Add prompt indicator to `completing-read-multiple'. + ;; We display [CRM], e.g., [CRM,] if the separator is a comma. + (defun crm-indicator (args) + (cons (format "[CRM%s] %s" + (replace-regexp-in-string + "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" + crm-separator) + (car args)) + (cdr args))) + (advice-add #'completing-read-multiple :filter-args #'crm-indicator) + + ;; Do not allow the cursor in the minibuffer prompt + (setq minibuffer-prompt-properties + '(read-only t cursor-intangible t face minibuffer-prompt)) + (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)) + +;; Optionally use the `orderless' completion style. +(use-package orderless + :ensure t + :custom + ;; Configure a custom style dispatcher (see the Consult wiki) + ;; (orderless-style-dispatchers '(+orderless-consult-dispatch orderless-affix-dispatch)) + ;; (orderless-component-separator #'orderless-escapable-split-on-space) + (completion-styles '(orderless flex basic partial-completion)) + + (completion-category-defaults nil) + (completion-category-overrides '((file (styles partial-completion))))) + +;; Example configuration for Consult + (use-package consult + :ensure t + ;; Replace bindings. Lazily loaded by `use-package'. + :bind (;; C-c bindings in `mode-specific-map' + ("C-c M-x" . consult-mode-command) + ("C-c h" . consult-history) + ("C-c k" . consult-kmacro) + ("C-c m" . consult-man) + ("C-c i" . consult-info) + ([remap Info-search] . consult-info) + ;; C-x bindings in `ctl-x-map' + ("C-x M-:" . consult-complex-command) ;; orig. repeat-complex-command + ("C-x b" . consult-buffer) ;; orig. switch-to-buffer + ("C-x 4 b" . consult-buffer-other-window) ;; orig. switch-to-buffer-other-window + ("C-x 5 b" . consult-buffer-other-frame) ;; orig. switch-to-buffer-other-frame + ("C-x t b" . consult-buffer-other-tab) ;; orig. switch-to-buffer-other-tab + ("C-x r b" . consult-bookmark) ;; orig. bookmark-jump + ("C-x p b" . consult-project-buffer) ;; orig. project-switch-to-buffer + ;; Custom M-# bindings for fast register access + ("M-#" . consult-register-load) + ("M-'" . consult-register-store) ;; orig. abbrev-prefix-mark (unrelated) + ("C-M-#" . consult-register) + ;; Other custom bindings + ("M-y" . consult-yank-pop) ;; orig. yank-pop + ;; M-g bindings in `goto-map' + ("M-g e" . consult-compile-error) + ("M-g f" . consult-flymake) ;; Alternative: consult-flycheck + ("M-g g" . consult-goto-line) ;; orig. goto-line + ("M-g M-g" . consult-goto-line) ;; orig. goto-line + ("M-g o" . consult-outline) ;; Alternative: consult-org-heading + ("M-g m" . consult-mark) + ("M-g k" . consult-global-mark) + ("M-g i" . consult-imenu) + ("M-g I" . consult-imenu-multi) + ;; M-s bindings in `search-map' + ("M-s d" . consult-find) ;; Alternative: consult-fd + ("M-s c" . consult-locate) + ("M-s g" . consult-grep) + ("M-s G" . consult-git-grep) + ("M-s r" . consult-ripgrep) + ("M-s l" . consult-line) + ("M-s L" . consult-line-multi) + ("M-s k" . consult-keep-lines) + ("M-s u" . consult-focus-lines) + ;; Isearch integration + ("M-s e" . consult-isearch-history) + :map isearch-mode-map + ("M-e" . consult-isearch-history) ;; orig. isearch-edit-string + ("M-s e" . consult-isearch-history) ;; orig. isearch-edit-string + ("M-s l" . consult-line) ;; needed by consult-line to detect isearch + ("M-s L" . consult-line-multi) ;; needed by consult-line to detect isearch + ;; Minibuffer history + :map minibuffer-local-map + ("M-s" . consult-history) ;; orig. next-matching-history-element + ("M-r" . consult-history)) ;; orig. previous-matching-history-element + + ;; Enable automatic preview at point in the *Completions* buffer. This is + ;; relevant when you use the default completion UI. + :hook (completion-list-mode . consult-preview-at-point-mode) + + ;; The :init configuration is always executed (Not lazy) + :init + + ;; Tweak the register preview for `consult-register-load', + ;; `consult-register-store' and the built-in commands. This improves the + ;; register formatting, adds thin separator lines, register sorting and hides + ;; the window mode line. + (advice-add #'register-preview :override #'consult-register-window) + (setq register-preview-delay 0.5) + + ;; Use Consult to select xref locations with preview + (setq xref-show-xrefs-function #'consult-xref + xref-show-definitions-function #'consult-xref) + + ;; Configure other variables and modes in the :config section, + ;; after lazily loading the package. + :config + + ;; Optionally configure preview. The default value + ;; is 'any, such that any key triggers the preview. + ;; (setq consult-preview-key 'any) + ;; (setq consult-preview-key "M-.") + ;; (setq consult-preview-key '("S-" "S-")) + ;; For some commands and buffer sources it is useful to configure the + ;; :preview-key on a per-command basis using the `consult-customize' macro. + (consult-customize + consult-theme :preview-key '(:debounce 0.2 any) + consult-ripgrep consult-git-grep consult-grep consult-man + consult-bookmark consult-recent-file consult-xref + consult--source-bookmark consult--source-file-register + consult--source-recent-file consult--source-project-recent-file + ;; :preview-key "M-." + :preview-key '(:debounce 0.4 any)) + + ;; Optionally configure the narrowing key. + ;; Both < and C-+ work reasonably well. + (setq consult-narrow-key "<") ;; "C-+" + + ;; Optionally make narrowing help available in the minibuffer. + ;; You may want to use `embark-prefix-help-command' or which-key instead. + ;; (keymap-set consult-narrow-map (concat consult-narrow-key " ?") #'consult-narrow-help) + ) +(require 'consult) +;;(setq read-file-name-function #'consult-find-file-with-preview) + +;;Previewing files in find-file +(defun consult-find-file-with-preview (prompt &optional dir default mustmatch initial pred) + (interactive) + (let ((default-directory (expand-file-name (or dir default-directory))) + (minibuffer-completing-file-name t)) + (consult--read #'read-file-name-internal :state (consult--file-preview) + :prompt prompt + :initial initial + :require-match mustmatch + :predicate pred))) + +;;Previewing files for project-find-file +(setq project-read-file-name-function #'consult-project-find-file-with-preview) + +(defun consult-project-find-file-with-preview (prompt all-files &optional pred hist _mb) + (let ((prompt (if (and all-files + (file-name-absolute-p (car all-files))) + prompt + ( concat prompt + ( format " in %s" + (consult--fast-abbreviate-file-name default-directory))))) + (minibuffer-completing-file-name t)) + (consult--read (mapcar + (lambda (file) + (file-relative-name file)) + all-files) + :state (consult--file-preview) + :prompt (concat prompt ": ") + :require-match t + :history hist + :category 'file + :predicate pred))) + +;; Enable rich annotations using the Marginalia package +(use-package marginalia + :ensure t + ;; Bind `marginalia-cycle' locally in the minibuffer. To make the binding + ;; available in the *Completions* buffer, add it to the + ;; `completion-list-mode-map'. + :bind (:map minibuffer-local-map + ("M-A" . marginalia-cycle)) + + ;; The :init section is always executed. + :init + + ;; Marginalia must be activated in the :init section of use-package such that + ;; the mode gets enabled right away. Note that this forces loading the + ;; package. + (marginalia-mode)) + +(use-package doom-themes + :ensure t + :config + ;; Global settings (defaults) + (setq doom-themes-enable-bold t ; if nil, bold is universally disabled + doom-themes-enable-italic t) ; if nil, italics is universally disabled + (load-theme 'doom-one t) + + ;; Enable flashing mode-line on errors + (doom-themes-visual-bell-config) + ;; Enable custom neotree theme (nerd-icons must be installed!) + (doom-themes-neotree-config) + ;; or for treemacs users + (setq doom-themes-treemacs-theme "doom-atom") ; use "doom-colors" for less minimal icon theme + (doom-themes-treemacs-config) + ;; Corrects (and improves) org-mode's native fontification. + (doom-themes-org-config)) + +(unless (kylekrein/detect-wsl) + (add-to-list 'default-frame-alist '(alpha-background . 90))) ; For all new frames henceforth + +(use-package sudo-edit + :ensure t) + +(use-package which-key + :ensure t + :init + (which-key-mode 1) + :config + (setq which-key-side-window-location 'bottom + which-key-sort-order #'which-key-key-order-alpha + which-key-sort-uppercase-first nil + which-key-add-column-padding 1 + which-key-max-display-columns nil + which-key-min-display-lines 6 + which-key-side-window-slot -10 + which-key-side-window-max-height 0.25 + which-key-idle-delay 0.8 + which-key-max-description-length 25 + which-key-allow-imprecise-window-fit nil + which-key-separator " → " )) + +(when (kylekrein/detect-wsl) + (setq select-active-regions nil) + (setq select-enable-clipboard 't) + (setq select-enable-primary nil) + (setq interprogram-cut-function #'gui-select-text) +) diff --git a/emacs/.config/emacs/init.el b/emacs/.config/emacs/init.el new file mode 100644 index 0000000..1416633 --- /dev/null +++ b/emacs/.config/emacs/init.el @@ -0,0 +1,15 @@ +;;; -*- lexical-binding: t; -*- +(setq custom-file "~/.config/emacs/custom.el") +(when (file-exists-p custom-file) + (load custom-file)) +(setq package-user-dir "~/.cache/emacs/elpa") +(make-directory package-user-dir t) +(require 'package) +;;https://github.com/wbolster/emacs-direnv/issues/85 +(setenv "PATH" (mapconcat 'identity exec-path ":")) ;;fixes direnv losing nix pkgs +(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) +(package-initialize) +(package-refresh-contents) +(add-to-list 'load-path (getenv "EMACSLOADPATH")) + +(load "~/.config/emacs/config.el") diff --git a/git/.config/git/config b/git/.config/git/config new file mode 100644 index 0000000..7652017 --- /dev/null +++ b/git/.config/git/config @@ -0,0 +1,7 @@ +[user] + name = Aleksandr Lebedev + email = alex.lebedev2003@icloud.com +[core] + editor = emacsclient -c +[color] + ui = auto \ No newline at end of file diff --git a/niri/.config/niri/config.kdl b/niri/.config/niri/config.kdl new file mode 100644 index 0000000..5c10671 --- /dev/null +++ b/niri/.config/niri/config.kdl @@ -0,0 +1,631 @@ +// This config is in the KDL format: https://kdl.dev +// "/-" comments out the following node. +// Check the wiki for a full description of the configuration: +// https://yalter.github.io/niri/Configuration:-Introduction + +// Input device configuration. +// Find the full list of options on the wiki: +// https://yalter.github.io/niri/Configuration:-Input +input { + keyboard { + xkb { + // You can set rules, model, layout, variant and options. + // For more information, see xkeyboard-config(7). + + // For example: + layout "eu,ru" + options "grp:lctrl_toggle, ctrl:nocaps" + + // If this section is empty, niri will fetch xkb settings + // from org.freedesktop.locale1. You can control these using + // localectl set-x11-keymap. + } + + // Enable numlock on startup, omitting this setting disables it. + numlock + track-layout "window" + } + + // Next sections include libinput settings. + // Omitting settings disables them, or leaves them at their default values. + // All commented-out settings here are examples, not defaults. + touchpad { + // off + tap + dwt + // dwtp + drag true + drag-lock + natural-scroll + // accel-speed 0.2 + // accel-profile "flat" + // scroll-method "two-finger" + // disabled-on-external-mouse + } + + mouse { + // off + // natural-scroll + // accel-speed 0.2 + // accel-profile "flat" + // scroll-method "no-scroll" + } + + trackpoint { + // off + // natural-scroll + // accel-speed 0.2 + // accel-profile "flat" + // scroll-method "on-button-down" + // scroll-button 273 + // scroll-button-lock + // middle-emulation + } + + // Uncomment this to make the mouse warp to the center of newly focused windows. + // warp-mouse-to-focus + + // Focus windows and outputs automatically when moving the mouse into them. + // Setting max-scroll-amount="0%" makes it work only on windows already fully on screen. + // focus-follows-mouse max-scroll-amount="0%" + + //disable-power-key-handling +} + +// You can configure outputs by their name, which you can find +// by running `niri msg outputs` while inside a niri instance. +// The built-in laptop monitor is usually called "eDP-1". +// Find more information on the wiki: +// https://yalter.github.io/niri/Configuration:-Outputs +// Remember to uncomment the node by removing "/-"! +/-output "eDP-1" { + // Uncomment this line to disable this output. + // off + + // Resolution and, optionally, refresh rate of the output. + // The format is "x" or "x@". + // If the refresh rate is omitted, niri will pick the highest refresh rate + // for the resolution. + // If the mode is omitted altogether or is invalid, niri will pick one automatically. + // Run `niri msg outputs` while inside a niri instance to list all outputs and their modes. + mode "1920x1080@120.030" + + // You can use integer or fractional scale, for example use 1.5 for 150% scale. + scale 2 + + // Transform allows to rotate the output counter-clockwise, valid values are: + // normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270. + transform "normal" + + // Position of the output in the global coordinate space. + // This affects directional monitor actions like "focus-monitor-left", and cursor movement. + // The cursor can only move between directly adjacent outputs. + // Output scale and rotation has to be taken into account for positioning: + // outputs are sized in logical, or scaled, pixels. + // For example, a 3840×2160 output with scale 2.0 will have a logical size of 1920×1080, + // so to put another output directly adjacent to it on the right, set its x to 1920. + // If the position is unset or results in an overlap, the output is instead placed + // automatically. + position x=1280 y=0 +} + +// Settings that influence how windows are positioned and sized. +// Find more information on the wiki: +// https://yalter.github.io/niri/Configuration:-Layout +layout { + // Set gaps around windows in logical pixels. + gaps 16 + + // When to center a column when changing focus, options are: + // - "never", default behavior, focusing an off-screen column will keep at the left + // or right edge of the screen. + // - "always", the focused column will always be centered. + // - "on-overflow", focusing a column will center it if it doesn't fit + // together with the previously focused column. + center-focused-column "never" + + // You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between. + preset-column-widths { + // Proportion sets the width as a fraction of the output width, taking gaps into account. + // For example, you can perfectly fit four windows sized "proportion 0.25" on an output. + // The default preset widths are 1/3, 1/2 and 2/3 of the output. + proportion 1.0 + proportion 0.5 + proportion 0.33333 + proportion 0.66667 + + // Fixed sets the width in logical pixels exactly. + // fixed 1920 + } + + // You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between. + // preset-window-heights { } + + // You can change the default width of the new windows. + default-column-width { proportion 1.0; } + // If you leave the brackets empty, the windows themselves will decide their initial width. + // default-column-width {} + + // By default focus ring and border are rendered as a solid background rectangle + // behind windows. That is, they will show up through semitransparent windows. + // This is because windows using client-side decorations can have an arbitrary shape. + // + // If you don't like that, you should uncomment `prefer-no-csd` below. + // Niri will draw focus ring and border *around* windows that agree to omit their + // client-side decorations. + // + // Alternatively, you can override it with a window rule called + // `draw-border-with-background`. + + // You can change how the focus ring looks. + focus-ring { + // Uncomment this line to disable the focus ring. + // off + + // How many logical pixels the ring extends out from the windows. + width 4 + + // Colors can be set in a variety of ways: + // - CSS named colors: "red" + // - RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa" + // - CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others. + + // Color of the ring on the active monitor. + active-color "#b19cd9" //Light Pastel Purple + + // Color of the ring on inactive monitors. + // + // The focus ring only draws around the active window, so the only place + // where you can see its inactive-color is on other monitors. + inactive-color "#e6e6fa" //Lavender Mist + + // You can also use gradients. They take precedence over solid colors. + // Gradients are rendered the same as CSS linear-gradient(angle, from, to). + // The angle is the same as in linear-gradient, and is optional, + // defaulting to 180 (top-to-bottom gradient). + // You can use any CSS linear-gradient tool on the web to set these up. + // Changing the color space is also supported, check the wiki for more info. + // + // active-gradient from="#80c8ff" to="#c7ff7f" angle=45 + + // You can also color the gradient relative to the entire view + // of the workspace, rather than relative to just the window itself. + // To do that, set relative-to="workspace-view". + // + // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" + } + + // You can also add a border. It's similar to the focus ring, but always visible. + border { + // The settings are the same as for the focus ring. + // If you enable the border, you probably want to disable the focus ring. + off + + width 4 + active-color "#ffc87f" + inactive-color "#505050" + + // Color of the border around windows that request your attention. + urgent-color "#9b0000" + + // Gradients can use a few different interpolation color spaces. + // For example, this is a pastel rainbow gradient via in="oklch longer hue". + // + // active-gradient from="#e5989b" to="#ffb4a2" angle=45 relative-to="workspace-view" in="oklch longer hue" + + // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" + } + + // You can enable drop shadows for windows. + shadow { + // Uncomment the next line to enable shadows. + // on + + // By default, the shadow draws only around its window, and not behind it. + // Uncomment this setting to make the shadow draw behind its window. + // + // Note that niri has no way of knowing about the CSD window corner + // radius. It has to assume that windows have square corners, leading to + // shadow artifacts inside the CSD rounded corners. This setting fixes + // those artifacts. + // + // However, instead you may want to set prefer-no-csd and/or + // geometry-corner-radius. Then, niri will know the corner radius and + // draw the shadow correctly, without having to draw it behind the + // window. These will also remove client-side shadows if the window + // draws any. + // + // draw-behind-window true + + // You can change how shadows look. The values below are in logical + // pixels and match the CSS box-shadow properties. + + // Softness controls the shadow blur radius. + softness 30 + + // Spread expands the shadow. + spread 5 + + // Offset moves the shadow relative to the window. + offset x=0 y=5 + + // You can also change the shadow color and opacity. + color "#0007" + } + + // Struts shrink the area occupied by windows, similarly to layer-shell panels. + // You can think of them as a kind of outer gaps. They are set in logical pixels. + // Left and right struts will cause the next window to the side to always be visible. + // Top and bottom struts will simply add outer gaps in addition to the area occupied by + // layer-shell panels and regular gaps. + struts { + // left 64 + // right 64 + // top 64 + // bottom 64 + } +} + +// Add lines like this to spawn processes at startup. +// Note that running niri as a session supports xdg-desktop-autostart, +// which may be more convenient to use. +// See the binds section below for more spawn examples. + +spawn-sh-at-startup "/usr/bin/pipewire-launcher.sh" +spawn-sh-at-startup "gnome-keyring-daemon" +spawn-sh-at-startup "alacritty --daemon" +spawn-sh-at-startup "emacs --daemon" +spawn-sh-at-startup "qs" +spawn-sh-at-startup "wl-paste --watch cliphist store &" +spawn-sh-at-startup "nextcloud --background" + +config-notification { + disable-failed +} + +debug { + honor-xdg-activation-with-invalid-serial +} + +hotkey-overlay { + // Uncomment this line to disable the "Important Hotkeys" pop-up at startup. + // skip-at-startup +} + +// Uncomment this line to ask the clients to omit their client-side decorations if possible. +// If the client will specifically ask for CSD, the request will be honored. +// Additionally, clients will be informed that they are tiled, removing some client-side rounded corners. +// This option will also fix border/focus ring drawing behind some semitransparent windows. +// After enabling or disabling this, you need to restart the apps for this to take effect. +prefer-no-csd + +// You can change the path where screenshots are saved. +// A ~ at the front will be expanded to the home directory. +// The path is formatted with strftime(3) to give you the screenshot date and time. +screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" + +// You can also set this to null to disable saving screenshots to disk. +// screenshot-path null + +// Animation settings. +// The wiki explains how to configure individual animations: +// https://yalter.github.io/niri/Configuration:-Animations +animations { + // Uncomment to turn off all animations. + // off + + // Slow down all animations by this factor. Values below 1 speed them up instead. + // slowdown 3.0 +} + +// Window rules let you adjust behavior for individual windows. +// Find more information on the wiki: +// https://yalter.github.io/niri/Configuration:-Window-Rules + +// Work around WezTerm's initial configure bug +// by setting an empty default-column-width. +window-rule { + // This regular expression is intentionally made as specific as possible, + // since this is the default config, and we want no false positives. + // You can get away with just app-id="wezterm" if you want. + match app-id=r#"^org\.wezfurlong\.wezterm$"# + default-column-width {} +} + +// Open the Firefox picture-in-picture player as floating by default. +window-rule { + // This app-id regular expression will work for both: + // - host Firefox (app-id is "firefox") + // - Flatpak Firefox (app-id is "org.mozilla.firefox") + match app-id=r#"librewolf$"# title="^Picture-in-Picture$" + open-floating true +} + +// Example: block out two password managers from screen capture. +// (This example rule is commented out with a "/-" in front.) +/-window-rule { + match app-id=r#"^org\.keepassxc\.KeePassXC$"# + match app-id=r#"^org\.gnome\.World\.Secrets$"# + + block-out-from "screen-capture" + + // Use this instead if you want them visible on third-party screenshot tools. + // block-out-from "screencast" +} + +// Example: enable rounded corners for all windows. +// (This example rule is commented out with a "/-" in front.) +window-rule { + geometry-corner-radius 12 + clip-to-geometry true +} + +binds { + // Keys consist of modifiers separated by + signs, followed by an XKB key name + // in the end. To find an XKB name for a particular key, you may use a program + // like wev. + // + // "Mod" is a special modifier equal to Super when running on a TTY, and to Alt + // when running as a winit window. + // + // Most actions that you can bind here can also be invoked programmatically with + // `niri msg action do-something`. + + // Mod-Shift-/, which is usually the same as Mod-?, + // shows a list of important hotkeys. + Mod+Shift+Slash { show-hotkey-overlay; } + + // Suggested binds for running programs: terminal, app launcher, screen locker. + Mod+T hotkey-overlay-title="Open a Terminal: Alacritty" { spawn-sh "alacritty msg create-window"; } + Mod+E hotkey-overlay-title="Run Emacs" { spawn-sh "emacsclient -c"; } + Mod+B hotkey-overlay-title="Open a browser: Librewolf" { spawn "librewolf"; } + Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; } + + // Use spawn-sh to run a shell command. Do this if you need pipes, multiple commands, etc. + Mod+Space hotkey-overlay-title="Application Launcher" { + spawn-sh "qs ipc call spotlight toggle"; + } + Mod+V hotkey-overlay-title="Clipboard Manager" { + spawn-sh "qs ipc call clipboard toggle"; + } + Mod+M hotkey-overlay-title="Task Manager" { + spawn-sh "qs ipc call processlist toggle"; + } + Super+L hotkey-overlay-title="Lock Screen" { + spawn-sh "qs ipc call lock lock"; + } + Mod+Y hotkey-overlay-title="Browse Wallpapers" { + spawn-sh "qs ipc call dankdash wallpaper"; + } + XF86AudioRaiseVolume allow-when-locked=true { + spawn-sh "qs ipc call audio increment 3"; + } + XF86AudioLowerVolume allow-when-locked=true { + spawn-sh "qs ipc call audio decrement 3"; + } + XF86AudioMute allow-when-locked=true { + spawn-sh "qs ipc call audio mute"; + } + XF86AudioMicMute allow-when-locked=true { + spawn-sh "qs ipc call audio micmute"; + } + XF86MonBrightnessUp allow-when-locked=true { + spawn-sh "qs ipc call brightness increment 5 ''"; + } + // You can override the default device for e.g. keyboards by adding the device name to the last param + XF86MonBrightnessDown allow-when-locked=true { + spawn-sh "qs ipc call brightness decrement 5 ''"; + } + // Night mode toggle + Mod+Shift+N allow-when-locked=true { + spawn-sh "qs ipc call night toggle"; + } + // Open/close the Overview: a zoomed-out view of workspaces and windows. + // You can also move the mouse into the top-left hot corner, + // or do a four-finger swipe up on a touchpad. + Mod+Tab repeat=false { toggle-overview; } + + Mod+Q repeat=false { close-window; } + + Mod+Left { focus-column-left; } + Mod+Down { focus-window-or-workspace-down; } + Mod+Up { focus-window-or-workspace-up; } + Mod+Right { focus-column-right; } + + Mod+Shift+Left { move-column-left; } + Mod+Shift+Down { move-window-down-or-to-workspace-down; } + Mod+Shift+Up { move-window-up-or-to-workspace-up; } + Mod+Shift+Right { move-column-right; } + + Mod+Home { focus-column-first; } + Mod+End { focus-column-last; } + Mod+Ctrl+Home { move-column-to-first; } + Mod+Ctrl+End { move-column-to-last; } + + Mod+Ctrl+Left { focus-monitor-left; } + Mod+Ctrl+Down { focus-monitor-down; } + Mod+Ctrl+Up { focus-monitor-up; } + Mod+Ctrl+Right { focus-monitor-right; } + + Mod+Shift+Ctrl+Left { move-column-to-monitor-left; } + Mod+Shift+Ctrl+Down { move-column-to-monitor-down; } + Mod+Shift+Ctrl+Up { move-column-to-monitor-up; } + Mod+Shift+Ctrl+Right { move-column-to-monitor-right; } + + // Alternatively, there are commands to move just a single window: + // Mod+Shift+Ctrl+Left { move-window-to-monitor-left; } + // ... + + // And you can also move a whole workspace to another monitor: + // Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; } + // ... + + Mod+Page_Down { focus-workspace-down; } + Mod+Page_Up { focus-workspace-up; } + Mod+U { focus-workspace-down; } + Mod+I { focus-workspace-up; } + Mod+Ctrl+Page_Down { move-column-to-workspace-down; } + Mod+Ctrl+Page_Up { move-column-to-workspace-up; } + Mod+Ctrl+U { move-column-to-workspace-down; } + Mod+Ctrl+I { move-column-to-workspace-up; } + + // Alternatively, there are commands to move just a single window: + // Mod+Ctrl+Page_Down { move-window-to-workspace-down; } + // ... + + Mod+Shift+Page_Down { move-workspace-down; } + Mod+Shift+Page_Up { move-workspace-up; } + Mod+Shift+U { move-workspace-down; } + Mod+Shift+I { move-workspace-up; } + + // You can bind mouse wheel scroll ticks using the following syntax. + // These binds will change direction based on the natural-scroll setting. + // + // To avoid scrolling through workspaces really fast, you can use + // the cooldown-ms property. The bind will be rate-limited to this value. + // You can set a cooldown on any bind, but it's most useful for the wheel. + Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; } + Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; } + Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; } + Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; } + + Mod+WheelScrollRight { focus-column-right; } + Mod+WheelScrollLeft { focus-column-left; } + Mod+Ctrl+WheelScrollRight { move-column-right; } + Mod+Ctrl+WheelScrollLeft { move-column-left; } + + // Usually scrolling up and down with Shift in applications results in + // horizontal scrolling; these binds replicate that. + Mod+Shift+WheelScrollDown { focus-column-right; } + Mod+Shift+WheelScrollUp { focus-column-left; } + Mod+Ctrl+Shift+WheelScrollDown { move-column-right; } + Mod+Ctrl+Shift+WheelScrollUp { move-column-left; } + + // Similarly, you can bind touchpad scroll "ticks". + // Touchpad scrolling is continuous, so for these binds it is split into + // discrete intervals. + // These binds are also affected by touchpad's natural-scroll, so these + // example binds are "inverted", since we have natural-scroll enabled for + // touchpads by default. + // Mod+TouchpadScrollDown { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02+"; } + // Mod+TouchpadScrollUp { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02-"; } + + // You can refer to workspaces by index. However, keep in mind that + // niri is a dynamic workspace system, so these commands are kind of + // "best effort". Trying to refer to a workspace index bigger than + // the current workspace count will instead refer to the bottommost + // (empty) workspace. + // + // For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on + // will all refer to the 3rd workspace. + Mod+1 { focus-workspace 1; } + Mod+2 { focus-workspace 2; } + Mod+3 { focus-workspace 3; } + Mod+4 { focus-workspace 4; } + Mod+5 { focus-workspace 5; } + Mod+6 { focus-workspace 6; } + Mod+7 { focus-workspace 7; } + Mod+8 { focus-workspace 8; } + Mod+9 { focus-workspace 9; } + Mod+Ctrl+1 { move-column-to-workspace 1; } + Mod+Ctrl+2 { move-column-to-workspace 2; } + Mod+Ctrl+3 { move-column-to-workspace 3; } + Mod+Ctrl+4 { move-column-to-workspace 4; } + Mod+Ctrl+5 { move-column-to-workspace 5; } + Mod+Ctrl+6 { move-column-to-workspace 6; } + Mod+Ctrl+7 { move-column-to-workspace 7; } + Mod+Ctrl+8 { move-column-to-workspace 8; } + Mod+Ctrl+9 { move-column-to-workspace 9; } + + // Alternatively, there are commands to move just a single window: + // Mod+Ctrl+1 { move-window-to-workspace 1; } + + // Switches focus between the current and the previous workspace. + // Mod+Tab { focus-workspace-previous; } + + // The following binds move the focused window in and out of a column. + // If the window is alone, they will consume it into the nearby column to the side. + // If the window is already in a column, they will expel it out. + Mod+BracketLeft { consume-or-expel-window-left; } + Mod+BracketRight { consume-or-expel-window-right; } + + // Consume one window from the right to the bottom of the focused column. + Mod+Comma { consume-window-into-column; } + // Expel the bottom window from the focused column to the right. + Mod+Period { expel-window-from-column; } + + Mod+R { switch-preset-column-width; } + // Cycling through the presets in reverse order is also possible. + // Mod+R { switch-preset-column-width-back; } + Mod+Shift+R { switch-preset-window-height; } + Mod+Ctrl+R { reset-window-height; } + Mod+Shift+F { maximize-column; } + Mod+F { fullscreen-window; } + + // Expand the focused column to space not taken up by other fully visible columns. + // Makes the column "fill the rest of the space". + Mod+Ctrl+F { expand-column-to-available-width; } + + Mod+C { center-column; } + + // Center all fully visible columns on screen. + Mod+Ctrl+C { center-visible-columns; } + + // Finer width adjustments. + // This command can also: + // * set width in pixels: "1000" + // * adjust width in pixels: "-5" or "+5" + // * set width as a percentage of screen width: "25%" + // * adjust width as a percentage of screen width: "-10%" or "+10%" + // Pixel sizes use logical, or scaled, pixels. I.e. on an output with scale 2.0, + // set-column-width "100" will make the column occupy 200 physical screen pixels. + Mod+Minus { set-column-width "-10%"; } + Mod+Equal { set-column-width "+10%"; } + + // Finer height adjustments when in column with other windows. + Mod+Shift+Minus { set-window-height "-10%"; } + Mod+Shift+Equal { set-window-height "+10%"; } + + // Move the focused window between the floating and the tiling layout. + Mod+Shift+V { toggle-window-floating; } + Mod+Ctrl+V { switch-focus-between-floating-and-tiling; } + + // Toggle tabbed column display mode. + // Windows in this column will appear as vertical tabs, + // rather than stacked on top of each other. + Mod+W { toggle-column-tabbed-display; } + + // Actions to switch layouts. + // Note: if you uncomment these, make sure you do NOT have + // a matching layout switch hotkey configured in xkb options above. + // Having both at once on the same hotkey will break the switching, + // since it will switch twice upon pressing the hotkey (once by xkb, once by niri). + // Mod+Space { switch-layout "next"; } + // Mod+Shift+Space { switch-layout "prev"; } + + Print { screenshot; } + Ctrl+Print { screenshot-screen; } + Alt+Print { screenshot-window; } + + // Applications such as remote-desktop clients and software KVM switches may + // request that niri stops processing the keyboard shortcuts defined here + // so they may, for example, forward the key presses as-is to a remote machine. + // It's a good idea to bind an escape hatch to toggle the inhibitor, + // so a buggy application can't hold your session hostage. + // + // The allow-inhibiting=false property can be applied to other binds as well, + // which ensures niri always processes them, even when an inhibitor is active. + Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; } + + // The quit action will show a confirmation dialog to avoid accidental exits. + Mod+Shift+E { quit; } + Ctrl+Alt+Delete { quit; } + + // Powers off the monitors. To turn them back on, do any input like + // moving the mouse or pressing any other key. + Mod+Shift+P { power-off-monitors; } +} + +cursor { + hide-after-inactive-ms 10000 +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/.gitignore b/quickshell/.config/quickshell/.gitignore new file mode 100644 index 0000000..08f100f --- /dev/null +++ b/quickshell/.config/quickshell/.gitignore @@ -0,0 +1,99 @@ +# C++ objects and libs +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.so.* +*.dll +*.dylib + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +*.qm +*.prl + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database +compile_commands.json + +# QtCreator local machine specific files for imported projects +*creator.user* + +*_qmlcache.qrc +UNUSED +.qmlls.ini + +CLAUDE-activeContext.md +CLAUDE-temp.md + +# Auto-generated theme files +*.generated.* +niri-colors.generated.kdl +ghostty-colors.generated.conf + +result + +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +# .idea/ +# .vscode/ diff --git a/quickshell/.config/quickshell/Common/Anims.qml b/quickshell/.config/quickshell/Common/Anims.qml new file mode 100644 index 0000000..349e991 --- /dev/null +++ b/quickshell/.config/quickshell/Common/Anims.qml @@ -0,0 +1,25 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell + +Singleton { + id: root + + readonly property int durShort: 200 + readonly property int durMed: 450 + readonly property int durLong: 600 + + readonly property int slidePx: 80 + + readonly property var emphasized: [0.05, 0.00, 0.133333, 0.06, 0.166667, 0.40, 0.208333, 0.82, 0.25, 1.00, 1.00, 1.00] + + readonly property var emphasizedDecel: [0.05, 0.70, 0.10, 1.00, 1.00, 1.00] + + readonly property var emphasizedAccel: [0.30, 0.00, 0.80, 0.15, 1.00, 1.00] + + readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00] + readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00] + readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00] +} diff --git a/quickshell/.config/quickshell/Common/AppUsageHistoryData.qml b/quickshell/.config/quickshell/Common/AppUsageHistoryData.qml new file mode 100644 index 0000000..cac3383 --- /dev/null +++ b/quickshell/.config/quickshell/Common/AppUsageHistoryData.qml @@ -0,0 +1,131 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtCore +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + + id: root + + property var appUsageRanking: { + + } + + Component.onCompleted: { + loadSettings() + } + + function loadSettings() { + parseSettings(settingsFile.text()) + } + + function parseSettings(content) { + try { + if (content && content.trim()) { + var settings = JSON.parse(content) + appUsageRanking = settings.appUsageRanking || {} + } + } catch (e) { + + } + } + + function saveSettings() { + settingsFile.setText(JSON.stringify({ + "appUsageRanking": appUsageRanking + }, null, 2)) + } + + function addAppUsage(app) { + if (!app) + return + + var appId = app.id || (app.execString || app.exec || "") + if (!appId) + return + + var currentRanking = Object.assign({}, appUsageRanking) + + if (currentRanking[appId]) { + currentRanking[appId].usageCount = (currentRanking[appId].usageCount + || 1) + 1 + currentRanking[appId].lastUsed = Date.now() + currentRanking[appId].icon = app.icon || currentRanking[appId].icon + || "application-x-executable" + currentRanking[appId].name = app.name + || currentRanking[appId].name || "" + } else { + currentRanking[appId] = { + "name": app.name || "", + "exec": app.execString || app.exec || "", + "icon": app.icon || "application-x-executable", + "comment": app.comment || "", + "usageCount": 1, + "lastUsed": Date.now() + } + } + + appUsageRanking = currentRanking + saveSettings() + } + + function getAppUsageRanking() { + return appUsageRanking + } + + function getRankedApps() { + var apps = [] + for (var appId in appUsageRanking) { + var appData = appUsageRanking[appId] + apps.push({ + "id": appId, + "name": appData.name, + "exec": appData.exec, + "icon": appData.icon, + "comment": appData.comment, + "usageCount": appData.usageCount, + "lastUsed": appData.lastUsed + }) + } + + return apps.sort(function (a, b) { + if (a.usageCount !== b.usageCount) + return b.usageCount - a.usageCount + return a.name.localeCompare(b.name) + }) + } + + function cleanupAppUsageRanking(availableAppIds) { + var currentRanking = Object.assign({}, appUsageRanking) + var hasChanges = false + + for (var appId in currentRanking) { + if (availableAppIds.indexOf(appId) === -1) { + delete currentRanking[appId] + hasChanges = true + } + } + + if (hasChanges) { + appUsageRanking = currentRanking + saveSettings() + } + } + + FileView { + id: settingsFile + + path: StandardPaths.writableLocation( + StandardPaths.GenericStateLocation) + "/DankMaterialShell/appusage.json" + blockLoading: true + blockWrites: true + watchChanges: true + onLoaded: { + parseSettings(settingsFile.text()) + } + onLoadFailed: error => {} + } +} diff --git a/quickshell/.config/quickshell/Common/Appearance.qml b/quickshell/.config/quickshell/Common/Appearance.qml new file mode 100644 index 0000000..3c22566 --- /dev/null +++ b/quickshell/.config/quickshell/Common/Appearance.qml @@ -0,0 +1,66 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell + +Singleton { + id: root + + readonly property Rounding rounding: Rounding {} + readonly property Spacing spacing: Spacing {} + readonly property FontSize fontSize: FontSize {} + readonly property Anim anim: Anim {} + + component Rounding: QtObject { + readonly property int small: 8 + readonly property int normal: 12 + readonly property int large: 16 + readonly property int extraLarge: 24 + readonly property int full: 1000 + } + + component Spacing: QtObject { + readonly property int small: 4 + readonly property int normal: 8 + readonly property int large: 12 + readonly property int extraLarge: 16 + readonly property int huge: 24 + } + + component FontSize: QtObject { + readonly property int small: 12 + readonly property int normal: 14 + readonly property int large: 16 + readonly property int extraLarge: 20 + readonly property int huge: 24 + } + + component AnimCurves: QtObject { + readonly property list standard: [0.2, 0, 0, 1, 1, 1] + readonly property list standardAccel: [0.3, 0, 1, 1, 1, 1] + readonly property list standardDecel: [0, 0, 0, 1, 1, 1] + readonly property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 + / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + readonly property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + readonly property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + readonly property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] + readonly property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] + readonly property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] + } + + component AnimDurations: QtObject { + readonly property int quick: 150 + readonly property int normal: 300 + readonly property int slow: 500 + readonly property int extraSlow: 1000 + readonly property int expressiveFastSpatial: 350 + readonly property int expressiveDefaultSpatial: 500 + readonly property int expressiveEffects: 200 + } + + component Anim: QtObject { + readonly property AnimCurves curves: AnimCurves {} + readonly property AnimDurations durations: AnimDurations {} + } +} diff --git a/quickshell/.config/quickshell/Common/CacheUtils.qml b/quickshell/.config/quickshell/Common/CacheUtils.qml new file mode 100644 index 0000000..0de1049 --- /dev/null +++ b/quickshell/.config/quickshell/Common/CacheUtils.qml @@ -0,0 +1,45 @@ +import Quickshell +pragma Singleton + +Singleton { + id: root + + // Clear all image cache + function clearImageCache() { + Quickshell.execDetached(["rm", "-rf", Paths.stringify( + Paths.imagecache)]) + Paths.mkdir(Paths.imagecache) + } + + // Clear cache older than specified minutes + function clearOldCache(ageInMinutes) { + Quickshell.execDetached( + ["find", Paths.stringify( + Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]) + } + + // Clear cache for specific size + function clearCacheForSize(size) { + Quickshell.execDetached( + ["find", Paths.stringify( + Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]) + } + + // Get cache size in MB + function getCacheSize(callback) { + var process = Qt.createQmlObject(` + import Quickshell.Io + Process { + command: ["du", "-sm", "${Paths.stringify( + Paths.imagecache)}"] + running: true + stdout: StdioCollector { + onStreamFinished: { + var sizeMB = parseInt(text.split("\\t")[0]) || 0 + callback(sizeMB) + } + } + } + `, root) + } +} diff --git a/quickshell/.config/quickshell/Common/ModalManager.qml b/quickshell/.config/quickshell/Common/ModalManager.qml new file mode 100644 index 0000000..9119cb1 --- /dev/null +++ b/quickshell/.config/quickshell/Common/ModalManager.qml @@ -0,0 +1,16 @@ +pragma Singleton + +import Quickshell +import QtQuick + +Singleton { + id: modalManager + + signal closeAllModalsExcept(var excludedModal) + + function openModal(modal) { + if (!modal.allowStacking) { + closeAllModalsExcept(modal) + } + } +} diff --git a/quickshell/.config/quickshell/Common/Paths.qml b/quickshell/.config/quickshell/Common/Paths.qml new file mode 100644 index 0000000..ec41330 --- /dev/null +++ b/quickshell/.config/quickshell/Common/Paths.qml @@ -0,0 +1,61 @@ +pragma Singleton + +import Quickshell +import QtCore + +Singleton { + id: root + + readonly property url home: StandardPaths.standardLocations( + StandardPaths.HomeLocation)[0] + readonly property url pictures: StandardPaths.standardLocations( + StandardPaths.PicturesLocation)[0] + + readonly property url data: `${StandardPaths.standardLocations( + StandardPaths.GenericDataLocation)[0]}/DankMaterialShell` + readonly property url state: `${StandardPaths.standardLocations( + StandardPaths.GenericStateLocation)[0]}/DankMaterialShell` + readonly property url cache: `${StandardPaths.standardLocations( + StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell` + readonly property url config: `${StandardPaths.standardLocations( + StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell` + + readonly property url imagecache: `${cache}/imagecache` + + function stringify(path: url): string { + return path.toString().replace(/%20/g, " ") + } + + function expandTilde(path: string): string { + return strip(path.replace("~", stringify(root.home))) + } + + function shortenHome(path: string): string { + return path.replace(strip(root.home), "~") + } + + function strip(path: url): string { + return stringify(path).replace("file://", "") + } + + function mkdir(path: url): void { + Quickshell.execDetached(["mkdir", "-p", strip(path)]) + } + + function copy(from: url, to: url): void { + Quickshell.execDetached(["cp", strip(from), strip(to)]) + } + + // ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case + function moddedAppId(appId: string): string { + if (appId === "Spotify") + return "spotify-launcher" + if (appId === "beepertexts") + return "beeper" + if (appId === "home assistant desktop") + return "homeassistant-desktop" + if (appId.includes("com.transmissionbt.transmission")) + return "transmission-gtk" + return appId + } +} diff --git a/quickshell/.config/quickshell/Common/Ref.qml b/quickshell/.config/quickshell/Common/Ref.qml new file mode 100644 index 0000000..406b50c --- /dev/null +++ b/quickshell/.config/quickshell/Common/Ref.qml @@ -0,0 +1,9 @@ +import QtQuick +import Quickshell + +QtObject { + required property Singleton service + + Component.onCompleted: service.refCount++ + Component.onDestruction: service.refCount-- +} diff --git a/quickshell/.config/quickshell/Common/SessionData.qml b/quickshell/.config/quickshell/Common/SessionData.qml new file mode 100644 index 0000000..ab1c0a4 --- /dev/null +++ b/quickshell/.config/quickshell/Common/SessionData.qml @@ -0,0 +1,597 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtCore +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Services + +Singleton { + + id: root + + property bool isLightMode: false + property string wallpaperPath: "" + property string wallpaperLastPath: "" + property string profileLastPath: "" + property bool perMonitorWallpaper: false + property var monitorWallpapers: ({}) + property bool doNotDisturb: false + property bool nightModeEnabled: false + property int nightModeTemperature: 4500 + property bool nightModeAutoEnabled: false + property string nightModeAutoMode: "time" + + property bool hasTriedDefaultSession: false + readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + readonly property string _stateDir: _stateUrl.startsWith("file://") ? _stateUrl.substring(7) : _stateUrl + property int nightModeStartHour: 18 + property int nightModeStartMinute: 0 + property int nightModeEndHour: 6 + property int nightModeEndMinute: 0 + property real latitude: 0.0 + property real longitude: 0.0 + property string nightModeLocationProvider: "" + property var pinnedApps: [] + property int selectedGpuIndex: 0 + property bool nvidiaGpuTempEnabled: false + property bool nonNvidiaGpuTempEnabled: false + property var enabledGpuPciIds: [] + property bool wallpaperCyclingEnabled: false + property string wallpaperCyclingMode: "interval" // "interval" or "time" + property int wallpaperCyclingInterval: 300 // seconds (5 minutes) + property string wallpaperCyclingTime: "06:00" // HH:mm format + property string lastBrightnessDevice: "" + property string notepadContent: "" + property string notepadCurrentFileName: "" + property string notepadCurrentFileUrl: "" + property string notepadLastSavedContent: "" + property var notepadTabs: [] + property int notepadCurrentTabIndex: 0 + + Component.onCompleted: { + loadSettings() + } + + function loadSettings() { + parseSettings(settingsFile.text()) + } + + function parseSettings(content) { + try { + if (content && content.trim()) { + var settings = JSON.parse(content) + isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false + wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : "" + wallpaperLastPath = settings.wallpaperLastPath !== undefined ? settings.wallpaperLastPath : "" + profileLastPath = settings.profileLastPath !== undefined ? settings.profileLastPath : "" + perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false + monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {} + doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false + nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false + nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500 + nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false + nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time" + // Handle legacy time format + if (settings.nightModeStartTime !== undefined) { + const parts = settings.nightModeStartTime.split(":") + nightModeStartHour = parseInt(parts[0]) || 18 + nightModeStartMinute = parseInt(parts[1]) || 0 + } else { + nightModeStartHour = settings.nightModeStartHour !== undefined ? settings.nightModeStartHour : 18 + nightModeStartMinute = settings.nightModeStartMinute !== undefined ? settings.nightModeStartMinute : 0 + } + if (settings.nightModeEndTime !== undefined) { + const parts = settings.nightModeEndTime.split(":") + nightModeEndHour = parseInt(parts[0]) || 6 + nightModeEndMinute = parseInt(parts[1]) || 0 + } else { + nightModeEndHour = settings.nightModeEndHour !== undefined ? settings.nightModeEndHour : 6 + nightModeEndMinute = settings.nightModeEndMinute !== undefined ? settings.nightModeEndMinute : 0 + } + latitude = settings.latitude !== undefined ? settings.latitude : 0.0 + longitude = settings.longitude !== undefined ? settings.longitude : 0.0 + nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : "" + pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : [] + selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0 + nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false + nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false + enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : [] + wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false + wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval" + wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300 + wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00" + lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : "" + notepadContent = settings.notepadContent !== undefined ? settings.notepadContent : "" + + // Generate system themes but don't override user's theme choice + if (typeof Theme !== "undefined") { + Theme.generateSystemThemesFromCurrentTheme() + } + notepadCurrentFileName = settings.notepadCurrentFileName !== undefined ? settings.notepadCurrentFileName : "" + notepadCurrentFileUrl = settings.notepadCurrentFileUrl !== undefined ? settings.notepadCurrentFileUrl : "" + notepadLastSavedContent = settings.notepadLastSavedContent !== undefined ? settings.notepadLastSavedContent : "" + notepadTabs = settings.notepadTabs !== undefined ? settings.notepadTabs : [] + notepadCurrentTabIndex = settings.notepadCurrentTabIndex !== undefined ? settings.notepadCurrentTabIndex : 0 + + // Migrate legacy single notepad to tabs if needed + if (notepadTabs.length === 0 && (notepadContent || notepadCurrentFileName)) { + notepadTabs = [{ + id: Date.now(), + title: notepadCurrentFileName || "Untitled", + content: notepadContent, + fileName: notepadCurrentFileName, + fileUrl: notepadCurrentFileUrl, + lastSavedContent: notepadLastSavedContent, + hasUnsavedChanges: false + }] + notepadCurrentTabIndex = 0 + } + + // Ensure at least one tab exists + if (notepadTabs.length === 0) { + notepadTabs = [{ + id: Date.now(), + title: "Untitled", + content: "", + fileName: "", + fileUrl: "", + lastSavedContent: "", + hasUnsavedChanges: false + }] + notepadCurrentTabIndex = 0 + } + } + } catch (e) { + + } + } + + function saveSettings() { + settingsFile.setText(JSON.stringify({ + "isLightMode": isLightMode, + "wallpaperPath": wallpaperPath, + "wallpaperLastPath": wallpaperLastPath, + "profileLastPath": profileLastPath, + "perMonitorWallpaper": perMonitorWallpaper, + "monitorWallpapers": monitorWallpapers, + "doNotDisturb": doNotDisturb, + "nightModeEnabled": nightModeEnabled, + "nightModeTemperature": nightModeTemperature, + "nightModeAutoEnabled": nightModeAutoEnabled, + "nightModeAutoMode": nightModeAutoMode, + "nightModeStartHour": nightModeStartHour, + "nightModeStartMinute": nightModeStartMinute, + "nightModeEndHour": nightModeEndHour, + "nightModeEndMinute": nightModeEndMinute, + "latitude": latitude, + "longitude": longitude, + "nightModeLocationProvider": nightModeLocationProvider, + "pinnedApps": pinnedApps, + "selectedGpuIndex": selectedGpuIndex, + "nvidiaGpuTempEnabled": nvidiaGpuTempEnabled, + "nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled, + "enabledGpuPciIds": enabledGpuPciIds, + "wallpaperCyclingEnabled": wallpaperCyclingEnabled, + "wallpaperCyclingMode": wallpaperCyclingMode, + "wallpaperCyclingInterval": wallpaperCyclingInterval, + "wallpaperCyclingTime": wallpaperCyclingTime, + "lastBrightnessDevice": lastBrightnessDevice, + "notepadContent": notepadContent, + "notepadCurrentFileName": notepadCurrentFileName, + "notepadCurrentFileUrl": notepadCurrentFileUrl, + "notepadLastSavedContent": notepadLastSavedContent, + "notepadTabs": notepadTabs, + "notepadCurrentTabIndex": notepadCurrentTabIndex + }, null, 2)) + } + + function setLightMode(lightMode) { + isLightMode = lightMode + saveSettings() + } + + function setDoNotDisturb(enabled) { + doNotDisturb = enabled + saveSettings() + } + + function setNightModeEnabled(enabled) { + nightModeEnabled = enabled + saveSettings() + } + + function setNightModeTemperature(temperature) { + nightModeTemperature = temperature + saveSettings() + } + + function setNightModeAutoEnabled(enabled) { + console.log("SessionData: Setting nightModeAutoEnabled to", enabled) + nightModeAutoEnabled = enabled + saveSettings() + } + + function setNightModeAutoMode(mode) { + nightModeAutoMode = mode + saveSettings() + } + + function setNightModeStartHour(hour) { + nightModeStartHour = hour + saveSettings() + } + + function setNightModeStartMinute(minute) { + nightModeStartMinute = minute + saveSettings() + } + + function setNightModeEndHour(hour) { + nightModeEndHour = hour + saveSettings() + } + + function setNightModeEndMinute(minute) { + nightModeEndMinute = minute + saveSettings() + } + + function setLatitude(lat) { + console.log("SessionData: Setting latitude to", lat) + latitude = lat + saveSettings() + } + + function setLongitude(lng) { + console.log("SessionData: Setting longitude to", lng) + longitude = lng + saveSettings() + } + + function setNightModeLocationProvider(provider) { + nightModeLocationProvider = provider + saveSettings() + } + + function setWallpaperPath(path) { + wallpaperPath = path + saveSettings() + } + + function setWallpaper(imagePath) { + wallpaperPath = imagePath + saveSettings() + + if (typeof Theme !== "undefined") { + if (Theme.currentTheme === Theme.dynamic) { + Theme.extractColors() + } + Theme.generateSystemThemesFromCurrentTheme() + } + } + + function setWallpaperColor(color) { + wallpaperPath = color + saveSettings() + + if (typeof Theme !== "undefined") { + if (Theme.currentTheme === Theme.dynamic) { + Theme.extractColors() + } + Theme.generateSystemThemesFromCurrentTheme() + } + } + + function clearWallpaper() { + wallpaperPath = "" + saveSettings() + + if (typeof Theme !== "undefined") { + if (typeof SettingsData !== "undefined" && SettingsData.theme) { + Theme.switchTheme(SettingsData.theme) + } else { + Theme.switchTheme("blue") + } + } + } + + function setWallpaperLastPath(path) { + wallpaperLastPath = path + saveSettings() + } + + function setProfileLastPath(path) { + profileLastPath = path + saveSettings() + } + + function setPinnedApps(apps) { + pinnedApps = apps + saveSettings() + } + + function addPinnedApp(appId) { + if (!appId) + return + var currentPinned = [...pinnedApps] + if (currentPinned.indexOf(appId) === -1) { + currentPinned.push(appId) + setPinnedApps(currentPinned) + } + } + + function removePinnedApp(appId) { + if (!appId) + return + var currentPinned = pinnedApps.filter(id => id !== appId) + setPinnedApps(currentPinned) + } + + function isPinnedApp(appId) { + return appId && pinnedApps.indexOf(appId) !== -1 + } + + function setSelectedGpuIndex(index) { + selectedGpuIndex = index + saveSettings() + } + + function setNvidiaGpuTempEnabled(enabled) { + nvidiaGpuTempEnabled = enabled + saveSettings() + } + + function setNonNvidiaGpuTempEnabled(enabled) { + nonNvidiaGpuTempEnabled = enabled + saveSettings() + } + + function setEnabledGpuPciIds(pciIds) { + enabledGpuPciIds = pciIds + saveSettings() + } + + function setWallpaperCyclingEnabled(enabled) { + wallpaperCyclingEnabled = enabled + saveSettings() + } + + function setWallpaperCyclingMode(mode) { + wallpaperCyclingMode = mode + saveSettings() + } + + function setWallpaperCyclingInterval(interval) { + wallpaperCyclingInterval = interval + saveSettings() + } + + function setWallpaperCyclingTime(time) { + wallpaperCyclingTime = time + saveSettings() + } + + function setPerMonitorWallpaper(enabled) { + perMonitorWallpaper = enabled + + // Disable automatic cycling when per-monitor mode is enabled + if (enabled && wallpaperCyclingEnabled) { + wallpaperCyclingEnabled = false + } + + saveSettings() + + // Refresh dynamic theming when per-monitor mode changes + if (typeof Theme !== "undefined") { + Theme.generateSystemThemesFromCurrentTheme() + } + } + + function setMonitorWallpaper(screenName, path) { + var newMonitorWallpapers = Object.assign({}, monitorWallpapers) + if (path && path !== "") { + newMonitorWallpapers[screenName] = path + } else { + delete newMonitorWallpapers[screenName] + } + monitorWallpapers = newMonitorWallpapers + saveSettings() + + // Trigger dynamic theming if this is the first monitor and dynamic theming is enabled + if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined") { + var screens = Quickshell.screens + if (screens.length > 0 && screenName === screens[0].name) { + if (typeof SettingsData !== "undefined" && SettingsData.wallpaperDynamicTheming) { + Theme.switchTheme("dynamic") + Theme.extractColors() + } + Theme.generateSystemThemesFromCurrentTheme() + } + } + } + + function getMonitorWallpaper(screenName) { + if (!perMonitorWallpaper) { + return wallpaperPath + } + return monitorWallpapers[screenName] || wallpaperPath + } + + function setLastBrightnessDevice(device) { + lastBrightnessDevice = device + saveSettings() + } + + FileView { + id: settingsFile + + path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json" + blockLoading: true + blockWrites: true + watchChanges: true + onLoaded: { + parseSettings(settingsFile.text()) + hasTriedDefaultSession = false + } + onLoadFailed: error => { + if (!hasTriedDefaultSession) { + hasTriedDefaultSession = true + defaultSessionCheckProcess.running = true + } + } + } + + Process { + id: defaultSessionCheckProcess + + command: ["sh", "-c", "CONFIG_DIR=\"" + _stateDir + + "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-session.json\" ] && [ ! -f \"$CONFIG_DIR/session.json\" ]; then cp \"$CONFIG_DIR/default-session.json\" \"$CONFIG_DIR/session.json\" && echo 'copied'; else echo 'not_found'; fi"] + running: false + onExited: exitCode => { + if (exitCode === 0) { + console.log("Copied default-session.json to session.json") + settingsFile.reload() + } + } + } + + IpcHandler { + target: "wallpaper" + + function get(): string { + if (root.perMonitorWallpaper) { + return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead." + } + return root.wallpaperPath || "" + } + + function set(path: string): string { + if (root.perMonitorWallpaper) { + return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead." + } + + if (!path) { + return "ERROR: No path provided" + } + + var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path + + try { + root.setWallpaper(absolutePath) + return "SUCCESS: Wallpaper set to " + absolutePath + } catch (e) { + return "ERROR: Failed to set wallpaper: " + e.toString() + } + } + + function clear(): string { + root.setWallpaper("") + root.setPerMonitorWallpaper(false) + root.monitorWallpapers = {} + root.saveSettings() + return "SUCCESS: All wallpapers cleared" + } + + function next(): string { + if (root.perMonitorWallpaper) { + return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead." + } + + if (!root.wallpaperPath) { + return "ERROR: No wallpaper set" + } + + try { + WallpaperCyclingService.cycleNextManually() + return "SUCCESS: Cycling to next wallpaper" + } catch (e) { + return "ERROR: Failed to cycle wallpaper: " + e.toString() + } + } + + function prev(): string { + if (root.perMonitorWallpaper) { + return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead." + } + + if (!root.wallpaperPath) { + return "ERROR: No wallpaper set" + } + + try { + WallpaperCyclingService.cyclePrevManually() + return "SUCCESS: Cycling to previous wallpaper" + } catch (e) { + return "ERROR: Failed to cycle wallpaper: " + e.toString() + } + } + + function getFor(screenName: string): string { + if (!screenName) { + return "ERROR: No screen name provided" + } + return root.getMonitorWallpaper(screenName) || "" + } + + function setFor(screenName: string, path: string): string { + if (!screenName) { + return "ERROR: No screen name provided" + } + + if (!path) { + return "ERROR: No path provided" + } + + var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path + + try { + if (!root.perMonitorWallpaper) { + root.setPerMonitorWallpaper(true) + } + root.setMonitorWallpaper(screenName, absolutePath) + return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath + } catch (e) { + return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString() + } + } + + function nextFor(screenName: string): string { + if (!screenName) { + return "ERROR: No screen name provided" + } + + var currentWallpaper = root.getMonitorWallpaper(screenName) + if (!currentWallpaper) { + return "ERROR: No wallpaper set for " + screenName + } + + try { + WallpaperCyclingService.cycleNextForMonitor(screenName) + return "SUCCESS: Cycling to next wallpaper for " + screenName + } catch (e) { + return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString() + } + } + + function prevFor(screenName: string): string { + if (!screenName) { + return "ERROR: No screen name provided" + } + + var currentWallpaper = root.getMonitorWallpaper(screenName) + if (!currentWallpaper) { + return "ERROR: No wallpaper set for " + screenName + } + + try { + WallpaperCyclingService.cyclePrevForMonitor(screenName) + return "SUCCESS: Cycling to previous wallpaper for " + screenName + } catch (e) { + return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString() + } + } + } +} diff --git a/quickshell/.config/quickshell/Common/SettingsData.qml b/quickshell/.config/quickshell/Common/SettingsData.qml new file mode 100644 index 0000000..b9b6e57 --- /dev/null +++ b/quickshell/.config/quickshell/Common/SettingsData.qml @@ -0,0 +1,1123 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtCore +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Services + +Singleton { + id: root + + // Theme settings + property string currentThemeName: "blue" + property string customThemeFile: "" + property real topBarTransparency: 0.75 + property real topBarWidgetTransparency: 0.85 + property real popupTransparency: 0.92 + property real dockTransparency: 1 + property bool use24HourClock: true + property bool useFahrenheit: false + property bool nightModeEnabled: false + property string weatherLocation: "New York, NY" + property string weatherCoordinates: "40.7128,-74.0060" + property bool useAutoLocation: false + property bool showLauncherButton: true + property bool showWorkspaceSwitcher: true + property bool showFocusedWindow: true + property bool showWeather: true + property bool showMusic: true + property bool showClipboard: true + property bool showCpuUsage: true + property bool showMemUsage: true + property bool showCpuTemp: true + property bool showGpuTemp: true + property int selectedGpuIndex: 0 + property var enabledGpuPciIds: [] + property bool showSystemTray: true + property bool showClock: true + property bool showNotificationButton: true + property bool showBattery: true + property bool showControlCenterButton: true + property bool controlCenterShowNetworkIcon: true + property bool controlCenterShowBluetoothIcon: true + property bool controlCenterShowAudioIcon: true + property bool showWorkspaceIndex: false + property bool showWorkspacePadding: false + property bool showWorkspaceApps: false + property int maxWorkspaceIcons: 3 + property bool workspacesPerMonitor: true + property var workspaceNameIcons: ({}) + property bool clockCompactMode: false + property bool focusedWindowCompactMode: false + property bool runningAppsCompactMode: true + property bool runningAppsCurrentWorkspace: false + property string clockDateFormat: "" + property string lockDateFormat: "" + property int mediaSize: 1 + property var topBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"] + property var topBarCenterWidgets: ["music", "clock", "weather"] + property var topBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"] + property alias topBarLeftWidgetsModel: leftWidgetsModel + property alias topBarCenterWidgetsModel: centerWidgetsModel + property alias topBarRightWidgetsModel: rightWidgetsModel + property string appLauncherViewMode: "list" + property string spotlightModalViewMode: "list" + property string networkPreference: "auto" + property string iconTheme: "System Default" + property var availableIconThemes: ["System Default"] + property string systemDefaultIconTheme: "" + property bool qt5ctAvailable: false + property bool qt6ctAvailable: false + property bool gtkAvailable: false + property bool useOSLogo: false + property string osLogoColorOverride: "" + property real osLogoBrightness: 0.5 + property real osLogoContrast: 1 + property bool wallpaperDynamicTheming: true + property bool weatherEnabled: true + property string fontFamily: "Inter Variable" + property string monoFontFamily: "Fira Code" + property int fontWeight: Font.Normal + property real fontScale: 1.0 + property bool gtkThemingEnabled: false + property bool qtThemingEnabled: false + property bool showDock: false + property bool dockAutoHide: false + property real cornerRadius: 12 + property bool notificationOverlayEnabled: false + property bool topBarAutoHide: false + property bool topBarOpenOnOverview: false + property bool topBarVisible: true + property real topBarSpacing: 4 + property real topBarBottomGap: 0 + property real topBarInnerPadding: 8 + property bool topBarSquareCorners: false + property bool topBarNoBackground: false + property bool lockScreenShowPowerActions: true + property bool hideBrightnessSlider: false + property int notificationTimeoutLow: 5000 + property int notificationTimeoutNormal: 5000 + property int notificationTimeoutCritical: 0 + property var screenPreferences: ({}) + readonly property string defaultFontFamily: "Inter Variable" + readonly property string defaultMonoFontFamily: "Fira Code" + readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation) + readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + readonly property string _configDir: _configUrl.startsWith("file://") ? _configUrl.substring(7) : _configUrl + + signal forceTopBarLayoutRefresh + signal widgetDataChanged + signal workspaceIconsUpdated + + function getEffectiveTimeFormat() { + if (use24HourClock) { + return Locale.ShortFormat + } else { + return "h:mm AP" + } + } + + function getEffectiveClockDateFormat() { + return clockDateFormat && clockDateFormat.length > 0 ? clockDateFormat : "ddd d" + } + + function getEffectiveLockDateFormat() { + return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat + } + + function initializeListModels() { + // ! Hack-ish to add all properties to the listmodel once + // ! allows the properties to be bound on new widget addtions + var dummyItem = { + "widgetId": "dummy", + "enabled": true, + "size": 20, + "selectedGpuIndex": 0, + "pciId": "" + } + leftWidgetsModel.append(dummyItem) + centerWidgetsModel.append(dummyItem) + rightWidgetsModel.append(dummyItem) + + updateListModel(leftWidgetsModel, topBarLeftWidgets) + updateListModel(centerWidgetsModel, topBarCenterWidgets) + updateListModel(rightWidgetsModel, topBarRightWidgets) + } + + function loadSettings() { + parseSettings(settingsFile.text()) + } + + function parseSettings(content) { + try { + if (content && content.trim()) { + var settings = JSON.parse(content) + // Auto-migrate from old theme system + if (settings.themeIndex !== undefined || settings.themeIsDynamic !== undefined) { + const themeNames = ["blue", "deepBlue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral"] + if (settings.themeIsDynamic) { + currentThemeName = "dynamic" + } else if (settings.themeIndex >= 0 && settings.themeIndex < themeNames.length) { + currentThemeName = themeNames[settings.themeIndex] + } + console.log("Auto-migrated theme from index", settings.themeIndex, "to", currentThemeName) + } else { + currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "blue" + } + customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "" + topBarTransparency = settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 0.75 + topBarWidgetTransparency = settings.topBarWidgetTransparency !== undefined ? (settings.topBarWidgetTransparency > 1 ? settings.topBarWidgetTransparency / 100 : settings.topBarWidgetTransparency) : 0.85 + popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 0.92 + dockTransparency = settings.dockTransparency !== undefined ? (settings.dockTransparency > 1 ? settings.dockTransparency / 100 : settings.dockTransparency) : 1 + use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true + useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false + nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false + weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY" + weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060" + useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false + weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true + showLauncherButton = settings.showLauncherButton !== undefined ? settings.showLauncherButton : true + showWorkspaceSwitcher = settings.showWorkspaceSwitcher !== undefined ? settings.showWorkspaceSwitcher : true + showFocusedWindow = settings.showFocusedWindow !== undefined ? settings.showFocusedWindow : true + showWeather = settings.showWeather !== undefined ? settings.showWeather : true + showMusic = settings.showMusic !== undefined ? settings.showMusic : true + showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true + showCpuUsage = settings.showCpuUsage !== undefined ? settings.showCpuUsage : true + showMemUsage = settings.showMemUsage !== undefined ? settings.showMemUsage : true + showCpuTemp = settings.showCpuTemp !== undefined ? settings.showCpuTemp : true + showGpuTemp = settings.showGpuTemp !== undefined ? settings.showGpuTemp : true + selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0 + enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : [] + showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true + showClock = settings.showClock !== undefined ? settings.showClock : true + showNotificationButton = settings.showNotificationButton !== undefined ? settings.showNotificationButton : true + showBattery = settings.showBattery !== undefined ? settings.showBattery : true + showControlCenterButton = settings.showControlCenterButton !== undefined ? settings.showControlCenterButton : true + controlCenterShowNetworkIcon = settings.controlCenterShowNetworkIcon !== undefined ? settings.controlCenterShowNetworkIcon : true + controlCenterShowBluetoothIcon = settings.controlCenterShowBluetoothIcon !== undefined ? settings.controlCenterShowBluetoothIcon : true + controlCenterShowAudioIcon = settings.controlCenterShowAudioIcon !== undefined ? settings.controlCenterShowAudioIcon : true + showWorkspaceIndex = settings.showWorkspaceIndex !== undefined ? settings.showWorkspaceIndex : false + showWorkspacePadding = settings.showWorkspacePadding !== undefined ? settings.showWorkspacePadding : false + showWorkspaceApps = settings.showWorkspaceApps !== undefined ? settings.showWorkspaceApps : false + maxWorkspaceIcons = settings.maxWorkspaceIcons !== undefined ? settings.maxWorkspaceIcons : 3 + workspaceNameIcons = settings.workspaceNameIcons !== undefined ? settings.workspaceNameIcons : ({}) + workspacesPerMonitor = settings.workspacesPerMonitor !== undefined ? settings.workspacesPerMonitor : true + clockCompactMode = settings.clockCompactMode !== undefined ? settings.clockCompactMode : false + focusedWindowCompactMode = settings.focusedWindowCompactMode !== undefined ? settings.focusedWindowCompactMode : false + runningAppsCompactMode = settings.runningAppsCompactMode !== undefined ? settings.runningAppsCompactMode : true + runningAppsCurrentWorkspace = settings.runningAppsCurrentWorkspace !== undefined ? settings.runningAppsCurrentWorkspace : false + clockDateFormat = settings.clockDateFormat !== undefined ? settings.clockDateFormat : "" + lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "" + mediaSize = settings.mediaSize !== undefined ? settings.mediaSize : (settings.mediaCompactMode !== undefined ? (settings.mediaCompactMode ? 0 : 1) : 1) + if (settings.topBarWidgetOrder) { + topBarLeftWidgets = settings.topBarWidgetOrder.filter(w => { + return ["launcherButton", "workspaceSwitcher", "focusedWindow"].includes(w) + }) + topBarCenterWidgets = settings.topBarWidgetOrder.filter(w => { + return ["clock", "music", "weather"].includes(w) + }) + topBarRightWidgets = settings.topBarWidgetOrder.filter(w => { + return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].includes(w) + }) + } else { + var leftWidgets = settings.topBarLeftWidgets !== undefined ? settings.topBarLeftWidgets : ["launcherButton", "workspaceSwitcher", "focusedWindow"] + var centerWidgets = settings.topBarCenterWidgets !== undefined ? settings.topBarCenterWidgets : ["music", "clock", "weather"] + var rightWidgets = settings.topBarRightWidgets !== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"] + topBarLeftWidgets = leftWidgets + topBarCenterWidgets = centerWidgets + topBarRightWidgets = rightWidgets + updateListModel(leftWidgetsModel, leftWidgets) + updateListModel(centerWidgetsModel, centerWidgets) + updateListModel(rightWidgetsModel, rightWidgets) + } + appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list" + spotlightModalViewMode = settings.spotlightModalViewMode !== undefined ? settings.spotlightModalViewMode : "list" + networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto" + iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default" + useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false + osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "" + osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5 + osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1 + wallpaperDynamicTheming = settings.wallpaperDynamicTheming !== undefined ? settings.wallpaperDynamicTheming : true + fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : defaultFontFamily + monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : defaultMonoFontFamily + fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal + fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0 + gtkThemingEnabled = settings.gtkThemingEnabled !== undefined ? settings.gtkThemingEnabled : false + qtThemingEnabled = settings.qtThemingEnabled !== undefined ? settings.qtThemingEnabled : false + showDock = settings.showDock !== undefined ? settings.showDock : false + dockAutoHide = settings.dockAutoHide !== undefined ? settings.dockAutoHide : false + cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12 + notificationOverlayEnabled = settings.notificationOverlayEnabled !== undefined ? settings.notificationOverlayEnabled : false + topBarAutoHide = settings.topBarAutoHide !== undefined ? settings.topBarAutoHide : false + topBarOpenOnOverview = settings.topBarOpenOnOverview !== undefined ? settings.topBarOpenOnOverview : false + topBarVisible = settings.topBarVisible !== undefined ? settings.topBarVisible : true + notificationTimeoutLow = settings.notificationTimeoutLow !== undefined ? settings.notificationTimeoutLow : 5000 + notificationTimeoutNormal = settings.notificationTimeoutNormal !== undefined ? settings.notificationTimeoutNormal : 5000 + notificationTimeoutCritical = settings.notificationTimeoutCritical !== undefined ? settings.notificationTimeoutCritical : 0 + topBarSpacing = settings.topBarSpacing !== undefined ? settings.topBarSpacing : 4 + topBarBottomGap = settings.topBarBottomGap !== undefined ? settings.topBarBottomGap : 0 + topBarInnerPadding = settings.topBarInnerPadding !== undefined ? settings.topBarInnerPadding : 8 + topBarSquareCorners = settings.topBarSquareCorners !== undefined ? settings.topBarSquareCorners : false + topBarNoBackground = settings.topBarNoBackground !== undefined ? settings.topBarNoBackground : false + lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true + hideBrightnessSlider = settings.hideBrightnessSlider !== undefined ? settings.hideBrightnessSlider : false + screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({}) + applyStoredTheme() + detectAvailableIconThemes() + detectQtTools() + updateGtkIconTheme(iconTheme) + applyStoredIconTheme() + } else { + applyStoredTheme() + } + } catch (e) { + applyStoredTheme() + } + } + + function saveSettings() { + settingsFile.setText(JSON.stringify({ + "currentThemeName": currentThemeName, + "customThemeFile": customThemeFile, + "topBarTransparency": topBarTransparency, + "topBarWidgetTransparency": topBarWidgetTransparency, + "popupTransparency": popupTransparency, + "dockTransparency": dockTransparency, + "use24HourClock": use24HourClock, + "useFahrenheit": useFahrenheit, + "nightModeEnabled": nightModeEnabled, + "weatherLocation": weatherLocation, + "weatherCoordinates": weatherCoordinates, + "useAutoLocation": useAutoLocation, + "weatherEnabled": weatherEnabled, + "showLauncherButton": showLauncherButton, + "showWorkspaceSwitcher": showWorkspaceSwitcher, + "showFocusedWindow": showFocusedWindow, + "showWeather": showWeather, + "showMusic": showMusic, + "showClipboard": showClipboard, + "showCpuUsage": showCpuUsage, + "showMemUsage": showMemUsage, + "showCpuTemp": showCpuTemp, + "showGpuTemp": showGpuTemp, + "selectedGpuIndex": selectedGpuIndex, + "enabledGpuPciIds": enabledGpuPciIds, + "showSystemTray": showSystemTray, + "showClock": showClock, + "showNotificationButton": showNotificationButton, + "showBattery": showBattery, + "showControlCenterButton": showControlCenterButton, + "controlCenterShowNetworkIcon": controlCenterShowNetworkIcon, + "controlCenterShowBluetoothIcon": controlCenterShowBluetoothIcon, + "controlCenterShowAudioIcon": controlCenterShowAudioIcon, + "showWorkspaceIndex": showWorkspaceIndex, + "showWorkspacePadding": showWorkspacePadding, + "showWorkspaceApps": showWorkspaceApps, + "maxWorkspaceIcons": maxWorkspaceIcons, + "workspacesPerMonitor": workspacesPerMonitor, + "workspaceNameIcons": workspaceNameIcons, + "clockCompactMode": clockCompactMode, + "focusedWindowCompactMode": focusedWindowCompactMode, + "runningAppsCompactMode": runningAppsCompactMode, + "runningAppsCurrentWorkspace": runningAppsCurrentWorkspace, + "clockDateFormat": clockDateFormat, + "lockDateFormat": lockDateFormat, + "mediaSize": mediaSize, + "topBarLeftWidgets": topBarLeftWidgets, + "topBarCenterWidgets": topBarCenterWidgets, + "topBarRightWidgets": topBarRightWidgets, + "appLauncherViewMode": appLauncherViewMode, + "spotlightModalViewMode": spotlightModalViewMode, + "networkPreference": networkPreference, + "iconTheme": iconTheme, + "useOSLogo": useOSLogo, + "osLogoColorOverride": osLogoColorOverride, + "osLogoBrightness": osLogoBrightness, + "osLogoContrast": osLogoContrast, + "wallpaperDynamicTheming": wallpaperDynamicTheming, + "fontFamily": fontFamily, + "monoFontFamily": monoFontFamily, + "fontWeight": fontWeight, + "fontScale": fontScale, + "gtkThemingEnabled": gtkThemingEnabled, + "qtThemingEnabled": qtThemingEnabled, + "showDock": showDock, + "dockAutoHide": dockAutoHide, + "cornerRadius": cornerRadius, + "notificationOverlayEnabled": notificationOverlayEnabled, + "topBarAutoHide": topBarAutoHide, + "topBarOpenOnOverview": topBarOpenOnOverview, + "topBarVisible": topBarVisible, + "topBarSpacing": topBarSpacing, + "topBarBottomGap": topBarBottomGap, + "topBarInnerPadding": topBarInnerPadding, + "topBarSquareCorners": topBarSquareCorners, + "topBarNoBackground": topBarNoBackground, + "lockScreenShowPowerActions": lockScreenShowPowerActions, + "hideBrightnessSlider": hideBrightnessSlider, + "notificationTimeoutLow": notificationTimeoutLow, + "notificationTimeoutNormal": notificationTimeoutNormal, + "notificationTimeoutCritical": notificationTimeoutCritical, + "screenPreferences": screenPreferences + }, null, 2)) + } + + function setShowWorkspaceIndex(enabled) { + showWorkspaceIndex = enabled + saveSettings() + } + + function setShowWorkspacePadding(enabled) { + showWorkspacePadding = enabled + saveSettings() + } + + function setShowWorkspaceApps(enabled) { + showWorkspaceApps = enabled + saveSettings() + } + + function setMaxWorkspaceIcons(maxIcons) { + maxWorkspaceIcons = maxIcons + saveSettings() + } + + function setWorkspacesPerMonitor(enabled) { + workspacesPerMonitor = enabled + saveSettings() + } + + function setWorkspaceNameIcon(workspaceName, iconData) { + var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons)) + iconMap[workspaceName] = iconData + workspaceNameIcons = iconMap + saveSettings() + workspaceIconsUpdated() + } + + function removeWorkspaceNameIcon(workspaceName) { + var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons)) + delete iconMap[workspaceName] + workspaceNameIcons = iconMap + saveSettings() + workspaceIconsUpdated() + } + + function getWorkspaceNameIcon(workspaceName) { + return workspaceNameIcons[workspaceName] || null + } + + function hasNamedWorkspaces() { + if (typeof NiriService === "undefined" || !CompositorService.isNiri) + return false + + for (var i = 0; i < NiriService.allWorkspaces.length; i++) { + var ws = NiriService.allWorkspaces[i] + if (ws.name && ws.name.trim() !== "") + return true + } + return false + } + + function getNamedWorkspaces() { + var namedWorkspaces = [] + if (typeof NiriService === "undefined" || !CompositorService.isNiri) + return namedWorkspaces + + for (const ws of NiriService.allWorkspaces) { + if (ws.name && ws.name.trim() !== "") { + namedWorkspaces.push(ws.name) + } + } + return namedWorkspaces + } + + function setClockCompactMode(enabled) { + clockCompactMode = enabled + saveSettings() + } + + function setFocusedWindowCompactMode(enabled) { + focusedWindowCompactMode = enabled + saveSettings() + } + + function setRunningAppsCompactMode(enabled) { + runningAppsCompactMode = enabled + saveSettings() + } + + function setRunningAppsCurrentWorkspace(enabled) { + runningAppsCurrentWorkspace = enabled + saveSettings() + } + + function setClockDateFormat(format) { + clockDateFormat = format || "" + saveSettings() + } + + function setLockDateFormat(format) { + lockDateFormat = format || "" + saveSettings() + } + + function setMediaSize(size) { + mediaSize = size + saveSettings() + } + + function applyStoredTheme() { + if (typeof Theme !== "undefined") + Theme.switchTheme(currentThemeName, false) + else + Qt.callLater(() => { + if (typeof Theme !== "undefined") + Theme.switchTheme(currentThemeName, false) + }) + } + + function setTheme(themeName) { + currentThemeName = themeName + saveSettings() + } + + function setCustomThemeFile(filePath) { + customThemeFile = filePath + saveSettings() + } + + function setTopBarTransparency(transparency) { + topBarTransparency = transparency + saveSettings() + } + + function setTopBarWidgetTransparency(transparency) { + topBarWidgetTransparency = transparency + saveSettings() + } + + function setPopupTransparency(transparency) { + popupTransparency = transparency + saveSettings() + } + + function setDockTransparency(transparency) { + dockTransparency = transparency + saveSettings() + } + + // New preference setters + function setClockFormat(use24Hour) { + use24HourClock = use24Hour + saveSettings() + } + + function setTemperatureUnit(fahrenheit) { + useFahrenheit = fahrenheit + saveSettings() + } + + function setNightModeEnabled(enabled) { + nightModeEnabled = enabled + saveSettings() + } + + // Widget visibility setters + function setShowLauncherButton(enabled) { + showLauncherButton = enabled + saveSettings() + } + + function setShowWorkspaceSwitcher(enabled) { + showWorkspaceSwitcher = enabled + saveSettings() + } + + function setShowFocusedWindow(enabled) { + showFocusedWindow = enabled + saveSettings() + } + + function setShowWeather(enabled) { + showWeather = enabled + saveSettings() + } + + function setShowMusic(enabled) { + showMusic = enabled + saveSettings() + } + + function setShowClipboard(enabled) { + showClipboard = enabled + saveSettings() + } + + function setShowCpuUsage(enabled) { + showCpuUsage = enabled + saveSettings() + } + + function setShowMemUsage(enabled) { + showMemUsage = enabled + saveSettings() + } + + function setShowCpuTemp(enabled) { + showCpuTemp = enabled + saveSettings() + } + + function setShowGpuTemp(enabled) { + showGpuTemp = enabled + saveSettings() + } + + function setSelectedGpuIndex(index) { + selectedGpuIndex = index + saveSettings() + } + + function setEnabledGpuPciIds(pciIds) { + enabledGpuPciIds = pciIds + saveSettings() + } + + function setShowSystemTray(enabled) { + showSystemTray = enabled + saveSettings() + } + + function setShowClock(enabled) { + showClock = enabled + saveSettings() + } + + function setShowNotificationButton(enabled) { + showNotificationButton = enabled + saveSettings() + } + + function setShowBattery(enabled) { + showBattery = enabled + saveSettings() + } + + function setShowControlCenterButton(enabled) { + showControlCenterButton = enabled + saveSettings() + } + + function setControlCenterShowNetworkIcon(enabled) { + controlCenterShowNetworkIcon = enabled + saveSettings() + } + + function setControlCenterShowBluetoothIcon(enabled) { + controlCenterShowBluetoothIcon = enabled + saveSettings() + } + + function setControlCenterShowAudioIcon(enabled) { + controlCenterShowAudioIcon = enabled + saveSettings() + } + + function setTopBarWidgetOrder(order) { + topBarWidgetOrder = order + saveSettings() + } + + function setTopBarLeftWidgets(order) { + topBarLeftWidgets = order + updateListModel(leftWidgetsModel, order) + saveSettings() + } + + function setTopBarCenterWidgets(order) { + topBarCenterWidgets = order + updateListModel(centerWidgetsModel, order) + saveSettings() + } + + function setTopBarRightWidgets(order) { + topBarRightWidgets = order + updateListModel(rightWidgetsModel, order) + saveSettings() + } + + function updateListModel(listModel, order) { + listModel.clear() + for (var i = 0; i < order.length; i++) { + var widgetId = typeof order[i] === "string" ? order[i] : order[i].id + var enabled = typeof order[i] === "string" ? true : order[i].enabled + var size = typeof order[i] === "string" ? undefined : order[i].size + var selectedGpuIndex = typeof order[i] === "string" ? undefined : order[i].selectedGpuIndex + var pciId = typeof order[i] === "string" ? undefined : order[i].pciId + var item = { + "widgetId": widgetId, + "enabled": enabled + } + if (size !== undefined) + item.size = size + if (selectedGpuIndex !== undefined) + item.selectedGpuIndex = selectedGpuIndex + if (pciId !== undefined) + item.pciId = pciId + + listModel.append(item) + } + // Emit signal to notify widgets that data has changed + widgetDataChanged() + } + + function resetTopBarWidgetsToDefault() { + var defaultLeft = ["launcherButton", "workspaceSwitcher", "focusedWindow"] + var defaultCenter = ["music", "clock", "weather"] + var defaultRight = ["systemTray", "clipboard", "notificationButton", "battery", "controlCenterButton"] + topBarLeftWidgets = defaultLeft + topBarCenterWidgets = defaultCenter + topBarRightWidgets = defaultRight + updateListModel(leftWidgetsModel, defaultLeft) + updateListModel(centerWidgetsModel, defaultCenter) + updateListModel(rightWidgetsModel, defaultRight) + showLauncherButton = true + showWorkspaceSwitcher = true + showFocusedWindow = true + showWeather = true + showMusic = true + showClipboard = true + showCpuUsage = true + showMemUsage = true + showCpuTemp = true + showGpuTemp = true + showSystemTray = true + showClock = true + showNotificationButton = true + showBattery = true + showControlCenterButton = true + saveSettings() + } + + // View mode setters + function setAppLauncherViewMode(mode) { + appLauncherViewMode = mode + saveSettings() + } + + function setSpotlightModalViewMode(mode) { + spotlightModalViewMode = mode + saveSettings() + } + + // Weather location setter + function setWeatherLocation(displayName, coordinates) { + weatherLocation = displayName + weatherCoordinates = coordinates + saveSettings() + } + + function setAutoLocation(enabled) { + useAutoLocation = enabled + saveSettings() + } + + function setWeatherEnabled(enabled) { + weatherEnabled = enabled + saveSettings() + } + + // Network preference setter + function setNetworkPreference(preference) { + networkPreference = preference + saveSettings() + } + + function detectAvailableIconThemes() { + // First detect system default, then available themes + systemDefaultDetectionProcess.running = true + } + + function detectQtTools() { + qtToolsDetectionProcess.running = true + } + + function setIconTheme(themeName) { + iconTheme = themeName + updateGtkIconTheme(themeName) + updateQtIconTheme(themeName) + saveSettings() + if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic) + Theme.generateSystemThemes() + } + + function updateGtkIconTheme(themeName) { + var gtkThemeName = (themeName === "System Default") ? systemDefaultIconTheme : themeName + if (gtkThemeName !== "System Default" && gtkThemeName !== "") { + var script = "if command -v gsettings >/dev/null 2>&1 && gsettings list-schemas | grep -q org.gnome.desktop.interface; then\n" + + " gsettings set org.gnome.desktop.interface icon-theme '" + gtkThemeName + "'\n" + " echo 'Updated via gsettings'\n" + "elif command -v dconf >/dev/null 2>&1; then\n" + " dconf write /org/gnome/desktop/interface/icon-theme \\\"" + gtkThemeName + "\\\"\n" + + " echo 'Updated via dconf'\n" + "fi\n" + "\n" + "# Ensure config directories exist\n" + "mkdir -p " + _configDir + "/gtk-3.0 " + _configDir + + "/gtk-4.0\n" + "\n" + "# Update settings.ini files (keep existing gtk-theme-name)\n" + "for config_dir in " + _configDir + "/gtk-3.0 " + _configDir + "/gtk-4.0; do\n" + + " settings_file=\"$config_dir/settings.ini\"\n" + " if [ -f \"$settings_file\" ]; then\n" + " # Update existing icon-theme-name line or add it\n" + " if grep -q '^gtk-icon-theme-name=' \"$settings_file\"; then\n" + " sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=" + gtkThemeName + "/' \"$settings_file\"\n" + " else\n" + + " # Add icon theme setting to [Settings] section or create it\n" + " if grep -q '\\[Settings\\]' \"$settings_file\"; then\n" + " sed -i '/\\[Settings\\]/a gtk-icon-theme-name=" + gtkThemeName + "' \"$settings_file\"\n" + " else\n" + " echo -e '\\n[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + + "' >> \"$settings_file\"\n" + " fi\n" + " fi\n" + " else\n" + " # Create new settings.ini file\n" + " echo -e '[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + "' > \"$settings_file\"\n" + + " fi\n" + " echo \"Updated $settings_file\"\n" + "done\n" + "\n" + "# Clear icon cache and force refresh\n" + "rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true\n" + "# Send SIGHUP to running GTK applications to reload themes (Fedora-specific)\n" + "pkill -HUP -f 'gtk' 2>/dev/null || true\n" + Quickshell.execDetached(["sh", "-lc", script]) + } + } + + function updateQtIconTheme(themeName) { + var qtThemeName = (themeName === "System Default") ? "" : themeName + var home = _shq(root._homeUrl.replace("file://", "")) + if (!qtThemeName) { + // When "System Default" is selected, don't modify the config files at all + // This preserves the user's existing qt6ct configuration + return + } + var script = "mkdir -p " + _configDir + "/qt5ct " + _configDir + "/qt6ct " + _configDir + "/environment.d 2>/dev/null || true\n" + "update_qt_icon_theme() {\n" + " local config_file=\"$1\"\n" + + " local theme_name=\"$2\"\n" + " if [ -f \"$config_file\" ]; then\n" + " if grep -q '^\\[Appearance\\]' \"$config_file\"; then\n" + " if grep -q '^icon_theme=' \"$config_file\"; then\n" + " sed -i \"s/^icon_theme=.*/icon_theme=$theme_name/\" \"$config_file\"\n" + " else\n" + " sed -i \"/^\\[Appearance\\]/a icon_theme=$theme_name\" \"$config_file\"\n" + " fi\n" + + " else\n" + " printf '\\n[Appearance]\\nicon_theme=%s\\n' \"$theme_name\" >> \"$config_file\"\n" + " fi\n" + " else\n" + " printf '[Appearance]\\nicon_theme=%s\\n' \"$theme_name\" > \"$config_file\"\n" + " fi\n" + "}\n" + "update_qt_icon_theme " + _configDir + "/qt5ct/qt5ct.conf " + _shq( + qtThemeName) + "\n" + "update_qt_icon_theme " + _configDir + "/qt6ct/qt6ct.conf " + _shq(qtThemeName) + "\n" + "rm -rf " + home + "/.cache/icon-cache " + home + "/.cache/thumbnails 2>/dev/null || true\n" + Quickshell.execDetached(["sh", "-lc", script]) + } + + function applyStoredIconTheme() { + updateGtkIconTheme(iconTheme) + updateQtIconTheme(iconTheme) + } + + function setUseOSLogo(enabled) { + useOSLogo = enabled + saveSettings() + } + + function setOSLogoColorOverride(color) { + osLogoColorOverride = color + saveSettings() + } + + function setOSLogoBrightness(brightness) { + osLogoBrightness = brightness + saveSettings() + } + + function setOSLogoContrast(contrast) { + osLogoContrast = contrast + saveSettings() + } + + function setWallpaperDynamicTheming(enabled) { + wallpaperDynamicTheming = enabled + saveSettings() + } + + function setFontFamily(family) { + fontFamily = family + saveSettings() + } + + function setFontWeight(weight) { + fontWeight = weight + saveSettings() + } + + function setMonoFontFamily(family) { + monoFontFamily = family + saveSettings() + } + + function setFontScale(scale) { + fontScale = scale + saveSettings() + } + + function setGtkThemingEnabled(enabled) { + gtkThemingEnabled = enabled + saveSettings() + if (enabled && typeof Theme !== "undefined") { + Theme.generateSystemThemesFromCurrentTheme() + } + } + + function setQtThemingEnabled(enabled) { + qtThemingEnabled = enabled + saveSettings() + if (enabled && typeof Theme !== "undefined") { + Theme.generateSystemThemesFromCurrentTheme() + } + } + + function setShowDock(enabled) { + showDock = enabled + saveSettings() + } + + function setDockAutoHide(enabled) { + dockAutoHide = enabled + saveSettings() + } + + function setCornerRadius(radius) { + cornerRadius = radius + saveSettings() + } + + function setNotificationOverlayEnabled(enabled) { + notificationOverlayEnabled = enabled + saveSettings() + } + + function setTopBarAutoHide(enabled) { + topBarAutoHide = enabled + saveSettings() + } + + function setTopBarOpenOnOverview(enabled) { + topBarOpenOnOverview = enabled + saveSettings() + } + + function setTopBarVisible(visible) { + topBarVisible = visible + saveSettings() + } + + function toggleTopBarVisible() { + topBarVisible = !topBarVisible + saveSettings() + } + + function setNotificationTimeoutLow(timeout) { + notificationTimeoutLow = timeout + saveSettings() + } + + function setNotificationTimeoutNormal(timeout) { + notificationTimeoutNormal = timeout + saveSettings() + } + + function setNotificationTimeoutCritical(timeout) { + notificationTimeoutCritical = timeout + saveSettings() + } + + function setTopBarSpacing(spacing) { + topBarSpacing = spacing + saveSettings() + } + + function setTopBarBottomGap(gap) { + topBarBottomGap = gap + saveSettings() + } + + function setTopBarInnerPadding(padding) { + topBarInnerPadding = padding + saveSettings() + } + + function setTopBarSquareCorners(enabled) { + topBarSquareCorners = enabled + saveSettings() + } + + function setTopBarNoBackground(enabled) { + topBarNoBackground = enabled + saveSettings() + } + + function setLockScreenShowPowerActions(enabled) { + lockScreenShowPowerActions = enabled + saveSettings() + } + + function setHideBrightnessSlider(enabled) { + hideBrightnessSlider = enabled + saveSettings() + } + + function setScreenPreferences(prefs) { + screenPreferences = prefs + saveSettings() + } + + function getFilteredScreens(componentId) { + var prefs = screenPreferences && screenPreferences[componentId] || ["all"] + if (prefs.includes("all")) { + return Quickshell.screens + } + return Quickshell.screens.filter(screen => prefs.includes(screen.name)) + } + + function _shq(s) { + return "'" + String(s).replace(/'/g, "'\\''") + "'" + } + + Component.onCompleted: { + loadSettings() + fontCheckTimer.start() + initializeListModels() + } + + ListModel { + id: leftWidgetsModel + } + + ListModel { + id: centerWidgetsModel + } + + ListModel { + id: rightWidgetsModel + } + + Timer { + id: fontCheckTimer + + interval: 3000 + repeat: false + onTriggered: { + var availableFonts = Qt.fontFamilies() + var missingFonts = [] + if (fontFamily === defaultFontFamily && !availableFonts.includes(defaultFontFamily)) + missingFonts.push(defaultFontFamily) + + if (monoFontFamily === defaultMonoFontFamily && !availableFonts.includes(defaultMonoFontFamily)) + missingFonts.push(defaultMonoFontFamily) + + if (missingFonts.length > 0) { + var message = "Missing fonts: " + missingFonts.join(", ") + ". Using system defaults." + ToastService.showWarning(message) + } + } + } + + property bool hasTriedDefaultSettings: false + + FileView { + id: settingsFile + + path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json" + blockLoading: true + blockWrites: true + watchChanges: true + onLoaded: { + parseSettings(settingsFile.text()) + hasTriedDefaultSettings = false + } + onLoadFailed: error => { + if (!hasTriedDefaultSettings) { + hasTriedDefaultSettings = true + defaultSettingsCheckProcess.running = true + } else { + applyStoredTheme() + } + } + } + + Process { + id: systemDefaultDetectionProcess + + command: ["sh", "-c", "gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed \"s/'//g\" || echo ''"] + running: false + onExited: exitCode => { + if (exitCode === 0 && stdout && stdout.length > 0) + systemDefaultIconTheme = stdout.trim() + else + systemDefaultIconTheme = "" + iconThemeDetectionProcess.running = true + } + } + + Process { + id: iconThemeDetectionProcess + + command: ["sh", "-c", "find /usr/share/icons ~/.local/share/icons ~/.icons -maxdepth 1 -type d 2>/dev/null | sed 's|.*/||' | grep -v '^icons$' | sort -u"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + var detectedThemes = ["System Default"] + if (text && text.trim()) { + var themes = text.trim().split('\n') + for (var i = 0; i < themes.length; i++) { + var theme = themes[i].trim() + if (theme && theme !== "" && theme !== "default" && theme !== "hicolor" && theme !== "locolor") + detectedThemes.push(theme) + } + } + availableIconThemes = detectedThemes + } + } + } + + Process { + id: qtToolsDetectionProcess + + command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + var lines = text.trim().split('\n') + for (var i = 0; i < lines.length; i++) { + var line = lines[i] + if (line.startsWith('qt5ct:')) + qt5ctAvailable = line.split(':')[1] === 'true' + else if (line.startsWith('qt6ct:')) + qt6ctAvailable = line.split(':')[1] === 'true' + else if (line.startsWith('gtk:')) + gtkAvailable = line.split(':')[1] === 'true' + } + } + } + } + } + + Process { + id: defaultSettingsCheckProcess + + command: ["sh", "-c", "CONFIG_DIR=\"" + _configDir + + "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-settings.json\" ] && [ ! -f \"$CONFIG_DIR/settings.json\" ]; then cp \"$CONFIG_DIR/default-settings.json\" \"$CONFIG_DIR/settings.json\" && echo 'copied'; else echo 'not_found'; fi"] + running: false + onExited: exitCode => { + if (exitCode === 0) { + console.log("Copied default-settings.json to settings.json") + settingsFile.reload() + } else { + // No default settings file found, just apply stored theme + applyStoredTheme() + } + } + } + + IpcHandler { + function reveal(): string { + root.setTopBarVisible(true) + return "BAR_SHOW_SUCCESS" + } + + function hide(): string { + root.setTopBarVisible(false) + return "BAR_HIDE_SUCCESS" + } + + function toggle(): string { + root.toggleTopBarVisible() + return topBarVisible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS" + } + + function status(): string { + return topBarVisible ? "visible" : "hidden" + } + + target: "bar" + } +} diff --git a/quickshell/.config/quickshell/Common/StockThemes.js b/quickshell/.config/quickshell/Common/StockThemes.js new file mode 100644 index 0000000..4bdb8e4 --- /dev/null +++ b/quickshell/.config/quickshell/Common/StockThemes.js @@ -0,0 +1,380 @@ +// Stock theme definitions for DankMaterialShell +// Separated from Theme.qml to keep that file clean + +const StockThemes = { + DARK: { + blue: { + name: "Blue", + primary: "#42a5f5", + primaryText: "#ffffff", + primaryContainer: "#1976d2", + secondary: "#8ab4f8", + surface: "#1a1c1e", + surfaceText: "#e3e8ef", + surfaceVariant: "#44464f", + surfaceVariantText: "#c4c7c5", + surfaceTint: "#8ab4f8", + background: "#1a1c1e", + backgroundText: "#e3e8ef", + outline: "#8e918f", + surfaceContainer: "#1e2023", + surfaceContainerHigh: "#292b2f" + }, + deepBlue: { + name: "Deep Blue", + primary: "#0061a4", + primaryText: "#ffffff", + primaryContainer: "#004881", + secondary: "#42a5f5", + surface: "#1a1c1e", + surfaceText: "#e3e8ef", + surfaceVariant: "#44464f", + surfaceVariantText: "#c4c7c5", + surfaceTint: "#8ab4f8", + background: "#1a1c1e", + backgroundText: "#e3e8ef", + outline: "#8e918f", + surfaceContainer: "#1e2023", + surfaceContainerHigh: "#292b2f" + }, + purple: { + name: "Purple", + primary: "#D0BCFF", + primaryText: "#381E72", + primaryContainer: "#4F378B", + secondary: "#CCC2DC", + surface: "#10121E", + surfaceText: "#E6E0E9", + surfaceVariant: "#49454F", + surfaceVariantText: "#CAC4D0", + surfaceTint: "#D0BCFF", + background: "#10121E", + backgroundText: "#E6E0E9", + outline: "#938F99", + surfaceContainer: "#1D1B20", + surfaceContainerHigh: "#2B2930" + }, + green: { + name: "Green", + primary: "#4caf50", + primaryText: "#ffffff", + primaryContainer: "#388e3c", + secondary: "#81c995", + surface: "#0f1411", + surfaceText: "#e1f5e3", + surfaceVariant: "#404943", + surfaceVariantText: "#c1cbc4", + surfaceTint: "#81c995", + background: "#0f1411", + backgroundText: "#e1f5e3", + outline: "#8b938c", + surfaceContainer: "#1a1f1b", + surfaceContainerHigh: "#252a26" + }, + orange: { + name: "Orange", + primary: "#ff6d00", + primaryText: "#ffffff", + primaryContainer: "#e65100", + secondary: "#ffb74d", + surface: "#1c1410", + surfaceText: "#f5f1ea", + surfaceVariant: "#4a453a", + surfaceVariantText: "#cbc5b8", + surfaceTint: "#ffb74d", + background: "#1c1410", + backgroundText: "#f5f1ea", + outline: "#958f84", + surfaceContainer: "#211e17", + surfaceContainerHigh: "#2c291f" + }, + red: { + name: "Red", + primary: "#f44336", + primaryText: "#ffffff", + primaryContainer: "#d32f2f", + secondary: "#f28b82", + surface: "#1c1011", + surfaceText: "#f5e8ea", + surfaceVariant: "#4a3f41", + surfaceVariantText: "#cbc2c4", + surfaceTint: "#f28b82", + background: "#1c1011", + backgroundText: "#f5e8ea", + outline: "#958b8d", + surfaceContainer: "#211b1c", + surfaceContainerHigh: "#2c2426" + }, + cyan: { + name: "Cyan", + primary: "#00bcd4", + primaryText: "#ffffff", + primaryContainer: "#0097a7", + secondary: "#4dd0e1", + surface: "#0f1617", + surfaceText: "#e8f4f5", + surfaceVariant: "#3f474a", + surfaceVariantText: "#c2c9cb", + surfaceTint: "#4dd0e1", + background: "#0f1617", + backgroundText: "#e8f4f5", + outline: "#8c9194", + surfaceContainer: "#1a1f20", + surfaceContainerHigh: "#252b2c" + }, + pink: { + name: "Pink", + primary: "#e91e63", + primaryText: "#ffffff", + primaryContainer: "#c2185b", + secondary: "#f8bbd9", + surface: "#1a1014", + surfaceText: "#f3e8ee", + surfaceVariant: "#483f45", + surfaceVariantText: "#c9c2c7", + surfaceTint: "#f8bbd9", + background: "#1a1014", + backgroundText: "#f3e8ee", + outline: "#938a90", + surfaceContainer: "#1f1b1e", + surfaceContainerHigh: "#2a2428" + }, + amber: { + name: "Amber", + primary: "#ffc107", + primaryText: "#000000", + primaryContainer: "#ff8f00", + secondary: "#ffd54f", + surface: "#1a1710", + surfaceText: "#f3f0e8", + surfaceVariant: "#49453a", + surfaceVariantText: "#cac5b8", + surfaceTint: "#ffd54f", + background: "#1a1710", + backgroundText: "#f3f0e8", + outline: "#949084", + surfaceContainer: "#1f1e17", + surfaceContainerHigh: "#2a281f" + }, + coral: { + name: "Coral", + primary: "#ffb4ab", + primaryText: "#5f1412", + primaryContainer: "#8c1d18", + secondary: "#f9dedc", + surface: "#1a1110", + surfaceText: "#f1e8e7", + surfaceVariant: "#4a4142", + surfaceVariantText: "#cdc2c1", + surfaceTint: "#ffb4ab", + background: "#1a1110", + backgroundText: "#f1e8e7", + outline: "#968b8a", + surfaceContainer: "#201a19", + surfaceContainerHigh: "#2b2221" + } + }, + LIGHT: { + blue: { + name: "Blue Light", + primary: "#1976d2", + primaryText: "#ffffff", + primaryContainer: "#e3f2fd", + secondary: "#42a5f5", + surface: "#fefefe", + surfaceText: "#1a1c1e", + surfaceVariant: "#e7e0ec", + surfaceVariantText: "#49454f", + surfaceTint: "#1976d2", + background: "#fefefe", + backgroundText: "#1a1c1e", + outline: "#79747e", + surfaceContainer: "#f3f3f3", + surfaceContainerHigh: "#ececec" + }, + deepBlue: { + name: "Deep Blue Light", + primary: "#0061a4", + primaryText: "#ffffff", + primaryContainer: "#cfe5ff", + secondary: "#1976d2", + surface: "#fefefe", + surfaceText: "#1a1c1e", + surfaceVariant: "#e7e0ec", + surfaceVariantText: "#49454f", + surfaceTint: "#0061a4", + background: "#fefefe", + backgroundText: "#1a1c1e", + outline: "#79747e", + surfaceContainer: "#f3f3f3", + surfaceContainerHigh: "#ececec" + }, + purple: { + name: "Purple Light", + primary: "#6750A4", + primaryText: "#ffffff", + primaryContainer: "#EADDFF", + secondary: "#625B71", + surface: "#FFFBFE", + surfaceText: "#1C1B1F", + surfaceVariant: "#E7E0EC", + surfaceVariantText: "#49454F", + surfaceTint: "#6750A4", + background: "#FFFBFE", + backgroundText: "#1C1B1F", + outline: "#79747E", + surfaceContainer: "#F3EDF7", + surfaceContainerHigh: "#ECE6F0" + }, + green: { + name: "Green Light", + primary: "#2e7d32", + primaryText: "#ffffff", + primaryContainer: "#e8f5e8", + secondary: "#4caf50", + surface: "#fefefe", + surfaceText: "#1a1c1e", + surfaceVariant: "#e7e0ec", + surfaceVariantText: "#49454f", + surfaceTint: "#2e7d32", + background: "#fefefe", + backgroundText: "#1a1c1e", + outline: "#79747e", + surfaceContainer: "#f3f3f3", + surfaceContainerHigh: "#ececec" + }, + orange: { + name: "Orange Light", + primary: "#e65100", + primaryText: "#ffffff", + primaryContainer: "#ffecb3", + secondary: "#ff9800", + surface: "#fefefe", + surfaceText: "#1a1c1e", + surfaceVariant: "#e7e0ec", + surfaceVariantText: "#49454f", + surfaceTint: "#e65100", + background: "#fefefe", + backgroundText: "#1a1c1e", + outline: "#79747e", + surfaceContainer: "#f3f3f3", + surfaceContainerHigh: "#ececec" + }, + red: { + name: "Red Light", + primary: "#d32f2f", + primaryText: "#ffffff", + primaryContainer: "#ffebee", + secondary: "#f44336", + surface: "#fefefe", + surfaceText: "#1a1c1e", + surfaceVariant: "#e7e0ec", + surfaceVariantText: "#49454f", + surfaceTint: "#d32f2f", + background: "#fefefe", + backgroundText: "#1a1c1e", + outline: "#79747e", + surfaceContainer: "#f3f3f3", + surfaceContainerHigh: "#ececec" + }, + cyan: { + name: "Cyan Light", + primary: "#0097a7", + primaryText: "#ffffff", + primaryContainer: "#e0f2f1", + secondary: "#00bcd4", + surface: "#fefefe", + surfaceText: "#1a1c1e", + surfaceVariant: "#e7e0ec", + surfaceVariantText: "#49454f", + surfaceTint: "#0097a7", + background: "#fefefe", + backgroundText: "#1a1c1e", + outline: "#79747e", + surfaceContainer: "#f3f3f3", + surfaceContainerHigh: "#ececec" + }, + pink: { + name: "Pink Light", + primary: "#c2185b", + primaryText: "#ffffff", + primaryContainer: "#fce4ec", + secondary: "#e91e63", + surface: "#fefefe", + surfaceText: "#1a1c1e", + surfaceVariant: "#e7e0ec", + surfaceVariantText: "#49454f", + surfaceTint: "#c2185b", + background: "#fefefe", + backgroundText: "#1a1c1e", + outline: "#79747e", + surfaceContainer: "#f3f3f3", + surfaceContainerHigh: "#ececec" + }, + amber: { + name: "Amber Light", + primary: "#ff8f00", + primaryText: "#000000", + primaryContainer: "#fff8e1", + secondary: "#ffc107", + surface: "#fefefe", + surfaceText: "#1a1c1e", + surfaceVariant: "#e7e0ec", + surfaceVariantText: "#49454f", + surfaceTint: "#ff8f00", + background: "#fefefe", + backgroundText: "#1a1c1e", + outline: "#79747e", + surfaceContainer: "#f3f3f3", + surfaceContainerHigh: "#ececec" + }, + coral: { + name: "Coral Light", + primary: "#8c1d18", + primaryText: "#ffffff", + primaryContainer: "#ffdad6", + secondary: "#ff5449", + surface: "#fefefe", + surfaceText: "#1a1c1e", + surfaceVariant: "#e7e0ec", + surfaceVariantText: "#49454f", + surfaceTint: "#8c1d18", + background: "#fefefe", + backgroundText: "#1a1c1e", + outline: "#79747e", + surfaceContainer: "#f3f3f3", + surfaceContainerHigh: "#ececec" + } + } +} + +const ThemeNames = { + BLUE: "blue", + DEEP_BLUE: "deepBlue", + PURPLE: "purple", + GREEN: "green", + ORANGE: "orange", + RED: "red", + CYAN: "cyan", + PINK: "pink", + AMBER: "amber", + CORAL: "coral", + DYNAMIC: "dynamic" +} + +function isStockTheme(themeName) { + return Object.keys(StockThemes.DARK).includes(themeName) +} + +function getAvailableThemes(isLight = false) { + return isLight ? StockThemes.LIGHT : StockThemes.DARK +} + +function getThemeByName(themeName, isLight = false) { + const themes = getAvailableThemes(isLight) + return themes[themeName] || themes.blue +} + +function getAllThemeNames() { + return Object.keys(StockThemes.DARK) +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Common/Theme.qml b/quickshell/.config/quickshell/Common/Theme.qml new file mode 100644 index 0000000..f786890 --- /dev/null +++ b/quickshell/.config/quickshell/Common/Theme.qml @@ -0,0 +1,819 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtCore +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.UPower +import qs.Services +import "StockThemes.js" as StockThemes + +Singleton { + id: root + + property string currentTheme: "blue" + property bool isLightMode: false + + readonly property string dynamic: "dynamic" + + readonly property string homeDir: { + const url = StandardPaths.writableLocation(StandardPaths.HomeLocation).toString() + return url.startsWith("file://") ? url.substring(7) : url + } + readonly property string configDir: { + const url = StandardPaths.writableLocation(StandardPaths.ConfigLocation).toString() + return url.startsWith("file://") ? url.substring(7) : url + } + readonly property string shellDir: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Common/", "") + readonly property string wallpaperPath: { + if (typeof SessionData === "undefined") return "" + + if (SessionData.perMonitorWallpaper) { + // Use first monitor's wallpaper for dynamic theming + var screens = Quickshell.screens + if (screens.length > 0) { + var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name) + return firstMonitorWallpaper || SessionData.wallpaperPath + } + } + + return SessionData.wallpaperPath + } + + property bool matugenAvailable: false + property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false + property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false + property var workerRunning: false + property var matugenColors: ({}) + property bool extractionRequested: false + property int colorUpdateTrigger: 0 + property var customThemeData: null + + readonly property string stateDir: { + const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString() + const path = cacheHome.startsWith("file://") ? cacheHome.substring(7) : cacheHome + return path + "/dankshell" + } + + Component.onCompleted: { + Quickshell.execDetached(["mkdir", "-p", stateDir]) + matugenCheck.running = true + if (typeof SessionData !== "undefined") + SessionData.isLightModeChanged.connect(root.onLightModeChanged) + + if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) { + switchTheme(SettingsData.currentThemeName, false) + } + } + + function getMatugenColor(path, fallback) { + colorUpdateTrigger + const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark" + let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode] + for (const part of path.split(".")) { + if (!cur || typeof cur !== "object" || !(part in cur)) + return fallback + cur = cur[part] + } + return cur || fallback + } + + readonly property var currentThemeData: { + if (currentTheme === "custom") { + return customThemeData || StockThemes.getThemeByName("blue", isLightMode) + } else if (currentTheme === dynamic) { + return { + "primary": getMatugenColor("primary", "#42a5f5"), + "primaryText": getMatugenColor("on_primary", "#ffffff"), + "primaryContainer": getMatugenColor("primary_container", "#1976d2"), + "secondary": getMatugenColor("secondary", "#8ab4f8"), + "surface": getMatugenColor("surface", "#1a1c1e"), + "surfaceText": getMatugenColor("on_background", "#e3e8ef"), + "surfaceVariant": getMatugenColor("surface_variant", "#44464f"), + "surfaceVariantText": getMatugenColor("on_surface_variant", "#c4c7c5"), + "surfaceTint": getMatugenColor("surface_tint", "#8ab4f8"), + "background": getMatugenColor("background", "#1a1c1e"), + "backgroundText": getMatugenColor("on_background", "#e3e8ef"), + "outline": getMatugenColor("outline", "#8e918f"), + "surfaceContainer": getMatugenColor("surface_container", "#1e2023"), + "surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"), + "error": "#F2B8B5", + "warning": "#FF9800", + "info": "#2196F3", + "success": "#4CAF50" + } + } else { + return StockThemes.getThemeByName(currentTheme, isLightMode) + } + } + + property color primary: currentThemeData.primary + property color primaryText: currentThemeData.primaryText + property color primaryContainer: currentThemeData.primaryContainer + property color secondary: currentThemeData.secondary + property color surface: currentThemeData.surface + property color surfaceText: currentThemeData.surfaceText + property color surfaceVariant: currentThemeData.surfaceVariant + property color surfaceVariantText: currentThemeData.surfaceVariantText + property color surfaceTint: currentThemeData.surfaceTint + property color background: currentThemeData.background + property color backgroundText: currentThemeData.backgroundText + property color outline: currentThemeData.outline + property color surfaceContainer: currentThemeData.surfaceContainer + property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh + + property color error: currentThemeData.error || "#F2B8B5" + property color warning: currentThemeData.warning || "#FF9800" + property color info: currentThemeData.info || "#2196F3" + property color tempWarning: "#ff9933" + property color tempDanger: "#ff5555" + property color success: currentThemeData.success || "#4CAF50" + + property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12) + property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08) + property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16) + property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3) + property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04) + + property color secondaryHover: Qt.rgba(secondary.r, secondary.g, secondary.b, 0.08) + + property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08) + property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12) + property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15) + property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1) + property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2) + property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08) + property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3) + property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06) + property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7) + + property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5) + property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05) + property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08) + property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12) + + property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12) + + property color shadowMedium: Qt.rgba(0, 0, 0, 0.08) + property color shadowStrong: Qt.rgba(0, 0, 0, 0.3) + + property int shorterDuration: 100 + property int shortDuration: 150 + property int mediumDuration: 300 + property int longDuration: 500 + property int extraLongDuration: 1000 + property int standardEasing: Easing.OutCubic + property int emphasizedEasing: Easing.OutQuart + + property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12 + property real spacingXS: 4 + property real spacingS: 8 + property real spacingM: 12 + property real spacingL: 16 + property real spacingXL: 24 + property real fontSizeSmall: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 12 + property real fontSizeMedium: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 14 + property real fontSizeLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 16 + property real fontSizeXLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 20 + property real barHeight: 48 + property real iconSize: 24 + property real iconSizeSmall: 16 + property real iconSizeLarge: 32 + + property real panelTransparency: 0.85 + property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.topBarWidgetTransparency !== undefined ? SettingsData.topBarWidgetTransparency : 0.85 + property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 0.92 + + function switchTheme(themeName, savePrefs = true) { + if (themeName === dynamic) { + currentTheme = dynamic + extractColors() + } else if (themeName === "custom") { + currentTheme = "custom" + if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) { + loadCustomThemeFromFile(SettingsData.customThemeFile) + } + } else { + currentTheme = themeName + } + if (savePrefs && typeof SettingsData !== "undefined") + SettingsData.setTheme(currentTheme) + + generateSystemThemesFromCurrentTheme() + } + + function setLightMode(light, savePrefs = true) { + isLightMode = light + if (savePrefs && typeof SessionData !== "undefined") + SessionData.setLightMode(isLightMode) + PortalService.setLightMode(isLightMode) + generateSystemThemesFromCurrentTheme() + } + + function toggleLightMode(savePrefs = true) { + setLightMode(!isLightMode, savePrefs) + } + + function forceGenerateSystemThemes() { + if (!matugenAvailable) { + if (typeof ToastService !== "undefined") { + ToastService.showWarning("matugen not available - cannot generate system themes") + } + return + } + generateSystemThemesFromCurrentTheme() + } + + function getAvailableThemes() { + return StockThemes.getAllThemeNames() + } + + function getThemeDisplayName(themeName) { + const themeData = StockThemes.getThemeByName(themeName, isLightMode) + return themeData.name + } + + function getThemeColors(themeName) { + if (themeName === "custom" && customThemeData) { + return customThemeData + } + return StockThemes.getThemeByName(themeName, isLightMode) + } + + function loadCustomTheme(themeData) { + if (themeData.dark || themeData.light) { + const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark" + const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light + customThemeData = selectedTheme + } else { + customThemeData = themeData + } + + generateSystemThemesFromCurrentTheme() + } + + function loadCustomThemeFromFile(filePath) { + customThemeFileView.path = filePath + } + + property alias availableThemeNames: root._availableThemeNames + readonly property var _availableThemeNames: StockThemes.getAllThemeNames() + property string currentThemeName: currentTheme + + function popupBackground() { + return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency) + } + + function contentBackground() { + return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency) + } + + function panelBackground() { + return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency) + } + + function widgetBackground() { + return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency) + } + + function getPopupBackgroundAlpha() { + return popupTransparency + } + + function getContentBackgroundAlpha() { + return popupTransparency + } + + function isColorDark(c) { + return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5 + } + + function getBatteryIcon(level, isCharging, batteryAvailable) { + if (!batteryAvailable) + return _getBatteryPowerProfileIcon() + + if (isCharging) { + if (level >= 90) + return "battery_charging_full" + if (level >= 80) + return "battery_charging_90" + if (level >= 60) + return "battery_charging_80" + if (level >= 50) + return "battery_charging_60" + if (level >= 30) + return "battery_charging_50" + if (level >= 20) + return "battery_charging_30" + return "battery_charging_20" + } else { + if (level >= 95) + return "battery_full" + if (level >= 85) + return "battery_6_bar" + if (level >= 70) + return "battery_5_bar" + if (level >= 55) + return "battery_4_bar" + if (level >= 40) + return "battery_3_bar" + if (level >= 25) + return "battery_2_bar" + if (level >= 10) + return "battery_1_bar" + return "battery_alert" + } + } + + function _getBatteryPowerProfileIcon() { + if (typeof PowerProfiles === "undefined") + return "balance" + + switch (PowerProfiles.profile) { + case PowerProfile.PowerSaver: + return "energy_savings_leaf" + case PowerProfile.Performance: + return "rocket_launch" + default: + return "balance" + } + } + + function getPowerProfileIcon(profile) { + switch (profile) { + case PowerProfile.PowerSaver: + return "battery_saver" + case PowerProfile.Balanced: + return "battery_std" + case PowerProfile.Performance: + return "flash_on" + default: + return "settings" + } + } + + function getPowerProfileLabel(profile) { + switch (profile) { + case PowerProfile.PowerSaver: + return "Power Saver" + case PowerProfile.Balanced: + return "Balanced" + case PowerProfile.Performance: + return "Performance" + default: + return profile.charAt(0).toUpperCase() + profile.slice(1) + } + } + + function getPowerProfileDescription(profile) { + switch (profile) { + case PowerProfile.PowerSaver: + return "Extend battery life" + case PowerProfile.Balanced: + return "Balance power and performance" + case PowerProfile.Performance: + return "Prioritize performance" + default: + return "Custom power profile" + } + } + + function extractColors() { + extractionRequested = true + if (matugenAvailable) + fileChecker.running = true + else + matugenCheck.running = true + } + + function onLightModeChanged() { + if (matugenColors && Object.keys(matugenColors).length > 0) { + colorUpdateTrigger++ + } + + if (currentTheme === "custom" && customThemeFileView.path) { + customThemeFileView.reload() + } + + generateSystemThemesFromCurrentTheme() + } + + function setDesiredTheme(kind, value, isLight, iconTheme) { + if (!matugenAvailable) { + console.warn("matugen not available - cannot set system theme") + return + } + + const desired = { + "kind": kind, + "value": value, + "mode": isLight ? "light" : "dark", + "iconTheme": iconTheme || "System Default" + } + + const json = JSON.stringify(desired) + const desiredPath = stateDir + "/matugen.desired.json" + + Quickshell.execDetached(["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`]) + workerRunning = true + systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, "--run"] + systemThemeGenerator.running = true + } + + function generateSystemThemesFromCurrentTheme() { + if (!matugenAvailable) + return + + const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) + const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default" + + if (currentTheme === dynamic) { + if (!wallpaperPath) { + return + } + if (wallpaperPath.startsWith("#")) { + setDesiredTheme("hex", wallpaperPath, isLight, iconTheme) + } else { + setDesiredTheme("image", wallpaperPath, isLight, iconTheme) + } + } else { + let primaryColor + if (currentTheme === "custom") { + if (!customThemeData || !customThemeData.primary) { + console.warn("Custom theme data not available for system theme generation") + return + } + primaryColor = customThemeData.primary + } else { + primaryColor = currentThemeData.primary + } + + if (!primaryColor) { + console.warn("No primary color available for theme:", currentTheme) + return + } + setDesiredTheme("hex", primaryColor, isLight, iconTheme) + } + } + + function applyGtkColors() { + if (!matugenAvailable) { + if (typeof ToastService !== "undefined") { + ToastService.showError("matugen not available - cannot apply GTK colors") + } + return + } + + const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false" + gtkApplier.command = [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir] + gtkApplier.running = true + } + + function applyQtColors() { + if (!matugenAvailable) { + if (typeof ToastService !== "undefined") { + ToastService.showError("matugen not available - cannot apply Qt colors") + } + return + } + + qtApplier.command = [shellDir + "/scripts/qt.sh", configDir] + qtApplier.running = true + } + + function extractJsonFromText(text) { + if (!text) + return null + + const start = text.search(/[{\[]/) + if (start === -1) + return null + + const open = text[start] + const pairs = { + "{": '}', + "[": ']' + } + const close = pairs[open] + if (!close) + return null + + let inString = false + let escape = false + const stack = [open] + + for (var i = start + 1; i < text.length; i++) { + const ch = text[i] + + if (inString) { + if (escape) { + escape = false + } else if (ch === '\\') { + escape = true + } else if (ch === '"') { + inString = false + } + continue + } + + if (ch === '"') { + inString = true + continue + } + if (ch === '{' || ch === '[') { + stack.push(ch) + continue + } + if (ch === '}' || ch === ']') { + const last = stack.pop() + if (!last || pairs[last] !== ch) { + return null + } + if (stack.length === 0) { + return text.slice(start, i + 1) + } + } + } + return null + } + + Process { + id: matugenCheck + command: ["which", "matugen"] + onExited: code => { + matugenAvailable = (code === 0) + if (!matugenAvailable) { + if (typeof ToastService !== "undefined") { + ToastService.wallpaperErrorStatus = "matugen_missing" + ToastService.showWarning("matugen not found - dynamic theming disabled") + } + return + } + if (extractionRequested) { + fileChecker.running = true + } + + const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) + const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default" + + if (currentTheme === dynamic) { + if (wallpaperPath) { + Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"]) + if (wallpaperPath.startsWith("#")) { + setDesiredTheme("hex", wallpaperPath, isLight, iconTheme) + } else { + setDesiredTheme("image", wallpaperPath, isLight, iconTheme) + } + } + } else { + let primaryColor + if (currentTheme === "custom") { + if (customThemeData && customThemeData.primary) { + primaryColor = customThemeData.primary + } + } else { + primaryColor = currentThemeData.primary + } + + if (primaryColor) { + Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"]) + setDesiredTheme("hex", primaryColor, isLight, iconTheme) + } + } + } + } + + Process { + id: fileChecker + command: ["test", "-r", wallpaperPath] + onExited: code => { + if (code === 0) { + matugenProcess.running = true + } else if (wallpaperPath.startsWith("#")) { + colorMatugenProcess.running = true + } + } + } + + Process { + id: matugenProcess + command: ["matugen", "image", wallpaperPath, "--json", "hex"] + + stdout: StdioCollector { + id: matugenCollector + onStreamFinished: { + if (!matugenCollector.text) { + if (typeof ToastService !== "undefined") { + ToastService.wallpaperErrorStatus = "error" + ToastService.showError("Wallpaper Processing Failed: Empty JSON extracted from matugen output.") + } + return + } + const extractedJson = extractJsonFromText(matugenCollector.text) + if (!extractedJson) { + if (typeof ToastService !== "undefined") { + ToastService.wallpaperErrorStatus = "error" + ToastService.showError("Wallpaper Processing Failed: Invalid JSON extracted from matugen output.") + } + console.log("Raw matugen output:", matugenCollector.text) + return + } + try { + root.matugenColors = JSON.parse(extractedJson) + root.colorUpdateTrigger++ + if (typeof ToastService !== "undefined") { + ToastService.clearWallpaperError() + } + } catch (e) { + if (typeof ToastService !== "undefined") { + ToastService.wallpaperErrorStatus = "error" + ToastService.showError("Wallpaper processing failed (JSON parse error after extraction)") + } + } + } + } + + onExited: code => { + if (code !== 0) { + if (typeof ToastService !== "undefined") { + ToastService.wallpaperErrorStatus = "error" + ToastService.showError("Matugen command failed with exit code " + code) + } + } + } + } + + Process { + id: colorMatugenProcess + command: ["matugen", "color", "hex", wallpaperPath, "--json", "hex"] + + stdout: StdioCollector { + id: colorMatugenCollector + onStreamFinished: { + if (!colorMatugenCollector.text) { + if (typeof ToastService !== "undefined") { + ToastService.wallpaperErrorStatus = "error" + ToastService.showError("Color Processing Failed: Empty JSON extracted from matugen output.") + } + return + } + const extractedJson = extractJsonFromText(colorMatugenCollector.text) + if (!extractedJson) { + if (typeof ToastService !== "undefined") { + ToastService.wallpaperErrorStatus = "error" + ToastService.showError("Color Processing Failed: Invalid JSON extracted from matugen output.") + } + console.log("Raw matugen output:", colorMatugenCollector.text) + return + } + try { + root.matugenColors = JSON.parse(extractedJson) + root.colorUpdateTrigger++ + if (typeof ToastService !== "undefined") { + ToastService.clearWallpaperError() + } + } catch (e) { + if (typeof ToastService !== "undefined") { + ToastService.wallpaperErrorStatus = "error" + ToastService.showError("Color processing failed (JSON parse error after extraction)") + } + } + } + } + + onExited: code => { + if (code !== 0) { + if (typeof ToastService !== "undefined") { + ToastService.wallpaperErrorStatus = "error" + ToastService.showError("Matugen color command failed with exit code " + code) + } + } + } + } + + Process { + id: ensureStateDir + } + + Process { + id: systemThemeGenerator + running: false + + onExited: exitCode => { + workerRunning = false + + if (exitCode === 2) { + // Exit code 2 means wallpaper/color not found - this is expected on first run + console.log("Theme worker: wallpaper/color not found, skipping theme generation") + } else if (exitCode !== 0) { + if (typeof ToastService !== "undefined") { + ToastService.showError("Theme worker failed (" + exitCode + ")") + } + console.warn("Theme worker failed with exit code:", exitCode) + } + } + } + + Process { + id: gtkApplier + running: false + + stdout: StdioCollector { + id: gtkStdout + } + + stderr: StdioCollector { + id: gtkStderr + } + + onExited: exitCode => { + if (exitCode === 0) { + if (typeof ToastService !== "undefined") { + ToastService.showInfo("GTK colors applied successfully") + } + } else { + if (typeof ToastService !== "undefined") { + ToastService.showError("Failed to apply GTK colors: " + gtkStderr.text) + } + } + } + } + + Process { + id: qtApplier + running: false + + stdout: StdioCollector { + id: qtStdout + } + + stderr: StdioCollector { + id: qtStderr + } + + onExited: exitCode => { + if (exitCode === 0) { + if (typeof ToastService !== "undefined") { + ToastService.showInfo("Qt colors applied successfully") + } + } else { + if (typeof ToastService !== "undefined") { + ToastService.showError("Failed to apply Qt colors: " + qtStderr.text) + } + } + } + } + + FileView { + id: customThemeFileView + watchChanges: currentTheme === "custom" + + function parseAndLoadTheme() { + try { + var themeData = JSON.parse(customThemeFileView.text()) + loadCustomTheme(themeData) + } catch (e) { + ToastService.showError("Invalid JSON format: " + e.message) + } + } + + onLoaded: { + parseAndLoadTheme() + } + + onFileChanged: { + customThemeFileView.reload() + } + + onLoadFailed: function (error) { + if (typeof ToastService !== "undefined") { + ToastService.showError("Failed to read theme file: " + error) + } + } + } + + IpcHandler { + target: "theme" + + function toggle(): string { + root.toggleLightMode() + return root.isLightMode ? "light" : "dark" + } + + function light(): string { + root.setLightMode(true) + return "light" + } + + function dark(): string { + root.setLightMode(false) + return "dark" + } + + function getMode(): string { + return root.isLightMode ? "light" : "dark" + } + } +} diff --git a/quickshell/.config/quickshell/Common/fzf.js b/quickshell/.config/quickshell/Common/fzf.js new file mode 100644 index 0000000..43bf341 --- /dev/null +++ b/quickshell/.config/quickshell/Common/fzf.js @@ -0,0 +1,1307 @@ +.pragma library + +/* +https://github.com/ajitid/fzf-for-js + +BSD 3-Clause License + +Copyright (c) 2021, Ajit +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +const normalized = { + 216: "O", + 223: "s", + 248: "o", + 273: "d", + 295: "h", + 305: "i", + 320: "l", + 322: "l", + 359: "t", + 383: "s", + 384: "b", + 385: "B", + 387: "b", + 390: "O", + 392: "c", + 393: "D", + 394: "D", + 396: "d", + 398: "E", + 400: "E", + 402: "f", + 403: "G", + 407: "I", + 409: "k", + 410: "l", + 412: "M", + 413: "N", + 414: "n", + 415: "O", + 421: "p", + 427: "t", + 429: "t", + 430: "T", + 434: "V", + 436: "y", + 438: "z", + 477: "e", + 485: "g", + 544: "N", + 545: "d", + 549: "z", + 564: "l", + 565: "n", + 566: "t", + 567: "j", + 570: "A", + 571: "C", + 572: "c", + 573: "L", + 574: "T", + 575: "s", + 576: "z", + 579: "B", + 580: "U", + 581: "V", + 582: "E", + 583: "e", + 584: "J", + 585: "j", + 586: "Q", + 587: "q", + 588: "R", + 589: "r", + 590: "Y", + 591: "y", + 592: "a", + 593: "a", + 595: "b", + 596: "o", + 597: "c", + 598: "d", + 599: "d", + 600: "e", + 603: "e", + 604: "e", + 605: "e", + 606: "e", + 607: "j", + 608: "g", + 609: "g", + 610: "G", + 613: "h", + 614: "h", + 616: "i", + 618: "I", + 619: "l", + 620: "l", + 621: "l", + 623: "m", + 624: "m", + 625: "m", + 626: "n", + 627: "n", + 628: "N", + 629: "o", + 633: "r", + 634: "r", + 635: "r", + 636: "r", + 637: "r", + 638: "r", + 639: "r", + 640: "R", + 641: "R", + 642: "s", + 647: "t", + 648: "t", + 649: "u", + 651: "v", + 652: "v", + 653: "w", + 654: "y", + 655: "Y", + 656: "z", + 657: "z", + 663: "c", + 665: "B", + 666: "e", + 667: "G", + 668: "H", + 669: "j", + 670: "k", + 671: "L", + 672: "q", + 686: "h", + 867: "a", + 868: "e", + 869: "i", + 870: "o", + 871: "u", + 872: "c", + 873: "d", + 874: "h", + 875: "m", + 876: "r", + 877: "t", + 878: "v", + 879: "x", + 7424: "A", + 7427: "B", + 7428: "C", + 7429: "D", + 7431: "E", + 7432: "e", + 7433: "i", + 7434: "J", + 7435: "K", + 7436: "L", + 7437: "M", + 7438: "N", + 7439: "O", + 7440: "O", + 7441: "o", + 7442: "o", + 7443: "o", + 7446: "o", + 7447: "o", + 7448: "P", + 7449: "R", + 7450: "R", + 7451: "T", + 7452: "U", + 7453: "u", + 7454: "u", + 7455: "m", + 7456: "V", + 7457: "W", + 7458: "Z", + 7522: "i", + 7523: "r", + 7524: "u", + 7525: "v", + 7834: "a", + 7835: "s", + 8305: "i", + 8341: "h", + 8342: "k", + 8343: "l", + 8344: "m", + 8345: "n", + 8346: "p", + 8347: "s", + 8348: "t", + 8580: "c" +}; +for (let i = "\u0300".codePointAt(0); i <= "\u036F".codePointAt(0); ++i) { + const diacritic = String.fromCodePoint(i); + for (const asciiChar of "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") { + const withDiacritic = (asciiChar + diacritic).normalize(); + const withDiacriticCodePoint = withDiacritic.codePointAt(0); + if (withDiacriticCodePoint > 126) { + normalized[withDiacriticCodePoint] = asciiChar; + } + } +} +const ranges = { + a: [7844, 7863], + e: [7870, 7879], + o: [7888, 7907], + u: [7912, 7921] +}; +for (const lowerChar of Object.keys(ranges)) { + const upperChar = lowerChar.toUpperCase(); + for (let i = ranges[lowerChar][0]; i <= ranges[lowerChar][1]; ++i) { + normalized[i] = i % 2 === 0 ? upperChar : lowerChar; + } +} +function normalizeRune(rune) { + if (rune < 192 || rune > 8580) { + return rune; + } + const normalizedChar = normalized[rune]; + if (normalizedChar !== void 0) + return normalizedChar.codePointAt(0); + return rune; +} +function toShort(number) { + return number; +} +function toInt(number) { + return number; +} +function maxInt16(num1, num2) { + return num1 > num2 ? num1 : num2; +} +const strToRunes = (str) => str.split("").map((s) => s.codePointAt(0)); +const runesToStr = (runes) => runes.map((r) => String.fromCodePoint(r)).join(""); +const whitespaceRunes = new Set( + " \f\n\r \v\xA0\u1680\u2028\u2029\u202F\u205F\u3000\uFEFF".split("").map((v) => v.codePointAt(0)) +); +for (let codePoint = "\u2000".codePointAt(0); codePoint <= "\u200A".codePointAt(0); codePoint++) { + whitespaceRunes.add(codePoint); +} +const isWhitespace = (rune) => whitespaceRunes.has(rune); +const whitespacesAtStart = (runes) => { + let whitespaces = 0; + for (const rune of runes) { + if (isWhitespace(rune)) + whitespaces++; + else + break; + } + return whitespaces; +}; +const whitespacesAtEnd = (runes) => { + let whitespaces = 0; + for (let i = runes.length - 1; i >= 0; i--) { + if (isWhitespace(runes[i])) + whitespaces++; + else + break; + } + return whitespaces; +}; +const MAX_ASCII = "\x7F".codePointAt(0); +const CAPITAL_A_RUNE = "A".codePointAt(0); +const CAPITAL_Z_RUNE = "Z".codePointAt(0); +const SMALL_A_RUNE = "a".codePointAt(0); +const SMALL_Z_RUNE = "z".codePointAt(0); +const NUMERAL_ZERO_RUNE = "0".codePointAt(0); +const NUMERAL_NINE_RUNE = "9".codePointAt(0); +function indexAt(index, max, forward) { + if (forward) { + return index; + } + return max - index - 1; +} +const SCORE_MATCH = 16, SCORE_GAP_START = -3, SCORE_GAP_EXTENTION = -1, BONUS_BOUNDARY = SCORE_MATCH / 2, BONUS_NON_WORD = SCORE_MATCH / 2, BONUS_CAMEL_123 = BONUS_BOUNDARY + SCORE_GAP_EXTENTION, BONUS_CONSECUTIVE = -(SCORE_GAP_START + SCORE_GAP_EXTENTION), BONUS_FIRST_CHAR_MULTIPLIER = 2; +function createPosSet(withPos) { + if (withPos) { + return /* @__PURE__ */ new Set(); + } + return null; +} +function alloc16(offset, slab2, size) { + if (slab2 !== null && slab2.i16.length > offset + size) { + const subarray = slab2.i16.subarray(offset, offset + size); + return [offset + size, subarray]; + } + return [offset, new Int16Array(size)]; +} +function alloc32(offset, slab2, size) { + if (slab2 !== null && slab2.i32.length > offset + size) { + const subarray = slab2.i32.subarray(offset, offset + size); + return [offset + size, subarray]; + } + return [offset, new Int32Array(size)]; +} +function charClassOfAscii(rune) { + if (rune >= SMALL_A_RUNE && rune <= SMALL_Z_RUNE) { + return 1; + } else if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { + return 2; + } else if (rune >= NUMERAL_ZERO_RUNE && rune <= NUMERAL_NINE_RUNE) { + return 4; + } else { + return 0; + } +} +function charClassOfNonAscii(rune) { + const char = String.fromCodePoint(rune); + if (char !== char.toUpperCase()) { + return 1; + } else if (char !== char.toLowerCase()) { + return 2; + } else if (char.match(/\p{Number}/gu) !== null) { + return 4; + } else if (char.match(/\p{Letter}/gu) !== null) { + return 3; + } + return 0; +} +function charClassOf(rune) { + if (rune <= MAX_ASCII) { + return charClassOfAscii(rune); + } + return charClassOfNonAscii(rune); +} +function bonusFor(prevClass, currClass) { + if (prevClass === 0 && currClass !== 0) { + return BONUS_BOUNDARY; + } else if (prevClass === 1 && currClass === 2 || prevClass !== 4 && currClass === 4) { + return BONUS_CAMEL_123; + } else if (currClass === 0) { + return BONUS_NON_WORD; + } + return 0; +} +function bonusAt(input, idx) { + if (idx === 0) { + return BONUS_BOUNDARY; + } + return bonusFor(charClassOf(input[idx - 1]), charClassOf(input[idx])); +} +function trySkip(input, caseSensitive, char, from) { + let rest = input.slice(from); + let idx = rest.indexOf(char); + if (idx === 0) { + return from; + } + if (!caseSensitive && char >= SMALL_A_RUNE && char <= SMALL_Z_RUNE) { + if (idx > 0) { + rest = rest.slice(0, idx); + } + const uidx = rest.indexOf(char - 32); + if (uidx >= 0) { + idx = uidx; + } + } + if (idx < 0) { + return -1; + } + return from + idx; +} +function isAscii(runes) { + for (const rune of runes) { + if (rune >= 128) { + return false; + } + } + return true; +} +function asciiFuzzyIndex(input, pattern, caseSensitive) { + if (!isAscii(input)) { + return 0; + } + if (!isAscii(pattern)) { + return -1; + } + let firstIdx = 0, idx = 0; + for (let pidx = 0; pidx < pattern.length; pidx++) { + idx = trySkip(input, caseSensitive, pattern[pidx], idx); + if (idx < 0) { + return -1; + } + if (pidx === 0 && idx > 0) { + firstIdx = idx - 1; + } + idx++; + } + return firstIdx; +} +const fuzzyMatchV2 = (caseSensitive, normalize, forward, input, pattern, withPos, slab2) => { + const M = pattern.length; + if (M === 0) { + return [{ start: 0, end: 0, score: 0 }, createPosSet(withPos)]; + } + const N = input.length; + if (slab2 !== null && N * M > slab2.i16.length) { + return fuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos); + } + const idx = asciiFuzzyIndex(input, pattern, caseSensitive); + if (idx < 0) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + let offset16 = 0, offset32 = 0, H0 = null, C0 = null, B = null, F = null; + [offset16, H0] = alloc16(offset16, slab2, N); + [offset16, C0] = alloc16(offset16, slab2, N); + [offset16, B] = alloc16(offset16, slab2, N); + [offset32, F] = alloc32(offset32, slab2, M); + const [, T] = alloc32(offset32, slab2, N); + for (let i = 0; i < T.length; i++) { + T[i] = input[i]; + } + let maxScore = toShort(0), maxScorePos = 0; + let pidx = 0, lastIdx = 0; + const pchar0 = pattern[0]; + let pchar = pattern[0], prevH0 = toShort(0), prevCharClass = 0, inGap = false; + let Tsub = T.subarray(idx); + let H0sub = H0.subarray(idx).subarray(0, Tsub.length), C0sub = C0.subarray(idx).subarray(0, Tsub.length), Bsub = B.subarray(idx).subarray(0, Tsub.length); + for (let [off, char] of Tsub.entries()) { + let charClass = null; + if (char <= MAX_ASCII) { + charClass = charClassOfAscii(char); + if (!caseSensitive && charClass === 2) { + char += 32; + } + } else { + charClass = charClassOfNonAscii(char); + if (!caseSensitive && charClass === 2) { + char = String.fromCodePoint(char).toLowerCase().codePointAt(0); + } + if (normalize) { + char = normalizeRune(char); + } + } + Tsub[off] = char; + const bonus = bonusFor(prevCharClass, charClass); + Bsub[off] = bonus; + prevCharClass = charClass; + if (char === pchar) { + if (pidx < M) { + F[pidx] = toInt(idx + off); + pidx++; + pchar = pattern[Math.min(pidx, M - 1)]; + } + lastIdx = idx + off; + } + if (char === pchar0) { + const score = SCORE_MATCH + bonus * BONUS_FIRST_CHAR_MULTIPLIER; + H0sub[off] = score; + C0sub[off] = 1; + if (M === 1 && (forward && score > maxScore || !forward && score >= maxScore)) { + maxScore = score; + maxScorePos = idx + off; + if (forward && bonus === BONUS_BOUNDARY) { + break; + } + } + inGap = false; + } else { + if (inGap) { + H0sub[off] = maxInt16(prevH0 + SCORE_GAP_EXTENTION, 0); + } else { + H0sub[off] = maxInt16(prevH0 + SCORE_GAP_START, 0); + } + C0sub[off] = 0; + inGap = true; + } + prevH0 = H0sub[off]; + } + if (pidx !== M) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + if (M === 1) { + const result = { + start: maxScorePos, + end: maxScorePos + 1, + score: maxScore + }; + if (!withPos) { + return [result, null]; + } + const pos2 = /* @__PURE__ */ new Set(); + pos2.add(maxScorePos); + return [result, pos2]; + } + const f0 = F[0]; + const width = lastIdx - f0 + 1; + let H = null; + [offset16, H] = alloc16(offset16, slab2, width * M); + { + const toCopy = H0.subarray(f0, lastIdx + 1); + for (const [i, v] of toCopy.entries()) { + H[i] = v; + } + } + let [, C] = alloc16(offset16, slab2, width * M); + { + const toCopy = C0.subarray(f0, lastIdx + 1); + for (const [i, v] of toCopy.entries()) { + C[i] = v; + } + } + const Fsub = F.subarray(1); + const Psub = pattern.slice(1).slice(0, Fsub.length); + for (const [off, f] of Fsub.entries()) { + let inGap2 = false; + const pchar2 = Psub[off], pidx2 = off + 1, row = pidx2 * width, Tsub2 = T.subarray(f, lastIdx + 1), Bsub2 = B.subarray(f).subarray(0, Tsub2.length), Csub = C.subarray(row + f - f0).subarray(0, Tsub2.length), Cdiag = C.subarray(row + f - f0 - 1 - width).subarray(0, Tsub2.length), Hsub = H.subarray(row + f - f0).subarray(0, Tsub2.length), Hdiag = H.subarray(row + f - f0 - 1 - width).subarray(0, Tsub2.length), Hleft = H.subarray(row + f - f0 - 1).subarray(0, Tsub2.length); + Hleft[0] = 0; + for (const [off2, char] of Tsub2.entries()) { + const col = off2 + f; + let s1 = 0, s2 = 0, consecutive = 0; + if (inGap2) { + s2 = Hleft[off2] + SCORE_GAP_EXTENTION; + } else { + s2 = Hleft[off2] + SCORE_GAP_START; + } + if (pchar2 === char) { + s1 = Hdiag[off2] + SCORE_MATCH; + let b = Bsub2[off2]; + consecutive = Cdiag[off2] + 1; + if (b === BONUS_BOUNDARY) { + consecutive = 1; + } else if (consecutive > 1) { + b = maxInt16(b, maxInt16(BONUS_CONSECUTIVE, B[col - consecutive + 1])); + } + if (s1 + b < s2) { + s1 += Bsub2[off2]; + consecutive = 0; + } else { + s1 += b; + } + } + Csub[off2] = consecutive; + inGap2 = s1 < s2; + const score = maxInt16(maxInt16(s1, s2), 0); + if (pidx2 === M - 1 && (forward && score > maxScore || !forward && score >= maxScore)) { + maxScore = score; + maxScorePos = col; + } + Hsub[off2] = score; + } + } + const pos = createPosSet(withPos); + let j = f0; + if (withPos && pos !== null) { + let i = M - 1; + j = maxScorePos; + let preferMatch = true; + while (true) { + const I = i * width, j0 = j - f0, s = H[I + j0]; + let s1 = 0, s2 = 0; + if (i > 0 && j >= F[i]) { + s1 = H[I - width + j0 - 1]; + } + if (j > F[i]) { + s2 = H[I + j0 - 1]; + } + if (s > s1 && (s > s2 || s === s2 && preferMatch)) { + pos.add(j); + if (i === 0) { + break; + } + i--; + } + preferMatch = C[I + j0] > 1 || I + width + j0 + 1 < C.length && C[I + width + j0 + 1] > 0; + j--; + } + } + return [{ start: j, end: maxScorePos + 1, score: maxScore }, pos]; +}; +function calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos) { + let pidx = 0, score = 0, inGap = false, consecutive = 0, firstBonus = toShort(0); + const pos = createPosSet(withPos); + let prevCharClass = 0; + if (sidx > 0) { + prevCharClass = charClassOf(text[sidx - 1]); + } + for (let idx = sidx; idx < eidx; idx++) { + let rune = text[idx]; + const charClass = charClassOf(rune); + if (!caseSensitive) { + if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { + rune += 32; + } else if (rune > MAX_ASCII) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + } + if (normalize) { + rune = normalizeRune(rune); + } + if (rune === pattern[pidx]) { + if (withPos && pos !== null) { + pos.add(idx); + } + score += SCORE_MATCH; + let bonus = bonusFor(prevCharClass, charClass); + if (consecutive === 0) { + firstBonus = bonus; + } else { + if (bonus === BONUS_BOUNDARY) { + firstBonus = bonus; + } + bonus = maxInt16(maxInt16(bonus, firstBonus), BONUS_CONSECUTIVE); + } + if (pidx === 0) { + score += bonus * BONUS_FIRST_CHAR_MULTIPLIER; + } else { + score += bonus; + } + inGap = false; + consecutive++; + pidx++; + } else { + if (inGap) { + score += SCORE_GAP_EXTENTION; + } else { + score += SCORE_GAP_START; + } + inGap = true; + consecutive = 0; + firstBonus = 0; + } + prevCharClass = charClass; + } + return [score, pos]; +} +function fuzzyMatchV1(caseSensitive, normalize, forward, text, pattern, withPos, slab2) { + if (pattern.length === 0) { + return [{ start: 0, end: 0, score: 0 }, null]; + } + if (asciiFuzzyIndex(text, pattern, caseSensitive) < 0) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + let pidx = 0, sidx = -1, eidx = -1; + const lenRunes = text.length; + const lenPattern = pattern.length; + for (let index = 0; index < lenRunes; index++) { + let rune = text[indexAt(index, lenRunes, forward)]; + if (!caseSensitive) { + if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { + rune += 32; + } else if (rune > MAX_ASCII) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + } + if (normalize) { + rune = normalizeRune(rune); + } + const pchar = pattern[indexAt(pidx, lenPattern, forward)]; + if (rune === pchar) { + if (sidx < 0) { + sidx = index; + } + pidx++; + if (pidx === lenPattern) { + eidx = index + 1; + break; + } + } + } + if (sidx >= 0 && eidx >= 0) { + pidx--; + for (let index = eidx - 1; index >= sidx; index--) { + const tidx = indexAt(index, lenRunes, forward); + let rune = text[tidx]; + if (!caseSensitive) { + if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { + rune += 32; + } else if (rune > MAX_ASCII) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + } + const pidx_ = indexAt(pidx, lenPattern, forward); + const pchar = pattern[pidx_]; + if (rune === pchar) { + pidx--; + if (pidx < 0) { + sidx = index; + break; + } + } + } + if (!forward) { + const sidxTemp = sidx; + sidx = lenRunes - eidx; + eidx = lenRunes - sidxTemp; + } + const [score, pos] = calculateScore( + caseSensitive, + normalize, + text, + pattern, + sidx, + eidx, + withPos + ); + return [{ start: sidx, end: eidx, score }, pos]; + } + return [{ start: -1, end: -1, score: 0 }, null]; +}; +const exactMatchNaive = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { + if (pattern.length === 0) { + return [{ start: 0, end: 0, score: 0 }, null]; + } + const lenRunes = text.length; + const lenPattern = pattern.length; + if (lenRunes < lenPattern) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + if (asciiFuzzyIndex(text, pattern, caseSensitive) < 0) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + let pidx = 0; + let bestPos = -1, bonus = toShort(0), bestBonus = toShort(-1); + for (let index = 0; index < lenRunes; index++) { + const index_ = indexAt(index, lenRunes, forward); + let rune = text[index_]; + if (!caseSensitive) { + if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { + rune += 32; + } else if (rune > MAX_ASCII) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + } + if (normalize) { + rune = normalizeRune(rune); + } + const pidx_ = indexAt(pidx, lenPattern, forward); + const pchar = pattern[pidx_]; + if (pchar === rune) { + if (pidx_ === 0) { + bonus = bonusAt(text, index_); + } + pidx++; + if (pidx === lenPattern) { + if (bonus > bestBonus) { + bestPos = index; + bestBonus = bonus; + } + if (bonus === BONUS_BOUNDARY) { + break; + } + index -= pidx - 1; + pidx = 0; + bonus = 0; + } + } else { + index -= pidx; + pidx = 0; + bonus = 0; + } + } + if (bestPos >= 0) { + let sidx = 0, eidx = 0; + if (forward) { + sidx = bestPos - lenPattern + 1; + eidx = bestPos + 1; + } else { + sidx = lenRunes - (bestPos + 1); + eidx = lenRunes - (bestPos - lenPattern + 1); + } + const [score] = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false); + return [{ start: sidx, end: eidx, score }, null]; + } + return [{ start: -1, end: -1, score: 0 }, null]; +}; +const prefixMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { + if (pattern.length === 0) { + return [{ start: 0, end: 0, score: 0 }, null]; + } + let trimmedLen = 0; + if (!isWhitespace(pattern[0])) { + trimmedLen = whitespacesAtStart(text); + } + if (text.length - trimmedLen < pattern.length) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + for (const [index, r] of pattern.entries()) { + let rune = text[trimmedLen + index]; + if (!caseSensitive) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + if (normalize) { + rune = normalizeRune(rune); + } + if (rune !== r) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + } + const lenPattern = pattern.length; + const [score] = calculateScore( + caseSensitive, + normalize, + text, + pattern, + trimmedLen, + trimmedLen + lenPattern, + false + ); + return [{ start: trimmedLen, end: trimmedLen + lenPattern, score }, null]; +}; +const suffixMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { + const lenRunes = text.length; + let trimmedLen = lenRunes; + if (pattern.length === 0 || !isWhitespace(pattern[pattern.length - 1])) { + trimmedLen -= whitespacesAtEnd(text); + } + if (pattern.length === 0) { + return [{ start: trimmedLen, end: trimmedLen, score: 0 }, null]; + } + const diff = trimmedLen - pattern.length; + if (diff < 0) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + for (const [index, r] of pattern.entries()) { + let rune = text[index + diff]; + if (!caseSensitive) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + if (normalize) { + rune = normalizeRune(rune); + } + if (rune !== r) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + } + const lenPattern = pattern.length; + const sidx = trimmedLen - lenPattern; + const eidx = trimmedLen; + const [score] = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false); + return [{ start: sidx, end: eidx, score }, null]; +}; +const equalMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { + const lenPattern = pattern.length; + if (lenPattern === 0) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + let trimmedLen = 0; + if (!isWhitespace(pattern[0])) { + trimmedLen = whitespacesAtStart(text); + } + let trimmedEndLen = 0; + if (!isWhitespace(pattern[lenPattern - 1])) { + trimmedEndLen = whitespacesAtEnd(text); + } + if (text.length - trimmedLen - trimmedEndLen != lenPattern) { + return [{ start: -1, end: -1, score: 0 }, null]; + } + let match = true; + if (normalize) { + const runes = text; + for (const [idx, pchar] of pattern.entries()) { + let rune = runes[trimmedLen + idx]; + if (!caseSensitive) { + rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); + } + if (normalizeRune(pchar) !== normalizeRune(rune)) { + match = false; + break; + } + } + } else { + let runesStr = runesToStr(text).substring(trimmedLen, text.length - trimmedEndLen); + if (!caseSensitive) { + runesStr = runesStr.toLowerCase(); + } + match = runesStr === runesToStr(pattern); + } + if (match) { + return [ + { + start: trimmedLen, + end: trimmedLen + lenPattern, + score: (SCORE_MATCH + BONUS_BOUNDARY) * lenPattern + (BONUS_FIRST_CHAR_MULTIPLIER - 1) * BONUS_BOUNDARY + }, + null + ]; + } + return [{ start: -1, end: -1, score: 0 }, null]; +}; +const SLAB_16_SIZE = 100 * 1024; +const SLAB_32_SIZE = 2048; +function makeSlab(size16, size32) { + return { + i16: new Int16Array(size16), + i32: new Int32Array(size32) + }; +} +const slab = makeSlab(SLAB_16_SIZE, SLAB_32_SIZE); +var TermType = /* @__PURE__ */ ((TermType2) => { + TermType2[TermType2["Fuzzy"] = 0] = "Fuzzy"; + TermType2[TermType2["Exact"] = 1] = "Exact"; + TermType2[TermType2["Prefix"] = 2] = "Prefix"; + TermType2[TermType2["Suffix"] = 3] = "Suffix"; + TermType2[TermType2["Equal"] = 4] = "Equal"; + return TermType2; +})(TermType || {}); +const termTypeMap = { + [0]: fuzzyMatchV2, + [1]: exactMatchNaive, + [2]: prefixMatch, + [3]: suffixMatch, + [4]: equalMatch +}; +function buildPatternForExtendedMatch(fuzzy, caseMode, normalize, str) { + let cacheable = true; + str = str.trimLeft(); + { + const trimmedAtRightStr = str.trimRight(); + if (trimmedAtRightStr.endsWith("\\") && str[trimmedAtRightStr.length] === " ") { + str = trimmedAtRightStr + " "; + } else { + str = trimmedAtRightStr; + } + } + let sortable = false; + let termSets = []; + termSets = parseTerms(fuzzy, caseMode, normalize, str); + Loop: + for (const termSet of termSets) { + for (const [idx, term] of termSet.entries()) { + if (!term.inv) { + sortable = true; + } + if (!cacheable || idx > 0 || term.inv || fuzzy && term.typ !== 0 || !fuzzy && term.typ !== 1) { + cacheable = false; + if (sortable) { + break Loop; + } + } + } + } + return { + str, + termSets, + sortable, + cacheable, + fuzzy + }; +} +function parseTerms(fuzzy, caseMode, normalize, str) { + str = str.replace(/\\ /g, " "); + const tokens = str.split(/ +/); + const sets = []; + let set = []; + let switchSet = false; + let afterBar = false; + for (const token of tokens) { + let typ = 0, inv = false, text = token.replace(/\t/g, " "); + const lowerText = text.toLowerCase(); + const caseSensitive = caseMode === "case-sensitive" || caseMode === "smart-case" && text !== lowerText; + const normalizeTerm = normalize && lowerText === runesToStr(strToRunes(lowerText).map(normalizeRune)); + if (!caseSensitive) { + text = lowerText; + } + if (!fuzzy) { + typ = 1; + } + if (set.length > 0 && !afterBar && text === "|") { + switchSet = false; + afterBar = true; + continue; + } + afterBar = false; + if (text.startsWith("!")) { + inv = true; + typ = 1; + text = text.substring(1); + } + if (text !== "$" && text.endsWith("$")) { + typ = 3; + text = text.substring(0, text.length - 1); + } + if (text.startsWith("'")) { + if (fuzzy && !inv) { + typ = 1; + } else { + typ = 0; + } + text = text.substring(1); + } else if (text.startsWith("^")) { + if (typ === 3) { + typ = 4; + } else { + typ = 2; + } + text = text.substring(1); + } + if (text.length > 0) { + if (switchSet) { + sets.push(set); + set = []; + } + let textRunes = strToRunes(text); + if (normalizeTerm) { + textRunes = textRunes.map(normalizeRune); + } + set.push({ + typ, + inv, + text: textRunes, + caseSensitive, + normalize: normalizeTerm + }); + switchSet = true; + } + } + if (set.length > 0) { + sets.push(set); + } + return sets; +} +const buildPatternForBasicMatch = (query, casing, normalize) => { + let caseSensitive = false; + switch (casing) { + case "smart-case": + if (query.toLowerCase() !== query) { + caseSensitive = true; + } + break; + case "case-sensitive": + caseSensitive = true; + break; + case "case-insensitive": + query = query.toLowerCase(); + caseSensitive = false; + break; + } + let queryRunes = strToRunes(query); + if (normalize) { + queryRunes = queryRunes.map(normalizeRune); + } + return { + queryRunes, + caseSensitive + }; +}; +function iter(algoFn, tokens, caseSensitive, normalize, forward, pattern, slab2) { + for (const part of tokens) { + const [res, pos] = algoFn(caseSensitive, normalize, forward, part.text, pattern, true, slab2); + if (res.start >= 0) { + const sidx = res.start + part.prefixLength; + const eidx = res.end + part.prefixLength; + if (pos !== null) { + const newPos = /* @__PURE__ */ new Set(); + pos.forEach((v) => newPos.add(part.prefixLength + v)); + return [[sidx, eidx], res.score, newPos]; + } + return [[sidx, eidx], res.score, pos]; + } + } + return [[-1, -1], 0, null]; +} +function computeExtendedMatch(text, pattern, fuzzyAlgo, forward) { + const input = [ + { + text, + prefixLength: 0 + } + ]; + const offsets = []; + let totalScore = 0; + const allPos = /* @__PURE__ */ new Set(); + for (const termSet of pattern.termSets) { + let offset = [0, 0]; + let currentScore = 0; + let matched = false; + for (const term of termSet) { + let algoFn = termTypeMap[term.typ]; + if (term.typ === TermType.Fuzzy) { + algoFn = fuzzyAlgo; + } + const [off, score, pos] = iter( + algoFn, + input, + term.caseSensitive, + term.normalize, + forward, + term.text, + slab + ); + const sidx = off[0]; + if (sidx >= 0) { + if (term.inv) { + continue; + } + offset = off; + currentScore = score; + matched = true; + if (pos !== null) { + pos.forEach((v) => allPos.add(v)); + } else { + for (let idx = off[0]; idx < off[1]; ++idx) { + allPos.add(idx); + } + } + break; + } else if (term.inv) { + offset = [0, 0]; + currentScore = 0; + matched = true; + continue; + } + } + if (matched) { + offsets.push(offset); + totalScore += currentScore; + } + } + return { offsets, totalScore, allPos }; +} +function getResultFromScoreMap(scoreMap, limit) { + const scoresInDesc = Object.keys(scoreMap).map((v) => parseInt(v, 10)).sort((a, b) => b - a); + let result = []; + for (const score of scoresInDesc) { + result = result.concat(scoreMap[score]); + if (result.length >= limit) { + break; + } + } + return result; +} +function getBasicMatchIter(scoreMap, queryRunes, caseSensitive) { + return (idx) => { + const itemRunes = this.runesList[idx]; + if (queryRunes.length > itemRunes.length) + return; + let [match, positions] = this.algoFn( + caseSensitive, + this.opts.normalize, + this.opts.forward, + itemRunes, + queryRunes, + true, + slab + ); + if (match.start === -1) + return; + if (this.opts.fuzzy === false) { + positions = /* @__PURE__ */ new Set(); + for (let position = match.start; position < match.end; ++position) { + positions.add(position); + } + } + const scoreKey = this.opts.sort ? match.score : 0; + if (scoreMap[scoreKey] === void 0) { + scoreMap[scoreKey] = []; + } + scoreMap[scoreKey].push(Object.assign({ + item: this.items[idx], + positions: positions != null ? positions : /* @__PURE__ */ new Set() + }, match)); + }; +} +function getExtendedMatchIter(scoreMap, pattern) { + return (idx) => { + const runes = this.runesList[idx]; + const match = computeExtendedMatch(runes, pattern, this.algoFn, this.opts.forward); + if (match.offsets.length !== pattern.termSets.length) + return; + let sidx = -1, eidx = -1; + if (match.allPos.size > 0) { + sidx = Math.min(...match.allPos); + eidx = Math.max(...match.allPos) + 1; + } + const scoreKey = this.opts.sort ? match.totalScore : 0; + if (scoreMap[scoreKey] === void 0) { + scoreMap[scoreKey] = []; + } + scoreMap[scoreKey].push({ + score: match.totalScore, + item: this.items[idx], + positions: match.allPos, + start: sidx, + end: eidx + }); + }; +} +function basicMatch(query) { + const { queryRunes, caseSensitive } = buildPatternForBasicMatch( + query, + this.opts.casing, + this.opts.normalize + ); + const scoreMap = {}; + const iter2 = getBasicMatchIter.bind(this)( + scoreMap, + queryRunes, + caseSensitive + ); + for (let i = 0, len = this.runesList.length; i < len; ++i) { + iter2(i); + } + return getResultFromScoreMap(scoreMap, this.opts.limit); +} +function extendedMatch(query) { + const pattern = buildPatternForExtendedMatch( + Boolean(this.opts.fuzzy), + this.opts.casing, + this.opts.normalize, + query + ); + const scoreMap = {}; + const iter2 = getExtendedMatchIter.bind(this)(scoreMap, pattern); + for (let i = 0, len = this.runesList.length; i < len; ++i) { + iter2(i); + } + return getResultFromScoreMap(scoreMap, this.opts.limit); +} +const defaultOpts = { + limit: Infinity, + selector: (v) => v, + casing: "smart-case", + normalize: true, + fuzzy: "v2", + tiebreakers: [], + sort: true, + forward: true, + match: basicMatch +}; +class Finder { + constructor(list, ...optionsTuple) { + this.opts = Object.assign(defaultOpts, optionsTuple[0]); + this.items = list; + this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize())); + this.algoFn = exactMatchNaive; + switch (this.opts.fuzzy) { + case "v2": + this.algoFn = fuzzyMatchV2; + break; + case "v1": + this.algoFn = fuzzyMatchV1; + break; + } + } + find(query) { + if (query.length === 0 || this.items.length === 0) + return this.items.slice(0, this.opts.limit).map(createResultItemWithEmptyPos); + query = query.normalize(); + let result = this.opts.match.bind(this)(query); + return postProcessResultItems(result, this.opts); + } +} +function createResultItemWithEmptyPos(item) { + return ({ + item, + start: -1, + end: -1, + score: 0, + positions: /* @__PURE__ */ new Set() + }) +}; +function postProcessResultItems(result, opts) { + if (opts.sort) { + const { selector } = opts; + result.sort((a, b) => { + if (a.score === b.score) { + for (const tiebreaker of opts.tiebreakers) { + const diff = tiebreaker(a, b, selector); + if (diff !== 0) { + return diff; + } + } + } + return 0; + }); + } + if (Number.isFinite(opts.limit)) { + result.splice(opts.limit); + } + return result; +} +function byLengthAsc(a, b, selector) { + return selector(a.item).length - selector(b.item).length; +} +function byStartAsc(a, b) { + return a.start - b.start; +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Common/markdown2html.js b/quickshell/.config/quickshell/Common/markdown2html.js new file mode 100644 index 0000000..0a62b3f --- /dev/null +++ b/quickshell/.config/quickshell/Common/markdown2html.js @@ -0,0 +1,106 @@ +.pragma library +// This exists only beacause I haven't been able to get linkColor to work with MarkdownText +// May not be necessary if that's possible tbh. +function markdownToHtml(text) { + if (!text) return ""; + + // Store code blocks and inline code to protect them from further processing + const codeBlocks = []; + const inlineCode = []; + let blockIndex = 0; + let inlineIndex = 0; + + // First, extract and replace code blocks with placeholders + let html = text.replace(/```([\s\S]*?)```/g, (match, code) => { + // Trim leading and trailing blank lines only + const trimmedCode = code.replace(/^\n+|\n+$/g, ''); + // Escape HTML entities in code + const escapedCode = trimmedCode.replace(/&/g, '&') + .replace(//g, '>'); + codeBlocks.push(`
${escapedCode}
`); + return `\x00CODEBLOCK${blockIndex++}\x00`; + }); + + // Extract and replace inline code + html = html.replace(/`([^`]+)`/g, (match, code) => { + // Escape HTML entities in code + const escapedCode = code.replace(/&/g, '&') + .replace(//g, '>'); + inlineCode.push(`${escapedCode}`); + return `\x00INLINECODE${inlineIndex++}\x00`; + }); + + // Now process everything else + // Escape HTML entities (but not in code blocks) + html = html.replace(/&/g, '&') + .replace(//g, '>'); + + // Headers + html = html.replace(/^### (.*?)$/gm, '

$1

'); + html = html.replace(/^## (.*?)$/gm, '

$1

'); + html = html.replace(/^# (.*?)$/gm, '

$1

'); + + // Bold and italic (order matters!) + html = html.replace(/\*\*\*(.*?)\*\*\*/g, '$1'); + html = html.replace(/\*\*(.*?)\*\*/g, '$1'); + html = html.replace(/\*(.*?)\*/g, '$1'); + html = html.replace(/___(.*?)___/g, '$1'); + html = html.replace(/__(.*?)__/g, '$1'); + html = html.replace(/_(.*?)_/g, '$1'); + + // Links + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // Lists + html = html.replace(/^\* (.*?)$/gm, '
  • $1
  • '); + html = html.replace(/^- (.*?)$/gm, '
  • $1
  • '); + html = html.replace(/^\d+\. (.*?)$/gm, '
  • $1
  • '); + + // Wrap consecutive list items in ul/ol tags + html = html.replace(/(
  • [\s\S]*?<\/li>\s*)+/g, function(match) { + return '
      ' + match + '
    '; + }); + + // Detect plain URLs and wrap them in anchor tags (but not inside existing or markdown links) + html = html.replace(/(^|[^"'>])((https?|file):\/\/[^\s<]+)/g, '$1$2'); + + // Restore code blocks and inline code BEFORE line break processing + html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => { + return codeBlocks[parseInt(index)]; + }); + + html = html.replace(/\x00INLINECODE(\d+)\x00/g, (match, index) => { + return inlineCode[parseInt(index)]; + }); + + // Line breaks (after code blocks are restored) + html = html.replace(/\n\n/g, '

    '); + html = html.replace(/\n/g, '
    '); + + // Wrap in paragraph tags if not already wrapped + if (!html.startsWith('<')) { + html = '

    ' + html + '

    '; + } + + // Clean up the final HTML + // Remove
    tags immediately before block elements + html = html.replace(/\s*
    /g, '
    ');
    +    html = html.replace(/\s*
      /g, '
        '); + html = html.replace(/\s*/g, ''); + + // Remove empty paragraphs + html = html.replace(/

        \s*<\/p>/g, ''); + html = html.replace(/

        \s*\s*<\/p>/g, ''); + + // Remove excessive line breaks + html = html.replace(/(){3,}/g, '

        '); // Max 2 consecutive line breaks + html = html.replace(/(<\/p>)\s*(

        )/g, '$1$2'); // Remove whitespace between paragraphs + + // Remove leading/trailing whitespace + html = html.trim(); + + return html; +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modals/Clipboard/ClipboardConstants.qml b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardConstants.qml new file mode 100644 index 0000000..8850b95 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardConstants.qml @@ -0,0 +1,19 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + id: root + readonly property int previewLength: 100 + readonly property int longTextThreshold: 200 + readonly property int modalWidth: 650 + readonly property int modalHeight: 550 + readonly property int itemHeight: 72 + readonly property int thumbnailSize: 48 + readonly property int retryInterval: 50 + readonly property int viewportBuffer: 100 + readonly property int extendedBuffer: 200 + readonly property int keyboardHintsHeight: 80 + readonly property int headerHeight: 40 +} diff --git a/quickshell/.config/quickshell/Modals/Clipboard/ClipboardContent.qml b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardContent.qml new file mode 100644 index 0000000..8c5d196 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardContent.qml @@ -0,0 +1,169 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modals.Clipboard + +Item { + id: clipboardContent + + required property var modal + required property var filteredModel + required property var clearConfirmDialog + + property alias searchField: searchField + property alias clipboardListView: clipboardListView + + anchors.fill: parent + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + focus: false + + // Header + ClipboardHeader { + id: header + width: parent.width + totalCount: modal.totalCount + showKeyboardHints: modal.showKeyboardHints + onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints + onClearAllClicked: { + clearConfirmDialog.show("Clear All History?", "This will permanently delete all clipboard history.", function () { + modal.clearAll() + modal.hide() + }, function () {}) + } + onCloseClicked: modal.hide() + } + + // Search Field + DankTextField { + id: searchField + width: parent.width + placeholderText: "" + leftIconName: "search" + showClearButton: true + focus: true + ignoreLeftRightKeys: true + keyForwardTargets: [modal.modalFocusScope] + onTextChanged: { + modal.searchText = text + modal.updateFilteredModel() + } + Keys.onEscapePressed: function (event) { + modal.hide() + event.accepted = true + } + Component.onCompleted: { + Qt.callLater(function () { + forceActiveFocus() + }) + } + + Connections { + target: modal + function onOpened() { + Qt.callLater(function () { + searchField.forceActiveFocus() + }) + } + } + } + + // List Container + Rectangle { + width: parent.width + height: parent.height - ClipboardConstants.headerHeight - 70 + radius: Theme.cornerRadius + color: Theme.surfaceLight + border.color: Theme.outlineLight + border.width: 1 + clip: true + + DankListView { + id: clipboardListView + anchors.fill: parent + anchors.margins: Theme.spacingS + model: filteredModel + + currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0 + spacing: Theme.spacingXS + interactive: true + flickDeceleration: 1500 + maximumFlickVelocity: 2000 + boundsBehavior: Flickable.DragAndOvershootBounds + boundsMovement: Flickable.FollowBoundsBehavior + pressDelay: 0 + flickableDirection: Flickable.VerticalFlick + + function ensureVisible(index) { + if (index < 0 || index >= count) { + return + } + const itemHeight = ClipboardConstants.itemHeight + spacing + const itemY = index * itemHeight + const itemBottom = itemY + itemHeight + if (itemY < contentY) { + contentY = itemY + } else if (itemBottom > contentY + height) { + contentY = itemBottom - height + } + } + + onCurrentIndexChanged: { + if (clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && currentIndex >= 0) { + ensureVisible(currentIndex) + } + } + + StyledText { + text: "No clipboard entries found" + anchors.centerIn: parent + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + visible: filteredModel.count === 0 + } + + delegate: ClipboardEntry { + required property int index + required property var model + + width: clipboardListView.width + height: ClipboardConstants.itemHeight + entryData: model.entry + entryIndex: index + 1 + itemIndex: index + isSelected: clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex + modal: clipboardContent.modal + listView: clipboardListView + onCopyRequested: clipboardContent.modal.copyEntry(model.entry) + onDeleteRequested: clipboardContent.modal.deleteEntry(model.entry) + } + } + } + + // Spacer for keyboard hints + Item { + width: parent.width + height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0 + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + // Keyboard Hints Overlay + ClipboardKeyboardHints { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + visible: modal.showKeyboardHints + } +} diff --git a/quickshell/.config/quickshell/Modals/Clipboard/ClipboardEntry.qml b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardEntry.qml new file mode 100644 index 0000000..0ce5c80 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardEntry.qml @@ -0,0 +1,137 @@ +import QtQuick +import QtQuick.Effects +import Quickshell.Io +import qs.Common +import qs.Widgets +import qs.Modals.Clipboard + +Rectangle { + id: entry + + required property string entryData + required property int entryIndex + required property int itemIndex + required property bool isSelected + required property var modal + required property var listView + + signal copyRequested + signal deleteRequested + + readonly property string entryType: modal ? modal.getEntryType(entryData) : "text" + readonly property string entryPreview: modal ? modal.getEntryPreview(entryData) : entryData + + radius: Theme.cornerRadius + color: { + if (isSelected) { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2) + } + return mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground + } + border.color: { + if (isSelected) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) + } + return Theme.outlineStrong + } + border.width: isSelected ? 1.5 : 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingL + + // Index indicator + Rectangle { + width: 24 + height: 24 + radius: 12 + color: Theme.primarySelected + anchors.verticalCenter: parent.verticalCenter + + StyledText { + anchors.centerIn: parent + text: entryIndex.toString() + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.primary + } + } + + // Content area + Row { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 68 + spacing: Theme.spacingM + + // Thumbnail/Icon + ClipboardThumbnail { + width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize + height: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize + anchors.verticalCenter: parent.verticalCenter + entryData: entry.entryData + entryType: entry.entryType + modal: entry.modal + listView: entry.listView + itemIndex: entry.itemIndex + } + + // Text content + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - (entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize) - Theme.spacingM + spacing: Theme.spacingXS + + StyledText { + text: { + switch (entryType) { + case "image": + return "Image • " + entryPreview + case "long_text": + return "Long Text" + default: + return "Text" + } + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + width: parent.width + elide: Text.ElideRight + } + + StyledText { + text: entryPreview + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + maximumLineCount: entryType === "long_text" ? 3 : 1 + elide: Text.ElideRight + } + } + } + } + + // Delete button + DankActionButton { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + iconName: "close" + iconSize: Theme.iconSize - 6 + iconColor: Theme.surfaceText + onClicked: deleteRequested() + } + + // Click area + MouseArea { + id: mouseArea + anchors.fill: parent + anchors.rightMargin: 40 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: copyRequested() + } +} diff --git a/quickshell/.config/quickshell/Modals/Clipboard/ClipboardHeader.qml b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardHeader.qml new file mode 100644 index 0000000..9c238c7 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardHeader.qml @@ -0,0 +1,65 @@ +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modals.Clipboard + +Item { + id: header + + property int totalCount: 0 + property bool showKeyboardHints: false + + signal keyboardHintsToggled + signal clearAllClicked + signal closeClicked + + height: ClipboardConstants.headerHeight + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "content_paste" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: `Clipboard History (${totalCount})` + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankActionButton { + iconName: "info" + iconSize: Theme.iconSize - 4 + iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText + onClicked: keyboardHintsToggled() + } + + DankActionButton { + iconName: "delete_sweep" + iconSize: Theme.iconSize + iconColor: Theme.surfaceText + onClicked: clearAllClicked() + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: closeClicked() + } + } +} diff --git a/quickshell/.config/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml new file mode 100644 index 0000000..d6477ea --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml @@ -0,0 +1,216 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Modals.Common +import qs.Services +import qs.Widgets + +DankModal { + id: clipboardHistoryModal + + property int totalCount: 0 + property var clipboardEntries: [] + property string searchText: "" + property int selectedIndex: 0 + property bool keyboardNavigationActive: false + property bool showKeyboardHints: false + property Component clipboardContent + property int activeImageLoads: 0 + readonly property int maxConcurrentLoads: 3 + + function updateFilteredModel() { + filteredClipboardModel.clear() + for (var i = 0; i < clipboardModel.count; i++) { + const entry = clipboardModel.get(i).entry + if (searchText.trim().length === 0) { + filteredClipboardModel.append({ + "entry": entry + }) + } else { + const content = getEntryPreview(entry).toLowerCase() + if (content.includes(searchText.toLowerCase())) { + filteredClipboardModel.append({ + "entry": entry + }) + } + } + } + clipboardHistoryModal.totalCount = filteredClipboardModel.count + if (filteredClipboardModel.count === 0) { + keyboardNavigationActive = false + selectedIndex = 0 + } else if (selectedIndex >= filteredClipboardModel.count) { + selectedIndex = filteredClipboardModel.count - 1 + } + } + + function toggle() { + if (shouldBeVisible) { + hide() + } else { + show() + } + } + + function show() { + open() + clipboardHistoryModal.searchText = "" + clipboardHistoryModal.activeImageLoads = 0 + refreshClipboard() + keyboardController.reset() + + Qt.callLater(function () { + if (contentLoader.item && contentLoader.item.searchField) { + contentLoader.item.searchField.text = "" + contentLoader.item.searchField.forceActiveFocus() + } + }) + } + + function hide() { + close() + clipboardHistoryModal.searchText = "" + clipboardHistoryModal.activeImageLoads = 0 + updateFilteredModel() + keyboardController.reset() + cleanupTempFiles() + } + + function cleanupTempFiles() { + Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"]) + } + + function refreshClipboard() { + clipboardProcesses.refresh() + } + + function copyEntry(entry) { + const entryId = entry.split('\t')[0] + Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`]) + ToastService.showInfo("Copied to clipboard") + hide() + } + + function deleteEntry(entry) { + clipboardProcesses.deleteEntry(entry) + } + + function clearAll() { + clipboardProcesses.clearAll() + } + + function getEntryPreview(entry) { + let content = entry.replace(/^\s*\d+\s+/, "") + if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) { + const dimensionMatch = content.match(/(\d+)x(\d+)/) + if (dimensionMatch) { + return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}` + } + const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i) + if (typeMatch) { + return `Image (${typeMatch[1].toUpperCase()})` + } + return "Image" + } + if (content.length > ClipboardConstants.previewLength) { + return content.substring(0, ClipboardConstants.previewLength) + "..." + } + return content + } + + function getEntryType(entry) { + if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) { + return "image" + } + if (entry.length > ClipboardConstants.longTextThreshold) { + return "long_text" + } + return "text" + } + + visible: false + width: ClipboardConstants.modalWidth + height: ClipboardConstants.modalHeight + backgroundColor: Theme.popupBackground() + cornerRadius: Theme.cornerRadius + borderColor: Theme.outlineMedium + borderWidth: 1 + enableShadow: true + onBackgroundClicked: hide() + modalFocusScope.Keys.onPressed: function (event) { + keyboardController.handleKey(event) + } + content: clipboardContent + + ClipboardKeyboardController { + id: keyboardController + modal: clipboardHistoryModal + } + + ConfirmModal { + id: clearConfirmDialog + confirmButtonText: "Clear All" + confirmButtonColor: Theme.primary + onVisibleChanged: { + if (visible) { + clipboardHistoryModal.shouldHaveFocus = false + } else if (clipboardHistoryModal.shouldBeVisible) { + clipboardHistoryModal.shouldHaveFocus = true + clipboardHistoryModal.modalFocusScope.forceActiveFocus() + if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) { + clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus() + } + } + } + } + + property alias filteredClipboardModel: filteredClipboardModel + property alias clipboardModel: clipboardModel + property var confirmDialog: clearConfirmDialog + + ListModel { + id: clipboardModel + } + + ListModel { + id: filteredClipboardModel + } + + ClipboardProcesses { + id: clipboardProcesses + modal: clipboardHistoryModal + clipboardModel: clipboardModel + filteredClipboardModel: filteredClipboardModel + } + + IpcHandler { + function open(): string { + clipboardHistoryModal.show() + return "CLIPBOARD_OPEN_SUCCESS" + } + + function close(): string { + clipboardHistoryModal.hide() + return "CLIPBOARD_CLOSE_SUCCESS" + } + + function toggle(): string { + clipboardHistoryModal.toggle() + return "CLIPBOARD_TOGGLE_SUCCESS" + } + + target: "clipboard" + } + + clipboardContent: Component { + ClipboardContent { + modal: clipboardHistoryModal + filteredModel: filteredClipboardModel + clearConfirmDialog: clipboardHistoryModal.confirmDialog + } + } +} diff --git a/quickshell/.config/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml new file mode 100644 index 0000000..10db17c --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml @@ -0,0 +1,95 @@ +import QtQuick +import qs.Common + +QtObject { + id: keyboardController + + required property var modal + + function reset() { + modal.selectedIndex = 0 + modal.keyboardNavigationActive = false + modal.showKeyboardHints = false + } + + function selectNext() { + if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) { + return + } + modal.keyboardNavigationActive = true + modal.selectedIndex = Math.min(modal.selectedIndex + 1, modal.filteredClipboardModel.count - 1) + } + + function selectPrevious() { + if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) { + return + } + modal.keyboardNavigationActive = true + modal.selectedIndex = Math.max(modal.selectedIndex - 1, 0) + } + + function copySelected() { + if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) { + return + } + const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry + modal.copyEntry(selectedEntry) + } + + function deleteSelected() { + if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) { + return + } + const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry + modal.deleteEntry(selectedEntry) + } + + function handleKey(event) { + if (event.key === Qt.Key_Escape) { + if (modal.keyboardNavigationActive) { + modal.keyboardNavigationActive = false + event.accepted = true + } else { + modal.hide() + event.accepted = true + } + } else if (event.key === Qt.Key_Down) { + if (!modal.keyboardNavigationActive) { + modal.keyboardNavigationActive = true + modal.selectedIndex = 0 + event.accepted = true + } else { + selectNext() + event.accepted = true + } + } else if (event.key === Qt.Key_Up) { + if (!modal.keyboardNavigationActive) { + modal.keyboardNavigationActive = true + modal.selectedIndex = 0 + event.accepted = true + } else if (modal.selectedIndex === 0) { + modal.keyboardNavigationActive = false + event.accepted = true + } else { + selectPrevious() + event.accepted = true + } + } else if (event.key === Qt.Key_Delete && (event.modifiers & Qt.ShiftModifier)) { + modal.clearAll() + modal.hide() + event.accepted = true + } else if (modal.keyboardNavigationActive) { + if ((event.key === Qt.Key_C && (event.modifiers & Qt.ControlModifier)) || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + copySelected() + event.accepted = true + } else if (event.key === Qt.Key_Delete) { + deleteSelected() + event.accepted = true + } + } + if (event.key === Qt.Key_F10) { + modal.showKeyboardHints = !modal.showKeyboardHints + event.accepted = true + } + } +} diff --git a/quickshell/.config/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml new file mode 100644 index 0000000..a7aeb8a --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml @@ -0,0 +1,42 @@ +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modals.Clipboard + +Rectangle { + id: keyboardHints + + height: ClipboardConstants.keyboardHintsHeight + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) + border.color: Theme.primary + border.width: 2 + opacity: visible ? 1 : 0 + z: 100 + + Column { + anchors.centerIn: parent + spacing: 2 + + StyledText { + text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: "Shift+Del: Clear All • Esc: Close" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/quickshell/.config/quickshell/Modals/Clipboard/ClipboardProcesses.qml b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardProcesses.qml new file mode 100644 index 0000000..bb3c24d --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardProcesses.qml @@ -0,0 +1,94 @@ +import QtQuick +import Quickshell.Io + +QtObject { + id: clipboardProcesses + + required property var modal + required property var clipboardModel + required property var filteredClipboardModel + + // Load clipboard entries + property var loadProcess: Process { + id: loadProcess + command: ["cliphist", "list"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + clipboardModel.clear() + const lines = text.trim().split('\n') + for (const line of lines) { + if (line.trim().length > 0) { + clipboardModel.append({ + "entry": line + }) + } + } + modal.updateFilteredModel() + } + } + } + + // Delete single entry + property var deleteProcess: Process { + id: deleteProcess + property string deletedEntry: "" + running: false + + onExited: exitCode => { + if (exitCode === 0) { + for (var i = 0; i < clipboardModel.count; i++) { + if (clipboardModel.get(i).entry === deleteProcess.deletedEntry) { + clipboardModel.remove(i) + break + } + } + for (var j = 0; j < filteredClipboardModel.count; j++) { + if (filteredClipboardModel.get(j).entry === deleteProcess.deletedEntry) { + filteredClipboardModel.remove(j) + break + } + } + modal.totalCount = filteredClipboardModel.count + if (filteredClipboardModel.count === 0) { + modal.keyboardNavigationActive = false + modal.selectedIndex = 0 + } else if (modal.selectedIndex >= filteredClipboardModel.count) { + modal.selectedIndex = filteredClipboardModel.count - 1 + } + } else { + console.warn("Failed to delete clipboard entry") + } + } + } + + // Clear all entries + property var clearProcess: Process { + id: clearProcess + command: ["cliphist", "wipe"] + running: false + + onExited: exitCode => { + if (exitCode === 0) { + clipboardModel.clear() + filteredClipboardModel.clear() + modal.totalCount = 0 + } + } + } + + function refresh() { + loadProcess.running = true + } + + function deleteEntry(entry) { + deleteProcess.deletedEntry = entry + deleteProcess.command = ["sh", "-c", `echo '${entry.replace(/'/g, "'\\''")}' | cliphist delete`] + deleteProcess.running = true + } + + function clearAll() { + clearProcess.running = true + } +} diff --git a/quickshell/.config/quickshell/Modals/Clipboard/ClipboardThumbnail.qml b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardThumbnail.qml new file mode 100644 index 0000000..bbed879 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Clipboard/ClipboardThumbnail.qml @@ -0,0 +1,174 @@ +import QtQuick +import QtQuick.Effects +import Quickshell.Io +import qs.Common +import qs.Widgets +import qs.Modals.Clipboard + +Item { + id: thumbnail + + required property string entryData + required property string entryType + required property var modal + required property var listView + required property int itemIndex + + Image { + id: thumbnailImage + + property string entryId: entryData.split('\t')[0] + property bool isVisible: false + property string cachedImageData: "" + property bool loadQueued: false + + anchors.fill: parent + source: "" + fillMode: Image.PreserveAspectCrop + smooth: true + cache: false + visible: false + asynchronous: true + sourceSize.width: 128 + sourceSize.height: 128 + + onCachedImageDataChanged: { + if (cachedImageData) { + source = "" + source = `data:image/png;base64,${cachedImageData}` + } + } + + function tryLoadImage() { + if (!loadQueued && entryType === "image" && !cachedImageData) { + loadQueued = true + if (modal.activeImageLoads < modal.maxConcurrentLoads) { + modal.activeImageLoads++ + imageLoader.running = true + } else { + retryTimer.restart() + } + } + } + + Timer { + id: retryTimer + interval: ClipboardConstants.retryInterval + onTriggered: { + if (thumbnailImage.loadQueued && !imageLoader.running) { + if (modal.activeImageLoads < modal.maxConcurrentLoads) { + modal.activeImageLoads++ + imageLoader.running = true + } else { + retryTimer.restart() + } + } + } + } + + Component.onCompleted: { + if (entryType !== "image") { + return + } + + // Check if item is visible on screen initially + const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing) + const viewTop = listView.contentY + const viewBottom = viewTop + listView.height + isVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom) + + if (isVisible) { + tryLoadImage() + } + } + + Connections { + target: listView + function onContentYChanged() { + if (entryType !== "image") { + return + } + + const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing) + const viewTop = listView.contentY - ClipboardConstants.viewportBuffer + const viewBottom = viewTop + listView.height + ClipboardConstants.extendedBuffer + const nowVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom) + + if (nowVisible && !thumbnailImage.isVisible) { + thumbnailImage.isVisible = true + thumbnailImage.tryLoadImage() + } + } + } + + Process { + id: imageLoader + running: false + command: ["sh", "-c", `cliphist decode ${thumbnailImage.entryId} | base64 -w 0`] + + stdout: StdioCollector { + onStreamFinished: { + const imageData = text.trim() + if (imageData && imageData.length > 0) { + thumbnailImage.cachedImageData = imageData + } + } + } + + onExited: exitCode => { + thumbnailImage.loadQueued = false + if (modal.activeImageLoads > 0) { + modal.activeImageLoads-- + } + if (exitCode !== 0) { + console.warn("Failed to load clipboard image:", thumbnailImage.entryId) + } + } + } + } + + // Rounded mask effect for images + MultiEffect { + anchors.fill: parent + anchors.margins: 2 + source: thumbnailImage + maskEnabled: true + maskSource: clipboardCircularMask + visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "" + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: clipboardCircularMask + width: ClipboardConstants.thumbnailSize - 4 + height: ClipboardConstants.thumbnailSize - 4 + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "black" + antialiasing: true + } + } + + // Fallback icon + DankIcon { + visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "") + name: { + if (entryType === "image") { + return "image" + } + if (entryType === "long_text") { + return "subject" + } + return "content_copy" + } + size: Theme.iconSize + color: Theme.primary + anchors.centerIn: parent + } +} diff --git a/quickshell/.config/quickshell/Modals/Common/ConfirmModal.qml b/quickshell/.config/quickshell/Modals/Common/ConfirmModal.qml new file mode 100644 index 0000000..9e35bee --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Common/ConfirmModal.qml @@ -0,0 +1,227 @@ +import QtQuick +import qs.Common +import qs.Modals.Common +import qs.Widgets + +DankModal { + id: root + + property string confirmTitle: "" + property string confirmMessage: "" + property string confirmButtonText: "Confirm" + property string cancelButtonText: "Cancel" + property color confirmButtonColor: Theme.primary + property var onConfirm: function () {} + property var onCancel: function () {} + property int selectedButton: -1 + property bool keyboardNavigation: false + + function show(title, message, onConfirmCallback, onCancelCallback) { + confirmTitle = title || "" + confirmMessage = message || "" + confirmButtonText = "Confirm" + cancelButtonText = "Cancel" + confirmButtonColor = Theme.primary + onConfirm = onConfirmCallback || (() => {}) + onCancel = onCancelCallback || (() => {}) + selectedButton = -1 + keyboardNavigation = false + open() + } + + function showWithOptions(options) { + confirmTitle = options.title || "" + confirmMessage = options.message || "" + confirmButtonText = options.confirmText || "Confirm" + cancelButtonText = options.cancelText || "Cancel" + confirmButtonColor = options.confirmColor || Theme.primary + onConfirm = options.onConfirm || (() => {}) + onCancel = options.onCancel || (() => {}) + selectedButton = -1 + keyboardNavigation = false + open() + } + + function selectButton() { + close() + if (selectedButton === 0) { + if (onCancel) { + onCancel() + } + } else { + if (onConfirm) { + onConfirm() + } + } + } + + shouldBeVisible: false + allowStacking: true + width: 350 + height: 160 + enableShadow: true + shouldHaveFocus: true + onBackgroundClicked: { + close() + if (onCancel) { + onCancel() + } + } + onOpened: { + modalFocusScope.forceActiveFocus() + modalFocusScope.focus = true + shouldHaveFocus = true + } + modalFocusScope.Keys.onPressed: function (event) { + switch (event.key) { + case Qt.Key_Escape: + close() + if (onCancel) { + onCancel() + } + event.accepted = true + break + case Qt.Key_Left: + case Qt.Key_Up: + keyboardNavigation = true + selectedButton = 0 + event.accepted = true + break + case Qt.Key_Right: + case Qt.Key_Down: + keyboardNavigation = true + selectedButton = 1 + event.accepted = true + break + case Qt.Key_Tab: + keyboardNavigation = true + selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2 + event.accepted = true + break + case Qt.Key_Return: + case Qt.Key_Enter: + if (selectedButton !== -1) { + selectButton() + } else { + selectedButton = 1 + selectButton() + } + event.accepted = true + break + } + } + + content: Component { + Item { + anchors.fill: parent + + Column { + anchors.centerIn: parent + width: parent.width - Theme.spacingM * 2 + spacing: Theme.spacingM + + StyledText { + text: confirmTitle + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + width: parent.width + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: confirmMessage + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + Item { + height: Theme.spacingS + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + Rectangle { + width: 120 + height: 40 + radius: Theme.cornerRadius + color: { + if (keyboardNavigation && selectedButton === 0) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + } else if (cancelButton.containsMouse) { + return Theme.surfacePressed + } else { + return Theme.surfaceVariantAlpha + } + } + border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent" + border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0 + + StyledText { + text: cancelButtonText + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + id: cancelButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + selectedButton = 0 + selectButton() + } + } + } + + Rectangle { + width: 120 + height: 40 + radius: Theme.cornerRadius + color: { + const baseColor = confirmButtonColor + if (keyboardNavigation && selectedButton === 1) { + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1) + } else if (confirmButton.containsMouse) { + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) + } else { + return baseColor + } + } + border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent" + border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0 + + StyledText { + text: confirmButtonText + font.pixelSize: Theme.fontSizeMedium + color: Theme.primaryText + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + id: confirmButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + selectedButton = 1 + selectButton() + } + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modals/Common/DankModal.qml b/quickshell/.config/quickshell/Modals/Common/DankModal.qml new file mode 100644 index 0000000..025f7e3 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Common/DankModal.qml @@ -0,0 +1,234 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Common + +PanelWindow { + id: root + + WlrLayershell.namespace: "quickshell:modal" + + property alias content: contentLoader.sourceComponent + property alias contentLoader: contentLoader + property real width: 400 + property real height: 300 + readonly property real screenWidth: screen ? screen.width : 1920 + readonly property real screenHeight: screen ? screen.height : 1080 + property bool showBackground: true + property real backgroundOpacity: 0.5 + property string positioning: "center" + property point customPosition: Qt.point(0, 0) + property bool closeOnEscapeKey: true + property bool closeOnBackgroundClick: true + property string animationType: "scale" + property int animationDuration: Theme.shorterDuration + property var animationEasing: Theme.emphasizedEasing + property color backgroundColor: Theme.surfaceContainer + property color borderColor: Theme.outlineMedium + property real borderWidth: 1 + property real cornerRadius: Theme.cornerRadius + property bool enableShadow: false + property alias modalFocusScope: focusScope + property bool shouldBeVisible: false + property bool shouldHaveFocus: shouldBeVisible + property bool allowFocusOverride: false + property bool allowStacking: false + + signal opened + signal dialogClosed + signal backgroundClicked + + function open() { + ModalManager.openModal(root) + closeTimer.stop() + shouldBeVisible = true + visible = true + focusScope.forceActiveFocus() + } + + function close() { + shouldBeVisible = false + closeTimer.restart() + } + + function toggle() { + if (shouldBeVisible) { + close() + } else { + open() + } + } + + visible: shouldBeVisible + color: "transparent" + WlrLayershell.layer: WlrLayershell.Top // if set to overlay -> virtual keyboards can be stuck under modal + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + onVisibleChanged: { + if (root.visible) { + opened() + } else { + if (Qt.inputMethod) { + Qt.inputMethod.hide() + Qt.inputMethod.reset() + } + dialogClosed() + } + } + + Connections { + function onCloseAllModalsExcept(excludedModal) { + if (excludedModal !== root && !allowStacking && shouldBeVisible) { + close() + } + } + + target: ModalManager + } + + Timer { + id: closeTimer + + interval: animationDuration + 50 + onTriggered: { + visible = false + } + } + + anchors { + top: true + left: true + right: true + bottom: true + } + + Rectangle { + id: background + + anchors.fill: parent + color: "black" + opacity: root.showBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 + visible: root.showBackground + + MouseArea { + anchors.fill: parent + enabled: root.closeOnBackgroundClick + onClicked: mouse => { + const localPos = mapToItem(contentContainer, mouse.x, mouse.y) + if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) { + root.backgroundClicked() + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: root.animationDuration + easing.type: root.animationEasing + } + } + } + + Rectangle { + id: contentContainer + + width: root.width + height: root.height + anchors.centerIn: positioning === "center" ? parent : undefined + x: { + if (positioning === "top-right") { + return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL) + } else if (positioning === "custom") { + return root.customPosition.x + } + return 0 + } + y: { + if (positioning === "top-right") { + return Theme.barHeight + Theme.spacingXS + } else if (positioning === "custom") { + return root.customPosition.y + } + return 0 + } + color: root.backgroundColor + radius: root.cornerRadius + border.color: root.borderColor + border.width: root.borderWidth + layer.enabled: root.enableShadow + opacity: root.shouldBeVisible ? 1 : 0 + scale: root.animationType === "scale" ? (root.shouldBeVisible ? 1 : 0.9) : 1 + transform: root.animationType === "slide" ? slideTransform : null + + Translate { + id: slideTransform + + x: root.shouldBeVisible ? 0 : 15 + y: root.shouldBeVisible ? 0 : -30 + } + + Loader { + id: contentLoader + + anchors.fill: parent + active: root.visible + asynchronous: false + } + + Behavior on opacity { + NumberAnimation { + duration: root.animationDuration + easing.type: root.animationEasing + } + } + + Behavior on scale { + enabled: root.animationType === "scale" + + NumberAnimation { + duration: root.animationDuration + easing.type: root.animationEasing + } + } + + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 8 + shadowBlur: 1 + shadowColor: Theme.shadowStrong + shadowOpacity: 0.3 + } + } + + FocusScope { + id: focusScope + + objectName: "modalFocusScope" + anchors.fill: parent + visible: root.visible // Only active when the modal is visible + focus: root.visible + Keys.onEscapePressed: event => { + if (root.closeOnEscapeKey && shouldHaveFocus) { + root.close() + event.accepted = true + } + } + onVisibleChanged: { + if (visible && shouldHaveFocus) { + Qt.callLater(() => focusScope.forceActiveFocus()) + } + } + + Connections { + function onShouldHaveFocusChanged() { + if (shouldHaveFocus && visible) { + Qt.callLater(() => focusScope.forceActiveFocus()) + } + } + + target: root + } + } +} diff --git a/quickshell/.config/quickshell/Modals/FileBrowser/FileBrowserModal.qml b/quickshell/.config/quickshell/Modals/FileBrowser/FileBrowserModal.qml new file mode 100644 index 0000000..c18a543 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/FileBrowser/FileBrowserModal.qml @@ -0,0 +1,818 @@ +import Qt.labs.folderlistmodel +import QtCore +import QtQuick +import QtQuick.Controls +import Quickshell.Io +import qs.Common +import qs.Modals.Common +import qs.Widgets + +DankModal { + id: fileBrowserModal + + property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation) + property string currentPath: "" + property var fileExtensions: ["*.*"] + property alias filterExtensions: fileBrowserModal.fileExtensions + property string browserTitle: "Select File" + property string browserIcon: "folder_open" + property string browserType: "generic" // "wallpaper" or "profile" for last path memory + property bool showHiddenFiles: false + property int selectedIndex: -1 + property bool keyboardNavigationActive: false + property bool backButtonFocused: false + property bool saveMode: false // Enable save functionality + property string defaultFileName: "" // Default filename for save mode + property int keyboardSelectionIndex: -1 + property bool keyboardSelectionRequested: false + property bool showKeyboardHints: false + property bool showFileInfo: false + property string selectedFilePath: "" + property string selectedFileName: "" + property bool selectedFileIsDir: false + property bool showOverwriteConfirmation: false + property string pendingFilePath: "" + + signal fileSelected(string path) + + function isImageFile(fileName) { + if (!fileName) { + return false + } + const ext = fileName.toLowerCase().split('.').pop() + return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext) + } + + function getLastPath() { + const lastPath = browserType === "wallpaper" ? SessionData.wallpaperLastPath : browserType === "profile" ? SessionData.profileLastPath : "" + return (lastPath && lastPath !== "") ? lastPath : homeDir + } + + function saveLastPath(path) { + if (browserType === "wallpaper") { + SessionData.setWallpaperLastPath(path) + } else if (browserType === "profile") { + SessionData.setProfileLastPath(path) + } + } + + function setSelectedFileData(path, name, isDir) { + selectedFilePath = path + selectedFileName = name + selectedFileIsDir = isDir + } + + function navigateUp() { + const path = currentPath + if (path === homeDir) + return + + const lastSlash = path.lastIndexOf('/') + if (lastSlash > 0) { + const newPath = path.substring(0, lastSlash) + if (newPath.length < homeDir.length) { + currentPath = homeDir + saveLastPath(homeDir) + } else { + currentPath = newPath + saveLastPath(newPath) + } + } + } + + function navigateTo(path) { + currentPath = path + saveLastPath(path) + selectedIndex = -1 + backButtonFocused = false + } + + function keyboardFileSelection(index) { + if (index >= 0) { + keyboardSelectionTimer.targetIndex = index + keyboardSelectionTimer.start() + } + } + + function executeKeyboardSelection(index) { + keyboardSelectionIndex = index + keyboardSelectionRequested = true + } + + function handleSaveFile(filePath) { + // Ensure the filePath has the correct file:// protocol format + var normalizedPath = filePath + if (!normalizedPath.startsWith("file://")) { + normalizedPath = "file://" + filePath + } + + // Check if file exists by looking through the folder model + var exists = false + var fileName = filePath.split('/').pop() + + for (var i = 0; i < folderModel.count; i++) { + if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) { + exists = true + break + } + } + + if (exists) { + pendingFilePath = normalizedPath + showOverwriteConfirmation = true + } else { + fileSelected(normalizedPath) + fileBrowserModal.close() + } + } + + objectName: "fileBrowserModal" + allowStacking: true + Component.onCompleted: { + currentPath = getLastPath() + } + width: 800 + height: 600 + enableShadow: true + visible: false + onBackgroundClicked: close() + onOpened: { + modalFocusScope.forceActiveFocus() + } + modalFocusScope.Keys.onPressed: function (event) { + keyboardController.handleKey(event) + } + onVisibleChanged: { + if (visible) { + currentPath = getLastPath() + selectedIndex = -1 + keyboardNavigationActive = false + backButtonFocused = false + } + } + onCurrentPathChanged: { + selectedFilePath = "" + selectedFileName = "" + selectedFileIsDir = false + } + onSelectedIndexChanged: { + if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) { + selectedFilePath = "" + selectedFileName = "" + selectedFileIsDir = false + } + } + + FolderListModel { + id: folderModel + + showDirsFirst: true + showDotAndDotDot: false + showHidden: fileBrowserModal.showHiddenFiles + nameFilters: fileExtensions + showFiles: true + showDirs: true + folder: currentPath ? "file://" + currentPath : "file://" + homeDir + } + + QtObject { + id: keyboardController + + property int totalItems: folderModel.count + property int gridColumns: 5 + + function handleKey(event) { + if (event.key === Qt.Key_Escape) { + close() + event.accepted = true + return + } + // F10 toggles keyboard hints + if (event.key === Qt.Key_F10) { + showKeyboardHints = !showKeyboardHints + event.accepted = true + return + } + // F1 or I key for file information + if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) { + showFileInfo = !showFileInfo + event.accepted = true + return + } + // Alt+Left or Backspace to go back + if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) { + if (currentPath !== homeDir) { + navigateUp() + event.accepted = true + } + return + } + if (!keyboardNavigationActive) { + if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right) { + keyboardNavigationActive = true + if (currentPath !== homeDir) { + backButtonFocused = true + selectedIndex = -1 + } else { + backButtonFocused = false + selectedIndex = 0 + } + event.accepted = true + } + return + } + switch (event.key) { + case Qt.Key_Tab: + if (backButtonFocused) { + backButtonFocused = false + selectedIndex = 0 + } else if (selectedIndex < totalItems - 1) { + selectedIndex++ + } else if (currentPath !== homeDir) { + backButtonFocused = true + selectedIndex = -1 + } else { + selectedIndex = 0 + } + event.accepted = true + break + case Qt.Key_Backtab: + if (backButtonFocused) { + backButtonFocused = false + selectedIndex = totalItems - 1 + } else if (selectedIndex > 0) { + selectedIndex-- + } else if (currentPath !== homeDir) { + backButtonFocused = true + selectedIndex = -1 + } else { + selectedIndex = totalItems - 1 + } + event.accepted = true + break + case Qt.Key_Left: + if (backButtonFocused) + return + + if (selectedIndex > 0) { + selectedIndex-- + } else if (currentPath !== homeDir) { + backButtonFocused = true + selectedIndex = -1 + } + event.accepted = true + break + case Qt.Key_Right: + if (backButtonFocused) { + backButtonFocused = false + selectedIndex = 0 + } else if (selectedIndex < totalItems - 1) { + selectedIndex++ + } + event.accepted = true + break + case Qt.Key_Up: + if (backButtonFocused) { + backButtonFocused = false + // Go to first row, appropriate column + var col = selectedIndex % gridColumns + selectedIndex = Math.min(col, totalItems - 1) + } else if (selectedIndex >= gridColumns) { + // Move up one row + selectedIndex -= gridColumns + } else if (currentPath !== homeDir) { + // At top row, go to back button + backButtonFocused = true + selectedIndex = -1 + } + event.accepted = true + break + case Qt.Key_Down: + if (backButtonFocused) { + backButtonFocused = false + selectedIndex = 0 + } else { + // Move down one row if possible + var newIndex = selectedIndex + gridColumns + if (newIndex < totalItems) { + selectedIndex = newIndex + } else { + // If can't go down a full row, go to last item in the column if exists + var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns + var col = selectedIndex % gridColumns + var targetIndex = lastRowStart + col + if (targetIndex < totalItems && targetIndex > selectedIndex) { + selectedIndex = targetIndex + } + } + } + event.accepted = true + break + case Qt.Key_Return: + case Qt.Key_Enter: + case Qt.Key_Space: + if (backButtonFocused) + navigateUp() + else if (selectedIndex >= 0 && selectedIndex < totalItems) + // Trigger selection by setting the grid's current index and using signal + fileBrowserModal.keyboardFileSelection(selectedIndex) + event.accepted = true + break + } + } + } + + Timer { + id: keyboardSelectionTimer + + property int targetIndex: -1 + + interval: 1 + onTriggered: { + // Access the currently selected item through model role names + // This will work because QML models expose role data + executeKeyboardSelection(targetIndex) + } + } + + content: Component { + Item { + anchors.fill: parent + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Item { + width: parent.width + height: 40 + + Row { + spacing: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: browserIcon + size: Theme.iconSizeLarge + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: browserTitle + font.pixelSize: Theme.fontSizeXLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankActionButton { + circular: false + iconName: "info" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints + } + + DankActionButton { + circular: false + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: fileBrowserModal.close() + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledRect { + width: 32 + height: 32 + radius: Theme.cornerRadius + color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent" + opacity: currentPath !== homeDir ? 1 : 0 + + DankIcon { + anchors.centerIn: parent + name: "arrow_back" + size: Theme.iconSizeSmall + color: Theme.surfaceText + } + + MouseArea { + id: backButtonMouseArea + + anchors.fill: parent + hoverEnabled: currentPath !== homeDir + cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: currentPath !== homeDir + onClicked: navigateUp() + } + } + + StyledText { + text: fileBrowserModal.currentPath.replace("file://", "") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + width: parent.width - 40 - Theme.spacingS + elide: Text.ElideMiddle + anchors.verticalCenter: parent.verticalCenter + maximumLineCount: 1 + wrapMode: Text.NoWrap + } + } + + DankGridView { + id: fileGrid + + width: parent.width + height: parent.height - 80 + clip: true + cellWidth: 150 + cellHeight: 130 + cacheBuffer: 260 + model: folderModel + currentIndex: selectedIndex + onCurrentIndexChanged: { + if (keyboardNavigationActive && currentIndex >= 0) + positionViewAtIndex(currentIndex, GridView.Contain) + } + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AlwaysOff + } + + delegate: StyledRect { + id: delegateRoot + + required property bool fileIsDir + required property string filePath + required property string fileName + required property int index + + width: 140 + height: 120 + radius: Theme.cornerRadius + color: { + if (keyboardNavigationActive && delegateRoot.index === selectedIndex) + return Theme.surfacePressed + + return mouseArea.containsMouse ? Theme.surfaceVariant : "transparent" + } + border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.outline + border.width: (mouseArea.containsMouse || (keyboardNavigationActive && delegateRoot.index === selectedIndex)) ? 1 : 0 + // Update file info when this item gets selected via keyboard or initially + Component.onCompleted: { + if (keyboardNavigationActive && delegateRoot.index === selectedIndex) + setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) + } + + // Watch for selectedIndex changes to update file info during keyboard navigation + Connections { + function onSelectedIndexChanged() { + if (keyboardNavigationActive && selectedIndex === delegateRoot.index) + setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) + } + + target: fileBrowserModal + } + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Item { + width: 80 + height: 60 + anchors.horizontalCenter: parent.horizontalCenter + + CachingImage { + anchors.fill: parent + source: (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : "" + fillMode: Image.PreserveAspectCrop + visible: !delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName) + maxCacheSize: 80 + } + + DankIcon { + anchors.centerIn: parent + name: "description" + size: Theme.iconSizeLarge + color: Theme.primary + visible: !delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName) + } + + DankIcon { + anchors.centerIn: parent + name: "folder" + size: Theme.iconSizeLarge + color: Theme.primary + visible: delegateRoot.fileIsDir + } + } + + StyledText { + text: delegateRoot.fileName || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: 120 + elide: Text.ElideMiddle + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + maximumLineCount: 2 + wrapMode: Text.WordWrap + } + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + // Update selected file info and index first + selectedIndex = delegateRoot.index + setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) + if (delegateRoot.fileIsDir) { + navigateTo(delegateRoot.filePath) + } else { + fileSelected(delegateRoot.filePath) + fileBrowserModal.close() // Close modal after file selection + } + } + } + + // Handle keyboard selection + Connections { + function onKeyboardSelectionRequestedChanged() { + if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === delegateRoot.index) { + fileBrowserModal.keyboardSelectionRequested = false + selectedIndex = delegateRoot.index + setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) + if (delegateRoot.fileIsDir) { + navigateTo(delegateRoot.filePath) + } else { + fileSelected(delegateRoot.filePath) + fileBrowserModal.close() + } + } + } + + target: fileBrowserModal + } + } + } + } + + Row { + id: saveRow + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + height: saveMode ? 40 : 0 + visible: saveMode + spacing: Theme.spacingM + + DankTextField { + id: fileNameInput + + width: parent.width - saveButton.width - Theme.spacingM + height: 40 + text: defaultFileName + placeholderText: "Enter filename..." + ignoreLeftRightKeys: false + focus: saveMode + topPadding: Theme.spacingS + bottomPadding: Theme.spacingS + Component.onCompleted: { + if (saveMode) + Qt.callLater(() => { + forceActiveFocus() + }) + } + onAccepted: { + if (text.trim() !== "") { + // Remove file:// protocol from currentPath if present for proper construction + var basePath = currentPath.replace(/^file:\/\//, '') + var fullPath = basePath + "/" + text.trim() + // Ensure consistent path format - remove any double slashes and normalize + fullPath = fullPath.replace(/\/+/g, '/') + handleSaveFile(fullPath) + } + } + } + + StyledRect { + id: saveButton + + width: 80 + height: 40 + color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant + radius: Theme.cornerRadius + + StyledText { + anchors.centerIn: parent + text: "Save" + color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeMedium + } + + StateLayer { + stateColor: Theme.primary + cornerRadius: Theme.cornerRadius + enabled: fileNameInput.text.trim() !== "" + onClicked: { + if (fileNameInput.text.trim() !== "") { + // Remove file:// protocol from currentPath if present for proper construction + var basePath = currentPath.replace(/^file:\/\//, '') + var fullPath = basePath + "/" + fileNameInput.text.trim() + // Ensure consistent path format - remove any double slashes and normalize + fullPath = fullPath.replace(/\/+/g, '/') + handleSaveFile(fullPath) + } + } + } + } + } + + KeyboardHints { + id: keyboardHints + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + showHints: fileBrowserModal.showKeyboardHints + } + + FileInfo { + id: fileInfo + + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacingL + width: 300 + showFileInfo: fileBrowserModal.showFileInfo + selectedIndex: fileBrowserModal.selectedIndex + sourceFolderModel: folderModel + currentPath: fileBrowserModal.currentPath + currentFileName: fileBrowserModal.selectedFileName + currentFileIsDir: fileBrowserModal.selectedFileIsDir + currentFileExtension: { + if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName) + return "" + + var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.') + return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : "" + } + } + + // Overwrite confirmation dialog + Item { + id: overwriteDialog + anchors.fill: parent + visible: showOverwriteConfirmation + + Keys.onEscapePressed: { + showOverwriteConfirmation = false + pendingFilePath = "" + } + + Keys.onReturnPressed: { + showOverwriteConfirmation = false + fileSelected(pendingFilePath) + pendingFilePath = "" + Qt.callLater(() => fileBrowserModal.close()) + } + + focus: showOverwriteConfirmation + + Rectangle { + anchors.fill: parent + color: Theme.shadowStrong + opacity: 0.8 + + MouseArea { + anchors.fill: parent + onClicked: { + showOverwriteConfirmation = false + pendingFilePath = "" + } + } + } + + StyledRect { + anchors.centerIn: parent + width: 400 + height: 160 + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Theme.outlineMedium + border.width: 1 + + Column { + anchors.centerIn: parent + width: parent.width - Theme.spacingL * 2 + spacing: Theme.spacingM + + StyledText { + text: qsTr("File Already Exists") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: qsTr("A file with this name already exists. Do you want to overwrite it?") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceTextMedium + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + StyledRect { + width: 80 + height: 36 + radius: Theme.cornerRadius + color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + + StyledText { + anchors.centerIn: parent + text: qsTr("Cancel") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + MouseArea { + id: cancelArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + showOverwriteConfirmation = false + pendingFilePath = "" + } + } + } + + StyledRect { + width: 90 + height: 36 + radius: Theme.cornerRadius + color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary + + StyledText { + anchors.centerIn: parent + text: qsTr("Overwrite") + font.pixelSize: Theme.fontSizeMedium + color: Theme.background + font.weight: Font.Medium + } + + MouseArea { + id: overwriteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + showOverwriteConfirmation = false + fileSelected(pendingFilePath) + pendingFilePath = "" + Qt.callLater(() => fileBrowserModal.close()) + } + } + } + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modals/FileBrowser/FileInfo.qml b/quickshell/.config/quickshell/Modals/FileBrowser/FileInfo.qml new file mode 100644 index 0000000..bcf7a6b --- /dev/null +++ b/quickshell/.config/quickshell/Modals/FileBrowser/FileInfo.qml @@ -0,0 +1,237 @@ +import QtQuick +import QtCore +import Quickshell.Io +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool showFileInfo: false + property int selectedIndex: -1 + property var sourceFolderModel: null + property string currentPath: "" + + height: 200 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) + border.color: Theme.secondary + border.width: 2 + opacity: showFileInfo ? 1 : 0 + z: 100 + + onShowFileInfoChanged: { + if (showFileInfo && currentFileName && currentPath) { + const fullPath = currentPath + "/" + currentFileName + fileStatProcess.selectedFilePath = fullPath + fileStatProcess.running = true + } + } + + Process { + id: fileStatProcess + command: ["stat", "-c", "%y|%A|%s|%n", selectedFilePath] + property string selectedFilePath: "" + property var fileStats: null + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + const parts = text.trim().split('|') + if (parts.length >= 4) { + fileStatProcess.fileStats = { + "modifiedTime": parts[0], + "permissions": parts[1], + "size": parseInt(parts[2]) || 0, + "fullPath": parts[3] + } + } + } + } + } + + onExited: function (exitCode) {} + } + + property string currentFileName: "" + property bool currentFileIsDir: false + property string currentFileExtension: "" + + onCurrentFileNameChanged: { + if (showFileInfo && currentFileName && currentPath) { + const fullPath = currentPath + "/" + currentFileName + if (fullPath !== fileStatProcess.selectedFilePath) { + fileStatProcess.selectedFilePath = fullPath + fileStatProcess.running = true + } + } + } + + function updateFileInfo(filePath, fileName, isDirectory) { + if (filePath && filePath !== fileStatProcess.selectedFilePath) { + fileStatProcess.selectedFilePath = filePath + currentFileName = fileName || "" + currentFileIsDir = isDirectory || false + + let ext = "" + if (!isDirectory && fileName) { + const lastDot = fileName.lastIndexOf('.') + if (lastDot > 0) { + ext = fileName.substring(lastDot + 1).toLowerCase() + } + } + currentFileExtension = ext + + if (showFileInfo) { + fileStatProcess.running = true + } + } + } + + readonly property var currentFileDisplayData: { + if (selectedIndex < 0 || !sourceFolderModel) { + return { + "exists": false, + "name": "No selection", + "type": "", + "size": "", + "modified": "", + "permissions": "", + "extension": "", + "position": "N/A" + } + } + + const hasValidFile = currentFileName !== "" + return { + "exists": hasValidFile, + "name": hasValidFile ? currentFileName : "Loading...", + "type": currentFileIsDir ? "Directory" : "File", + "size": fileStatProcess.fileStats ? formatFileSize(fileStatProcess.fileStats.size) : "Calculating...", + "modified": fileStatProcess.fileStats ? formatDateTime(fileStatProcess.fileStats.modifiedTime) : "Loading...", + "permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...", + "extension": currentFileExtension, + "position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A" + } + } + + Column { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: "info" + size: Theme.iconSize + color: Theme.secondary + } + + StyledText { + text: "File Information" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + + StyledText { + text: currentFileDisplayData.name + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideMiddle + wrapMode: Text.NoWrap + font.weight: Font.Medium + } + + StyledText { + text: currentFileDisplayData.type + (currentFileDisplayData.extension ? " (." + currentFileDisplayData.extension + ")" : "") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: parent.width + } + + StyledText { + text: currentFileDisplayData.size + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: parent.width + visible: currentFileDisplayData.exists && !currentFileIsDir + } + + StyledText { + text: currentFileDisplayData.modified + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: parent.width + elide: Text.ElideRight + visible: currentFileDisplayData.exists + } + + StyledText { + text: currentFileDisplayData.permissions + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + visible: currentFileDisplayData.exists + } + + StyledText { + text: currentFileDisplayData.position + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: parent.width + } + } + } + + StyledText { + text: "F1/I: Toggle • F10: Help" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingM + horizontalAlignment: Text.AlignHCenter + } + + function formatFileSize(bytes) { + if (bytes === 0 || !bytes) { + return "0 B" + } + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] + } + + function formatDateTime(dateTimeString) { + if (!dateTimeString) { + return "Unknown" + } + const parts = dateTimeString.split(' ') + if (parts.length >= 2) { + return parts[0] + " " + parts[1].split('.')[0] + } + return dateTimeString + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/quickshell/.config/quickshell/Modals/FileBrowser/KeyboardHints.qml b/quickshell/.config/quickshell/Modals/FileBrowser/KeyboardHints.qml new file mode 100644 index 0000000..2a0a102 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/FileBrowser/KeyboardHints.qml @@ -0,0 +1,50 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool showHints: false + + height: 80 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) + border.color: Theme.primary + border.width: 2 + opacity: showHints ? 1 : 0 + z: 100 + + Column { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingS + spacing: 2 + + StyledText { + text: "Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: "Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/quickshell/.config/quickshell/Modals/NetworkInfoModal.qml b/quickshell/.config/quickshell/Modals/NetworkInfoModal.qml new file mode 100644 index 0000000..d780bb8 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/NetworkInfoModal.qml @@ -0,0 +1,162 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Modals.Common +import qs.Services +import qs.Widgets + +DankModal { + id: root + + property bool networkInfoModalVisible: false + property string networkSSID: "" + property var networkData: null + + function showNetworkInfo(ssid, data) { + networkSSID = ssid + networkData = data + networkInfoModalVisible = true + open() + NetworkService.fetchNetworkInfo(ssid) + } + + function hideDialog() { + networkInfoModalVisible = false + close() + networkSSID = "" + networkData = null + } + + visible: networkInfoModalVisible + width: 600 + height: 500 + enableShadow: true + onBackgroundClicked: hideDialog() + onVisibleChanged: { + if (!visible) { + networkSSID = "" + networkData = null + } + } + + content: Component { + Item { + anchors.fill: parent + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + Row { + width: parent.width + + Column { + width: parent.width - 40 + spacing: Theme.spacingXS + + StyledText { + text: "Network Information" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: `Details for "${networkSSID}"` + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceTextMedium + width: parent.width + elide: Text.ElideRight + } + + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: root.hideDialog() + } + + } + + Rectangle { + id: detailsRect + + width: parent.width + height: parent.height - 140 + radius: Theme.cornerRadius + color: Theme.surfaceHover + border.color: Theme.outlineStrong + border.width: 1 + clip: true + + DankFlickable { + anchors.fill: parent + anchors.margins: Theme.spacingM + contentHeight: detailsText.contentHeight + + StyledText { + id: detailsText + + width: parent.width + text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + wrapMode: Text.WordWrap + } + } + + } + + Item { + width: parent.width + height: 40 + + Rectangle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2) + height: 36 + radius: Theme.cornerRadius + color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary + + StyledText { + id: closeText + + anchors.centerIn: parent + text: "Close" + font.pixelSize: Theme.fontSizeMedium + color: Theme.background + font.weight: Font.Medium + } + + MouseArea { + id: closeArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.hideDialog() + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modals/NotificationModal.qml b/quickshell/.config/quickshell/Modals/NotificationModal.qml new file mode 100644 index 0000000..3867992 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/NotificationModal.qml @@ -0,0 +1,146 @@ +import QtQuick +import Quickshell.Io +import qs.Common +import qs.Modals.Common +import qs.Modules.Notifications.Center +import qs.Services +import qs.Widgets + +DankModal { + id: notificationModal + + property bool notificationModalOpen: false + property var notificationListRef: null + + function show() { + notificationModalOpen = true + NotificationService.onOverlayOpen() + open() + modalKeyboardController.reset() + if (modalKeyboardController && notificationListRef) { + modalKeyboardController.listView = notificationListRef + modalKeyboardController.rebuildFlatNavigation() + + Qt.callLater(() => { + modalKeyboardController.keyboardNavigationActive = true + modalKeyboardController.selectedFlatIndex = 0 + modalKeyboardController.updateSelectedIdFromIndex() + if (notificationListRef) { + notificationListRef.keyboardActive = true + } + modalKeyboardController.selectionVersion++ + modalKeyboardController.ensureVisible() + }) + } + } + + function hide() { + notificationModalOpen = false + NotificationService.onOverlayClose() + close() + modalKeyboardController.reset() + } + + function toggle() { + if (shouldBeVisible) { + hide() + } else { + show() + } + } + + width: 500 + height: 700 + visible: false + onBackgroundClicked: hide() + onShouldBeVisibleChanged: (shouldBeVisible) => { + if (!shouldBeVisible) { + notificationModalOpen = false + modalKeyboardController.reset() + NotificationService.onOverlayClose() + } + } + modalFocusScope.Keys.onPressed: (event) => modalKeyboardController.handleKey(event) + + NotificationKeyboardController { + id: modalKeyboardController + + listView: null + isOpen: notificationModal.notificationModalOpen + onClose: () => notificationModal.hide() + } + + IpcHandler { + function open(): string { + notificationModal.show(); + return "NOTIFICATION_MODAL_OPEN_SUCCESS"; + } + + function close(): string { + notificationModal.hide(); + return "NOTIFICATION_MODAL_CLOSE_SUCCESS"; + } + + function toggle(): string { + notificationModal.toggle(); + return "NOTIFICATION_MODAL_TOGGLE_SUCCESS"; + } + + target: "notifications" + } + + content: Component { + Item { + id: notificationKeyHandler + + anchors.fill: parent + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + NotificationHeader { + id: notificationHeader + + keyboardController: modalKeyboardController + } + + NotificationSettings { + id: notificationSettings + + expanded: notificationHeader.showSettings + } + + KeyboardNavigatedNotificationList { + id: notificationList + + width: parent.width + height: parent.height - y + keyboardController: modalKeyboardController + Component.onCompleted: { + notificationModal.notificationListRef = notificationList + if (modalKeyboardController) { + modalKeyboardController.listView = notificationList + modalKeyboardController.rebuildFlatNavigation() + } + } + } + + } + + NotificationKeyboardHints { + id: keyboardHints + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + showHints: modalKeyboardController.showKeyboardHints + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modals/PowerMenuModal.qml b/quickshell/.config/quickshell/Modals/PowerMenuModal.qml new file mode 100644 index 0000000..53f3069 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/PowerMenuModal.qml @@ -0,0 +1,345 @@ +import QtQuick +import qs.Common +import qs.Modals.Common +import qs.Widgets + +DankModal { + id: root + + property int selectedIndex: 0 + property int optionCount: 4 + + signal powerActionRequested(string action, string title, string message) + + function selectOption() { + close(); + const actions = [{ + "action": "logout", + "title": "Log Out", + "message": "Are you sure you want to log out?" + }, { + "action": "suspend", + "title": "Suspend", + "message": "Are you sure you want to suspend the system?" + }, { + "action": "reboot", + "title": "Reboot", + "message": "Are you sure you want to reboot the system?" + }, { + "action": "poweroff", + "title": "Power Off", + "message": "Are you sure you want to power off the system?" + }]; + const selected = actions[selectedIndex]; + if (selected) { + root.powerActionRequested(selected.action, selected.title, selected.message); + } + + } + + shouldBeVisible: false + width: 320 + height: 300 + enableShadow: true + onBackgroundClicked: () => { + return close(); + } + onOpened: () => { + selectedIndex = 0; + modalFocusScope.forceActiveFocus(); + } + modalFocusScope.Keys.onPressed: (event) => { + switch (event.key) { + case Qt.Key_Up: + selectedIndex = (selectedIndex - 1 + optionCount) % optionCount; + event.accepted = true; + break; + case Qt.Key_Down: + selectedIndex = (selectedIndex + 1) % optionCount; + event.accepted = true; + break; + case Qt.Key_Tab: + selectedIndex = (selectedIndex + 1) % optionCount; + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + selectOption(); + event.accepted = true; + break; + } + } + + content: Component { + Item { + anchors.fill: parent + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + + StyledText { + text: "Power Options" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - 150 + height: 1 + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: () => { + return close(); + } + } + + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Rectangle { + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: { + if (selectedIndex === 0) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); + } else if (logoutArea.containsMouse) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08); + } else { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08); + } + } + border.color: selectedIndex === 0 ? Theme.primary : "transparent" + border.width: selectedIndex === 0 ? 1 : 0 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "logout" + size: Theme.iconSize + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Log Out" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + } + + MouseArea { + id: logoutArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + selectedIndex = 0; + selectOption(); + } + } + + } + + Rectangle { + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: { + if (selectedIndex === 1) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); + } else if (suspendArea.containsMouse) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08); + } else { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08); + } + } + border.color: selectedIndex === 1 ? Theme.primary : "transparent" + border.width: selectedIndex === 1 ? 1 : 0 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "bedtime" + size: Theme.iconSize + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Suspend" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + } + + MouseArea { + id: suspendArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + selectedIndex = 1; + selectOption(); + } + } + + } + + Rectangle { + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: { + if (selectedIndex === 2) { + return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12); + } else if (rebootArea.containsMouse) { + return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08); + } else { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08); + } + } + border.color: selectedIndex === 2 ? Theme.warning : "transparent" + border.width: selectedIndex === 2 ? 1 : 0 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "restart_alt" + size: Theme.iconSize + color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Reboot" + font.pixelSize: Theme.fontSizeMedium + color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + } + + MouseArea { + id: rebootArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + selectedIndex = 2; + selectOption(); + } + } + + } + + Rectangle { + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: { + if (selectedIndex === 3) { + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12); + } else if (powerOffArea.containsMouse) { + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08); + } else { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08); + } + } + border.color: selectedIndex === 3 ? Theme.error : "transparent" + border.width: selectedIndex === 3 ? 1 : 0 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "power_settings_new" + size: Theme.iconSize + color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Power Off" + font.pixelSize: Theme.fontSizeMedium + color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + } + + MouseArea { + id: powerOffArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + selectedIndex = 3; + selectOption(); + } + } + + } + + } + + Item { + height: Theme.spacingS + } + + StyledText { + text: "↑↓ Navigate • Tab Cycle • Enter Select • Esc Close" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + opacity: 0.7 + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modals/ProcessListModal.qml b/quickshell/.config/quickshell/Modals/ProcessListModal.qml new file mode 100644 index 0000000..c2918ab --- /dev/null +++ b/quickshell/.config/quickshell/Modals/ProcessListModal.qml @@ -0,0 +1,356 @@ +import QtQuick +import QtQuick.Layouts +import qs.Common +import qs.Modals.Common +import qs.Modules.ProcessList +import qs.Services +import qs.Widgets + +DankModal { + id: processListModal + + property int currentTab: 0 + property var tabNames: ["Processes", "Performance", "System"] + + function show() { + if (!DgopService.dgopAvailable) { + console.warn("ProcessListModal: dgop is not available"); + return ; + } + open(); + UserInfoService.getUptime(); + } + + function hide() { + close(); + if (processContextMenu.visible) { + processContextMenu.close(); + } + + } + + function toggle() { + if (!DgopService.dgopAvailable) { + console.warn("ProcessListModal: dgop is not available"); + return ; + } + if (shouldBeVisible) { + hide(); + } else { + show(); + } + } + + width: 900 + height: 680 + visible: false + backgroundColor: Theme.popupBackground() + cornerRadius: Theme.cornerRadius + enableShadow: true + onBackgroundClicked: () => { + return hide(); + } + + Component { + id: processesTabComponent + + ProcessesTab { + contextMenu: processContextMenu + } + + } + + Component { + id: performanceTabComponent + + PerformanceTab { + } + + } + + Component { + id: systemTabComponent + + SystemTab { + } + + } + + ProcessContextMenu { + id: processContextMenu + } + + content: Component { + Item { + anchors.fill: parent + focus: true + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + processListModal.hide(); + event.accepted = true; + } else if (event.key === Qt.Key_1) { + currentTab = 0; + event.accepted = true; + } else if (event.key === Qt.Key_2) { + currentTab = 1; + event.accepted = true; + } else if (event.key === Qt.Key_3) { + currentTab = 2; + event.accepted = true; + } + } + + // Show error message when dgop is not available + Rectangle { + anchors.centerIn: parent + width: 400 + height: 200 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1) + border.color: Theme.error + border.width: 2 + visible: !DgopService.dgopAvailable + + Column { + anchors.centerIn: parent + spacing: Theme.spacingL + + DankIcon { + name: "error" + size: 48 + color: Theme.error + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: "System Monitor Unavailable" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.error + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: "The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature." + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + } + + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + visible: DgopService.dgopAvailable + + RowLayout { + Layout.fillWidth: true + height: 40 + + StyledText { + text: "System Monitor" + font.pixelSize: Theme.fontSizeLarge + 4 + font.weight: Font.Bold + color: Theme.surfaceText + Layout.alignment: Qt.AlignVCenter + } + + Item { + Layout.fillWidth: true + } + + DankActionButton { + circular: false + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: () => { + return processListModal.hide(); + } + Layout.alignment: Qt.AlignVCenter + } + + } + + Rectangle { + Layout.fillWidth: true + height: 52 + color: Theme.surfaceSelected + radius: Theme.cornerRadius + border.color: Theme.outlineLight + border.width: 1 + + Row { + anchors.fill: parent + anchors.margins: 4 + spacing: 2 + + Repeater { + model: tabNames + + Rectangle { + width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length + height: 44 + radius: Theme.cornerRadius + color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent") + border.color: currentTab === index ? Theme.primary : "transparent" + border.width: currentTab === index ? 1 : 0 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: { + const tabIcons = ["list_alt", "analytics", "settings"]; + return tabIcons[index] || "tab"; + } + size: Theme.iconSize - 2 + color: currentTab === index ? Theme.primary : Theme.surfaceText + opacity: currentTab === index ? 1 : 0.7 + anchors.verticalCenter: parent.verticalCenter + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + StyledText { + text: modelData + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: currentTab === index ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: -1 + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + } + + MouseArea { + id: tabMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + currentTab = index; + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + } + + } + + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Theme.surfaceLight + border.color: Theme.outlineLight + border.width: 1 + + Loader { + id: processesTab + + anchors.fill: parent + anchors.margins: Theme.spacingS + active: processListModal.visible && currentTab === 0 + visible: currentTab === 0 + opacity: currentTab === 0 ? 1 : 0 + sourceComponent: processesTabComponent + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + } + + } + + Loader { + id: performanceTab + + anchors.fill: parent + anchors.margins: Theme.spacingS + active: processListModal.visible && currentTab === 1 + visible: currentTab === 1 + opacity: currentTab === 1 ? 1 : 0 + sourceComponent: performanceTabComponent + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + } + + } + + Loader { + id: systemTab + + anchors.fill: parent + anchors.margins: Theme.spacingS + active: processListModal.visible && currentTab === 2 + visible: currentTab === 2 + opacity: currentTab === 2 ? 1 : 0 + sourceComponent: systemTabComponent + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + } + + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modals/Settings/ProfileSection.qml b/quickshell/.config/quickshell/Modals/Settings/ProfileSection.qml new file mode 100644 index 0000000..c0389d1 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Settings/ProfileSection.qml @@ -0,0 +1,211 @@ +import QtQuick +import QtQuick.Effects +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var parentModal: null + + width: parent.width - Theme.spacingS * 2 + height: 110 + radius: Theme.cornerRadius + color: "transparent" + border.width: 0 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingM + + Item { + id: profileImageContainer + + property bool hasImage: profileImageSource.status === Image.Ready + + width: 80 + height: 80 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "transparent" + border.color: Theme.primary + border.width: 1 + visible: parent.hasImage + } + + Image { + id: profileImageSource + + source: { + if (PortalService.profileImage === "") { + return ""; + } + if (PortalService.profileImage.startsWith("/")) { + return "file://" + PortalService.profileImage; + } + return PortalService.profileImage; + } + smooth: true + asynchronous: true + mipmap: true + cache: true + visible: false + } + + MultiEffect { + anchors.fill: parent + anchors.margins: 5 + source: profileImageSource + maskEnabled: true + maskSource: profileCircularMask + visible: profileImageContainer.hasImage + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: profileCircularMask + + width: 70 + height: 70 + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "black" + antialiasing: true + } + + } + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: Theme.primary + visible: !parent.hasImage + + DankIcon { + anchors.centerIn: parent + name: "person" + size: Theme.iconSizeLarge + color: Theme.primaryText + } + + } + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: Qt.rgba(0, 0, 0, 0.7) + visible: profileMouseArea.containsMouse + + Row { + anchors.centerIn: parent + spacing: 4 + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: Qt.rgba(255, 255, 255, 0.9) + + DankIcon { + anchors.centerIn: parent + name: "edit" + size: 16 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: () => { + if (root.parentModal) { + root.parentModal.allowFocusOverride = true; + root.parentModal.shouldHaveFocus = false; + if (root.parentModal.profileBrowser) { + root.parentModal.profileBrowser.open(); + } + } + } + } + + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: Qt.rgba(255, 255, 255, 0.9) + visible: profileImageContainer.hasImage + + DankIcon { + anchors.centerIn: parent + name: "close" + size: 16 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: () => { + return PortalService.setProfileImage(""); + } + } + + } + + } + + } + + MouseArea { + id: profileMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + } + + } + + Column { + width: 120 + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + StyledText { + text: UserInfoService.fullName || "User" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: DgopService.distribution || "Linux" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + elide: Text.ElideRight + width: parent.width + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/.config/quickshell/Modals/Settings/SettingsContent.qml new file mode 100644 index 0000000..6ceaec2 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Settings/SettingsContent.qml @@ -0,0 +1,142 @@ +import QtQuick +import qs.Common +import qs.Modules.Settings + +Item { + id: root + + property int currentIndex: 0 + property var parentModal: null + + Rectangle { + anchors.fill: parent + anchors.leftMargin: 0 + anchors.rightMargin: Theme.spacingS + anchors.bottomMargin: Theme.spacingM + anchors.topMargin: 0 + color: "transparent" + + Loader { + id: personalizationLoader + + anchors.fill: parent + active: root.currentIndex === 0 + visible: active + asynchronous: true + + sourceComponent: Component { + PersonalizationTab { + parentModal: root.parentModal + } + + } + + } + + Loader { + id: timeLoader + + anchors.fill: parent + active: root.currentIndex === 1 + visible: active + asynchronous: true + + sourceComponent: TimeTab { + } + + } + + Loader { + id: weatherLoader + + anchors.fill: parent + active: root.currentIndex === 2 + visible: active + asynchronous: true + + sourceComponent: WeatherTab { + } + + } + + Loader { + id: topBarLoader + + anchors.fill: parent + active: root.currentIndex === 3 + visible: active + asynchronous: true + + sourceComponent: TopBarTab { + } + + } + + Loader { + id: widgetsLoader + + anchors.fill: parent + active: root.currentIndex === 4 + visible: active + asynchronous: true + + sourceComponent: WidgetTweaksTab { + } + + } + + Loader { + id: displaysLoader + + anchors.fill: parent + active: root.currentIndex === 6 + visible: active + asynchronous: true + + sourceComponent: DisplaysTab { + } + + } + + Loader { + id: recentAppsLoader + + anchors.fill: parent + active: root.currentIndex === 7 + visible: active + asynchronous: true + + sourceComponent: RecentAppsTab { + } + + } + + Loader { + id: themeColorsLoader + + anchors.fill: parent + active: root.currentIndex === 8 + visible: active + asynchronous: true + + sourceComponent: ThemeColorsTab { + } + + } + + Loader { + id: aboutLoader + + anchors.fill: parent + active: root.currentIndex === 9 + visible: active + asynchronous: true + + sourceComponent: AboutTab { + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/.config/quickshell/Modals/Settings/SettingsModal.qml new file mode 100644 index 0000000..8030082 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Settings/SettingsModal.qml @@ -0,0 +1,198 @@ +import QtQuick +import QtQuick.Effects +import Quickshell.Io +import qs.Common +import qs.Modals.Common +import qs.Modals.FileBrowser +import qs.Modules.Settings +import qs.Services +import qs.Widgets + +DankModal { + id: settingsModal + + property Component settingsContent + + signal closingModal() + + function show() { + open(); + } + + function hide() { + close(); + } + + function toggle() { + if (shouldBeVisible) { + hide(); + } else { + show(); + } + } + + objectName: "settingsModal" + width: 800 + height: 750 + visible: false + onBackgroundClicked: () => { + return hide(); + } + content: settingsContent + + IpcHandler { + function open(): string { + settingsModal.show(); + return "SETTINGS_OPEN_SUCCESS"; + } + + function close(): string { + settingsModal.hide(); + return "SETTINGS_CLOSE_SUCCESS"; + } + + function toggle(): string { + settingsModal.toggle(); + return "SETTINGS_TOGGLE_SUCCESS"; + } + + target: "settings" + } + + IpcHandler { + function browse(type: string) { + if (type === "wallpaper") { + wallpaperBrowser.allowStacking = false; + wallpaperBrowser.open(); + } else if (type === "profile") { + profileBrowser.allowStacking = false; + profileBrowser.open(); + } + } + + target: "file" + } + + FileBrowserModal { + id: profileBrowser + + allowStacking: true + browserTitle: "Select Profile Image" + browserIcon: "person" + browserType: "profile" + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + onFileSelected: (path) => { + PortalService.setProfileImage(path); + close(); + } + onDialogClosed: () => { + if (settingsModal) { + settingsModal.allowFocusOverride = false; + settingsModal.shouldHaveFocus = Qt.binding(() => { + return settingsModal.shouldBeVisible; + }); + } + allowStacking = true; + } + } + + FileBrowserModal { + id: wallpaperBrowser + + allowStacking: true + browserTitle: "Select Wallpaper" + browserIcon: "wallpaper" + browserType: "wallpaper" + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + onFileSelected: (path) => { + SessionData.setWallpaper(path); + close(); + } + onDialogClosed: () => { + allowStacking = true; + } + } + + settingsContent: Component { + Item { + anchors.fill: parent + focus: true + + Column { + anchors.fill: parent + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + anchors.topMargin: Theme.spacingM + anchors.bottomMargin: Theme.spacingL + spacing: 0 + + Item { + width: parent.width + height: 35 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "settings" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Settings" + font.pixelSize: Theme.fontSizeXLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + } + + DankActionButton { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + circular: false + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: () => { + return settingsModal.hide(); + } + } + + } + + Row { + width: parent.width + height: parent.height - 35 + spacing: 0 + + SettingsSidebar { + id: sidebar + + parentModal: settingsModal + onCurrentIndexChanged: content.currentIndex = currentIndex + } + + SettingsContent { + id: content + + width: parent.width - sidebar.width + height: parent.height + parentModal: settingsModal + currentIndex: sidebar.currentIndex + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modals/Settings/SettingsSidebar.qml b/quickshell/.config/quickshell/Modals/Settings/SettingsSidebar.qml new file mode 100644 index 0000000..42cc362 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Settings/SettingsSidebar.qml @@ -0,0 +1,130 @@ +import QtQuick +import qs.Common +import qs.Modals.Settings +import qs.Widgets + +Rectangle { + id: sidebarContainer + + property int currentIndex: 0 + property var parentModal: null + readonly property var sidebarItems: [{ + "text": "Personalization", + "icon": "person" + }, { + "text": "Time & Date", + "icon": "schedule" + }, { + "text": "Weather", + "icon": "cloud" + }, { + "text": "Top Bar", + "icon": "toolbar" + }, { + "text": "Widgets", + "icon": "widgets" + }, { + "text": "Displays", + "icon": "monitor" + }, { + "text": "Recent Apps", + "icon": "history" + }, { + "text": "Theme & Colors", + "icon": "palette" + }, { + "text": "About", + "icon": "info" + }] + + width: 270 + height: parent.height + color: Theme.surfaceContainer + radius: Theme.cornerRadius + + Column { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + anchors.bottomMargin: Theme.spacingS + anchors.topMargin: Theme.spacingM + 2 + spacing: Theme.spacingXS + + ProfileSection { + parentModal: sidebarContainer.parentModal + } + + Rectangle { + width: parent.width - Theme.spacingS * 2 + height: 1 + color: Theme.outline + opacity: 0.2 + } + + Item { + width: parent.width + height: Theme.spacingL + } + + Repeater { + id: sidebarRepeater + + model: sidebarContainer.sidebarItems + + Rectangle { + property bool isActive: sidebarContainer.currentIndex === index + + width: parent.width - Theme.spacingS * 2 + height: 44 + radius: Theme.cornerRadius + color: isActive ? Theme.surfaceContainerHigh : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: modelData.icon || "" + size: Theme.iconSize - 2 + color: parent.parent.isActive ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.text || "" + font.pixelSize: Theme.fontSizeMedium + color: parent.parent.isActive ? Theme.primary : Theme.surfaceText + font.weight: parent.parent.isActive ? Font.Medium : Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + + } + + MouseArea { + id: tabMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + sidebarContainer.currentIndex = index; + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modals/Spotlight/SpotlightContent.qml b/quickshell/.config/quickshell/Modals/Spotlight/SpotlightContent.qml new file mode 100644 index 0000000..f0535f2 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Spotlight/SpotlightContent.qml @@ -0,0 +1,226 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Modals.Spotlight +import qs.Modules.AppDrawer +import qs.Services +import qs.Widgets + +Item { + id: spotlightKeyHandler + + property alias appLauncher: appLauncher + property alias searchField: searchField + property var parentModal: null + + anchors.fill: parent + focus: true + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + if (parentModal) + parentModal.hide() + + event.accepted = true + } else if (event.key === Qt.Key_Down) { + appLauncher.selectNext() + event.accepted = true + } else if (event.key === Qt.Key_Up) { + appLauncher.selectPrevious() + event.accepted = true + } else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") { + appLauncher.selectNextInRow() + event.accepted = true + } else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") { + appLauncher.selectPreviousInRow() + event.accepted = true + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + appLauncher.launchSelected() + event.accepted = true + } else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) { + searchField.forceActiveFocus() + searchField.insertText(event.text) + event.accepted = true + } + } + + AppLauncher { + id: appLauncher + + viewMode: SettingsData.spotlightModalViewMode + gridColumns: 4 + onAppLaunched: () => { + if (parentModal) + parentModal.hide() + } + onViewModeSelected: mode => { + SettingsData.setSpotlightModalViewMode(mode) + } + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + Rectangle { + width: parent.width + height: categorySelector.height + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceVariantAlpha + border.color: Theme.outlineMedium + border.width: 1 + visible: appLauncher.categories.length > 1 || appLauncher.model.count > 0 + + CategorySelector { + id: categorySelector + + anchors.centerIn: parent + width: parent.width - Theme.spacingM * 2 + categories: appLauncher.categories + selectedCategory: appLauncher.selectedCategory + compact: false + onCategorySelected: category => { + appLauncher.setCategory(category) + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + DankTextField { + id: searchField + + width: parent.width - 80 - Theme.spacingM + height: 56 + cornerRadius: Theme.cornerRadius + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7) + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + leftIconName: "search" + leftIconSize: Theme.iconSize + leftIconColor: Theme.surfaceVariantText + leftIconFocusedColor: Theme.primary + showClearButton: true + textColor: Theme.surfaceText + font.pixelSize: Theme.fontSizeLarge + enabled: parentModal ? parentModal.spotlightOpen : true + placeholderText: "" + ignoreLeftRightKeys: true + keyForwardTargets: [spotlightKeyHandler] + text: appLauncher.searchQuery + onTextEdited: () => { + appLauncher.searchQuery = text + } + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + if (parentModal) + parentModal.hide() + + event.accepted = true + } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) { + if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) + appLauncher.launchSelected() + else if (appLauncher.model.count > 0) + appLauncher.launchApp(appLauncher.model.get(0)) + event.accepted = true + } else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) { + event.accepted = false + } + } + } + + Row { + spacing: Theme.spacingXS + visible: appLauncher.model.count > 0 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: 36 + height: 36 + radius: Theme.cornerRadius + color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent" + border.color: appLauncher.viewMode === "list" ? Theme.primarySelected : "transparent" + border.width: 1 + + DankIcon { + anchors.centerIn: parent + name: "view_list" + size: 18 + color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText + } + + MouseArea { + id: listViewArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + appLauncher.setViewMode("list") + } + } + } + + Rectangle { + width: 36 + height: 36 + radius: Theme.cornerRadius + color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent" + border.color: appLauncher.viewMode === "grid" ? Theme.primarySelected : "transparent" + border.width: 1 + + DankIcon { + anchors.centerIn: parent + name: "grid_view" + size: 18 + color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText + } + + MouseArea { + id: gridViewArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + appLauncher.setViewMode("grid") + } + } + } + } + } + + SpotlightResults { + appLauncher: spotlightKeyHandler.appLauncher + contextMenu: contextMenu + } + } + + SpotlightContextMenu { + id: contextMenu + + appLauncher: spotlightKeyHandler.appLauncher + parentHandler: spotlightKeyHandler + } + + MouseArea { + anchors.fill: parent + visible: contextMenu.visible + z: 999 + onClicked: () => { + contextMenu.close() + } + + MouseArea { + + // Prevent closing when clicking on the menu itself + x: contextMenu.x + y: contextMenu.y + width: contextMenu.width + height: contextMenu.height + onClicked: () => {} + } + } +} diff --git a/quickshell/.config/quickshell/Modals/Spotlight/SpotlightContextMenu.qml b/quickshell/.config/quickshell/Modals/Spotlight/SpotlightContextMenu.qml new file mode 100644 index 0000000..e6b6e89 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Spotlight/SpotlightContextMenu.qml @@ -0,0 +1,205 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: contextMenu + + property var currentApp: null + property bool menuVisible: false + property var appLauncher: null + property var parentHandler: null + + function show(x, y, app) { + currentApp = app + const menuWidth = 180 + const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2 + let finalX = x + 8 + let finalY = y + 8 + if (parentHandler) { + if (finalX + menuWidth > parentHandler.width) + finalX = x - menuWidth - 8 + + if (finalY + menuHeight > parentHandler.height) + finalY = y - menuHeight - 8 + + finalX = Math.max(8, Math.min(finalX, parentHandler.width - menuWidth - 8)) + finalY = Math.max(8, Math.min(finalY, parentHandler.height - menuHeight - 8)) + } + contextMenu.x = finalX + contextMenu.y = finalY + contextMenu.visible = true + contextMenu.menuVisible = true + } + + function close() { + contextMenu.menuVisible = false + Qt.callLater(() => { + contextMenu.visible = false + }) + } + + visible: false + width: 180 + height: menuColumn.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.popupBackground() + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + z: 1000 + opacity: menuVisible ? 1 : 0 + scale: menuVisible ? 1 : 0.85 + + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: parent.z - 1 + } + + Column { + id: menuColumn + + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 1 + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: { + if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) + return "push_pin" + + const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "" + return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin" + } + size: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) + return "Pin to Dock" + + const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "" + return SessionData.isPinnedApp(appId) ? "Unpin from Dock" : "Pin to Dock" + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: pinMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) + return + + const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "" + if (SessionData.isPinnedApp(appId)) + SessionData.removePinnedApp(appId) + else + SessionData.addPinnedApp(appId) + contextMenu.close() + } + } + } + + Rectangle { + width: parent.width - Theme.spacingS * 2 + height: 5 + anchors.horizontalCenter: parent.horizontalCenter + color: "transparent" + + Rectangle { + anchors.centerIn: parent + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "launch" + size: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Launch" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: launchMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + if (contextMenu.currentApp && appLauncher) + appLauncher.launchApp(contextMenu.currentApp) + + contextMenu.close() + } + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } +} diff --git a/quickshell/.config/quickshell/Modals/Spotlight/SpotlightModal.qml b/quickshell/.config/quickshell/Modals/Spotlight/SpotlightModal.qml new file mode 100644 index 0000000..764a771 --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Spotlight/SpotlightModal.qml @@ -0,0 +1,109 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import qs.Common +import qs.Modals.Common +import qs.Modules.AppDrawer +import qs.Services +import qs.Widgets + +DankModal { + id: spotlightModal + + property bool spotlightOpen: false + property Component spotlightContent + + function show() { + spotlightOpen = true + open() + if (contentLoader.item && contentLoader.item.appLauncher) { + contentLoader.item.appLauncher.searchQuery = "" + } + + Qt.callLater(() => { + if (contentLoader.item && contentLoader.item.searchField) { + contentLoader.item.searchField.forceActiveFocus() + } + }) + } + + function hide() { + spotlightOpen = false + close() + if (contentLoader.item && contentLoader.item.appLauncher) { + contentLoader.item.appLauncher.searchQuery = "" + contentLoader.item.appLauncher.selectedIndex = 0 + contentLoader.item.appLauncher.setCategory("All") + } + } + + function toggle() { + if (spotlightOpen) { + hide() + } else { + show() + } + } + + shouldBeVisible: spotlightOpen + width: 550 + height: 600 + backgroundColor: Theme.popupBackground() + cornerRadius: Theme.cornerRadius + borderColor: Theme.outlineMedium + borderWidth: 1 + enableShadow: true + onVisibleChanged: () => { + if (visible && !spotlightOpen) { + show() + } + if (visible && contentLoader.item) { + Qt.callLater(() => { + if (contentLoader.item.searchField) { + contentLoader.item.searchField.forceActiveFocus() + } + }) + } + } + onBackgroundClicked: () => { + return hide() + } + content: spotlightContent + + Connections { + function onCloseAllModalsExcept(excludedModal) { + if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) { + spotlightOpen = false + } + } + + target: ModalManager + } + + IpcHandler { + function open(): string { + spotlightModal.show() + return "SPOTLIGHT_OPEN_SUCCESS" + } + + function close(): string { + spotlightModal.hide() + return "SPOTLIGHT_CLOSE_SUCCESS" + } + + function toggle(): string { + spotlightModal.toggle() + return "SPOTLIGHT_TOGGLE_SUCCESS" + } + + target: "spotlight" + } + + spotlightContent: Component { + SpotlightContent { + parentModal: spotlightModal + } + } +} diff --git a/quickshell/.config/quickshell/Modals/Spotlight/SpotlightResults.qml b/quickshell/.config/quickshell/Modals/Spotlight/SpotlightResults.qml new file mode 100644 index 0000000..340321e --- /dev/null +++ b/quickshell/.config/quickshell/Modals/Spotlight/SpotlightResults.qml @@ -0,0 +1,324 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Common +import qs.Widgets +import qs.Services + +Rectangle { + id: resultsContainer + + property var appLauncher: null + property var contextMenu: null + + width: parent.width + height: parent.height - y + radius: Theme.cornerRadius + color: Theme.surfaceLight + border.color: Theme.outlineLight + border.width: 1 + + DankListView { + id: resultsList + + property int itemHeight: 60 + property int iconSize: 40 + property bool showDescription: true + property int itemSpacing: Theme.spacingS + property bool hoverUpdatesSelection: false + property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false + + signal keyboardNavigationReset + signal itemClicked(int index, var modelData) + signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) + + function ensureVisible(index) { + if (index < 0 || index >= count) + return + + const itemY = index * (itemHeight + itemSpacing) + const itemBottom = itemY + itemHeight + if (itemY < contentY) + contentY = itemY + else if (itemBottom > contentY + height) + contentY = itemBottom - height + } + + anchors.fill: parent + anchors.margins: Theme.spacingS + visible: appLauncher && appLauncher.viewMode === "list" + model: appLauncher ? appLauncher.model : null + currentIndex: appLauncher ? appLauncher.selectedIndex : -1 + clip: true + spacing: itemSpacing + focus: true + interactive: true + cacheBuffer: Math.max(0, Math.min(height * 2, 1000)) + reuseItems: true + onCurrentIndexChanged: { + if (keyboardNavigationActive) + ensureVisible(currentIndex) + } + onItemClicked: (index, modelData) => { + if (appLauncher) + appLauncher.launchApp(modelData) + } + onItemRightClicked: (index, modelData, mouseX, mouseY) => { + if (contextMenu) + contextMenu.show(mouseX, mouseY, modelData) + } + onKeyboardNavigationReset: () => { + if (appLauncher) + appLauncher.keyboardNavigationActive = false + } + + delegate: Rectangle { + width: ListView.view.width + height: resultsList.itemHeight + radius: Theme.cornerRadius + color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) + border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium + border.width: ListView.isCurrentItem ? 2 : 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingL + + Item { + width: resultsList.iconSize + height: resultsList.iconSize + anchors.verticalCenter: parent.verticalCenter + + IconImage { + id: listIconImg + + anchors.fill: parent + source: Quickshell.iconPath(model.icon, true) || DesktopService.resolveIconPath(model.icon) || DesktopService.resolveIconPath(model.startupClass) + asynchronous: true + visible: status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !listIconImg.visible + color: Theme.surfaceLight + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.primarySelected + + StyledText { + anchors.centerIn: parent + text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A" + font.pixelSize: resultsList.iconSize * 0.4 + color: Theme.primary + font.weight: Font.Bold + } + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - resultsList.iconSize - Theme.spacingL + spacing: Theme.spacingXS + + StyledText { + width: parent.width + text: model.name || "" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + } + + StyledText { + width: parent.width + text: model.comment || "Application" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + elide: Text.ElideRight + visible: resultsList.showDescription && model.comment && model.comment.length > 0 + } + } + } + + MouseArea { + id: listMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + z: 10 + onEntered: () => { + if (resultsList.hoverUpdatesSelection && !resultsList.keyboardNavigationActive) + resultsList.currentIndex = index + } + onPositionChanged: () => { + resultsList.keyboardNavigationReset() + } + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + resultsList.itemClicked(index, model) + } else if (mouse.button === Qt.RightButton) { + const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y) + resultsList.itemRightClicked(index, model, modalPos.x, modalPos.y) + } + } + } + } + } + + DankGridView { + id: resultsGrid + + property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1 + property int columns: 4 + property bool adaptiveColumns: false + property int minCellWidth: 120 + property int maxCellWidth: 160 + property int cellPadding: 8 + property real iconSizeRatio: 0.55 + property int maxIconSize: 48 + property int minIconSize: 32 + property bool hoverUpdatesSelection: false + property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false + property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns + property int baseCellHeight: baseCellWidth + 20 + property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns + property int remainingSpace: width - (actualColumns * cellWidth) + + signal keyboardNavigationReset + signal itemClicked(int index, var modelData) + signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) + + function ensureVisible(index) { + if (index < 0 || index >= count) + return + + const itemY = Math.floor(index / actualColumns) * cellHeight + const itemBottom = itemY + cellHeight + if (itemY < contentY) + contentY = itemY + else if (itemBottom > contentY + height) + contentY = itemBottom - height + } + + anchors.fill: parent + anchors.margins: Theme.spacingS + visible: appLauncher && appLauncher.viewMode === "grid" + model: appLauncher ? appLauncher.model : null + clip: true + cellWidth: baseCellWidth + cellHeight: baseCellHeight + leftMargin: Math.max(Theme.spacingS, remainingSpace / 2) + rightMargin: leftMargin + focus: true + interactive: true + cacheBuffer: Math.max(0, Math.min(height * 2, 1000)) + reuseItems: true + onCurrentIndexChanged: { + if (keyboardNavigationActive) + ensureVisible(currentIndex) + } + onItemClicked: (index, modelData) => { + if (appLauncher) + appLauncher.launchApp(modelData) + } + onItemRightClicked: (index, modelData, mouseX, mouseY) => { + if (contextMenu) + contextMenu.show(mouseX, mouseY, modelData) + } + onKeyboardNavigationReset: () => { + if (appLauncher) + appLauncher.keyboardNavigationActive = false + } + + delegate: Rectangle { + width: resultsGrid.cellWidth - resultsGrid.cellPadding + height: resultsGrid.cellHeight - resultsGrid.cellPadding + radius: Theme.cornerRadius + color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) + border.color: resultsGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium + border.width: resultsGrid.currentIndex === index ? 2 : 1 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + Item { + property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio)) + + width: iconSize + height: iconSize + anchors.horizontalCenter: parent.horizontalCenter + + IconImage { + id: gridIconImg + + anchors.fill: parent + source: Quickshell.iconPath(model.icon, true) || DesktopService.resolveIconPath(model.icon) || DesktopService.resolveIconPath(model.startupClass) + smooth: true + asynchronous: true + visible: status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !gridIconImg.visible + color: Theme.surfaceLight + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.primarySelected + + StyledText { + anchors.centerIn: parent + text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A" + font.pixelSize: Math.min(28, parent.width * 0.5) + color: Theme.primary + font.weight: Font.Bold + } + } + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + width: resultsGrid.cellWidth - 12 + text: model.name || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 2 + wrapMode: Text.WordWrap + } + } + + MouseArea { + id: gridMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + z: 10 + onEntered: () => { + if (resultsGrid.hoverUpdatesSelection && !resultsGrid.keyboardNavigationActive) + resultsGrid.currentIndex = index + } + onPositionChanged: () => { + resultsGrid.keyboardNavigationReset() + } + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + resultsGrid.itemClicked(index, model) + } else if (mouse.button === Qt.RightButton) { + const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y) + resultsGrid.itemRightClicked(index, model, modalPos.x, modalPos.y) + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modals/WifiPasswordModal.qml b/quickshell/.config/quickshell/Modals/WifiPasswordModal.qml new file mode 100644 index 0000000..36e425d --- /dev/null +++ b/quickshell/.config/quickshell/Modals/WifiPasswordModal.qml @@ -0,0 +1,296 @@ +import QtQuick +import qs.Common +import qs.Modals.Common +import qs.Services +import qs.Widgets + +DankModal { + id: root + + property string wifiPasswordSSID: "" + property string wifiPasswordInput: "" + + function show(ssid) { + wifiPasswordSSID = ssid + wifiPasswordInput = "" + open() + Qt.callLater(() => { + if (contentLoader.item && contentLoader.item.passwordInput) + contentLoader.item.passwordInput.forceActiveFocus() + }) + } + + shouldBeVisible: false + width: 420 + height: 230 + onShouldBeVisibleChanged: () => { + if (!shouldBeVisible) + wifiPasswordInput = "" + } + onOpened: { + Qt.callLater(() => { + if (contentLoader.item && contentLoader.item.passwordInput) + contentLoader.item.passwordInput.forceActiveFocus() + }) + } + onBackgroundClicked: () => { + close() + wifiPasswordInput = "" + } + + Connections { + target: NetworkService + + function onPasswordDialogShouldReopenChanged() { + if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") { + wifiPasswordSSID = NetworkService.connectingSSID + wifiPasswordInput = "" + open() + NetworkService.passwordDialogShouldReopen = false + } + } + } + + content: Component { + FocusScope { + id: wifiContent + + property alias passwordInput: passwordInput + + anchors.fill: parent + focus: true + Keys.onEscapePressed: event => { + close() + wifiPasswordInput = "" + event.accepted = true + } + + Column { + anchors.centerIn: parent + width: parent.width - Theme.spacingM * 2 + spacing: Theme.spacingM + + Row { + width: parent.width + + Column { + width: parent.width - 40 + spacing: Theme.spacingXS + + StyledText { + text: "Connect to Wi-Fi" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: `Enter password for "${wifiPasswordSSID}"` + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceTextMedium + width: parent.width + elide: Text.ElideRight + } + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: () => { + close() + wifiPasswordInput = "" + } + } + } + + Rectangle { + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: Theme.surfaceHover + border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong + border.width: passwordInput.activeFocus ? 2 : 1 + + MouseArea { + anchors.fill: parent + onClicked: () => { + passwordInput.forceActiveFocus() + } + } + + DankTextField { + id: passwordInput + + anchors.fill: parent + font.pixelSize: Theme.fontSizeMedium + textColor: Theme.surfaceText + text: wifiPasswordInput + echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password + placeholderText: "" + backgroundColor: "transparent" + focus: true + enabled: root.shouldBeVisible + onTextEdited: () => { + wifiPasswordInput = text + } + onAccepted: () => { + NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text) + close() + wifiPasswordInput = "" + passwordInput.text = "" + } + Component.onCompleted: () => { + if (root.shouldBeVisible) + focusDelayTimer.start() + } + + Timer { + id: focusDelayTimer + + interval: 100 + repeat: false + onTriggered: () => { + if (root.shouldBeVisible) + passwordInput.forceActiveFocus() + } + } + + Connections { + target: root + + function onShouldBeVisibleChanged() { + if (root.shouldBeVisible) + focusDelayTimer.start() + } + } + } + } + + Row { + spacing: Theme.spacingS + + Rectangle { + id: showPasswordCheckbox + + property bool checked: false + + width: 20 + height: 20 + radius: 4 + color: checked ? Theme.primary : "transparent" + border.color: checked ? Theme.primary : Theme.outlineButton + border.width: 2 + + DankIcon { + anchors.centerIn: parent + name: "check" + size: 12 + color: Theme.background + visible: parent.checked + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + showPasswordCheckbox.checked = !showPasswordCheckbox.checked + } + } + } + + StyledText { + text: "Show password" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Item { + width: parent.width + height: 40 + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Rectangle { + width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2) + height: 36 + radius: Theme.cornerRadius + color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent" + border.color: Theme.surfaceVariantAlpha + border.width: 1 + + StyledText { + id: cancelText + + anchors.centerIn: parent + text: "Cancel" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + MouseArea { + id: cancelArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + close() + wifiPasswordInput = "" + } + } + } + + Rectangle { + width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2) + height: 36 + radius: Theme.cornerRadius + color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary + enabled: passwordInput.text.length > 0 + opacity: enabled ? 1 : 0.5 + + StyledText { + id: connectText + + anchors.centerIn: parent + text: "Connect" + font.pixelSize: Theme.fontSizeMedium + color: Theme.background + font.weight: Font.Medium + } + + MouseArea { + id: connectArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: parent.enabled + onClicked: () => { + NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text) + close() + wifiPasswordInput = "" + passwordInput.text = "" + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/AppDrawer/AppDrawerPopout.qml b/quickshell/.config/quickshell/Modules/AppDrawer/AppDrawerPopout.qml new file mode 100644 index 0000000..bec4410 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/AppDrawer/AppDrawerPopout.qml @@ -0,0 +1,821 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Modules.AppDrawer +import qs.Services +import qs.Widgets + +DankPopout { + id: appDrawerPopout + + property string triggerSection: "left" + property var triggerScreen: null + + // Setting to Exclusive, so virtual keyboards can send input to app drawer + WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + + function show() { + open() + } + + function setTriggerPosition(x, y, width, section, screen) { + triggerX = x + triggerY = y + triggerWidth = width + triggerSection = section + triggerScreen = screen + } + + popupWidth: 520 + popupHeight: 600 + triggerX: Theme.spacingL + triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS + triggerWidth: 40 + positioning: "center" + screen: triggerScreen + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + appLauncher.searchQuery = "" + appLauncher.selectedIndex = 0 + appLauncher.setCategory("All") + Qt.callLater(() => { + if (contentLoader.item && contentLoader.item.searchField) { + contentLoader.item.searchField.text = "" + contentLoader.item.searchField.forceActiveFocus() + } + }) + } + } + + AppLauncher { + id: appLauncher + + viewMode: SettingsData.appLauncherViewMode + gridColumns: 4 + onAppLaunched: appDrawerPopout.close() + onViewModeSelected: function (mode) { + SettingsData.setAppLauncherViewMode(mode) + } + } + + content: Component { + Rectangle { + id: launcherPanel + + property alias searchField: searchField + + color: Theme.popupBackground() + radius: Theme.cornerRadius + antialiasing: true + smooth: true + + // Multi-layer border effect + Repeater { + model: [{ + "margin": -3, + "color": Qt.rgba(0, 0, 0, 0.05), + "z": -3 + }, { + "margin": -2, + "color": Qt.rgba(0, 0, 0, 0.08), + "z": -2 + }, { + "margin": 0, + "color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12), + "z": -1 + }] + Rectangle { + anchors.fill: parent + anchors.margins: modelData.margin + color: "transparent" + radius: parent.radius + Math.abs(modelData.margin) + border.color: modelData.color + border.width: 1 + z: modelData.z + } + } + + Item { + id: keyHandler + + anchors.fill: parent + focus: true + readonly property var keyMappings: { + const mappings = {} + mappings[Qt.Key_Escape] = () => appDrawerPopout.close() + mappings[Qt.Key_Down] = () => appLauncher.selectNext() + mappings[Qt.Key_Up] = () => appLauncher.selectPrevious() + mappings[Qt.Key_Return] = () => appLauncher.launchSelected() + mappings[Qt.Key_Enter] = () => appLauncher.launchSelected() + + if (appLauncher.viewMode === "grid") { + mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow() + mappings[Qt.Key_Left] = () => appLauncher.selectPreviousInRow() + } + + return mappings + } + + Keys.onPressed: function (event) { + if (keyMappings[event.key]) { + keyMappings[event.key]() + event.accepted = true + return + } + + if (!searchField.activeFocus && event.text && /[a-zA-Z0-9\s]/.test(event.text)) { + searchField.forceActiveFocus() + searchField.insertText(event.text) + event.accepted = true + } + } + + Column { + width: parent.width - Theme.spacingL * 2 + height: parent.height - Theme.spacingL * 2 + x: Theme.spacingL + y: Theme.spacingL + spacing: Theme.spacingL + + Row { + width: parent.width + height: 40 + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: "Applications" + font.pixelSize: Theme.fontSizeLarge + 4 + font.weight: Font.Bold + color: Theme.surfaceText + } + + Item { + width: parent.width - 200 + height: 1 + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: appLauncher.model.count + " apps" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + } + + DankTextField { + id: searchField + + width: parent.width + height: 52 + cornerRadius: Theme.cornerRadius + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7) + normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + focusedBorderColor: Theme.primary + leftIconName: "search" + leftIconSize: Theme.iconSize + leftIconColor: Theme.surfaceVariantText + leftIconFocusedColor: Theme.primary + showClearButton: true + font.pixelSize: Theme.fontSizeLarge + enabled: appDrawerPopout.shouldBeVisible + ignoreLeftRightKeys: true + keyForwardTargets: [keyHandler] + onTextEdited: { + appLauncher.searchQuery = text + } + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Escape) { + appDrawerPopout.close() + event.accepted = true + return + } + + const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key) + const hasText = text.length > 0 + + if (isEnterKey && hasText) { + if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) { + appLauncher.launchSelected() + } else if (appLauncher.model.count > 0) { + appLauncher.launchApp(appLauncher.model.get(0)) + } + event.accepted = true + return + } + + const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right] + const isNavigationKey = navigationKeys.includes(event.key) + const isEmptyEnter = isEnterKey && !hasText + + event.accepted = !(isNavigationKey || isEmptyEnter) + } + + Connections { + function onShouldBeVisibleChanged() { + if (!appDrawerPopout.shouldBeVisible) { + searchField.focus = false + } + } + + target: appDrawerPopout + } + } + + Row { + width: parent.width + height: 40 + spacing: Theme.spacingM + visible: searchField.text.length === 0 + + Item { + width: 200 + height: 36 + + DankDropdown { + anchors.fill: parent + text: "" + currentValue: appLauncher.selectedCategory + options: appLauncher.categories + optionIcons: appLauncher.categoryIcons + onValueChanged: function (value) { + appLauncher.setCategory(value) + } + } + } + + Item { + width: parent.width - 300 + height: 1 + } + + Row { + spacing: 4 + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + buttonSize: 36 + circular: false + iconName: "view_list" + iconSize: 20 + iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText + backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + onClicked: { + appLauncher.setViewMode("list") + } + } + + DankActionButton { + buttonSize: 36 + circular: false + iconName: "grid_view" + iconSize: 20 + iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText + backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + onClicked: { + appLauncher.setViewMode("grid") + } + } + } + } + + Rectangle { + width: parent.width + height: { + let usedHeight = 40 + Theme.spacingL + usedHeight += 52 + Theme.spacingL + usedHeight += (searchField.text.length === 0 ? 40 + Theme.spacingL : 0) + return parent.height - usedHeight + } + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) + border.width: 1 + + DankListView { + id: appList + + property int itemHeight: 72 + property int iconSize: 56 + property bool showDescription: true + property int itemSpacing: Theme.spacingS + property bool hoverUpdatesSelection: false + property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive + + signal keyboardNavigationReset + signal itemClicked(int index, var modelData) + signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) + + function ensureVisible(index) { + if (index < 0 || index >= count) + return + + var itemY = index * (itemHeight + itemSpacing) + var itemBottom = itemY + itemHeight + if (itemY < contentY) + contentY = itemY + else if (itemBottom > contentY + height) + contentY = itemBottom - height + } + + anchors.fill: parent + anchors.margins: Theme.spacingS + visible: appLauncher.viewMode === "list" + model: appLauncher.model + currentIndex: appLauncher.selectedIndex + clip: true + spacing: itemSpacing + focus: true + interactive: true + cacheBuffer: Math.max(0, Math.min(height * 2, 1000)) + reuseItems: true + + onCurrentIndexChanged: { + if (keyboardNavigationActive) + ensureVisible(currentIndex) + } + + onItemClicked: function (index, modelData) { + appLauncher.launchApp(modelData) + } + onItemRightClicked: function (index, modelData, mouseX, mouseY) { + contextMenu.show(mouseX, mouseY, modelData) + } + onKeyboardNavigationReset: { + appLauncher.keyboardNavigationActive = false + } + + delegate: Rectangle { + width: ListView.view.width + height: appList.itemHeight + radius: Theme.cornerRadius + color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) + border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium + border.width: ListView.isCurrentItem ? 2 : 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingL + + Item { + width: appList.iconSize + height: appList.iconSize + anchors.verticalCenter: parent.verticalCenter + + IconImage { + id: listIconImg + + anchors.fill: parent + source: Quickshell.iconPath(model.icon, true) || DesktopService.resolveIconPath(model.icon) || DesktopService.resolveIconPath(model.startupClass) + smooth: true + asynchronous: true + visible: status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !listIconImg.visible + color: Theme.surfaceLight + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.primarySelected + + StyledText { + anchors.centerIn: parent + text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A" + font.pixelSize: appList.iconSize * 0.4 + color: Theme.primary + font.weight: Font.Bold + } + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - appList.iconSize - Theme.spacingL + spacing: Theme.spacingXS + + StyledText { + width: parent.width + text: model.name || "" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + } + + StyledText { + width: parent.width + text: model.comment || "Application" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + elide: Text.ElideRight + visible: appList.showDescription && model.comment && model.comment.length > 0 + } + } + } + + MouseArea { + id: listMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + z: 10 + onEntered: { + if (appList.hoverUpdatesSelection && !appList.keyboardNavigationActive) + appList.currentIndex = index + } + onPositionChanged: { + appList.keyboardNavigationReset() + } + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + appList.itemClicked(index, model) + } else if (mouse.button === Qt.RightButton) { + var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y) + appList.itemRightClicked(index, model, panelPos.x, panelPos.y) + } + } + } + } + } + + DankGridView { + id: appGrid + + property int currentIndex: appLauncher.selectedIndex + property int columns: 4 + property bool adaptiveColumns: false + property int minCellWidth: 120 + property int maxCellWidth: 160 + property int cellPadding: 8 + property real iconSizeRatio: 0.6 + property int maxIconSize: 56 + property int minIconSize: 32 + property bool hoverUpdatesSelection: false + property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive + property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns + property int baseCellHeight: baseCellWidth + 20 + property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns + property int remainingSpace: width - (actualColumns * cellWidth) + + signal keyboardNavigationReset + signal itemClicked(int index, var modelData) + signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) + + function ensureVisible(index) { + if (index < 0 || index >= count) + return + + var itemY = Math.floor(index / actualColumns) * cellHeight + var itemBottom = itemY + cellHeight + if (itemY < contentY) + contentY = itemY + else if (itemBottom > contentY + height) + contentY = itemBottom - height + } + + anchors.fill: parent + anchors.margins: Theme.spacingS + visible: appLauncher.viewMode === "grid" + model: appLauncher.model + clip: true + cellWidth: baseCellWidth + cellHeight: baseCellHeight + leftMargin: Math.max(Theme.spacingS, remainingSpace / 2) + rightMargin: leftMargin + focus: true + interactive: true + cacheBuffer: Math.max(0, Math.min(height * 2, 1000)) + reuseItems: true + + onCurrentIndexChanged: { + if (keyboardNavigationActive) + ensureVisible(currentIndex) + } + + onItemClicked: function (index, modelData) { + appLauncher.launchApp(modelData) + } + onItemRightClicked: function (index, modelData, mouseX, mouseY) { + contextMenu.show(mouseX, mouseY, modelData) + } + onKeyboardNavigationReset: { + appLauncher.keyboardNavigationActive = false + } + + delegate: Rectangle { + width: appGrid.cellWidth - appGrid.cellPadding + height: appGrid.cellHeight - appGrid.cellPadding + radius: Theme.cornerRadius + color: appGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) + border.color: appGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium + border.width: appGrid.currentIndex === index ? 2 : 1 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + Item { + property int iconSize: Math.min(appGrid.maxIconSize, Math.max(appGrid.minIconSize, appGrid.cellWidth * appGrid.iconSizeRatio)) + + width: iconSize + height: iconSize + anchors.horizontalCenter: parent.horizontalCenter + + IconImage { + id: gridIconImg + + anchors.fill: parent + source: Quickshell.iconPath(model.icon, true) || DesktopService.resolveIconPath(model.icon) + smooth: true + asynchronous: true + visible: status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !gridIconImg.visible + color: Theme.surfaceLight + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.primarySelected + + StyledText { + anchors.centerIn: parent + text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A" + font.pixelSize: Math.min(28, parent.width * 0.5) + color: Theme.primary + font.weight: Font.Bold + } + } + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + width: appGrid.cellWidth - 12 + text: model.name || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 2 + wrapMode: Text.WordWrap + } + } + + MouseArea { + id: gridMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + z: 10 + onEntered: { + if (appGrid.hoverUpdatesSelection && !appGrid.keyboardNavigationActive) + appGrid.currentIndex = index + } + onPositionChanged: { + appGrid.keyboardNavigationReset() + } + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + appGrid.itemClicked(index, model) + } else if (mouse.button === Qt.RightButton) { + var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y) + appGrid.itemRightClicked(index, model, panelPos.x, panelPos.y) + } + } + } + } + } + } + } + } + } + } + + Rectangle { + id: contextMenu + + property var currentApp: null + property bool menuVisible: false + + readonly property string appId: (currentApp && currentApp.desktopEntry) ? (currentApp.desktopEntry.id || currentApp.desktopEntry.execString || "") : "" + readonly property bool isPinned: appId && SessionData.isPinnedApp(appId) + + function show(x, y, app) { + currentApp = app + + const menuWidth = 180 + const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2 + + let finalX = x + 8 + let finalY = y + 8 + + if (finalX + menuWidth > appDrawerPopout.popupWidth) { + finalX = x - menuWidth - 8 + } + + if (finalY + menuHeight > appDrawerPopout.popupHeight) { + finalY = y - menuHeight - 8 + } + + finalX = Math.max(8, Math.min(finalX, appDrawerPopout.popupWidth - menuWidth - 8)) + finalY = Math.max(8, Math.min(finalY, appDrawerPopout.popupHeight - menuHeight - 8)) + + contextMenu.x = finalX + contextMenu.y = finalY + contextMenu.visible = true + contextMenu.menuVisible = true + } + + function close() { + contextMenu.menuVisible = false + Qt.callLater(() => { + contextMenu.visible = false + }) + } + + visible: false + width: 180 + height: menuColumn.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.popupBackground() + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + z: 1000 + opacity: menuVisible ? 1 : 0 + scale: menuVisible ? 1 : 0.85 + + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: parent.z - 1 + } + + Column { + id: menuColumn + + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 1 + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: contextMenu.isPinned ? "keep_off" : "push_pin" + size: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: contextMenu.isPinned ? "Unpin from Dock" : "Pin to Dock" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: pinMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) { + return + } + + if (contextMenu.isPinned) { + SessionData.removePinnedApp(contextMenu.appId) + } else { + SessionData.addPinnedApp(contextMenu.appId) + } + contextMenu.close() + } + } + } + + Rectangle { + width: parent.width - Theme.spacingS * 2 + height: 5 + anchors.horizontalCenter: parent.horizontalCenter + color: "transparent" + + Rectangle { + anchors.centerIn: parent + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "launch" + size: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Launch" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: launchMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (contextMenu.currentApp) + appLauncher.launchApp(contextMenu.currentApp) + + contextMenu.close() + } + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + MouseArea { + anchors.fill: parent + visible: contextMenu.visible + z: 999 + onClicked: { + contextMenu.close() + } + + MouseArea { + x: contextMenu.x + y: contextMenu.y + width: contextMenu.width + height: contextMenu.height + onClicked: { + + // Prevent closing when clicking on the menu itself + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/AppDrawer/AppLauncher.qml b/quickshell/.config/quickshell/Modules/AppDrawer/AppLauncher.qml new file mode 100644 index 0000000..16a2b5a --- /dev/null +++ b/quickshell/.config/quickshell/Modules/AppDrawer/AppLauncher.qml @@ -0,0 +1,168 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property string searchQuery: "" + property string selectedCategory: "All" + property string viewMode: "list" // "list" or "grid" + property int selectedIndex: 0 + property int maxResults: 50 + property int gridColumns: 4 + property bool debounceSearch: true + property int debounceInterval: 50 + property bool keyboardNavigationActive: false + readonly property var categories: { + const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science") + const result = ["All"] + return result.concat(allCategories.filter(cat => cat !== "All")) + } + readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category)) + property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {} + property alias model: filteredModel + property var _watchApplications: AppSearchService.applications + + signal appLaunched(var app) + signal categorySelected(string category) + signal viewModeSelected(string mode) + + function updateFilteredModel() { + filteredModel.clear() + selectedIndex = 0 + keyboardNavigationActive = false + + let apps = [] + if (searchQuery.length === 0) { + apps = selectedCategory === "All" ? AppSearchService.getAppsInCategory("All") : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults) + } else { + if (selectedCategory === "All") { + apps = AppSearchService.searchApplications(searchQuery) + } else { + const categoryApps = AppSearchService.getAppsInCategory(selectedCategory) + if (categoryApps.length > 0) { + const allSearchResults = AppSearchService.searchApplications(searchQuery) + const categoryNames = new Set(categoryApps.map(app => app.name)) + apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults) + } else { + apps = [] + } + } + } + + if (searchQuery.length === 0) { + apps = apps.sort((a, b) => { + const aId = a.id || a.execString || a.exec || "" + const bId = b.id || b.execString || b.exec || "" + const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0 + const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0 + if (aUsage !== bUsage) { + return bUsage - aUsage + } + return (a.name || "").localeCompare(b.name || "") + }) + } + + apps.forEach(app => { + if (app) { + filteredModel.append({ + "name": app.name || "", + "exec": app.execString || "", + "icon": app.icon || "application-x-executable", + "comment": app.comment || "", + "categories": app.categories || [], + "desktopEntry": app + }) + } + }) + } + + function selectNext() { + if (filteredModel.count === 0) { + return + } + keyboardNavigationActive = true + selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1) + } + + function selectPrevious() { + if (filteredModel.count === 0) { + return + } + keyboardNavigationActive = true + selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0) + } + + function selectNextInRow() { + if (filteredModel.count === 0 || viewMode !== "grid") { + return + } + keyboardNavigationActive = true + selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1) + } + + function selectPreviousInRow() { + if (filteredModel.count === 0 || viewMode !== "grid") { + return + } + keyboardNavigationActive = true + selectedIndex = Math.max(selectedIndex - 1, 0) + } + + function launchSelected() { + if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) { + return + } + const selectedApp = filteredModel.get(selectedIndex) + launchApp(selectedApp) + } + + function launchApp(appData) { + if (!appData) { + return + } + appData.desktopEntry.execute() + appLaunched(appData) + AppUsageHistoryData.addAppUsage(appData.desktopEntry) + } + + function setCategory(category) { + selectedCategory = category + categorySelected(category) + } + + function setViewMode(mode) { + viewMode = mode + viewModeSelected(mode) + } + + onSearchQueryChanged: { + if (debounceSearch) { + searchDebounceTimer.restart() + } else { + updateFilteredModel() + } + } + onSelectedCategoryChanged: updateFilteredModel() + onAppUsageRankingChanged: updateFilteredModel() + on_WatchApplicationsChanged: updateFilteredModel() + Component.onCompleted: { + updateFilteredModel() + } + + ListModel { + id: filteredModel + } + + Timer { + id: searchDebounceTimer + + interval: root.debounceInterval + repeat: false + onTriggered: updateFilteredModel() + } +} diff --git a/quickshell/.config/quickshell/Modules/AppDrawer/CategorySelector.qml b/quickshell/.config/quickshell/Modules/AppDrawer/CategorySelector.qml new file mode 100644 index 0000000..910c7f7 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/AppDrawer/CategorySelector.qml @@ -0,0 +1,143 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +Item { + id: root + + property var categories: [] + property string selectedCategory: "All" + property bool compact: false + + signal categorySelected(string category) + + readonly property int maxCompactItems: 8 + readonly property int itemHeight: 36 + readonly property color selectedBorderColor: "transparent" + readonly property color unselectedBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + + function handleCategoryClick(category) { + selectedCategory = category + categorySelected(category) + } + + function getButtonWidth(itemCount, containerWidth) { + return itemCount > 0 ? (containerWidth - (itemCount - 1) * Theme.spacingS) / itemCount : 0 + } + + height: compact ? itemHeight : (itemHeight * 2 + Theme.spacingS) + + Row { + visible: compact + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: categories ? categories.slice(0, Math.min(categories.length || 0, maxCompactItems)) : [] + + Rectangle { + property int itemCount: Math.min(categories ? categories.length || 0 : 0, maxCompactItems) + + height: root.itemHeight + width: root.getButtonWidth(itemCount, parent.width) + radius: Theme.cornerRadius + color: selectedCategory === modelData ? Theme.primary : "transparent" + border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor + + StyledText { + anchors.centerIn: parent + text: modelData + color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.handleCategoryClick(modelData) + } + } + } + } + + Column { + visible: !compact + width: parent.width + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: categories ? categories.slice(0, Math.min(4, categories.length || 0)) : [] + + Rectangle { + property int itemCount: Math.min(4, categories ? categories.length || 0 : 0) + + height: root.itemHeight + width: root.getButtonWidth(itemCount, parent.width) + radius: Theme.cornerRadius + color: selectedCategory === modelData ? Theme.primary : "transparent" + border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor + + StyledText { + anchors.centerIn: parent + text: modelData + color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.handleCategoryClick(modelData) + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + visible: categories && categories.length > 4 + + Repeater { + model: categories && categories.length > 4 ? categories.slice(4) : [] + + Rectangle { + property int itemCount: categories && categories.length > 4 ? categories.length - 4 : 0 + + height: root.itemHeight + width: root.getButtonWidth(itemCount, parent.width) + radius: Theme.cornerRadius + color: selectedCategory === modelData ? Theme.primary : "transparent" + border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor + + StyledText { + anchors.centerIn: parent + text: modelData + color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.handleCategoryClick(modelData) + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/ControlCenterPopout.qml b/quickshell/.config/quickshell/Modules/ControlCenter/ControlCenterPopout.qml new file mode 100644 index 0000000..890d548 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/ControlCenterPopout.qml @@ -0,0 +1,750 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Modules.ControlCenter +import qs.Modules.ControlCenter.Widgets +import qs.Modules.ControlCenter.Details +import qs.Modules.ControlCenter.Details 1.0 as Details +import qs.Services +import qs.Widgets + +DankPopout { + id: root + + property string expandedSection: "" + property bool powerOptionsExpanded: false + property string triggerSection: "right" + property var triggerScreen: null + + function setTriggerPosition(x, y, width, section, screen) { + triggerX = x + triggerY = y + triggerWidth = width + triggerSection = section + triggerScreen = screen + } + + function openWithSection(section) { + if (shouldBeVisible) { + close() + } else { + expandedSection = section + open() + } + } + + function toggleSection(section) { + if (expandedSection === section) { + expandedSection = "" + } else { + expandedSection = section + } + } + + signal powerActionRequested(string action, string title, string message) + signal lockRequested + + popupWidth: 550 + popupHeight: Math.min((triggerScreen?.height ?? 1080) - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400) + triggerX: (triggerScreen?.width ?? 1920) - 600 - Theme.spacingL + triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS + triggerWidth: 80 + positioning: "center" + screen: triggerScreen + shouldBeVisible: false + visible: shouldBeVisible + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + Qt.callLater(() => { + NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled + if (UserInfoService) + UserInfoService.getUptime() + }) + } else { + Qt.callLater(() => { + NetworkService.autoRefreshEnabled = false + if (BluetoothService.adapter + && BluetoothService.adapter.discovering) + BluetoothService.adapter.discovering = false + }) + } + } + + content: Component { + Rectangle { + id: controlContent + + implicitHeight: mainColumn.implicitHeight + Theme.spacingM + property alias bluetoothCodecSelector: bluetoothCodecSelector + + color: { + const transparency = Theme.popupTransparency || 0.92 + const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1) + return Qt.rgba(surface.r, surface.g, surface.b, transparency) + } + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.08) + border.width: 1 + antialiasing: true + smooth: true + + Column { + id: mainColumn + width: parent.width - Theme.spacingL * 2 + x: Theme.spacingL + y: Theme.spacingL + spacing: Theme.spacingS + + Rectangle { + width: parent.width + height: 90 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + Theme.getContentBackgroundAlpha() * 0.4) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.08) + border.width: 1 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + spacing: Theme.spacingL + + Item { + id: avatarContainer + + property bool hasImage: profileImageLoader.status === Image.Ready + + width: 64 + height: 64 + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "transparent" + border.color: Theme.primary + border.width: 1 + visible: parent.hasImage + } + + Image { + id: profileImageLoader + + source: { + if (PortalService.profileImage === "") + return "" + + if (PortalService.profileImage.startsWith( + "/")) + return "file://" + PortalService.profileImage + + return PortalService.profileImage + } + smooth: true + asynchronous: true + mipmap: true + cache: true + visible: false + } + + MultiEffect { + anchors.fill: parent + anchors.margins: 5 + source: profileImageLoader + maskEnabled: true + maskSource: circularMask + visible: avatarContainer.hasImage + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: circularMask + + width: 64 - 10 + height: 64 - 10 + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "black" + antialiasing: true + } + } + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: Theme.primary + visible: !parent.hasImage + + DankIcon { + anchors.centerIn: parent + name: "person" + size: Theme.iconSize + 8 + color: Theme.primaryText + } + } + + DankIcon { + anchors.centerIn: parent + name: "warning" + size: Theme.iconSize + 8 + color: Theme.primaryText + visible: PortalService.profileImage !== "" + && profileImageLoader.status === Image.Error + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + StyledText { + text: UserInfoService.fullName + || UserInfoService.username || "User" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: (UserInfoService.uptime + || "Unknown") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + font.weight: Font.Normal + } + } + } + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: Theme.spacingL + spacing: Theme.spacingS + + DankActionButton { + buttonSize: 40 + iconName: "lock" + iconSize: Theme.iconSize - 2 + iconColor: Theme.surfaceText + backgroundColor: Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.5) + onClicked: { + root.close() + root.lockRequested() + } + } + + DankActionButton { + buttonSize: 40 + iconName: root.powerOptionsExpanded ? "expand_less" : "power_settings_new" + iconSize: Theme.iconSize - 2 + iconColor: Theme.surfaceText + backgroundColor: Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.5) + onClicked: { + root.powerOptionsExpanded = !root.powerOptionsExpanded + } + + } + + DankActionButton { + buttonSize: 40 + iconName: "settings" + iconSize: Theme.iconSize - 2 + iconColor: Theme.surfaceText + backgroundColor: Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.5) + onClicked: { + root.close() + settingsModal.show() + } + } + } + } + + Item { + width: parent.width + implicitHeight: root.powerOptionsExpanded ? 60 : 0 + height: implicitHeight + clip: true + + Rectangle { + width: parent.width + height: 60 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + Theme.getContentBackgroundAlpha() * 0.4) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.08) + border.width: root.powerOptionsExpanded ? 1 : 0 + opacity: root.powerOptionsExpanded ? 1 : 0 + clip: true + + Row { + anchors.centerIn: parent + spacing: Theme.spacingL + visible: root.powerOptionsExpanded + + Rectangle { + width: 100 + height: 34 + radius: Theme.cornerRadius + color: logoutButton.containsMouse ? Qt.rgba( + Theme.primary.r, + Theme.primary.g, + Theme.primary.b, + 0.12) : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.5) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "logout" + size: Theme.fontSizeSmall + color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Logout" + font.pixelSize: Theme.fontSizeSmall + color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: logoutButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.powerOptionsExpanded = false + root.close() + root.powerActionRequested( + "logout", "Logout", + "Are you sure you want to logout?") + } + } + } + + Rectangle { + width: 100 + height: 34 + radius: Theme.cornerRadius + color: rebootButton.containsMouse ? Qt.rgba( + Theme.primary.r, + Theme.primary.g, + Theme.primary.b, + 0.12) : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.5) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "restart_alt" + size: Theme.fontSizeSmall + color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Restart" + font.pixelSize: Theme.fontSizeSmall + color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: rebootButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.powerOptionsExpanded = false + root.close() + root.powerActionRequested( + "reboot", "Restart", + "Are you sure you want to restart?") + } + } + } + + Rectangle { + width: 100 + height: 34 + radius: Theme.cornerRadius + color: suspendButton.containsMouse ? Qt.rgba( + Theme.primary.r, + Theme.primary.g, + Theme.primary.b, + 0.12) : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.5) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "bedtime" + size: Theme.fontSizeSmall + color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Suspend" + font.pixelSize: Theme.fontSizeSmall + color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: suspendButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.powerOptionsExpanded = false + root.close() + root.powerActionRequested( + "suspend", "Suspend", + "Are you sure you want to suspend?") + } + } + } + + Rectangle { + width: 100 + height: 34 + radius: Theme.cornerRadius + color: shutdownButton.containsMouse ? Qt.rgba( + Theme.primary.r, + Theme.primary.g, + Theme.primary.b, + 0.12) : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.5) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "power_settings_new" + size: Theme.fontSizeSmall + color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Shutdown" + font.pixelSize: Theme.fontSizeSmall + color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: shutdownButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.powerOptionsExpanded = false + root.close() + root.powerActionRequested( + "poweroff", "Shutdown", + "Are you sure you want to shutdown?") + } + } + } + } + } + + } + + Item { + width: parent.width + height: audioSliderRow.implicitHeight + + Row { + id: audioSliderRow + x: -Theme.spacingS + width: parent.width + Theme.spacingS * 2 + spacing: Theme.spacingM + + AudioSliderRow { + width: SettingsData.hideBrightnessSlider ? parent.width - Theme.spacingM : (parent.width - Theme.spacingM) / 2 + } + + Item { + width: (parent.width - Theme.spacingM) / 2 + height: parent.height + visible: !SettingsData.hideBrightnessSlider + + BrightnessSliderRow { + width: parent.width + height: parent.height + x: -Theme.spacingS + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + NetworkPill { + width: (parent.width - Theme.spacingM) / 2 + expanded: root.expandedSection === "network" + onClicked: { + if (NetworkService.wifiToggling) { + return + } + if (NetworkService.networkStatus === "ethernet") { + if (NetworkService.ethernetConnected && !NetworkService.wifiEnabled) { + NetworkService.toggleWifiRadio() + return + } + root.toggleSection("network") + return + } + if (NetworkService.networkStatus === "wifi") { + if (NetworkService.ethernetConnected) { + NetworkService.toggleWifiRadio() + return + } + NetworkService.disconnectWifi() + return + } + if (!NetworkService.wifiEnabled) { + NetworkService.toggleWifiRadio() + return + } + if (NetworkService.wifiEnabled && NetworkService.networkStatus === "disconnected") { + root.toggleSection("network") + } + } + onExpandClicked: root.toggleSection("network") + } + + BluetoothPill { + width: (parent.width - Theme.spacingM) / 2 + expanded: root.expandedSection === "bluetooth" + onClicked: { + if (BluetoothService.adapter) + BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled + } + onExpandClicked: root.toggleSection("bluetooth") + visible: BluetoothService.available + } + } + + Loader { + width: parent.width + active: root.expandedSection === "network" || root.expandedSection === "bluetooth" + visible: active + sourceComponent: DetailView { + width: parent.width + isVisible: true + title: { + switch (root.expandedSection) { + case "network": return "Network Settings" + case "bluetooth": return "Bluetooth Settings" + default: return "" + } + } + content: { + switch (root.expandedSection) { + case "network": return networkDetailComponent + case "bluetooth": return bluetoothDetailComponent + default: return null + } + } + contentHeight: 250 + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + AudioOutputPill { + width: (parent.width - Theme.spacingM) / 2 + expanded: root.expandedSection === "audio_output" + onClicked: { + if (AudioService.sink) { + AudioService.sink.audio.muted = !AudioService.sink.audio.muted + } + } + onExpandClicked: root.toggleSection("audio_output") + } + + AudioInputPill { + width: (parent.width - Theme.spacingM) / 2 + expanded: root.expandedSection === "audio_input" + onClicked: { + if (AudioService.source) { + AudioService.source.audio.muted = !AudioService.source.audio.muted + } + } + onExpandClicked: root.toggleSection("audio_input") + } + } + + Loader { + width: parent.width + active: root.expandedSection === "audio_output" || root.expandedSection === "audio_input" + visible: active + sourceComponent: DetailView { + width: parent.width + isVisible: true + title: { + switch (root.expandedSection) { + case "audio_output": return "Audio Output" + case "audio_input": return "Audio Input" + default: return "" + } + } + content: { + switch (root.expandedSection) { + case "audio_output": return audioOutputDetailComponent + case "audio_input": return audioInputDetailComponent + default: return null + } + } + contentHeight: 250 + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + ToggleButton { + width: (parent.width - Theme.spacingM) / 2 + iconName: DisplayService.nightModeEnabled ? "nightlight" : "dark_mode" + text: "Night Mode" + secondaryText: SessionData.nightModeAutoEnabled ? "Auto" : (DisplayService.nightModeEnabled ? "On" : "Off") + isActive: DisplayService.nightModeEnabled + enabled: DisplayService.automationAvailable + onClicked: DisplayService.toggleNightMode() + + DankIcon { + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + name: "schedule" + size: 12 + color: Theme.primary + visible: SessionData.nightModeAutoEnabled + opacity: 0.8 + } + } + + ToggleButton { + width: (parent.width - Theme.spacingM) / 2 + iconName: SessionData.isLightMode ? "light_mode" : "palette" + text: "Theme" + secondaryText: SessionData.isLightMode ? "Light" : "Dark" + isActive: true + onClicked: Theme.toggleLightMode() + } + } + } + + Details.BluetoothCodecSelector { + id: bluetoothCodecSelector + anchors.fill: parent + z: 10000 + } + } + } + + Component { + id: networkDetailComponent + NetworkDetail {} + } + + Component { + id: bluetoothDetailComponent + BluetoothDetail { + id: bluetoothDetail + onShowCodecSelector: function(device) { + if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) { + contentLoader.item.bluetoothCodecSelector.show(device) + contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) { + bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName) + }) + } + } + } + } + + Component { + id: audioOutputDetailComponent + AudioOutputDetail {} + } + + Component { + id: audioInputDetailComponent + AudioInputDetail {} + } +} diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml new file mode 100644 index 0000000..d8e7dbd --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml @@ -0,0 +1,178 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Services.Pipewire +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + implicitHeight: headerRow.height + volumeSlider.height + audioContent.height + Theme.spacingM + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + Row { + id: headerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingS + height: 40 + + StyledText { + id: headerText + text: "Input Devices" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + DankSlider { + id: volumeSlider + anchors.left: parent.left + anchors.right: parent.right + anchors.top: headerRow.bottom + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingXS + height: 35 + value: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0 + minimum: 0 + maximum: 100 + leftIcon: { + if (!AudioService.source || !AudioService.source.audio) return "mic_off" + let muted = AudioService.source.audio.muted + return muted ? "mic_off" : "mic" + } + rightIcon: "volume_up" + enabled: AudioService.source && AudioService.source.audio + unit: "%" + showValue: true + visible: AudioService.source && AudioService.source.audio + + onSliderValueChanged: function(newValue) { + if (AudioService.source && AudioService.source.audio) { + AudioService.source.audio.volume = newValue / 100 + } + } + + MouseArea { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: Theme.iconSize + Theme.spacingS * 2 + height: parent.height + onClicked: { + if (AudioService.source && AudioService.source.audio) { + AudioService.source.audio.muted = !AudioService.source.audio.muted + } + } + } + } + + DankFlickable { + id: audioContent + anchors.top: volumeSlider.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Theme.spacingM + anchors.topMargin: Theme.spacingS + contentHeight: audioColumn.height + clip: true + + Column { + id: audioColumn + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: Pipewire.nodes.values.filter(node => { + return node.audio && !node.isSink && !node.isStream + }) + + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2) + border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: modelData === AudioService.source ? 2 : 1 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: { + if (modelData.name.includes("bluez")) + return "headset" + else if (modelData.name.includes("usb")) + return "headset" + else + return "mic" + } + size: Theme.iconSize - 4 + color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM + + StyledText { + text: AudioService.displayName(modelData) + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal + elide: Text.ElideRight + width: parent.width + wrapMode: Text.NoWrap + } + + StyledText { + text: modelData === AudioService.source ? "Active" : "Available" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + width: parent.width + wrapMode: Text.NoWrap + } + } + } + + MouseArea { + id: deviceMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData) { + Pipewire.preferredDefaultAudioSource = modelData + } + } + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + Behavior on border.color { + ColorAnimation { duration: Theme.shortDuration } + } + } + } + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml new file mode 100644 index 0000000..7755620 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml @@ -0,0 +1,138 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Services.Pipewire +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + implicitHeight: headerRow.height + audioContent.height + Theme.spacingM + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + Row { + id: headerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingS + height: 40 + + StyledText { + id: headerText + text: "Audio Devices" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + DankFlickable { + id: audioContent + anchors.top: headerRow.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Theme.spacingM + anchors.topMargin: Theme.spacingM + contentHeight: audioColumn.height + clip: true + + Column { + id: audioColumn + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: Pipewire.nodes.values.filter(node => { + return node.audio && node.isSink && !node.isStream + }) + + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2) + border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: modelData === AudioService.sink ? 2 : 1 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: { + if (modelData.name.includes("bluez")) + return "headset" + else if (modelData.name.includes("hdmi")) + return "tv" + else if (modelData.name.includes("usb")) + return "headset" + else + return "speaker" + } + size: Theme.iconSize - 4 + color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM + + StyledText { + text: AudioService.displayName(modelData) + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal + elide: Text.ElideRight + width: parent.width + wrapMode: Text.NoWrap + } + + StyledText { + text: modelData === AudioService.sink ? "Active" : "Available" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + width: parent.width + wrapMode: Text.NoWrap + } + } + } + + MouseArea { + id: deviceMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData) { + Pipewire.preferredDefaultAudioSink = modelData + } + } + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + Behavior on border.color { + ColorAnimation { duration: Theme.shortDuration } + } + } + } + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Details/BluetoothCodecSelector.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Details/BluetoothCodecSelector.qml new file mode 100644 index 0000000..d79e4e8 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Details/BluetoothCodecSelector.qml @@ -0,0 +1,307 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property var device: null + property bool modalVisible: false + property var parentItem + property var availableCodecs: [] + property string currentCodec: "" + property bool isLoading: false + + signal codecSelected(string deviceAddress, string codecName) + + function show(bluetoothDevice) { + device = bluetoothDevice; + isLoading = true; + availableCodecs = []; + currentCodec = ""; + visible = true; + modalVisible = true; + queryCodecs(); + Qt.callLater(() => { + focusScope.forceActiveFocus(); + }); + } + + function hide() { + modalVisible = false; + Qt.callLater(() => { + visible = false; + }); + } + + function queryCodecs() { + if (!device) + return; + + BluetoothService.getAvailableCodecs(device, function(codecs, current) { + availableCodecs = codecs; + currentCodec = current; + isLoading = false; + }); + } + + function selectCodec(profileName) { + if (!device || isLoading) + return; + + let selectedCodec = availableCodecs.find(c => c.profile === profileName); + if (selectedCodec && device) { + BluetoothService.updateDeviceCodec(device.address, selectedCodec.name); + codecSelected(device.address, selectedCodec.name); + } + + isLoading = true; + BluetoothService.switchCodec(device, profileName, function(success, message) { + isLoading = false; + if (success) { + ToastService.showToast(message, ToastService.levelInfo); + Qt.callLater(root.hide); + } else { + ToastService.showToast(message, ToastService.levelError); + } + }); + } + + visible: false + anchors.fill: parent + z: 2000 + + MouseArea { + id: modalBlocker + anchors.fill: parent + visible: modalVisible + enabled: modalVisible + hoverEnabled: true + preventStealing: true + propagateComposedEvents: false + + onClicked: root.hide() + onWheel: (wheel) => { wheel.accepted = true } + onPositionChanged: (mouse) => { mouse.accepted = true } + } + + Rectangle { + id: modalBackground + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.5) + opacity: modalVisible ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + FocusScope { + id: focusScope + + anchors.fill: parent + focus: root.visible + enabled: root.visible + + Keys.onEscapePressed: { + root.hide() + event.accepted = true + } + } + + Rectangle { + id: modalContent + anchors.centerIn: parent + width: 320 + height: contentColumn.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + opacity: modalVisible ? 1 : 0 + scale: modalVisible ? 1 : 0.9 + + MouseArea { + anchors.fill: parent + hoverEnabled: true + preventStealing: true + propagateComposedEvents: false + onClicked: (mouse) => { mouse.accepted = true } + onWheel: (wheel) => { wheel.accepted = true } + onPositionChanged: (mouse) => { mouse.accepted = true } + } + + Column { + id: contentColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: device ? BluetoothService.getDeviceIcon(device) : "headset" + size: Theme.iconSize + 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: device ? (device.name || device.deviceName) : "" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: "Audio Codec Selection" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + + } + + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) + } + + StyledText { + text: isLoading ? "Loading codecs..." : `Current: ${currentCodec}` + font.pixelSize: Theme.fontSizeSmall + color: isLoading ? Theme.primary : Theme.surfaceTextMedium + font.weight: Font.Medium + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: !isLoading + + Repeater { + model: availableCodecs + + Rectangle { + width: parent.width + height: 48 + radius: Theme.cornerRadius + color: { + if (modelData.name === currentCodec) + return Theme.surfaceContainerHigh; + else if (codecMouseArea.containsMouse) + return Theme.surfaceHover; + else + return "transparent"; + } + border.color: "transparent" + border.width: 1 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + Rectangle { + width: 6 + height: 6 + radius: 3 + color: modelData.qualityColor + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + color: modelData.name === currentCodec ? Theme.primary : Theme.surfaceText + font.weight: modelData.name === currentCodec ? Font.Medium : Font.Normal + } + + StyledText { + text: modelData.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + + } + + } + + DankIcon { + name: "check" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + visible: modelData.name === currentCodec + } + + MouseArea { + id: codecMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: modelData.name !== currentCodec && !isLoading + onClicked: { + selectCodec(modelData.profile); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + } + + } + + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + } + + } +} diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml new file mode 100644 index 0000000..7de1f6c --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml @@ -0,0 +1,543 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Bluetooth +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + implicitHeight: BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + property var bluetoothCodecModalRef: null + + signal showCodecSelector(var device) + + function updateDeviceCodecDisplay(deviceAddress, codecName) { + for (let i = 0; i < pairedRepeater.count; i++) { + let item = pairedRepeater.itemAt(i) + if (item && item.modelData && item.modelData.address === deviceAddress) { + item.currentCodec = codecName + break + } + } + } + + Row { + id: headerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingS + height: 40 + + StyledText { + id: headerText + text: "Bluetooth Settings" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: Math.max(0, parent.width - headerText.implicitWidth - scanButton.width - Theme.spacingM) + height: parent.height + } + + Rectangle { + id: scanButton + width: 100 + height: 36 + radius: 18 + color: { + if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + return scanMouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent" + } + border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + visible: BluetoothService.adapter && BluetoothService.adapter.enabled + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching" + size: 18 + color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Scanning" : "Scan" + color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: scanMouseArea + anchors.fill: parent + hoverEnabled: true + enabled: BluetoothService.adapter && BluetoothService.adapter.enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + if (BluetoothService.adapter) + BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + DankFlickable { + id: bluetoothContent + anchors.top: headerRow.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Theme.spacingM + anchors.topMargin: Theme.spacingM + visible: BluetoothService.adapter && BluetoothService.adapter.enabled + contentHeight: bluetoothColumn.height + clip: true + + Column { + id: bluetoothColumn + width: parent.width + spacing: Theme.spacingS + + + Repeater { + id: pairedRepeater + model: { + if (!BluetoothService.adapter || !BluetoothService.adapter.devices) + return [] + + let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))] + devices.sort((a, b) => { + if (a.connected && !b.connected) return -1 + if (!a.connected && b.connected) return 1 + return (b.signalStrength || 0) - (a.signalStrength || 0) + }) + return devices + } + + delegate: Rectangle { + required property var modelData + required property int index + + property string currentCodec: BluetoothService.deviceCodecs[modelData.address] || "" + + width: parent.width + height: 50 + radius: Theme.cornerRadius + + Component.onCompleted: { + if (modelData.connected && BluetoothService.isAudioDevice(modelData)) { + BluetoothService.refreshDeviceCodec(modelData) + } + } + color: { + if (modelData.state === BluetoothDeviceState.Connecting) + return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) + if (deviceMouseArea.containsMouse) + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2) + } + border.color: { + if (modelData.state === BluetoothDeviceState.Connecting) + return Theme.warning + if (modelData.connected) + return Theme.primary + return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + border.width: (modelData.connected || modelData.state === BluetoothDeviceState.Connecting) ? 2 : 1 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: BluetoothService.getDeviceIcon(modelData) + size: Theme.iconSize - 4 + color: { + if (modelData.state === BluetoothDeviceState.Connecting) + return Theme.warning + if (modelData.connected) + return Theme.primary + return Theme.surfaceText + } + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + StyledText { + text: modelData.name || modelData.deviceName || "Unknown Device" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: modelData.connected ? Font.Medium : Font.Normal + elide: Text.ElideRight + width: parent.width + } + + Row { + spacing: Theme.spacingXS + + StyledText { + text: { + if (modelData.state === BluetoothDeviceState.Connecting) + return "Connecting..." + if (modelData.connected) { + let status = "Connected" + if (currentCodec) { + status += " • " + currentCodec + } + return status + } + return "Paired" + } + font.pixelSize: Theme.fontSizeSmall + color: { + if (modelData.state === BluetoothDeviceState.Connecting) + return Theme.warning + return Theme.surfaceVariantText + } + } + + StyledText { + text: { + if (modelData.batteryAvailable && modelData.battery > 0) + return "• " + Math.round(modelData.battery * 100) + "%" + + var btBattery = BatteryService.bluetoothDevices.find(dev => { + return dev.name === (modelData.name || modelData.deviceName) || + dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) || + (modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase()) + }) + return btBattery ? "• " + btBattery.percentage + "%" : "" + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: text.length > 0 + } + + StyledText { + text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: text.length > 0 + } + } + } + } + + DankActionButton { + id: pairedOptionsButton + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + iconName: "more_horiz" + buttonSize: 28 + onClicked: { + if (bluetoothContextMenu.visible) { + bluetoothContextMenu.close() + } else { + bluetoothContextMenu.currentDevice = modelData + bluetoothContextMenu.popup(pairedOptionsButton, -bluetoothContextMenu.width + pairedOptionsButton.width, pairedOptionsButton.height + Theme.spacingXS) + } + } + } + + MouseArea { + id: deviceMouseArea + anchors.fill: parent + anchors.rightMargin: pairedOptionsButton.width + Theme.spacingS + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.connected) { + modelData.disconnect() + } else { + BluetoothService.connectDeviceWithTrust(modelData) + } + } + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + Behavior on border.color { + ColorAnimation { duration: Theme.shortDuration } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + visible: pairedRepeater.count > 0 && availableRepeater.count > 0 + } + + + Item { + width: parent.width + height: 80 + visible: BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0 + + DankIcon { + anchors.centerIn: parent + name: "sync" + size: 24 + color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4) + + RotationAnimation on rotation { + running: parent.visible && BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0 + loops: Animation.Infinite + from: 0 + to: 360 + duration: 1500 + } + } + } + + Repeater { + id: availableRepeater + model: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return [] + + var filtered = Bluetooth.devices.values.filter(dev => { + return dev && !dev.paired && !dev.pairing && !dev.blocked && + (dev.signalStrength === undefined || dev.signalStrength > 0) + }) + return BluetoothService.sortDevices(filtered) + } + + delegate: Rectangle { + required property var modelData + required property int index + + property bool canConnect: BluetoothService.canConnect(modelData) + property bool isBusy: BluetoothService.isDeviceBusy(modelData) + + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: availableMouseArea.containsMouse && !isBusy ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.15) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + opacity: canConnect ? 1 : 0.6 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: BluetoothService.getDeviceIcon(modelData) + size: Theme.iconSize - 4 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + StyledText { + text: modelData.name || modelData.deviceName || "Unknown Device" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.width + } + + Row { + spacing: Theme.spacingXS + + StyledText { + text: { + if (modelData.pairing) return "Pairing..." + if (modelData.blocked) return "Blocked" + return BluetoothService.getSignalStrength(modelData) + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: text.length > 0 && !modelData.pairing && !modelData.blocked + } + } + } + } + + StyledText { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + text: { + if (modelData.pairing) return "Pairing..." + if (!canConnect) return "Cannot pair" + return "Pair" + } + font.pixelSize: Theme.fontSizeSmall + color: canConnect ? Theme.primary : Theme.surfaceVariantText + font.weight: Font.Medium + } + + MouseArea { + id: availableMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: canConnect && !isBusy + onClicked: { + if (modelData) { + BluetoothService.connectDeviceWithTrust(modelData) + } + } + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + } + } + + Item { + width: parent.width + height: 60 + visible: !BluetoothService.adapter + + StyledText { + anchors.centerIn: parent + text: "No Bluetooth adapter found" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + } + } + } + + Menu { + id: bluetoothContextMenu + width: 150 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + + property var currentDevice: null + + background: Rectangle { + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.width: 1 + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + MenuItem { + text: bluetoothContextMenu.currentDevice && bluetoothContextMenu.currentDevice.connected ? "Disconnect" : "Connect" + height: 32 + + contentItem: StyledText { + text: parent.text + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + leftPadding: Theme.spacingS + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + radius: Theme.cornerRadius / 2 + } + + onTriggered: { + if (bluetoothContextMenu.currentDevice) { + if (bluetoothContextMenu.currentDevice.connected) { + bluetoothContextMenu.currentDevice.disconnect() + } else { + BluetoothService.connectDeviceWithTrust(bluetoothContextMenu.currentDevice) + } + } + } + } + + MenuItem { + text: "Audio Codec" + height: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected ? 32 : 0 + visible: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected + + contentItem: StyledText { + text: parent.text + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + leftPadding: Theme.spacingS + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + radius: Theme.cornerRadius / 2 + } + + onTriggered: { + if (bluetoothContextMenu.currentDevice) { + showCodecSelector(bluetoothContextMenu.currentDevice) + } + } + } + + MenuItem { + text: "Forget Device" + height: 32 + + contentItem: StyledText { + text: parent.text + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + leftPadding: Theme.spacingS + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent" + radius: Theme.cornerRadius / 2 + } + + onTriggered: { + if (bluetoothContextMenu.currentDevice) { + bluetoothContextMenu.currentDevice.forget() + } + } + } + } + +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml new file mode 100644 index 0000000..3207020 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml @@ -0,0 +1,533 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modals + +Rectangle { + implicitHeight: { + if (NetworkService.wifiToggling) { + return headerRow.height + wifiToggleContent.height + Theme.spacingM + } + if (NetworkService.wifiEnabled) { + return headerRow.height + wifiContent.height + Theme.spacingM + } + return headerRow.height + wifiOffContent.height + Theme.spacingM + } + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + Component.onCompleted: { + NetworkService.addRef() + if (NetworkService.wifiEnabled) { + NetworkService.scanWifi() + } + } + + Component.onDestruction: { + NetworkService.removeRef() + } + + property var wifiPasswordModalRef: { + wifiPasswordModalLoader.active = true + return wifiPasswordModalLoader.item + } + property var networkInfoModalRef: { + networkInfoModalLoader.active = true + return networkInfoModalLoader.item + } + + Row { + id: headerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingS + height: 40 + + StyledText { + id: headerText + text: "Network Settings" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: Math.max(0, parent.width - headerText.implicitWidth - preferenceControls.width - Theme.spacingM) + height: parent.height + } + + Row { + id: preferenceControls + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + visible: NetworkService.ethernetConnected && NetworkService.wifiConnected + + Rectangle { + property bool isActive: NetworkService.userPreference === "ethernet" + + width: 90 + height: 36 + radius: 18 + color: isActive ? Theme.surfaceContainerHigh : ethernetMouseArea.containsMouse ? Theme.surfaceHover : "transparent" + + StyledText { + anchors.centerIn: parent + text: "Ethernet" + color: parent.isActive ? Theme.primary : Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: parent.isActive ? Font.Medium : Font.Normal + } + + MouseArea { + id: ethernetMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: NetworkService.setNetworkPreference("ethernet") + cursorShape: Qt.PointingHandCursor + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + Rectangle { + property bool isActive: NetworkService.userPreference === "wifi" + + width: 70 + height: 36 + radius: 18 + color: isActive ? Theme.surfaceContainerHigh : wifiMouseArea.containsMouse ? Theme.surfaceHover : "transparent" + + StyledText { + anchors.centerIn: parent + text: "WiFi" + color: parent.isActive ? Theme.primary : Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: parent.isActive ? Font.Medium : Font.Normal + } + + MouseArea { + id: wifiMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: NetworkService.setNetworkPreference("wifi") + cursorShape: Qt.PointingHandCursor + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + + Item { + id: wifiToggleContent + anchors.top: headerRow.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingM + anchors.topMargin: Theme.spacingM + visible: NetworkService.wifiToggling + height: visible ? 80 : 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingM + + DankIcon { + anchors.horizontalCenter: parent.horizontalCenter + name: "sync" + size: 32 + color: Theme.primary + + RotationAnimation on rotation { + running: NetworkService.wifiToggling + loops: Animation.Infinite + from: 0 + to: 360 + duration: 1000 + } + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..." + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + } + } + } + + Item { + id: wifiOffContent + anchors.top: headerRow.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingM + anchors.topMargin: Theme.spacingM + visible: !NetworkService.wifiEnabled && !NetworkService.wifiToggling + height: visible ? 120 : 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingL + width: parent.width + + DankIcon { + anchors.horizontalCenter: parent.horizontalCenter + name: "wifi_off" + size: 48 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: "WiFi is off" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + horizontalAlignment: Text.AlignHCenter + } + + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + width: 120 + height: 36 + radius: 18 + color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) + border.width: 1 + border.color: Theme.primary + + StyledText { + anchors.centerIn: parent + text: "Enable WiFi" + color: Theme.primary + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + } + + MouseArea { + id: enableWifiButton + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NetworkService.toggleWifiRadio() + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + + DankFlickable { + id: wifiContent + anchors.top: headerRow.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Theme.spacingM + anchors.topMargin: Theme.spacingM + visible: NetworkService.wifiInterface && NetworkService.wifiEnabled && !NetworkService.wifiToggling + contentHeight: wifiColumn.height + clip: true + + Column { + id: wifiColumn + width: parent.width + spacing: Theme.spacingS + + Item { + width: parent.width + height: 200 + visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling + + DankIcon { + anchors.centerIn: parent + name: "refresh" + size: 48 + color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3) + + RotationAnimation on rotation { + running: true + loops: Animation.Infinite + from: 0 + to: 360 + duration: 1000 + } + } + } + + Repeater { + model: { + let networks = [...NetworkService.wifiNetworks] + networks.sort((a, b) => { + if (a.ssid === NetworkService.currentWifiSSID) return -1 + if (b.ssid === NetworkService.currentWifiSSID) return 1 + return b.signal - a.signal + }) + return networks + } + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2) + border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: modelData.ssid === NetworkService.currentWifiSSID ? 2 : 1 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: { + let strength = modelData.signal || 0 + if (strength >= 70) return "signal_wifi_4_bar" + if (strength >= 50) return "network_wifi_3_bar" + if (strength >= 25) return "network_wifi_2_bar" + if (strength >= 10) return "network_wifi_1_bar" + return "signal_wifi_bad" + } + size: Theme.iconSize - 4 + color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + StyledText { + text: modelData.ssid || "Unknown Network" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal + elide: Text.ElideRight + width: parent.width + } + + Row { + spacing: Theme.spacingXS + + StyledText { + text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: modelData.saved ? "• Saved" : "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + visible: text.length > 0 + } + + StyledText { + text: "• " + modelData.signal + "%" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + + DankActionButton { + id: optionsButton + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + iconName: "more_horiz" + buttonSize: 28 + onClicked: { + if (networkContextMenu.visible) { + networkContextMenu.close() + } else { + networkContextMenu.currentSSID = modelData.ssid + networkContextMenu.currentSecured = modelData.secured + networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID + networkContextMenu.currentSaved = modelData.saved + networkContextMenu.currentSignal = modelData.signal + networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS) + } + } + } + + MouseArea { + id: networkMouseArea + anchors.fill: parent + anchors.rightMargin: optionsButton.width + Theme.spacingS + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: function(event) { + if (modelData.ssid !== NetworkService.currentWifiSSID) { + if (modelData.secured && !modelData.saved) { + if (wifiPasswordModalRef) { + wifiPasswordModalRef.show(modelData.ssid) + } + } else { + NetworkService.connectToWifi(modelData.ssid) + } + } + event.accepted = true + } + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + Behavior on border.color { + ColorAnimation { duration: Theme.shortDuration } + } + } + } + } + } + + Menu { + id: networkContextMenu + width: 150 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + + property string currentSSID: "" + property bool currentSecured: false + property bool currentConnected: false + property bool currentSaved: false + property int currentSignal: 0 + + background: Rectangle { + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.width: 1 + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + MenuItem { + text: networkContextMenu.currentConnected ? "Disconnect" : "Connect" + height: 32 + + contentItem: StyledText { + text: parent.text + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + leftPadding: Theme.spacingS + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + radius: Theme.cornerRadius / 2 + } + + onTriggered: { + if (networkContextMenu.currentConnected) { + NetworkService.disconnectWifi() + } else { + if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) { + if (wifiPasswordModalRef) { + wifiPasswordModalRef.show(networkContextMenu.currentSSID) + } + } else { + NetworkService.connectToWifi(networkContextMenu.currentSSID) + } + } + } + } + + MenuItem { + text: "Network Info" + height: 32 + + contentItem: StyledText { + text: parent.text + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + leftPadding: Theme.spacingS + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + radius: Theme.cornerRadius / 2 + } + + onTriggered: { + if (networkInfoModalRef) { + let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID) + networkInfoModalRef.showNetworkInfo(networkContextMenu.currentSSID, networkData) + } + } + } + + MenuItem { + text: "Forget Network" + height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0 + visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected + + contentItem: StyledText { + text: parent.text + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + leftPadding: Theme.spacingS + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent" + radius: Theme.cornerRadius / 2 + } + + onTriggered: { + NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID) + } + } + } + + LazyLoader { + id: wifiPasswordModalLoader + active: false + + WifiPasswordModal { + id: wifiPasswordModal + } + } + + LazyLoader { + id: networkInfoModalLoader + active: false + + NetworkInfoModal { + id: networkInfoModal + } + } + + +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/PowerMenu.qml b/quickshell/.config/quickshell/Modules/ControlCenter/PowerMenu.qml new file mode 100644 index 0000000..a4acfeb --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/PowerMenu.qml @@ -0,0 +1,310 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Widgets + +PanelWindow { + id: root + + property bool powerMenuVisible: false + signal powerActionRequested(string action, string title, string message) + + visible: powerMenuVisible + implicitWidth: 400 + implicitHeight: 320 + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + MouseArea { + anchors.fill: parent + onClicked: { + powerMenuVisible = false + } + } + + Rectangle { + width: Math.min(320, parent.width - Theme.spacingL * 2) + height: 320 // Fixed height to prevent cropping + x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL) + y: Theme.barHeight + Theme.spacingXS + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.08) + border.width: 1 + opacity: powerMenuVisible ? 1 : 0 + scale: powerMenuVisible ? 1 : 0.85 + + MouseArea { + + anchors.fill: parent + onClicked: { + + } + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + + StyledText { + text: "Power Options" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - 150 + height: 1 + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: { + powerMenuVisible = false + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Rectangle { + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r, + Theme.primary.g, + Theme.primary.b, + 0.08) : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.08) + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "logout" + size: Theme.iconSize + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Log Out" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: logoutArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + powerMenuVisible = false + root.powerActionRequested( + "logout", "Log Out", + "Are you sure you want to log out?") + } + } + } + + Rectangle { + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r, + Theme.primary.g, + Theme.primary.b, + 0.08) : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.08) + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "bedtime" + size: Theme.iconSize + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Suspend" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: suspendArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + powerMenuVisible = false + root.powerActionRequested( + "suspend", "Suspend", + "Are you sure you want to suspend the system?") + } + } + } + + Rectangle { + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r, + Theme.warning.g, + Theme.warning.b, + 0.08) : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.08) + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "restart_alt" + size: Theme.iconSize + color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Reboot" + font.pixelSize: Theme.fontSizeMedium + color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: rebootArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + powerMenuVisible = false + root.powerActionRequested( + "reboot", "Reboot", + "Are you sure you want to reboot the system?") + } + } + } + + Rectangle { + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r, + Theme.error.g, + Theme.error.b, + 0.08) : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.08) + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "power_settings_new" + size: Theme.iconSize + color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Power Off" + font.pixelSize: Theme.fontSizeMedium + color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: powerOffArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + powerMenuVisible = false + root.powerActionRequested( + "poweroff", "Power Off", + "Are you sure you want to power off the system?") + } + } + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioInputPill.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioInputPill.qml new file mode 100644 index 0000000..298f3a3 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioInputPill.qml @@ -0,0 +1,57 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Services.Pipewire +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.ControlCenter.Widgets + +BasePill { + id: root + + property var defaultSource: AudioService.source + + iconName: { + if (!defaultSource) return "mic_off" + + let volume = defaultSource.audio.volume + let muted = defaultSource.audio.muted + + if (muted || volume === 0.0) return "mic_off" + return "mic" + } + + isActive: defaultSource && !defaultSource.audio.muted + + primaryText: { + if (!defaultSource) { + return "No input device" + } + return defaultSource.description || "Audio Input" + } + + secondaryText: { + if (!defaultSource) { + return "Select device" + } + if (defaultSource.audio.muted) { + return "Muted" + } + return Math.round(defaultSource.audio.volume * 100) + "%" + } + + onWheelEvent: function (wheelEvent) { + if (!defaultSource || !defaultSource.audio) return + let delta = wheelEvent.angleDelta.y + let currentVolume = defaultSource.audio.volume * 100 + let newVolume + if (delta > 0) + newVolume = Math.min(100, currentVolume + 5) + else + newVolume = Math.max(0, currentVolume - 5) + defaultSource.audio.muted = false + defaultSource.audio.volume = newVolume / 100 + wheelEvent.accepted = true + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioOutputPill.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioOutputPill.qml new file mode 100644 index 0000000..3d1c679 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioOutputPill.qml @@ -0,0 +1,60 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Services.Pipewire +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.ControlCenter.Widgets + +BasePill { + id: root + + property var defaultSink: AudioService.sink + + iconName: { + if (!defaultSink) return "volume_off" + + let volume = defaultSink.audio.volume + let muted = defaultSink.audio.muted + + if (muted || volume === 0.0) return "volume_off" + if (volume <= 0.33) return "volume_down" + if (volume <= 0.66) return "volume_up" + return "volume_up" + } + + isActive: defaultSink && !defaultSink.audio.muted + + primaryText: { + if (!defaultSink) { + return "No output device" + } + return defaultSink.description || "Audio Output" + } + + secondaryText: { + if (!defaultSink) { + return "Select device" + } + if (defaultSink.audio.muted) { + return "Muted" + } + return Math.round(defaultSink.audio.volume * 100) + "%" + } + + onWheelEvent: function (wheelEvent) { + if (!defaultSink || !defaultSink.audio) return + let delta = wheelEvent.angleDelta.y + let currentVolume = defaultSink.audio.volume * 100 + let newVolume + if (delta > 0) + newVolume = Math.min(100, currentVolume + 5) + else + newVolume = Math.max(0, currentVolume - 5) + defaultSink.audio.muted = false + defaultSink.audio.volume = newVolume / 100 + AudioService.volumeChanged() + wheelEvent.accepted = true + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioSlider.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioSlider.qml new file mode 100644 index 0000000..cc194ab --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioSlider.qml @@ -0,0 +1,50 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Services.Pipewire +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.ControlCenter.Widgets + +SimpleSlider { + id: root + + property var defaultSink: AudioService.sink + + iconName: { + if (!defaultSink) return "volume_off" + + let volume = defaultSink.audio.volume + let muted = defaultSink.audio.muted + + if (muted || volume === 0.0) return "volume_off" + if (volume <= 0.33) return "volume_down" + if (volume <= 0.66) return "volume_up" + return "volume_up" + } + + iconColor: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText + + enabled: defaultSink !== null + allowIconClick: defaultSink !== null + + value: defaultSink ? defaultSink.audio.volume : 0.0 + maximumValue: 1.0 + minimumValue: 0.0 + + onSliderValueChanged: function(newValue) { + if (defaultSink) { + defaultSink.audio.volume = newValue + if (newValue > 0 && defaultSink.audio.muted) { + defaultSink.audio.muted = false + } + } + } + + onIconClicked: function() { + if (defaultSink) { + defaultSink.audio.muted = !defaultSink.audio.muted + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml new file mode 100644 index 0000000..20e2236 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml @@ -0,0 +1,75 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Services.Pipewire +import qs.Common +import qs.Services +import qs.Widgets + +Row { + id: root + + property var defaultSink: AudioService.sink + + height: 40 + spacing: Theme.spacingS + + Rectangle { + width: Theme.iconSize + Theme.spacingS * 2 + height: Theme.iconSize + Theme.spacingS * 2 + anchors.verticalCenter: parent.verticalCenter + radius: (Theme.iconSize + Theme.spacingS * 2) / 2 // Make it circular + color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + MouseArea { + id: iconArea + anchors.fill: parent + visible: defaultSink !== null + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (defaultSink) { + defaultSink.audio.muted = !defaultSink.audio.muted + } + } + } + + DankIcon { + anchors.centerIn: parent + name: { + if (!defaultSink) return "volume_off" + + let volume = defaultSink.audio.volume + let muted = defaultSink.audio.muted + + if (muted || volume === 0.0) return "volume_off" + if (volume <= 0.33) return "volume_down" + if (volume <= 0.66) return "volume_up" + return "volume_up" + } + size: Theme.iconSize + color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText + } + } + + DankSlider { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM + enabled: defaultSink !== null + minimum: 0 + maximum: 100 + value: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0 + onSliderValueChanged: function(newValue) { + if (defaultSink) { + defaultSink.audio.volume = newValue / 100.0 + if (newValue > 0 && defaultSink.audio.muted) { + defaultSink.audio.muted = false + } + } + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BasePill.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BasePill.qml new file mode 100644 index 0000000..0e37299 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BasePill.qml @@ -0,0 +1,149 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string iconName: "" + property color iconColor: Theme.surfaceText + property string primaryText: "" + property string secondaryText: "" + property bool expanded: false + property bool isActive: false + + signal clicked() + signal expandClicked() + signal wheelEvent(var wheelEvent) + + width: parent ? parent.width : 200 + height: 60 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + Rectangle { + id: mainArea + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width - expandArea.width + topLeftRadius: Theme.cornerRadius + bottomLeftRadius: Theme.cornerRadius + topRightRadius: 0 + bottomRightRadius: 0 + color: mainAreaMouse.containsMouse ? + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : + "transparent" + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: root.iconName + size: Theme.iconSize + color: root.isActive ? Theme.primary : root.iconColor + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Theme.iconSize - Theme.spacingS + spacing: 2 + + StyledText { + width: parent.width + text: root.primaryText + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + StyledText { + width: parent.width + text: root.secondaryText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: text.length > 0 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + + MouseArea { + id: mainAreaMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + onWheel: function (wheelEvent) { + root.wheelEvent(wheelEvent) + } + } + } + + Rectangle { + id: expandArea + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + width: Theme.iconSize + Theme.spacingM * 2 + topLeftRadius: 0 + bottomLeftRadius: 0 + topRightRadius: Theme.cornerRadius + bottomRightRadius: Theme.cornerRadius + color: expandAreaMouse.containsMouse ? + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : + "transparent" + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + DankIcon { + id: expandIcon + anchors.centerIn: parent + name: expanded ? "expand_less" : "expand_more" + size: Theme.iconSize - 2 + color: Theme.surfaceVariantText + } + + MouseArea { + id: expandAreaMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.expandClicked() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BluetoothPill.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BluetoothPill.qml new file mode 100644 index 0000000..dccd295 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BluetoothPill.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.ControlCenter.Widgets + +BasePill { + id: root + + property var primaryDevice: { + if (!BluetoothService.adapter || !BluetoothService.adapter.devices) { + return null + } + + let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))] + for (let device of devices) { + if (device && device.connected) { + return device + } + } + return null + } + + iconName: { + if (!BluetoothService.available) { + return "bluetooth_disabled" + } + if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) { + return "bluetooth_disabled" + } + if (primaryDevice) { + return BluetoothService.getDeviceIcon(primaryDevice) + } + return "bluetooth" + } + + isActive: !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled) + + primaryText: { + if (!BluetoothService.available) { + return "Bluetooth unavailable" + } + if (!BluetoothService.adapter) { + return "No adapter" + } + if (!BluetoothService.adapter.enabled) { + return "Disabled" + } + return "Enabled" + } + + secondaryText: { + if (!BluetoothService.available) { + return "Hardware not found" + } + if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) { + return "Off" + } + if (primaryDevice) { + return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device" + } + return "No devices" + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BrightnessSlider.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BrightnessSlider.qml new file mode 100644 index 0000000..69cb6f5 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BrightnessSlider.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.ControlCenter.Widgets + +SimpleSlider { + id: root + + iconName: { + if (!DisplayService.brightnessAvailable) return "brightness_low" + + let brightness = DisplayService.brightnessLevel + if (brightness <= 33) return "brightness_low" + if (brightness <= 66) return "brightness_medium" + return "brightness_high" + } + + iconColor: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText + + enabled: DisplayService.brightnessAvailable + + value: DisplayService.brightnessLevel + maximumValue: 100.0 + minimumValue: 0.0 + + onSliderValueChanged: function(newValue) { + if (DisplayService.brightnessAvailable) { + DisplayService.brightnessLevel = newValue + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BrightnessSliderRow.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BrightnessSliderRow.qml new file mode 100644 index 0000000..b19ee73 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/BrightnessSliderRow.qml @@ -0,0 +1,142 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Row { + id: root + + height: 40 + spacing: Theme.spacingS + + Rectangle { + width: Theme.iconSize + Theme.spacingS * 2 + height: Theme.iconSize + Theme.spacingS * 2 + anchors.verticalCenter: parent.verticalCenter + radius: (Theme.iconSize + Theme.spacingS * 2) / 2 + color: iconArea.containsMouse + ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + : "transparent" + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + MouseArea { + id: iconArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: function(event) { + if (DisplayService.devices.length > 1) { + if (deviceMenu.visible) { + deviceMenu.close() + } else { + deviceMenu.popup(iconArea, 0, iconArea.height + Theme.spacingXS) + } + event.accepted = true + } + } + } + + DankIcon { + anchors.centerIn: parent + name: { + if (!DisplayService.brightnessAvailable) return "brightness_low" + + let brightness = DisplayService.brightnessLevel + if (brightness <= 33) return "brightness_low" + if (brightness <= 66) return "brightness_medium" + return "brightness_high" + } + size: Theme.iconSize + color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText + } + } + + DankSlider { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM + enabled: DisplayService.brightnessAvailable + minimum: 1 + maximum: 100 + value: { + let level = DisplayService.brightnessLevel + if (level > 100) { + let deviceInfo = DisplayService.getCurrentDeviceInfo() + if (deviceInfo && deviceInfo.max > 0) { + return Math.round((level / deviceInfo.max) * 100) + } + return 50 + } + return level + } + onSliderValueChanged: function(newValue) { + if (DisplayService.brightnessAvailable) { + DisplayService.setBrightness(newValue) + } + } + } + + Menu { + id: deviceMenu + width: 200 + closePolicy: Popup.CloseOnEscape + + background: Rectangle { + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.width: 1 + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Instantiator { + model: DisplayService.devices + delegate: MenuItem { + required property var modelData + required property int index + + property string deviceName: modelData.name || "" + property string deviceClass: modelData.class || "" + + text: deviceName + font.pixelSize: Theme.fontSizeMedium + height: 40 + + indicator: Rectangle { + visible: DisplayService.currentDevice === parent.deviceName + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingS + width: 4 + height: parent.height - Theme.spacingS * 2 + radius: 2 + color: Theme.primary + } + + contentItem: StyledText { + text: parent.text + font: parent.font + color: DisplayService.currentDevice === parent.deviceName ? Theme.primary : Theme.surfaceText + leftPadding: Theme.spacingL + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + radius: Theme.cornerRadius / 2 + } + + onTriggered: { + DisplayService.setCurrentDevice(deviceName, true) + deviceMenu.close() + } + } + onObjectAdded: (index, object) => deviceMenu.insertItem(index, object) + onObjectRemoved: (index, object) => deviceMenu.removeItem(object) + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/CompactSlider.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/CompactSlider.qml new file mode 100644 index 0000000..4f187f1 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/CompactSlider.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string iconName: "" + property color iconColor: Theme.surfaceText + property string labelText: "" + property real value: 0.0 + property real maximumValue: 1.0 + property real minimumValue: 0.0 + property bool enabled: true + + signal sliderValueChanged(real value) + + width: parent ? parent.width : 200 + height: 60 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + opacity: enabled ? 1.0 : 0.6 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.right: sliderContainer.left + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: root.iconName + size: Theme.iconSize + color: root.iconColor + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: root.labelText + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + id: sliderContainer + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: Theme.spacingM + width: 120 + height: parent.height - Theme.spacingS * 2 + + DankSlider { + anchors.centerIn: parent + width: parent.width + enabled: root.enabled + minimum: Math.round(root.minimumValue * 100) + maximum: Math.round(root.maximumValue * 100) + value: Math.round(root.value * 100) + onSliderValueChanged: root.sliderValueChanged(newValue / 100.0) + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/DetailView.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/DetailView.qml new file mode 100644 index 0000000..bffa0ff --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/DetailView.qml @@ -0,0 +1,29 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string title: "" + property Component content: null + property bool isVisible: true + property int contentHeight: 300 + + width: parent ? parent.width : 400 + implicitHeight: isVisible ? contentHeight : 0 + height: implicitHeight + color: "transparent" + clip: true + + Loader { + id: contentLoader + anchors.fill: parent + sourceComponent: root.content + asynchronous: true + } + + +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/NetworkPill.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/NetworkPill.qml new file mode 100644 index 0000000..51a673e --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/NetworkPill.qml @@ -0,0 +1,72 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.ControlCenter.Widgets + +BasePill { + id: root + + isActive: { + if (NetworkService.wifiToggling) { + return false + } + if (NetworkService.networkStatus === "ethernet") { + return true + } + if (NetworkService.networkStatus === "wifi") { + return true + } + return NetworkService.wifiEnabled + } + + iconName: { + if (NetworkService.wifiToggling) { + return "sync" + } + if (NetworkService.networkStatus === "ethernet") { + return "settings_ethernet" + } + if (NetworkService.networkStatus === "wifi") { + return NetworkService.wifiSignalIcon + } + if (NetworkService.wifiEnabled) { + return "signal_wifi_off" + } + return "wifi_off" + } + + primaryText: { + if (NetworkService.wifiToggling) { + return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..." + } + if (NetworkService.networkStatus === "ethernet") { + return "Ethernet" + } + if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) { + return NetworkService.currentWifiSSID + } + if (NetworkService.wifiEnabled) { + return "Not connected" + } + return "WiFi off" + } + + secondaryText: { + if (NetworkService.wifiToggling) { + return "Please wait..." + } + if (NetworkService.networkStatus === "ethernet") { + return "Connected" + } + if (NetworkService.networkStatus === "wifi") { + return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected" + } + if (NetworkService.wifiEnabled) { + return "Select network" + } + return "Tap to enable" + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/SimpleSlider.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/SimpleSlider.qml new file mode 100644 index 0000000..cc77999 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/SimpleSlider.qml @@ -0,0 +1,50 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Widgets + +Row { + id: root + + property string iconName: "" + property color iconColor: Theme.surfaceText + property real value: 0.0 + property real maximumValue: 1.0 + property real minimumValue: 0.0 + property bool enabled: true + property bool allowIconClick: false + + signal sliderValueChanged(real value) + signal iconClicked() + + height: 60 + spacing: Theme.spacingM + + DankIcon { + name: root.iconName + size: Theme.iconSize + color: root.iconColor + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + visible: root.allowIconClick + cursorShape: Qt.PointingHandCursor + onClicked: root.iconClicked() + } + } + + DankSlider { + anchors.verticalCenter: parent.verticalCenter + width: { + if (parent.width <= 0) return 80 + return Math.max(80, Math.min(400, parent.width - Theme.iconSize - Theme.spacingM)) + } + enabled: root.enabled + minimum: Math.round(root.minimumValue * 100) + maximum: Math.round(root.maximumValue * 100) + value: Math.round(root.value * 100) + onSliderValueChanged: function(newValue) { root.sliderValueChanged(newValue / 100.0) } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/ToggleButton.qml b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/ToggleButton.qml new file mode 100644 index 0000000..8a6b4b7 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ControlCenter/Widgets/ToggleButton.qml @@ -0,0 +1,95 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string iconName: "" + property string text: "" + property bool isActive: false + property bool enabled: true + property string secondaryText: "" + + signal clicked() + + width: parent ? parent.width : 200 + height: 60 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + opacity: enabled ? 1.0 : 0.6 + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: mouseArea.containsMouse ? + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : + "transparent" + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + } + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: root.iconName + size: Theme.iconSize + color: root.isActive ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Theme.iconSize - Theme.spacingS + spacing: 2 + + StyledText { + width: parent.width + text: root.text + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + StyledText { + width: parent.width + text: root.secondaryText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: text.length > 0 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: root.enabled + onClicked: root.clicked() + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/DankDashPopout.qml b/quickshell/.config/quickshell/Modules/DankDash/DankDashPopout.qml new file mode 100644 index 0000000..64f5d2a --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/DankDashPopout.qml @@ -0,0 +1,315 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Mpris +import Quickshell.Wayland +import qs.Common +import qs.Widgets +import qs.Modules.DankDash + +DankPopout { + id: root + + property bool dashVisible: false + property var triggerScreen: null + property int currentTabIndex: 0 + + function setTriggerPosition(x, y, width, section, screen) { + triggerScreen = screen + triggerY = y + + const screenWidth = screen ? screen.width : Screen.width + triggerX = (screenWidth - popupWidth) / 2 + triggerWidth = popupWidth + } + + popupWidth: 700 + popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500 + triggerX: Screen.width - 620 - Theme.spacingL + triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2 + triggerWidth: 80 + shouldBeVisible: dashVisible + visible: shouldBeVisible + + property bool __focusArmed: false + property bool __contentReady: false + + function __tryFocusOnce() { + if (!__focusArmed) + return + const win = root.window + if (!win || !win.visible) + return + if (!contentLoader.item) + return + + if (win.requestActivate) + win.requestActivate() + contentLoader.item.forceActiveFocus(Qt.TabFocusReason) + + if (contentLoader.item.activeFocus) + __focusArmed = false + } + + onDashVisibleChanged: { + if (dashVisible) { + __focusArmed = true + __contentReady = !!contentLoader.item + open() + __tryFocusOnce() + } else { + __focusArmed = false + __contentReady = false + close() + } + } + + Connections { + target: contentLoader + function onLoaded() { + __contentReady = true + if (__focusArmed) + __tryFocusOnce() + } + } + + Connections { + target: root.window ? root.window : null + enabled: !!root.window + function onVisibleChanged() { + if (__focusArmed) + __tryFocusOnce() + } + } + + onBackgroundClicked: { + dashVisible = false + } + + content: Component { + Rectangle { + id: mainContainer + + implicitHeight: contentColumn.height + Theme.spacingM * 2 + color: Theme.surfaceContainer + radius: Theme.cornerRadius + focus: true + + Component.onCompleted: { + if (root.shouldBeVisible) { + mainContainer.forceActiveFocus() + } + } + + Connections { + target: root + function onShouldBeVisibleChanged() { + if (root.shouldBeVisible) { + Qt.callLater(function () { + mainContainer.forceActiveFocus() + }) + } + } + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Escape) { + root.dashVisible = false + event.accepted = true + return + } + + if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) { + let nextIndex = root.currentTabIndex + 1 + while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) { + nextIndex++ + } + if (nextIndex >= tabBar.model.length) { + nextIndex = 0 + } + root.currentTabIndex = nextIndex + event.accepted = true + return + } + + if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) { + let prevIndex = root.currentTabIndex - 1 + while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) { + prevIndex-- + } + if (prevIndex < 0) { + prevIndex = tabBar.model.length - 1 + while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) { + prevIndex-- + } + } + if (prevIndex >= 0) { + root.currentTabIndex = prevIndex + } + event.accepted = true + return + } + + if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) { + if (wallpaperTab.handleKeyEvent(event)) { + event.accepted = true + return + } + } + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04) + radius: parent.radius + + SequentialAnimation on opacity { + running: root.shouldBeVisible + loops: Animation.Infinite + + NumberAnimation { + to: 0.08 + duration: Theme.extraLongDuration + easing.type: Theme.standardEasing + } + + NumberAnimation { + to: 0.02 + duration: Theme.extraLongDuration + easing.type: Theme.standardEasing + } + } + } + + Column { + id: contentColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + DankTabBar { + id: tabBar + + width: parent.width + height: 48 + currentIndex: root.currentTabIndex + spacing: Theme.spacingS + equalWidthTabs: true + enableArrowNavigation: false + focus: false + activeFocusOnTab: false + nextFocusTarget: { + const item = pages.currentItem + if (!item) + return null + if (item.focusTarget) + return item.focusTarget + return item + } + + model: { + let tabs = [{ + "icon": "dashboard", + "text": "Overview" + }, { + "icon": "music_note", + "text": "Media" + }, { + "icon": "wallpaper", + "text": "Wallpapers" + }] + + if (SettingsData.weatherEnabled) { + tabs.push({ + "icon": "wb_sunny", + "text": "Weather" + }) + } + + tabs.push({ + "icon": "settings", + "text": "Settings", + "isAction": true + }) + return tabs + } + + onTabClicked: function (index) { + root.currentTabIndex = index + } + + onActionTriggered: function (index) { + let settingsIndex = SettingsData.weatherEnabled ? 4 : 3 + if (index === settingsIndex) { + dashVisible = false + settingsModal.show() + } + } + } + + Item { + width: parent.width + height: Theme.spacingXS + } + + StackLayout { + id: pages + width: parent.width + implicitHeight: { + if (currentIndex === 0) + return overviewTab.implicitHeight + if (currentIndex === 1) + return mediaTab.implicitHeight + if (currentIndex === 2) + return wallpaperTab.implicitHeight + if (SettingsData.weatherEnabled && currentIndex === 3) + return weatherTab.implicitHeight + return overviewTab.implicitHeight + } + currentIndex: root.currentTabIndex + + OverviewTab { + id: overviewTab + + onCloseDash: { + root.dashVisible = false + } + + onSwitchToWeatherTab: { + if (SettingsData.weatherEnabled) { + tabBar.currentIndex = 3 + tabBar.tabClicked(3) + } + } + + onSwitchToMediaTab: { + tabBar.currentIndex = 1 + tabBar.tabClicked(1) + } + } + + MediaPlayerTab { + id: mediaTab + } + + WallpaperTab { + id: wallpaperTab + active: root.currentTabIndex === 2 + tabBarItem: tabBar + keyForwardTarget: mainContainer + targetScreen: root.triggerScreen + } + + WeatherTab { + id: weatherTab + visible: SettingsData.weatherEnabled && root.currentTabIndex === 3 + } + } + } + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/MediaPlayerTab.qml b/quickshell/.config/quickshell/Modules/DankDash/MediaPlayerTab.qml new file mode 100644 index 0000000..872cf25 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/MediaPlayerTab.qml @@ -0,0 +1,759 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Shapes +import Quickshell.Services.Mpris +import Quickshell.Services.Pipewire +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property MprisPlayer activePlayer: MprisController.activePlayer + property string lastValidTitle: "" + property string lastValidArtist: "" + property string lastValidAlbum: "" + property string lastValidArtUrl: "" + property real currentPosition: activePlayer && activePlayer.positionSupported ? activePlayer.position : 0 + property real displayPosition: currentPosition + property var defaultSink: AudioService.sink + + readonly property real ratio: { + if (!activePlayer || activePlayer.length <= 0) { + return 0 + } + const calculatedRatio = displayPosition / activePlayer.length + return Math.max(0, Math.min(1, calculatedRatio)) + } + + implicitWidth: 700 + implicitHeight: 410 + + onActivePlayerChanged: { + if (activePlayer && activePlayer.positionSupported) { + currentPosition = Qt.binding(() => activePlayer?.position || 0) + } else { + currentPosition = 0 + } + } + + Timer { + id: positionTimer + interval: 300 + running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressSliderArea.isSeeking + repeat: true + onTriggered: activePlayer && activePlayer.positionSupported && activePlayer.positionChanged() + } + + Timer { + id: cleanupTimer + interval: 2000 + running: !activePlayer + onTriggered: { + lastValidTitle = "" + lastValidArtist = "" + lastValidAlbum = "" + lastValidArtUrl = "" + currentPosition = 0 + stop() + } + } + + Column { + anchors.centerIn: parent + spacing: Theme.spacingM + visible: (!activePlayer && !lastValidTitle) || (activePlayer && activePlayer.trackTitle === "" && lastValidTitle === "") + + DankIcon { + name: "music_note" + size: Theme.iconSize * 3 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: "No Active Players" + font.pixelSize: Theme.fontSizeLarge + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Item { + anchors.fill: parent + visible: (activePlayer && activePlayer.trackTitle !== "") || lastValidTitle !== "" + + // Left Column: Album Art and Controls (60%) + Column { + x: 0 + y: 0 + width: parent.width * 0.6 - Theme.spacingM + height: parent.height + spacing: Theme.spacingL + + // Album Art Section + Item { + width: parent.width + height: parent.height * 0.55 + anchors.horizontalCenter: parent.horizontalCenter + + Item { + width: Math.min(parent.width * 0.8, parent.height * 0.9) + height: width + anchors.centerIn: parent + + Loader { + active: activePlayer?.playbackState === MprisPlaybackState.Playing + sourceComponent: Component { + Ref { + service: CavaService + } + } + } + + Shape { + id: morphingBlob + width: parent.width * 1.1 + height: parent.height * 1.1 + anchors.centerIn: parent + visible: activePlayer?.playbackState === MprisPlaybackState.Playing + asynchronous: false + antialiasing: true + preferredRendererType: Shape.CurveRenderer + z: 0 + + layer.enabled: true + layer.smooth: true + layer.samples: 4 + + readonly property real centerX: width / 2 + readonly property real centerY: height / 2 + readonly property real baseRadius: Math.min(width, height) * 0.35 + readonly property int segments: 24 + + property var audioLevels: { + if (!CavaService.cavaAvailable || CavaService.values.length === 0) { + return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5] + } + return CavaService.values + } + + property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5] + property var cubics: [] + + onAudioLevelsChanged: updatePath() + + Timer { + running: morphingBlob.visible + interval: 16 + repeat: true + onTriggered: morphingBlob.updatePath() + } + + Component { + id: cubicSegment + PathCubic {} + } + + Component.onCompleted: { + shapePath.pathElements.push(Qt.createQmlObject( + 'import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath + )) + + for (let i = 0; i < segments; i++) { + const seg = cubicSegment.createObject(shapePath) + shapePath.pathElements.push(seg) + cubics.push(seg) + } + + updatePath() + } + + function expSmooth(prev, next, alpha) { + return prev + alpha * (next - prev) + } + + function updatePath() { + if (cubics.length === 0) return + + for (let i = 0; i < Math.min(smoothedLevels.length, audioLevels.length); i++) { + smoothedLevels[i] = expSmooth(smoothedLevels[i], audioLevels[i], 0.2) + } + + const points = [] + for (let i = 0; i < segments; i++) { + const angle = (i / segments) * 2 * Math.PI + const audioIndex = i % Math.min(smoothedLevels.length, 6) + const audioLevel = Math.max(0.1, Math.min(1.5, (smoothedLevels[audioIndex] || 0) / 50)) + + const radius = baseRadius * (1.0 + audioLevel * 0.3) + const x = centerX + Math.cos(angle) * radius + const y = centerY + Math.sin(angle) * radius + points.push({x: x, y: y}) + } + + const startMove = shapePath.pathElements[0] + startMove.x = points[0].x + startMove.y = points[0].y + + const tension = 0.5 + for (let i = 0; i < segments; i++) { + const p0 = points[(i - 1 + segments) % segments] + const p1 = points[i] + const p2 = points[(i + 1) % segments] + const p3 = points[(i + 2) % segments] + + const c1x = p1.x + (p2.x - p0.x) * tension / 3 + const c1y = p1.y + (p2.y - p0.y) * tension / 3 + const c2x = p2.x - (p3.x - p1.x) * tension / 3 + const c2y = p2.y - (p3.y - p1.y) * tension / 3 + + const seg = cubics[i] + seg.control1X = c1x + seg.control1Y = c1y + seg.control2X = c2x + seg.control2Y = c2y + seg.x = p2.x + seg.y = p2.y + } + } + + ShapePath { + id: shapePath + fillColor: Theme.primary + strokeColor: "transparent" + strokeWidth: 0 + joinStyle: ShapePath.RoundJoin + fillRule: ShapePath.WindingFill + } + } + + Rectangle { + width: parent.width * 0.75 + height: width + radius: width / 2 + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Theme.surfaceContainer + border.width: 1 + anchors.centerIn: parent + z: 1 + + Image { + id: albumArt + source: (activePlayer && activePlayer.trackArtUrl) || lastValidArtUrl || "" + onSourceChanged: { + if (activePlayer && activePlayer.trackArtUrl && albumArt.status !== Image.Error) { + lastValidArtUrl = activePlayer.trackArtUrl + } + } + anchors.fill: parent + anchors.margins: 2 + fillMode: Image.PreserveAspectCrop + smooth: true + mipmap: true + cache: true + asynchronous: true + visible: false + onStatusChanged: { + if (status === Image.Error) { + console.warn("Failed to load album art:", source) + source = "" + if (activePlayer && activePlayer.trackArtUrl === source) { + lastValidArtUrl = "" + } + } + } + } + + MultiEffect { + anchors.fill: parent + anchors.margins: 2 + source: albumArt + maskEnabled: true + maskSource: circularMask + visible: albumArt.status === Image.Ready + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: circularMask + width: parent.width - 4 + height: parent.height - 4 + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "black" + antialiasing: true + } + } + + DankIcon { + anchors.centerIn: parent + name: "album" + size: parent.width * 0.3 + color: Theme.surfaceVariantText + visible: albumArt.status !== Image.Ready + } + } + } + } + + // Song Info and Controls Section + Column { + width: parent.width + height: parent.height * 0.45 + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + // Song Info + Column { + width: parent.width + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: (activePlayer && activePlayer.trackTitle) || lastValidTitle || "Unknown Track" + onTextChanged: { + if (activePlayer && activePlayer.trackTitle) { + lastValidTitle = activePlayer.trackTitle + } + } + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + wrapMode: Text.NoWrap + maximumLineCount: 1 + } + + StyledText { + text: (activePlayer && activePlayer.trackArtist) || lastValidArtist || "Unknown Artist" + onTextChanged: { + if (activePlayer && activePlayer.trackArtist) { + lastValidArtist = activePlayer.trackArtist + } + } + font.pixelSize: Theme.fontSizeMedium + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) + width: parent.width + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + wrapMode: Text.NoWrap + maximumLineCount: 1 + } + + StyledText { + text: (activePlayer && activePlayer.trackAlbum) || lastValidAlbum || "" + onTextChanged: { + if (activePlayer && activePlayer.trackAlbum) { + lastValidAlbum = activePlayer.trackAlbum + } + } + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + width: parent.width + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + wrapMode: Text.NoWrap + maximumLineCount: 1 + visible: text.length > 0 + } + } + + // Progress Bar + Item { + id: progressSlider + width: parent.width + height: 20 + visible: activePlayer?.length > 0 + + property real value: ratio + property real lineWidth: 2.5 + property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40) + property color fillColor: Theme.primary + property color playheadColor: Theme.primary + readonly property real midY: height / 2 + + // Background track + Rectangle { + width: parent.width + height: progressSlider.lineWidth + anchors.verticalCenter: parent.verticalCenter + color: progressSlider.trackColor + radius: height / 2 + } + + // Filled portion + Rectangle { + width: Math.max(0, Math.min(parent.width, parent.width * progressSlider.value)) + height: progressSlider.lineWidth + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + color: progressSlider.fillColor + radius: height / 2 + Behavior on width { NumberAnimation { duration: 80 } } + } + + // Playhead + Rectangle { + id: playhead + width: 2.5 + height: Math.max(progressSlider.lineWidth + 8, 12) + radius: width / 2 + color: progressSlider.playheadColor + x: Math.max(0, Math.min(progressSlider.width, progressSlider.width * progressSlider.value)) - width / 2 + y: progressSlider.midY - height / 2 + z: 3 + Behavior on x { NumberAnimation { duration: 80 } } + } + + MouseArea { + id: progressSliderArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false + + property bool isSeeking: false + property real pendingSeekPosition: -1 + + Timer { + id: seekDebounceTimer + interval: 150 + onTriggered: { + if (progressSliderArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) { + const clamped = Math.min(progressSliderArea.pendingSeekPosition, activePlayer.length * 0.99) + activePlayer.position = clamped + progressSliderArea.pendingSeekPosition = -1 + } + } + } + + onPressed: (mouse) => { + isSeeking = true + if (activePlayer?.length > 0 && activePlayer?.canSeek) { + const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width)) + pendingSeekPosition = r * activePlayer.length + displayPosition = pendingSeekPosition + seekDebounceTimer.restart() + } + } + onReleased: { + isSeeking = false + seekDebounceTimer.stop() + if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) { + const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99) + activePlayer.position = clamped + pendingSeekPosition = -1 + } + displayPosition = Qt.binding(() => currentPosition) + } + onPositionChanged: (mouse) => { + if (pressed && isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) { + const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width)) + pendingSeekPosition = r * activePlayer.length + displayPosition = pendingSeekPosition + seekDebounceTimer.restart() + } + } + onClicked: (mouse) => { + if (activePlayer?.length > 0 && activePlayer?.canSeek) { + const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width)) + activePlayer.position = r * activePlayer.length + } + } + } + } + + // Media Controls + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + height: 64 + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "skip_previous" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: prevBtnArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!activePlayer) { + return + } + + if (activePlayer.position > 8 && activePlayer.canSeek) { + activePlayer.position = 0 + } else { + activePlayer.previous() + } + } + } + } + + Rectangle { + width: 44 + height: 44 + radius: 22 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow" + size: 24 + color: Theme.background + weight: 500 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: activePlayer && activePlayer.togglePlaying() + } + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 6 + shadowBlur: 1.0 + shadowColor: Qt.rgba(0, 0, 0, 0.3) + shadowOpacity: 0.3 + } + } + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "skip_next" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: nextBtnArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: activePlayer && activePlayer.next() + } + } + } + } + } + + // Right Column: Audio Controls (40%) + Column { + x: parent.width * 0.6 + Theme.spacingM + y: 0 + width: parent.width * 0.4 - Theme.spacingM + height: parent.height + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + // Volume Control + Row { + x: -Theme.spacingS + width: parent.width + Theme.spacingS + height: 40 + spacing: Theme.spacingXS + + Rectangle { + width: Theme.iconSize + Theme.spacingS * 2 + height: Theme.iconSize + Theme.spacingS * 2 + anchors.verticalCenter: parent.verticalCenter + radius: (Theme.iconSize + Theme.spacingS * 2) / 2 + color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + MouseArea { + id: iconArea + anchors.fill: parent + visible: defaultSink !== null + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (defaultSink) { + defaultSink.audio.muted = !defaultSink.audio.muted + } + } + } + + DankIcon { + anchors.centerIn: parent + name: { + if (!defaultSink) return "volume_off" + + let volume = defaultSink.audio.volume + let muted = defaultSink.audio.muted + + if (muted || volume === 0.0) return "volume_off" + if (volume <= 0.33) return "volume_down" + if (volume <= 0.66) return "volume_up" + return "volume_up" + } + size: Theme.iconSize + color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText + } + } + + DankSlider { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingXS + enabled: defaultSink !== null + minimum: 0 + maximum: 100 + value: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0 + onSliderValueChanged: function(newValue) { + if (defaultSink) { + defaultSink.audio.volume = newValue / 100.0 + if (newValue > 0 && defaultSink.audio.muted) { + defaultSink.audio.muted = false + } + } + } + } + } + + // Audio Devices + DankFlickable { + width: parent.width + height: parent.height - y + contentHeight: deviceColumn.height + clip: true + + Column { + id: deviceColumn + width: parent.width + spacing: Theme.spacingXS + + Repeater { + model: Pipewire.nodes.values.filter(node => { + return node.audio && node.isSink && !node.isStream + }) + + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 42 + radius: Theme.cornerRadius + color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2) + border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: modelData === AudioService.sink ? 2 : 1 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: { + if (modelData.name.includes("bluez")) + return "headset" + else if (modelData.name.includes("hdmi")) + return "tv" + else if (modelData.name.includes("usb")) + return "headset" + else + return "speaker" + } + size: Theme.iconSize - 4 + color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingS * 2 + + StyledText { + text: AudioService.displayName(modelData) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal + elide: Text.ElideRight + width: parent.width + wrapMode: Text.NoWrap + } + + StyledText { + text: modelData === AudioService.sink ? "Active" : "Available" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + width: parent.width + wrapMode: Text.NoWrap + } + } + } + + MouseArea { + id: deviceMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData) { + Pipewire.preferredDefaultAudioSink = modelData + } + } + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + Behavior on border.color { + ColorAnimation { duration: Theme.shortDuration } + } + } + } + } + } + } + } + + MouseArea { + id: progressMouseArea + anchors.fill: parent + enabled: false + visible: false + property bool isSeeking: false + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/Overview/CalendarOverviewCard.qml b/quickshell/.config/quickshell/Modules/DankDash/Overview/CalendarOverviewCard.qml new file mode 100644 index 0000000..978ef6d --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/Overview/CalendarOverviewCard.qml @@ -0,0 +1,447 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property bool showEventDetails: false + property date selectedDate: systemClock.date + property var selectedDateEvents: [] + property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0 + + signal closeDash() + + function weekStartJs() { + return Qt.locale().firstDayOfWeek % 7 + } + + function startOfWeek(dateObj) { + const d = new Date(dateObj) + const jsDow = d.getDay() + const diff = (jsDow - weekStartJs() + 7) % 7 + d.setDate(d.getDate() - diff) + return d + } + + function endOfWeek(dateObj) { + const d = new Date(dateObj) + const jsDow = d.getDay() + const add = (weekStartJs() + 6 - jsDow + 7) % 7 + d.setDate(d.getDate() + add) + return d + } + + function updateSelectedDateEvents() { + if (CalendarService && CalendarService.khalAvailable) { + const events = CalendarService.getEventsForDate(selectedDate) + selectedDateEvents = events + } else { + selectedDateEvents = [] + } + } + + function loadEventsForMonth() { + if (!CalendarService || !CalendarService.khalAvailable) { + return + } + + const firstOfMonth = new Date(calendarGrid.displayDate.getFullYear(), + calendarGrid.displayDate.getMonth(), 1) + const lastOfMonth = new Date(calendarGrid.displayDate.getFullYear(), + calendarGrid.displayDate.getMonth() + 1, 0) + + const startDate = startOfWeek(firstOfMonth) + startDate.setDate(startDate.getDate() - 7) + + const endDate = endOfWeek(lastOfMonth) + endDate.setDate(endDate.getDate() + 7) + + CalendarService.loadEvents(startDate, endDate) + } + + onSelectedDateChanged: updateSelectedDateEvents() + Component.onCompleted: { + loadEventsForMonth() + updateSelectedDateEvents() + } + + Connections { + function onEventsByDateChanged() { + updateSelectedDateEvents() + } + + function onKhalAvailableChanged() { + if (CalendarService && CalendarService.khalAvailable) { + loadEventsForMonth() + } + updateSelectedDateEvents() + } + + target: CalendarService + enabled: CalendarService !== null + } + + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) + border.width: 1 + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Item { + width: parent.width + height: 40 + visible: showEventDetails + + Rectangle { + width: 32 + height: 32 + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + radius: Theme.cornerRadius + color: backButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "arrow_back" + size: 14 + color: Theme.primary + } + + MouseArea { + id: backButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.showEventDetails = false + } + } + + StyledText { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 32 + Theme.spacingS * 2 + anchors.rightMargin: Theme.spacingS + height: 40 + anchors.verticalCenter: parent.verticalCenter + text: { + const dateStr = Qt.formatDate(selectedDate, "MMM d") + if (selectedDateEvents && selectedDateEvents.length > 0) { + const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 event") : selectedDateEvents.length + " " + I18n.tr("events") + return dateStr + " • " + eventCount + } + return dateStr + } + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + } + Row { + width: parent.width + height: 28 + visible: !showEventDetails + + Rectangle { + width: 28 + height: 28 + radius: Theme.cornerRadius + color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "chevron_left" + size: 14 + color: Theme.primary + } + + MouseArea { + id: prevMonthArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + let newDate = new Date(calendarGrid.displayDate) + newDate.setMonth(newDate.getMonth() - 1) + calendarGrid.displayDate = newDate + loadEventsForMonth() + } + } + } + + StyledText { + width: parent.width - 56 + height: 28 + text: calendarGrid.displayDate.toLocaleDateString(Qt.locale(), "MMMM yyyy") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + Rectangle { + width: 28 + height: 28 + radius: Theme.cornerRadius + color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "chevron_right" + size: 14 + color: Theme.primary + } + + MouseArea { + id: nextMonthArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + let newDate = new Date(calendarGrid.displayDate) + newDate.setMonth(newDate.getMonth() + 1) + calendarGrid.displayDate = newDate + loadEventsForMonth() + } + } + } + } + + Row { + width: parent.width + height: 18 + visible: !showEventDetails + + Repeater { + model: { + const days = [] + const loc = Qt.locale() + const qtFirst = loc.firstDayOfWeek + for (let i = 0; i < 7; ++i) { + const qtDay = ((qtFirst - 1 + i) % 7) + 1 + days.push(loc.dayName(qtDay, Locale.ShortFormat)) + } + return days + } + + Rectangle { + width: parent.width / 7 + height: 18 + color: "transparent" + + StyledText { + anchors.centerIn: parent + text: modelData + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + font.weight: Font.Medium + } + } + } + } + + Grid { + id: calendarGrid + visible: !showEventDetails + + property date displayDate: systemClock.date + property date selectedDate: systemClock.date + + readonly property date firstDay: { + const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1) + return startOfWeek(firstOfMonth) + } + + width: parent.width + height: parent.height - 28 - 18 - Theme.spacingS * 2 + columns: 7 + rows: 6 + + Repeater { + model: 42 + + Rectangle { + readonly property date dayDate: { + const date = new Date(parent.firstDay) + date.setDate(date.getDate() + index) + return date + } + readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth() + readonly property bool isToday: dayDate.toDateString() === new Date().toDateString() + readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString() + + width: parent.width / 7 + height: parent.height / 6 + color: "transparent" + + Rectangle { + anchors.centerIn: parent + width: Math.min(parent.width - 4, parent.height - 4, 32) + height: width + color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + radius: width / 2 + + StyledText { + anchors.centerIn: parent + text: dayDate.getDate() + font.pixelSize: Theme.fontSizeSmall + color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) + font.weight: isToday ? Font.Medium : Font.Normal + } + + Rectangle { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 4 + width: 12 + height: 2 + radius: 1 + visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate) + color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary + opacity: isToday ? 0.9 : 0.7 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + MouseArea { + id: dayArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) { + root.selectedDate = dayDate + root.showEventDetails = true + } + } + } + } + } + } + DankListView { + width: parent.width - Theme.spacingS * 2 + height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + model: selectedDateEvents + visible: showEventDetails + clip: true + spacing: Theme.spacingXS + + delegate: Rectangle { + width: parent ? parent.width : 0 + height: eventContent.implicitHeight + Theme.spacingS + radius: Theme.cornerRadius + color: { + if (modelData.url && eventMouseArea.containsMouse) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + } else if (eventMouseArea.containsMouse) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) + } + return Theme.surfaceContainerHigh + } + border.color: { + if (modelData.url && eventMouseArea.containsMouse) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) + } else if (eventMouseArea.containsMouse) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) + } + return "transparent" + } + border.width: 1 + + Rectangle { + width: 3 + height: parent.height - 6 + anchors.left: parent.left + anchors.leftMargin: 3 + anchors.verticalCenter: parent.verticalCenter + radius: 2 + color: Theme.primary + opacity: 0.8 + } + + Column { + id: eventContent + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingS + 6 + anchors.rightMargin: Theme.spacingXS + spacing: 2 + + StyledText { + width: parent.width + text: modelData.title + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + } + + StyledText { + width: parent.width + text: { + if (!modelData || modelData.allDay) { + return I18n.tr("All day") + } else if (modelData.start && modelData.end) { + const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP" + const startTime = Qt.formatTime(modelData.start, timeFormat) + if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) { + return startTime + " – " + Qt.formatTime(modelData.end, timeFormat) + } + return startTime + } + return "" + } + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.weight: Font.Normal + visible: text !== "" + } + } + + MouseArea { + id: eventMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: modelData.url !== "" + onClicked: { + if (modelData.url && modelData.url !== "") { + if (Qt.openUrlExternally(modelData.url) === false) { + console.warn("Failed to open URL: " + modelData.url) + } else { + root.closeDash() + } + } + } + } + } + } + } + + SystemClock { + id: systemClock + precision: SystemClock.Hours + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/Overview/Card.qml b/quickshell/.config/quickshell/Modules/DankDash/Overview/Card.qml new file mode 100644 index 0000000..cd1f578 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/Overview/Card.qml @@ -0,0 +1,22 @@ +import QtQuick +import QtQuick.Controls +import qs.Common + +Rectangle { + id: card + + property int pad: Theme.spacingM + + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + default property alias content: contentItem.data + + Item { + id: contentItem + anchors.fill: parent + anchors.margins: card.pad + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/Overview/ClockCard.qml b/quickshell/.config/quickshell/Modules/DankDash/Overview/ClockCard.qml new file mode 100644 index 0000000..88bb346 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/Overview/ClockCard.qml @@ -0,0 +1,98 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import qs.Common +import qs.Widgets + +Card { + id: root + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + Column { + spacing: -8 + anchors.horizontalCenter: parent.horizontalCenter + + Row { + spacing: 0 + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: { + if (SettingsData.use24HourClock) { + return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(0) + } else { + const hours = systemClock?.date?.getHours() + const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours + return String(display).padStart(2, '0').charAt(0) + } + } + font.pixelSize: 48 + color: Theme.primary + font.weight: Font.Medium + width: 28 + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: { + if (SettingsData.use24HourClock) { + return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(1) + } else { + const hours = systemClock?.date?.getHours() + const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours + return String(display).padStart(2, '0').charAt(1) + } + } + font.pixelSize: 48 + color: Theme.primary + font.weight: Font.Medium + width: 28 + horizontalAlignment: Text.AlignHCenter + } + } + + Row { + spacing: 0 + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0) + font.pixelSize: 48 + color: Theme.primary + font.weight: Font.Medium + width: 28 + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1) + font.pixelSize: 48 + color: Theme.primary + font.weight: Font.Medium + width: 28 + horizontalAlignment: Text.AlignHCenter + } + } + } + + StyledText { + text: { + if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) { + return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) + } + return systemClock?.date?.toLocaleDateString(Qt.locale(), "MMM d") + } + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + } + + SystemClock { + id: systemClock + precision: SystemClock.Seconds + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml b/quickshell/.config/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml new file mode 100644 index 0000000..5ecef55 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/Overview/MediaOverviewCard.qml @@ -0,0 +1,481 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Shapes +import Quickshell.Services.Mpris +import qs.Common +import qs.Services +import qs.Widgets + +Card { + id: root + signal clicked() + + property MprisPlayer activePlayer: MprisController.activePlayer + property real currentPosition: activePlayer?.positionSupported ? activePlayer.position : 0 + property real displayPosition: currentPosition + + readonly property real ratio: { + if (!activePlayer || activePlayer.length <= 0) return 0 + const calculatedRatio = displayPosition / activePlayer.length + return Math.max(0, Math.min(1, calculatedRatio)) + } + + onActivePlayerChanged: { + if (activePlayer?.positionSupported) { + currentPosition = Qt.binding(() => activePlayer?.position || 0) + } else { + currentPosition = 0 + } + } + + Timer { + interval: 300 + running: activePlayer?.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking + repeat: true + onTriggered: activePlayer?.positionSupported && activePlayer.positionChanged() + } + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + visible: !activePlayer + + DankIcon { + name: "music_note" + size: Theme.iconSize + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: "No Media" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Column { + anchors.centerIn: parent + width: parent.width - Theme.spacingXS * 2 + spacing: Theme.spacingL + visible: activePlayer + + Item { + width: 110 + height: 80 + anchors.horizontalCenter: parent.horizontalCenter + + Loader { + active: activePlayer?.playbackState === MprisPlaybackState.Playing + sourceComponent: Component { + Ref { + service: CavaService + } + } + } + + Shape { + id: morphingBlob + width: 120 + height: 120 + anchors.centerIn: parent + visible: activePlayer?.playbackState === MprisPlaybackState.Playing + asynchronous: false + antialiasing: true + preferredRendererType: Shape.CurveRenderer + + layer.enabled: true + layer.smooth: true + layer.samples: 4 + + + readonly property real centerX: width / 2 + readonly property real centerY: height / 2 + readonly property real baseRadius: 40 + readonly property int segments: 24 + + property var audioLevels: { + if (!CavaService.cavaAvailable || CavaService.values.length === 0) { + return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5] + } + return CavaService.values + } + + property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5] + property var cubics: [] + + + onAudioLevelsChanged: updatePath() + + Timer { + running: morphingBlob.visible + interval: 16 + repeat: true + onTriggered: morphingBlob.updatePath() + } + + Component { + id: cubicSegment + PathCubic {} + } + + Component.onCompleted: { + shapePath.pathElements.push(Qt.createQmlObject( + 'import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath + )) + + for (let i = 0; i < segments; i++) { + const seg = cubicSegment.createObject(shapePath) + shapePath.pathElements.push(seg) + cubics.push(seg) + } + + updatePath() + } + + function expSmooth(prev, next, alpha) { + return prev + alpha * (next - prev) + } + + function updatePath() { + if (cubics.length === 0) return + + for (let i = 0; i < Math.min(smoothedLevels.length, audioLevels.length); i++) { + smoothedLevels[i] = expSmooth(smoothedLevels[i], audioLevels[i], 0.2) + } + + const points = [] + for (let i = 0; i < segments; i++) { + const angle = (i / segments) * 2 * Math.PI + const audioIndex = i % Math.min(smoothedLevels.length, 6) + const audioLevel = Math.max(0.1, Math.min(1.5, (smoothedLevels[audioIndex] || 0) / 50)) + + const radius = baseRadius * (1.0 + audioLevel * 0.3) + const x = centerX + Math.cos(angle) * radius + const y = centerY + Math.sin(angle) * radius + points.push({x: x, y: y}) + } + + const startMove = shapePath.pathElements[0] + startMove.x = points[0].x + startMove.y = points[0].y + + const tension = 0.5 + for (let i = 0; i < segments; i++) { + const p0 = points[(i - 1 + segments) % segments] + const p1 = points[i] + const p2 = points[(i + 1) % segments] + const p3 = points[(i + 2) % segments] + + const c1x = p1.x + (p2.x - p0.x) * tension / 3 + const c1y = p1.y + (p2.y - p0.y) * tension / 3 + const c2x = p2.x - (p3.x - p1.x) * tension / 3 + const c2y = p2.y - (p3.y - p1.y) * tension / 3 + + const seg = cubics[i] + seg.control1X = c1x + seg.control1Y = c1y + seg.control2X = c2x + seg.control2Y = c2y + seg.x = p2.x + seg.y = p2.y + } + } + + ShapePath { + id: shapePath + fillColor: Theme.primary + strokeColor: "transparent" + strokeWidth: 0 + joinStyle: ShapePath.RoundJoin + fillRule: ShapePath.WindingFill + } + } + + Rectangle { + width: 72 + height: 72 + radius: 36 + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Theme.surfaceContainer + border.width: 1 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + z: 1 + + Image { + id: albumArt + source: activePlayer?.trackArtUrl || "" + anchors.fill: parent + anchors.margins: 2 + fillMode: Image.PreserveAspectCrop + smooth: true + mipmap: true + cache: true + asynchronous: true + visible: false + } + + MultiEffect { + anchors.fill: parent + anchors.margins: 2 + source: albumArt + maskEnabled: true + maskSource: circularMask + visible: albumArt.status === Image.Ready + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: circularMask + width: 68 + height: 68 + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "black" + antialiasing: true + } + } + + DankIcon { + anchors.centerIn: parent + name: "album" + size: 20 + color: Theme.surfaceVariantText + visible: albumArt.status !== Image.Ready + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + topPadding: Theme.spacingL + + StyledText { + text: activePlayer?.trackTitle || "Unknown" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + maximumLineCount: 1 + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: activePlayer?.trackArtist || "Unknown Artist" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + width: parent.width + elide: Text.ElideRight + maximumLineCount: 1 + horizontalAlignment: Text.AlignHCenter + } + } + + Item { + id: progressSlider + width: parent.width + height: 20 + visible: activePlayer?.length > 0 + + property real value: ratio + property real lineWidth: 2.5 + property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40) + property color fillColor: Theme.primary + property color playheadColor: Theme.primary + + Rectangle { + width: parent.width + height: progressSlider.lineWidth + anchors.verticalCenter: parent.verticalCenter + color: progressSlider.trackColor + radius: height / 2 + } + + Rectangle { + width: Math.max(0, Math.min(parent.width, parent.width * progressSlider.value)) + height: progressSlider.lineWidth + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + color: progressSlider.fillColor + radius: height / 2 + Behavior on width { NumberAnimation { duration: 80 } } + } + + Rectangle { + id: playhead + width: 2.5 + height: Math.max(progressSlider.lineWidth + 8, 12) + radius: width / 2 + color: progressSlider.playheadColor + x: Math.max(0, Math.min(progressSlider.width, progressSlider.width * progressSlider.value)) - width / 2 + anchors.verticalCenter: parent.verticalCenter + z: 3 + Behavior on x { NumberAnimation { duration: 80 } } + } + + MouseArea { + id: progressMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false + + property bool isSeeking: false + property real pendingSeekPosition: -1 + + Timer { + id: seekDebounceTimer + interval: 150 + onTriggered: { + if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) { + const clamped = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99) + activePlayer.position = clamped + progressMouseArea.pendingSeekPosition = -1 + } + } + } + + onPressed: (mouse) => { + isSeeking = true + if (activePlayer?.length > 0 && activePlayer?.canSeek) { + const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width)) + pendingSeekPosition = r * activePlayer.length + displayPosition = pendingSeekPosition + seekDebounceTimer.restart() + } + } + onReleased: { + isSeeking = false + seekDebounceTimer.stop() + if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) { + const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99) + activePlayer.position = clamped + pendingSeekPosition = -1 + } + displayPosition = Qt.binding(() => currentPosition) + } + onPositionChanged: (mouse) => { + if (pressed && isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) { + const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width)) + pendingSeekPosition = r * activePlayer.length + displayPosition = pendingSeekPosition + seekDebounceTimer.restart() + } + } + onClicked: (mouse) => { + if (activePlayer?.length > 0 && activePlayer?.canSeek) { + const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width)) + activePlayer.position = r * activePlayer.length + } + } + } + } + + Item { + width: parent.width + height: 32 + + Row { + spacing: Theme.spacingS + anchors.centerIn: parent + + Rectangle { + width: 28 + height: 28 + radius: 14 + anchors.verticalCenter: playPauseButton.verticalCenter + color: prevArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "skip_previous" + size: 14 + color: Theme.surfaceText + } + + MouseArea { + id: prevArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!activePlayer) return + if (activePlayer.position > 8 && activePlayer.canSeek) { + activePlayer.position = 0 + } else { + activePlayer.previous() + } + } + } + } + + Rectangle { + id: playPauseButton + width: 32 + height: 32 + radius: 16 + color: Theme.primary + + DankIcon { + anchors.centerIn: parent + name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow" + size: 16 + color: Theme.background + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: activePlayer?.togglePlaying() + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + anchors.verticalCenter: playPauseButton.verticalCenter + color: nextArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "skip_next" + size: 14 + color: Theme.surfaceText + } + + MouseArea { + id: nextArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: activePlayer?.next() + } + } + } + } + } + + MouseArea { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: 123 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + visible: activePlayer + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/Overview/SystemMonitorCard.qml b/quickshell/.config/quickshell/Modules/DankDash/Overview/SystemMonitorCard.qml new file mode 100644 index 0000000..9251a36 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/Overview/SystemMonitorCard.qml @@ -0,0 +1,178 @@ +import QtQuick +import QtQuick.Effects +import qs.Common +import qs.Services +import qs.Widgets + +Card { + id: root + + Component.onCompleted: { + DgopService.addRef(["cpu", "memory", "system"]) + } + Component.onDestruction: { + DgopService.removeRef(["cpu", "memory", "system"]) + } + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingM + + // CPU Bar + Column { + width: (parent.width - 2 * Theme.spacingM) / 3 + height: parent.height + spacing: Theme.spacingS + + Rectangle { + width: 8 + height: parent.height - Theme.iconSizeSmall - Theme.spacingS + radius: 4 + anchors.horizontalCenter: parent.horizontalCenter + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + + Rectangle { + width: parent.width + height: parent.height * Math.min((DgopService.cpuUsage || 6) / 100, 1) + radius: parent.radius + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + color: { + if (DgopService.cpuUsage > 80) return Theme.error + if (DgopService.cpuUsage > 60) return Theme.warning + return Theme.primary + } + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + Item { + width: parent.width + height: Theme.iconSizeSmall + + DankIcon { + name: "memory" + size: Theme.iconSizeSmall + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + color: { + if (DgopService.cpuUsage > 80) return Theme.error + if (DgopService.cpuUsage > 60) return Theme.warning + return Theme.primary + } + } + } + } + + // Temperature Bar + Column { + width: (parent.width - 2 * Theme.spacingM) / 3 + height: parent.height + spacing: Theme.spacingS + + Rectangle { + width: 8 + height: parent.height - Theme.iconSizeSmall - Theme.spacingS + radius: 4 + anchors.horizontalCenter: parent.horizontalCenter + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + + Rectangle { + width: parent.width + height: parent.height * Math.min(Math.max((DgopService.cpuTemperature || 40) / 100, 0), 1) + radius: parent.radius + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + color: { + if (DgopService.cpuTemperature > 85) return Theme.error + if (DgopService.cpuTemperature > 69) return Theme.warning + return Theme.primary + } + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + Item { + width: parent.width + height: Theme.iconSizeSmall + + DankIcon { + name: "device_thermostat" + size: Theme.iconSizeSmall + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + color: { + if (DgopService.cpuTemperature > 85) return Theme.error + if (DgopService.cpuTemperature > 69) return Theme.warning + return Theme.primary + } + } + } + } + + // RAM Bar + Column { + width: (parent.width - 2 * Theme.spacingM) / 3 + height: parent.height + spacing: Theme.spacingS + + Rectangle { + width: 8 + height: parent.height - Theme.iconSizeSmall - Theme.spacingS + radius: 4 + anchors.horizontalCenter: parent.horizontalCenter + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + + Rectangle { + width: parent.width + height: parent.height * Math.min((DgopService.memoryUsage || 42) / 100, 1) + radius: parent.radius + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + color: { + if (DgopService.memoryUsage > 90) return Theme.error + if (DgopService.memoryUsage > 75) return Theme.warning + return Theme.primary + } + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + Item { + width: parent.width + height: Theme.iconSizeSmall + + DankIcon { + name: "developer_board" + size: Theme.iconSizeSmall + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + color: { + if (DgopService.memoryUsage > 90) return Theme.error + if (DgopService.memoryUsage > 75) return Theme.warning + return Theme.primary + } + } + } + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/Overview/UserInfoCard.qml b/quickshell/.config/quickshell/Modules/DankDash/Overview/UserInfoCard.qml new file mode 100644 index 0000000..2cbc593 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/Overview/UserInfoCard.qml @@ -0,0 +1,167 @@ +import QtQuick +import QtQuick.Effects +import qs.Common +import qs.Services +import qs.Widgets + +Card { + id: root + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Item { + id: avatarContainer + + property bool hasImage: profileImageLoader.status === Image.Ready + + width: 77 + height: 77 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.fill: parent + radius: 36 + color: Theme.primary + visible: !avatarContainer.hasImage + + StyledText { + anchors.centerIn: parent + text: UserInfoService.username.length > 0 ? UserInfoService.username.charAt(0).toUpperCase() : "b" + font.pixelSize: Theme.fontSizeXLarge + 4 + font.weight: Font.Bold + color: Theme.background + } + } + + Image { + id: profileImageLoader + + source: { + if (PortalService.profileImage === "") + return "" + + if (PortalService.profileImage.startsWith("/")) + return "file://" + PortalService.profileImage + + return PortalService.profileImage + } + smooth: true + asynchronous: true + mipmap: true + cache: true + visible: false + } + + MultiEffect { + anchors.fill: parent + anchors.margins: 2 + source: profileImageLoader + maskEnabled: true + maskSource: circularMask + visible: avatarContainer.hasImage + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: circularMask + width: 77 - 4 + height: 77 - 4 + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "black" + antialiasing: true + } + } + + DankIcon { + anchors.centerIn: parent + name: "person" + size: Theme.iconSize + 8 + color: Theme.error + visible: PortalService.profileImage !== "" && profileImageLoader.status === Image.Error + } + } + + Column { + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: UserInfoService.username || "brandon" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 + } + + Row { + spacing: Theme.spacingS + + SystemLogo { + width: 16 + height: 16 + anchors.verticalCenter: parent.verticalCenter + colorOverride: Theme.primary + } + + StyledText { + text: { + if (CompositorService.isNiri) return "on niri" + if (CompositorService.isHyprland) return "on Hyprland" + return "" + } + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + width: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS + } + } + + Row { + spacing: Theme.spacingS + + DankIcon { + name: "schedule" + size: 16 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: uptimeText + + property real availableWidth: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS + property real longTextWidth: { + const testMetrics = Qt.createQmlObject('import QtQuick; TextMetrics { font.pixelSize: ' + Theme.fontSizeSmall + ' }', uptimeText) + testMetrics.text = UserInfoService.uptime || "up 1 hour, 23 minutes" + const result = testMetrics.width + testMetrics.destroy() + return result + } + // Just using truncated is always true initially idk + property bool shouldUseShort: longTextWidth > availableWidth + + text: shouldUseShort ? UserInfoService.shortUptime : UserInfoService.uptime || "up 1h 23m" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + width: availableWidth + wrapMode: Text.NoWrap + } + } + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/Overview/WeatherOverviewCard.qml b/quickshell/.config/quickshell/Modules/DankDash/Overview/WeatherOverviewCard.qml new file mode 100644 index 0000000..4db0e97 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/Overview/WeatherOverviewCard.qml @@ -0,0 +1,91 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import qs.Common +import qs.Services +import qs.Widgets + +Card { + id: root + + signal clicked() + + Component.onCompleted: WeatherService.addRef() + Component.onDestruction: WeatherService.removeRef() + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + visible: !WeatherService.weather.available || WeatherService.weather.temp === 0 + + DankIcon { + name: "cloud_off" + size: 24 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: WeatherService.weather.loading ? "Loading..." : "No Weather" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + + Button { + text: "Refresh" + flat: true + visible: !WeatherService.weather.loading + anchors.horizontalCenter: parent.horizontalCenter + onClicked: WeatherService.forceRefresh() + } + } + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingL + visible: WeatherService.weather.available && WeatherService.weather.temp !== 0 + + DankIcon { + name: WeatherService.getWeatherIcon(WeatherService.weather.wCode) + size: 48 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: { + const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp; + if (temp === undefined || temp === null || temp === 0) { + return "--°" + (SettingsData.useFahrenheit ? "F" : "C"); + } + return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C"); + } + font.pixelSize: Theme.fontSizeXLarge + 4 + color: Theme.surfaceText + font.weight: Font.Light + } + + StyledText { + text: WeatherService.getWeatherCondition(WeatherService.weather.wCode) + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + elide: Text.ElideRight + width: parent.parent.parent.width - 48 - Theme.spacingL * 2 + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/OverviewTab.qml b/quickshell/.config/quickshell/Modules/DankDash/OverviewTab.qml new file mode 100644 index 0000000..eb900ff --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/OverviewTab.qml @@ -0,0 +1,76 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.DankDash.Overview + +Item { + id: root + + implicitWidth: 700 + implicitHeight: 410 + + signal switchToWeatherTab() + signal switchToMediaTab() + signal closeDash() + + Item { + anchors.fill: parent + // Clock - top left (narrower and shorter) + ClockCard { + x: 0 + y: 0 + width: parent.width * 0.2 - Theme.spacingM * 2 + height: 180 + } + + // Weather - top middle-left (narrower) + WeatherOverviewCard { + x: SettingsData.weatherEnabled ? parent.width * 0.2 - Theme.spacingM : 0 + y: 0 + width: SettingsData.weatherEnabled ? parent.width * 0.3 : 0 + height: 100 + visible: SettingsData.weatherEnabled + + onClicked: root.switchToWeatherTab() + } + + // UserInfo - top middle-right (extend when weather disabled) + UserInfoCard { + x: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.2 - Theme.spacingM + y: 0 + width: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.8 + height: 100 + } + + // SystemMonitor - middle left (narrow and shorter) + SystemMonitorCard { + x: 0 + y: 180 + Theme.spacingM + width: parent.width * 0.2 - Theme.spacingM * 2 + height: 220 + } + + // Calendar - bottom middle (wider and taller) + CalendarOverviewCard { + x: parent.width * 0.2 - Theme.spacingM + y: 100 + Theme.spacingM + width: parent.width * 0.6 + height: 300 + + onCloseDash: root.closeDash() + } + + // Media - bottom right (narrow and taller) + MediaOverviewCard { + x: parent.width * 0.8 + y: 100 + Theme.spacingM + width: parent.width * 0.2 + height: 300 + + onClicked: root.switchToMediaTab() + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/WallpaperTab.qml b/quickshell/.config/quickshell/Modules/DankDash/WallpaperTab.qml new file mode 100644 index 0000000..df5f763 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/WallpaperTab.qml @@ -0,0 +1,525 @@ +import Qt.labs.folderlistmodel +import QtCore +import QtQuick +import QtQuick.Effects +import qs.Common +import qs.Modals.FileBrowser +import qs.Widgets + +Item { + id: root + + implicitWidth: 700 + implicitHeight: 410 + + property string wallpaperDir: "" + property int currentPage: 0 + property int itemsPerPage: 16 + property int totalPages: Math.max(1, Math.ceil(wallpaperFolderModel.count / itemsPerPage)) + property bool active: false + property Item focusTarget: wallpaperGrid + property Item tabBarItem: null + property int gridIndex: 0 + property Item keyForwardTarget: null + property var parentPopout: null + property int lastPage: 0 + property bool enableAnimation: false + property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation) + property string selectedFileName: "" + property var targetScreen: null + property string targetScreenName: targetScreen ? targetScreen.name : "" + + signal requestTabChange(int newIndex) + + function getCurrentWallpaper() { + if (SessionData.perMonitorWallpaper && targetScreenName) { + return SessionData.getMonitorWallpaper(targetScreenName); + } + return SessionData.wallpaperPath; + } + + function setCurrentWallpaper(path) { + if (SessionData.perMonitorWallpaper && targetScreenName) { + SessionData.setMonitorWallpaper(targetScreenName, path); + } else { + SessionData.setWallpaper(path); + } + } + + onCurrentPageChanged: { + if (currentPage !== lastPage) { + enableAnimation = false; + lastPage = currentPage; + } + updateSelectedFileName(); + } + + onGridIndexChanged: { + updateSelectedFileName(); + } + + onVisibleChanged: { + if (visible && active) { + setInitialSelection(); + } + } + + Component.onCompleted: { + loadWallpaperDirectory(); + } + + onActiveChanged: { + if (active && visible) { + setInitialSelection(); + } + } + + function handleKeyEvent(event) { + const columns = 4; + const currentCol = gridIndex % columns; + const visibleCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage); + + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (gridIndex >= 0 && gridIndex < visibleCount) { + const absoluteIndex = currentPage * itemsPerPage + gridIndex; + if (absoluteIndex < wallpaperFolderModel.count) { + const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath"); + if (filePath) { + setCurrentWallpaper(filePath.toString().replace(/^file:\/\//, '')); + } + } + } + return true; + } + + if (event.key === Qt.Key_Right || event.key === Qt.Key_L) { + if (gridIndex + 1 < visibleCount) { + gridIndex++; + } else if (currentPage < totalPages - 1) { + gridIndex = 0; + currentPage++; + } + return true; + } + + if (event.key === Qt.Key_Left || event.key === Qt.Key_H) { + if (gridIndex > 0) { + gridIndex--; + } else if (currentPage > 0) { + currentPage--; + const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage); + gridIndex = prevPageCount - 1; + } + return true; + } + + if (event.key === Qt.Key_Down || event.key === Qt.Key_J) { + if (gridIndex + columns < visibleCount) { + gridIndex += columns; + } else if (currentPage < totalPages - 1) { + gridIndex = currentCol; + currentPage++; + } + return true; + } + + if (event.key === Qt.Key_Up || event.key === Qt.Key_K) { + if (gridIndex >= columns) { + gridIndex -= columns; + } else if (currentPage > 0) { + currentPage--; + const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage); + const prevPageRows = Math.ceil(prevPageCount / columns); + gridIndex = (prevPageRows - 1) * columns + currentCol; + gridIndex = Math.min(gridIndex, prevPageCount - 1); + } + return true; + } + + if (event.key === Qt.Key_PageUp && currentPage > 0) { + gridIndex = 0; + currentPage--; + return true; + } + + if (event.key === Qt.Key_PageDown && currentPage < totalPages - 1) { + gridIndex = 0; + currentPage++; + return true; + } + + if (event.key === Qt.Key_Home && event.modifiers & Qt.ControlModifier) { + gridIndex = 0; + currentPage = 0; + return true; + } + + if (event.key === Qt.Key_End && event.modifiers & Qt.ControlModifier) { + currentPage = totalPages - 1; + const lastPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage); + gridIndex = Math.max(0, lastPageCount - 1); + return true; + } + + return false; + } + + function setInitialSelection() { + const currentWallpaper = getCurrentWallpaper(); + if (!currentWallpaper || wallpaperFolderModel.count === 0) { + gridIndex = 0; + updateSelectedFileName(); + Qt.callLater(() => { + enableAnimation = true; + }); + return; + } + + for (var i = 0; i < wallpaperFolderModel.count; i++) { + const filePath = wallpaperFolderModel.get(i, "filePath"); + if (filePath && filePath.toString().replace(/^file:\/\//, '') === currentWallpaper) { + const targetPage = Math.floor(i / itemsPerPage); + const targetIndex = i % itemsPerPage; + currentPage = targetPage; + gridIndex = targetIndex; + updateSelectedFileName(); + Qt.callLater(() => { + enableAnimation = true; + }); + return; + } + } + gridIndex = 0; + updateSelectedFileName(); + Qt.callLater(() => { + enableAnimation = true; + }); + } + + function loadWallpaperDirectory() { + const currentWallpaper = getCurrentWallpaper(); + + if (!currentWallpaper || currentWallpaper.startsWith("#")) { + if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") { + wallpaperDir = CacheData.wallpaperLastPath; + } else { + wallpaperDir = ""; + } + return; + } + + wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/')); + } + + function updateSelectedFileName() { + if (wallpaperFolderModel.count === 0) { + selectedFileName = ""; + return; + } + + const absoluteIndex = currentPage * itemsPerPage + gridIndex; + if (absoluteIndex < wallpaperFolderModel.count) { + const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath"); + if (filePath) { + const pathStr = filePath.toString().replace(/^file:\/\//, ''); + selectedFileName = pathStr.substring(pathStr.lastIndexOf('/') + 1); + return; + } + } + selectedFileName = ""; + } + + Connections { + target: SessionData + function onWallpaperPathChanged() { + loadWallpaperDirectory(); + if (visible && active) { + setInitialSelection(); + } + } + function onMonitorWallpapersChanged() { + loadWallpaperDirectory(); + if (visible && active) { + setInitialSelection(); + } + } + function onPerMonitorWallpaperChanged() { + loadWallpaperDirectory(); + if (visible && active) { + setInitialSelection(); + } + } + } + + onTargetScreenNameChanged: { + loadWallpaperDirectory(); + if (visible && active) { + setInitialSelection(); + } + } + + Connections { + target: wallpaperFolderModel + function onCountChanged() { + if (wallpaperFolderModel.status === FolderListModel.Ready) { + if (visible && active) { + setInitialSelection(); + } + updateSelectedFileName(); + } + } + function onStatusChanged() { + if (wallpaperFolderModel.status === FolderListModel.Ready && wallpaperFolderModel.count > 0) { + if (visible && active) { + setInitialSelection(); + } + updateSelectedFileName(); + } + } + } + + FolderListModel { + id: wallpaperFolderModel + + showDirsFirst: false + showDotAndDotDot: false + showHidden: false + nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + showFiles: true + showDirs: false + sortField: FolderListModel.Name + folder: wallpaperDir ? "file://" + wallpaperDir.split('/').map(s => encodeURIComponent(s)).join('/') : "" + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + width: parent.width + height: parent.height - 50 + + GridView { + id: wallpaperGrid + anchors.centerIn: parent + width: parent.width - Theme.spacingS + height: parent.height - Theme.spacingS + cellWidth: width / 4 + cellHeight: height / 4 + clip: true + enabled: root.active + interactive: root.active + boundsBehavior: Flickable.StopAtBounds + keyNavigationEnabled: false + activeFocusOnTab: false + highlightFollowsCurrentItem: true + highlightMoveDuration: enableAnimation ? Theme.shortDuration : 0 + focus: false + + highlight: Item { + z: 1000 + Rectangle { + anchors.fill: parent + anchors.margins: Theme.spacingXS + color: "transparent" + border.width: 3 + border.color: Theme.primary + radius: Theme.cornerRadius + } + } + + model: { + const startIndex = currentPage * itemsPerPage; + const endIndex = Math.min(startIndex + itemsPerPage, wallpaperFolderModel.count); + const items = []; + for (var i = startIndex; i < endIndex; i++) { + const filePath = wallpaperFolderModel.get(i, "filePath"); + if (filePath) { + items.push(filePath.toString().replace(/^file:\/\//, '')); + } + } + return items; + } + + onModelChanged: { + const clampedIndex = model.length > 0 ? Math.min(Math.max(0, gridIndex), model.length - 1) : 0; + if (gridIndex !== clampedIndex) { + gridIndex = clampedIndex; + } + } + + onCountChanged: { + if (count > 0) { + const clampedIndex = Math.min(gridIndex, count - 1); + currentIndex = clampedIndex; + positionViewAtIndex(clampedIndex, GridView.Contain); + } + Qt.callLater(() => { + enableAnimation = true; + }); + } + + Connections { + target: root + function onGridIndexChanged() { + if (wallpaperGrid.count > 0) { + wallpaperGrid.currentIndex = gridIndex; + if (!enableAnimation) { + wallpaperGrid.positionViewAtIndex(gridIndex, GridView.Contain); + } + } + } + } + + delegate: Item { + width: wallpaperGrid.cellWidth + height: wallpaperGrid.cellHeight + + property string wallpaperPath: modelData || "" + property bool isSelected: getCurrentWallpaper() === modelData + + Rectangle { + id: wallpaperCard + anchors.fill: parent + anchors.margins: Theme.spacingXS + radius: Theme.cornerRadius + clip: true + + Rectangle { + anchors.fill: parent + color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent" + radius: parent.radius + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + CachingImage { + id: thumbnailImage + anchors.fill: parent + imagePath: modelData || "" + maxCacheSize: 256 + + layer.enabled: true + layer.effect: MultiEffect { + maskEnabled: true + maskThresholdMin: 0.5 + maskSpreadAtMin: 1.0 + maskSource: ShaderEffectSource { + sourceItem: Rectangle { + width: thumbnailImage.width + height: thumbnailImage.height + radius: Theme.cornerRadius + } + } + } + } + + StateLayer { + anchors.fill: parent + cornerRadius: parent.radius + stateColor: Theme.primary + } + + MouseArea { + id: wallpaperMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + gridIndex = index; + if (modelData) { + setCurrentWallpaper(modelData); + } + } + } + } + } + } + + StyledText { + anchors.centerIn: parent + visible: wallpaperFolderModel.count === 0 + text: "No wallpapers found\n\nClick the folder icon below to browse" + font.pixelSize: 14 + color: Theme.outline + horizontalAlignment: Text.AlignHCenter + } + } + + Column { + width: parent.width + height: 50 + + Row { + width: parent.width + height: 32 + spacing: Theme.spacingS + + Item { + width: (parent.width - controlsRow.width - Theme.spacingS) / 2 + height: parent.height + } + + Row { + id: controlsRow + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankActionButton { + anchors.verticalCenter: parent.verticalCenter + iconName: "skip_previous" + iconSize: 20 + buttonSize: 32 + enabled: currentPage > 0 + opacity: enabled ? 1.0 : 0.3 + onClicked: { + if (currentPage > 0) { + currentPage--; + } + } + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: wallpaperFolderModel.count > 0 ? `${wallpaperFolderModel.count} wallpapers • ${currentPage + 1} / ${totalPages}` : "No wallpapers" + font.pixelSize: 14 + color: Theme.surfaceText + opacity: 0.7 + } + + DankActionButton { + anchors.verticalCenter: parent.verticalCenter + iconName: "skip_next" + iconSize: 20 + buttonSize: 32 + enabled: currentPage < totalPages - 1 + opacity: enabled ? 1.0 : 0.3 + onClicked: { + if (currentPage < totalPages - 1) { + currentPage++; + } + } + } + } + } + + StyledText { + width: parent.width + height: 18 + text: selectedFileName + font.pixelSize: 12 + color: Theme.surfaceText + opacity: 0.5 + visible: selectedFileName !== "" + elide: Text.ElideMiddle + horizontalAlignment: Text.AlignHCenter + } + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/DankDash/WeatherTab.qml b/quickshell/.config/quickshell/Modules/DankDash/WeatherTab.qml new file mode 100644 index 0000000..bf5be6d --- /dev/null +++ b/quickshell/.config/quickshell/Modules/DankDash/WeatherTab.qml @@ -0,0 +1,642 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + implicitWidth: 700 + implicitHeight: 410 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingL + visible: !WeatherService.weather.available || WeatherService.weather.temp === 0 + + DankIcon { + name: "cloud_off" + size: Theme.iconSize * 2 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: "No Weather Data Available" + font.pixelSize: Theme.fontSizeLarge + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Column { + anchors.fill: parent + spacing: Theme.spacingM + visible: WeatherService.weather.available && WeatherService.weather.temp !== 0 + + Item { + width: parent.width + height: 70 + + DankIcon { + id: refreshButton + name: "refresh" + size: Theme.iconSize - 4 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) + anchors.right: parent.right + anchors.top: parent.top + + property bool isRefreshing: false + enabled: !isRefreshing + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor + onClicked: { + refreshButton.isRefreshing = true + WeatherService.forceRefresh() + refreshTimer.restart() + } + enabled: parent.enabled + } + + Timer { + id: refreshTimer + interval: 2000 + onTriggered: refreshButton.isRefreshing = false + } + + NumberAnimation on rotation { + running: refreshButton.isRefreshing + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + } + } + + Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + width: weatherIcon.width + tempColumn.width + sunriseColumn.width + Theme.spacingM * 2 + height: 70 + + DankIcon { + id: weatherIcon + name: WeatherService.getWeatherIcon(WeatherService.weather.wCode) + size: Theme.iconSize * 1.5 + color: Theme.primary + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 4 + shadowBlur: 0.8 + shadowColor: Qt.rgba(0, 0, 0, 0.2) + shadowOpacity: 0.2 + } + } + + Column { + id: tempColumn + spacing: Theme.spacingXS + anchors.left: weatherIcon.right + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + + Item { + width: tempText.width + unitText.width + Theme.spacingXS + height: tempText.height + + StyledText { + id: tempText + text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + font.pixelSize: Theme.fontSizeLarge + 4 + color: Theme.surfaceText + font.weight: Font.Light + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: unitText + text: SettingsData.useFahrenheit ? "F" : "C" + font.pixelSize: Theme.fontSizeMedium + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.left: tempText.right + anchors.leftMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (WeatherService.weather.available) { + SettingsData.setTemperatureUnit(!SettingsData.useFahrenheit) + } + } + enabled: WeatherService.weather.available + } + } + } + + StyledText { + text: WeatherService.weather.city || "" + font.pixelSize: Theme.fontSizeMedium + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + visible: text.length > 0 + } + } + + Column { + id: sunriseColumn + spacing: Theme.spacingXS + anchors.left: tempColumn.right + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + visible: WeatherService.weather.sunrise && WeatherService.weather.sunset + + Item { + width: sunriseIcon.width + sunriseText.width + Theme.spacingXS + height: sunriseIcon.height + + DankIcon { + id: sunriseIcon + name: "wb_twilight" + size: Theme.iconSize - 6 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: sunriseText + text: WeatherService.weather.sunrise || "" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.left: sunriseIcon.right + anchors.leftMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + } + } + + Item { + width: sunsetIcon.width + sunsetText.width + Theme.spacingXS + height: sunsetIcon.height + + DankIcon { + id: sunsetIcon + name: "bedtime" + size: Theme.iconSize - 6 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: sunsetText + text: WeatherService.weather.sunset || "" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.left: sunsetIcon.right + anchors.leftMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + } + + GridLayout { + width: parent.width + height: 95 + columns: 6 + columnSpacing: Theme.spacingS + rowSpacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + anchors.centerIn: parent + name: "device_thermostat" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + + StyledText { + text: "Feels Like" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: (SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)) + "°" + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + anchors.centerIn: parent + name: "humidity_low" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + + StyledText { + text: "Humidity" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--" + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + anchors.centerIn: parent + name: "air" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + + StyledText { + text: "Wind" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: WeatherService.weather.wind || "--" + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + anchors.centerIn: parent + name: "speed" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + + StyledText { + text: "Pressure" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: WeatherService.weather.pressure ? WeatherService.weather.pressure + " hPa" : "--" + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + anchors.centerIn: parent + name: "rainy" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + + StyledText { + text: "Rain Chance" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: WeatherService.weather.precipitationProbability ? WeatherService.weather.precipitationProbability + "%" : "0%" + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + anchors.centerIn: parent + name: "wb_sunny" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + + StyledText { + text: "Visibility" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: "Good" + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + } + + Column { + width: parent.width + height: parent.height - 70 - 95 - Theme.spacingM * 3 - 2 + spacing: Theme.spacingS + + StyledText { + text: "7-Day Forecast" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + Row { + width: parent.width + height: parent.height - Theme.fontSizeMedium - Theme.spacingS - Theme.spacingL + spacing: Theme.spacingXS + + Repeater { + model: 7 + + Rectangle { + width: (parent.width - Theme.spacingXS * 6) / 7 + height: parent.height + radius: Theme.cornerRadius + + property var dayDate: { + const date = new Date() + date.setDate(date.getDate() + index) + return date + } + property bool isToday: index === 0 + property var forecastData: { + if (WeatherService.weather.forecast && WeatherService.weather.forecast.length > index) { + return WeatherService.weather.forecast[index] + } + return null + } + + color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1) + border.color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent" + border.width: isToday ? 1 : 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: Qt.locale().dayName(dayDate.getDay(), Locale.ShortFormat) + font.pixelSize: Theme.fontSizeSmall + color: isToday ? Theme.primary : Theme.surfaceText + font.weight: isToday ? Font.Medium : Font.Normal + anchors.horizontalCenter: parent.horizontalCenter + } + + DankIcon { + name: forecastData ? WeatherService.getWeatherIcon(forecastData.wCode || 0) : "cloud" + size: Theme.iconSize + color: isToday ? Theme.primary : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8) + anchors.horizontalCenter: parent.horizontalCenter + } + + Column { + spacing: 2 + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: forecastData ? (SettingsData.useFahrenheit ? (forecastData.tempMaxF || forecastData.tempMax) : (forecastData.tempMax || 0)) + "°/" + (SettingsData.useFahrenheit ? (forecastData.tempMinF || forecastData.tempMin) : (forecastData.tempMin || 0)) + "°" : "--/--" + font.pixelSize: Theme.fontSizeSmall + color: isToday ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + Column { + spacing: 1 + anchors.horizontalCenter: parent.horizontalCenter + visible: forecastData && forecastData.sunrise && forecastData.sunset + + Row { + spacing: 2 + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + name: "wb_twilight" + size: 8 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: forecastData ? forecastData.sunrise : "" + font.pixelSize: Theme.fontSizeSmall - 2 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + spacing: 2 + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + name: "bedtime" + size: 8 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: forecastData ? forecastData.sunset : "" + font.pixelSize: Theme.fontSizeSmall - 2 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Modules/Lock/CustomButtonKeyboard.qml b/quickshell/.config/quickshell/Modules/Lock/CustomButtonKeyboard.qml new file mode 100644 index 0000000..8166905 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Lock/CustomButtonKeyboard.qml @@ -0,0 +1,24 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +DankActionButton { + id: customButtonKeyboard + circular: false + property string text: "" + width: 40 + height: 40 + property bool isShift: false + color: Theme.surface + + StyledText { + id: contentItem + anchors.centerIn: parent + text: parent.text + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeXLarge + font.weight: Font.Normal + } +} diff --git a/quickshell/.config/quickshell/Modules/Lock/Keyboard.qml b/quickshell/.config/quickshell/Modules/Lock/Keyboard.qml new file mode 100644 index 0000000..a80afd2 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Lock/Keyboard.qml @@ -0,0 +1,361 @@ +import QtQuick +import qs.Common + +Rectangle { + id: root + property Item target + height: 60 * 5 + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + color: Theme.widgetBackground() + + property double rowSpacing: 0.01 * width // horizontal spacing between keyboard + property double columnSpacing: 0.02 * height // vertical spacing between keyboard + property bool shift: false //Boolean for the shift state + property bool symbols: false //Boolean for the symbol state + property double columns: 10 // Number of column + property double rows: 4 // Number of row + + property string strShift: '\u2191' // UPWARDS ARROW unicode + property string strBackspace: "Backspace" + + property var modelKeyboard: { + "row_1": [ + { + text: 'q', + symbol: '1', + width: 1 + }, + { + text: 'w', + symbol: '2', + width: 1 + }, + { + text: 'e', + symbol: '3', + width: 1 + }, + { + text: 'r', + symbol: '4', + width: 1 + }, + { + text: 't', + symbol: '5', + width: 1 + }, + { + text: 'y', + symbol: '6', + width: 1 + }, + { + text: 'u', + symbol: '7', + width: 1 + }, + { + text: 'i', + symbol: '8', + width: 1 + }, + { + text: 'o', + symbol: '9', + width: 1 + }, + { + text: 'p', + symbol: '0', + width: 1 + }, + ], + "row_2": [ + { + text: 'a', + symbol: '-', + width: 1 + }, + { + text: 's', + symbol: '/', + width: 1 + }, + { + text: 'd', + symbol: ':', + width: 1 + }, + { + text: 'f', + symbol: ';', + width: 1 + }, + { + text: 'g', + symbol: '(', + width: 1 + }, + { + text: 'h', + symbol: ')', + width: 1 + }, + { + text: 'j', + symbol: '€', + width: 1 + }, + { + text: 'k', + symbol: '&', + width: 1 + }, + { + text: 'l', + symbol: '@', + width: 1 + } + ], + "row_3": [ + { + text: strShift, + symbol: strShift, + width: 1.5 + }, + { + text: 'z', + symbol: '.', + width: 1 + }, + { + text: 'x', + symbol: ',', + width: 1 + }, + { + text: 'c', + symbol: '?', + width: 1 + }, + { + text: 'v', + symbol: '!', + width: 1 + }, + { + text: 'b', + symbol: "'", + width: 1 + }, + { + text: 'n', + symbol: "%", + width: 1 + }, + { + text: 'm', + symbol: '"', + width: 1 + }, + { + text: "'", + symbol: "*", + width: 1.5 + } + ], + "row_4": [ + { + text: '123', + symbol: 'ABC', + width: 1.5 + }, + { + text: ' ', + symbol: ' ', + width: 6 + }, + { + text: '.', + symbol: '.', + width: 1 + }, + { + text: strBackspace, + symbol: strBackspace, + width: 1.5 + } + ] + } + + //Here is the corresponding table between the ascii and the key event + property var tableKeyEvent: { + "_0": Qt.Key_0, + "_1": Qt.Key_1, + "_2": Qt.Key_2, + "_3": Qt.Key_3, + "_4": Qt.Key_4, + "_5": Qt.Key_5, + "_6": Qt.Key_6, + "_7": Qt.Key_7, + "_8": Qt.Key_8, + "_9": Qt.Key_9, + "_a": Qt.Key_A, + "_b": Qt.Key_B, + "_c": Qt.Key_C, + "_d": Qt.Key_D, + "_e": Qt.Key_E, + "_f": Qt.Key_F, + "_g": Qt.Key_G, + "_h": Qt.Key_H, + "_i": Qt.Key_I, + "_j": Qt.Key_J, + "_k": Qt.Key_K, + "_l": Qt.Key_L, + "_m": Qt.Key_M, + "_n": Qt.Key_N, + "_o": Qt.Key_O, + "_p": Qt.Key_P, + "_q": Qt.Key_Q, + "_r": Qt.Key_R, + "_s": Qt.Key_S, + "_t": Qt.Key_T, + "_u": Qt.Key_U, + "_v": Qt.Key_V, + "_w": Qt.Key_W, + "_x": Qt.Key_X, + "_y": Qt.Key_Y, + "_z": Qt.Key_Z, + "_\u2190": Qt.Key_Backspace, + "_return": Qt.Key_Return, + "_ ": Qt.Key_Space, + "_-": Qt.Key_Minus, + "_/": Qt.Key_Slash, + "_:": Qt.Key_Colon, + "_;": Qt.Key_Semicolon, + "_(": Qt.Key_BracketLeft, + "_)": Qt.Key_BracketRight, + "_€": parseInt("20ac", 16) // I didn't find the appropriate Qt event so I used the hex format + , + "_&": Qt.Key_Ampersand, + "_@": Qt.Key_At, + '_"': Qt.Key_QuoteDbl, + "_.": Qt.Key_Period, + "_,": Qt.Key_Comma, + "_?": Qt.Key_Question, + "_!": Qt.Key_Exclam, + "_'": Qt.Key_Apostrophe, + "_%": Qt.Key_Percent, + "_*": Qt.Key_Asterisk + } + + Item { + id: keyboard_container + anchors.left: parent.left + anchors.leftMargin: 5 + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 5 + anchors.bottom: parent.bottom + anchors.bottomMargin: 5 + + //One column which contains 5 rows + Column { + spacing: columnSpacing + + Row { + id: row_1 + spacing: rowSpacing + Repeater { + model: modelKeyboard["row_1"] + delegate: CustomButtonKeyboard { + text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text + width: modelData.width * keyboard_container.width / columns - rowSpacing + height: keyboard_container.height / rows - columnSpacing + + onClicked: root.clicked(text) + } + } + } + Row { + id: row_2 + spacing: rowSpacing + Repeater { + model: modelKeyboard["row_2"] + delegate: CustomButtonKeyboard { + text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text + width: modelData.width * keyboard_container.width / columns - rowSpacing + height: keyboard_container.height / rows - columnSpacing + + onClicked: root.clicked(text) + } + } + } + Row { + id: row_3 + spacing: rowSpacing + Repeater { + model: modelKeyboard["row_3"] + delegate: CustomButtonKeyboard { + text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text + width: modelData.width * keyboard_container.width / columns - rowSpacing + height: keyboard_container.height / rows - columnSpacing + isShift: shift && text === strShift + + onClicked: root.clicked(text) + } + } + } + Row { + id: row_4 + spacing: rowSpacing + Repeater { + model: modelKeyboard["row_4"] + delegate: CustomButtonKeyboard { + text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text + width: modelData.width * keyboard_container.width / columns - rowSpacing + height: keyboard_container.height / rows - columnSpacing + + onClicked: root.clicked(text) + } + } + } + } + } + signal clicked(string text) + + Connections { + target: root + function onClicked(text) { + if (!keyboard_controller.target) + return; + if (text === strShift) { + root.shift = !root.shift; // toggle shift + } else if (text === '123') { + root.symbols = true; + } else if (text === 'ABC') { + root.symbols = false; + } else { + // insert text into target + if (text === strBackspace) { + var current = keyboard_controller.target.text; + keyboard_controller.target.text = current.slice(0, current.length - 1); + } else { + // normal character + var charToInsert = root.symbols ? text : (root.shift ? text.toUpperCase() : text); + var current = keyboard_controller.target.text; + var cursorPos = keyboard_controller.target.cursorPosition; + keyboard_controller.target.text = current.slice(0, cursorPos) + charToInsert + current.slice(cursorPos); + keyboard_controller.target.cursorPosition = cursorPos + 1; + } + + // shift is momentary + if (root.shift && text !== strShift) + root.shift = false; + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Lock/KeyboardController.qml b/quickshell/.config/quickshell/Modules/Lock/KeyboardController.qml new file mode 100644 index 0000000..4ee606e --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Lock/KeyboardController.qml @@ -0,0 +1,36 @@ +import QtQuick + +Item { + id: keyboard_controller + + // reference on the TextInput + property Item target + //Booléan on the state of the keyboard + property bool isKeyboardActive: false + + property var rootObject + + function show() { + if (!isKeyboardActive && keyboard === null) { + keyboard = keyboardComponent.createObject(keyboard_controller.rootObject); + keyboard.target = keyboard_controller.target; + isKeyboardActive = true; + } else + console.info("The keyboard is already shown"); + } + + function hide() { + if (isKeyboardActive && keyboard !== null) { + keyboard.destroy(); + isKeyboardActive = false; + } else + console.info("The keyboard is already hidden"); + } + + // private + property Item keyboard: null + Component { + id: keyboardComponent + Keyboard {} + } +} diff --git a/quickshell/.config/quickshell/Modules/Lock/Lock.qml b/quickshell/.config/quickshell/Modules/Lock/Lock.qml new file mode 100644 index 0000000..0d1e004 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Lock/Lock.qml @@ -0,0 +1,153 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import qs.Common +import qs.Services + +Item { + id: root + property string sid: Quickshell.env("XDG_SESSION_ID") || "self" + property string sessionPath: "" + + function activate() { + loader.activeAsync = true + } + + Component.onCompleted: { + getSessionPath.running = true + } + + Process { + id: getSessionPath + command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1", "--method", "org.freedesktop.login1.Manager.GetSession", sid] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const match = text.match(/objectpath '([^']+)'/) + if (match) { + root.sessionPath = match[1] + console.log("Found session path:", root.sessionPath) + checkCurrentLockState.running = true + lockStateMonitor.running = true + } else { + console.warn("Could not determine session path") + } + } + } + + onExited: (exitCode, exitStatus) => { + if (exitCode !== 0) { + console.warn("Failed to get session path, exit code:", exitCode) + } + } + } + + Process { + id: checkCurrentLockState + command: root.sessionPath ? ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.login1.Session", "LockedHint"] : [] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text.includes("true")) { + console.log("Session is locked on startup, activating lock screen") + LockScreenService.resetState() + loader.activeAsync = true + } + } + } + + onExited: (exitCode, exitStatus) => { + if (exitCode !== 0) { + console.warn("Failed to check initial lock state, exit code:", exitCode) + } + } + } + + Process { + id: lockStateMonitor + command: root.sessionPath ? ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath] : [] + running: false + + stdout: SplitParser { + splitMarker: "\n" + + onRead: line => { + if (line.includes("org.freedesktop.login1.Session.Lock")) { + console.log("login1: Lock signal received -> show lock") + LockScreenService.resetState() + loader.activeAsync = true + } else if (line.includes("org.freedesktop.login1.Session.Unlock")) { + console.log("login1: Unlock signal received -> hide lock") + loader.active = false + } else if (line.includes("LockedHint") && line.includes("true")) { + console.log("login1: LockedHint=true -> show lock") + LockScreenService.resetState() + loader.activeAsync = true + } else if (line.includes("LockedHint") && line.includes("false")) { + console.log("login1: LockedHint=false -> hide lock") + loader.active = false + } + } + } + + onExited: (exitCode, exitStatus) => { + if (exitCode !== 0) { + console.warn("gdbus monitor failed, exit code:", exitCode) + } + } + } + + LazyLoader { + id: loader + + WlSessionLock { + id: sessionLock + + property bool unlocked: false + property string sharedPasswordBuffer: "" + + locked: true + + onLockedChanged: { + if (!locked) { + loader.active = false + } + } + + LockSurface { + id: lockSurface + lock: sessionLock + sharedPasswordBuffer: sessionLock.sharedPasswordBuffer + onPasswordChanged: newPassword => { + sessionLock.sharedPasswordBuffer = newPassword + } + } + } + } + + LockScreenDemo { + id: demoWindow + } + + IpcHandler { + target: "lock" + + function lock() { + console.log("Lock screen requested via IPC") + LockScreenService.resetState() + loader.activeAsync = true + } + + function demo() { + console.log("Lock screen DEMO mode requested via IPC") + demoWindow.showDemo() + } + + function isLocked(): bool { + return loader.active + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Lock/LockScreenContent.qml b/quickshell/.config/quickshell/Modules/Lock/LockScreenContent.qml new file mode 100644 index 0000000..feaf561 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Lock/LockScreenContent.qml @@ -0,0 +1,1010 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pam +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property string passwordBuffer: "" + property bool demoMode: false + property string screenName: "" + + signal unlockRequested + + // Internal power dialog state + property bool powerDialogVisible: false + property string powerDialogTitle: "" + property string powerDialogMessage: "" + property string powerDialogConfirmText: "" + property color powerDialogConfirmColor: Theme.primary + property var powerDialogOnConfirm: function () {} + + function showPowerDialog(title, message, confirmText, confirmColor, onConfirm) { + powerDialogTitle = title + powerDialogMessage = message + powerDialogConfirmText = confirmText + powerDialogConfirmColor = confirmColor + powerDialogOnConfirm = onConfirm + powerDialogVisible = true + } + + function hidePowerDialog() { + powerDialogVisible = false + } + + Component.onCompleted: { + if (demoMode) { + LockScreenService.pickRandomFact() + } + + WeatherService.addRef() + UserInfoService.refreshUserInfo() + } + onDemoModeChanged: { + if (demoMode) { + LockScreenService.pickRandomFact() + } + } + Component.onDestruction: { + WeatherService.removeRef() + } + + Loader { + anchors.fill: parent + active: { + var currentWallpaper = SessionData.getMonitorWallpaper(screenName) + return !currentWallpaper || (currentWallpaper && currentWallpaper.startsWith("#")) + } + asynchronous: true + + sourceComponent: DankBackdrop { + screenName: root.screenName + } + } + + Image { + id: wallpaperBackground + + anchors.fill: parent + source: { + var currentWallpaper = SessionData.getMonitorWallpaper(screenName) + return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : "" + } + fillMode: Image.PreserveAspectCrop + smooth: true + asynchronous: false + cache: true + visible: source !== "" + layer.enabled: true + + layer.effect: MultiEffect { + autoPaddingEnabled: false + blurEnabled: true + blur: 0.8 + blurMax: 32 + blurMultiplier: 1 + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + } + + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.4 + } + + SystemClock { + id: systemClock + + precision: SystemClock.Minutes + } + + Rectangle { + anchors.fill: parent + color: "transparent" + + Item { + anchors.centerIn: parent + anchors.verticalCenterOffset: -100 + width: 400 + height: 140 + + StyledText { + id: clockText + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + text: { + const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP" + return systemClock.date.toLocaleTimeString(Qt.locale(), format) + } + font.pixelSize: 120 + font.weight: Font.Light + color: "white" + lineHeight: 0.8 + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: clockText.bottom + anchors.topMargin: -20 + text: { + if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) { + return systemClock.date.toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) + } + return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat) + } + font.pixelSize: Theme.fontSizeXLarge + color: "white" + opacity: 0.9 + } + } + + ColumnLayout { + anchors.centerIn: parent + anchors.verticalCenterOffset: 50 + spacing: Theme.spacingM + width: 380 + + RowLayout { + spacing: Theme.spacingL + Layout.fillWidth: true + + Item { + id: avatarContainer + + property bool hasImage: profileImageLoader.status === Image.Ready + + Layout.preferredWidth: 60 + Layout.preferredHeight: 60 + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "transparent" + border.color: Theme.primary + border.width: 1 + visible: parent.hasImage + } + + Image { + id: profileImageLoader + + source: { + if (PortalService.profileImage === "") { + return "" + } + + if (PortalService.profileImage.startsWith("/")) { + return "file://" + PortalService.profileImage + } + + return PortalService.profileImage + } + smooth: true + asynchronous: true + mipmap: true + cache: true + visible: false + } + + MultiEffect { + anchors.fill: parent + anchors.margins: 5 + source: profileImageLoader + maskEnabled: true + maskSource: circularMask + visible: avatarContainer.hasImage + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: circularMask + + width: 60 - 10 + height: 60 - 10 + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "black" + antialiasing: true + } + } + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: Theme.primary + visible: !parent.hasImage + + DankIcon { + anchors.centerIn: parent + name: "person" + size: Theme.iconSize + 4 + color: Theme.primaryText + } + } + + DankIcon { + anchors.centerIn: parent + name: "warning" + size: Theme.iconSize + 4 + color: Theme.primaryText + visible: PortalService.profileImage !== "" && profileImageLoader.status === Image.Error + } + } + + Rectangle { + property bool showPassword: false + + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9) + border.color: passwordField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3) + border.width: passwordField.activeFocus ? 2 : 1 + + DankIcon { + id: lockIcon + + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + name: "lock" + size: 20 + color: passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText + } + + TextInput { + id: passwordField + + anchors.fill: parent + anchors.leftMargin: lockIcon.width + Theme.spacingM * 2 + anchors.rightMargin: { + let margin = Theme.spacingM + if (loadingSpinner.visible) { + margin += loadingSpinner.width + } + if (enterButton.visible) { + margin += enterButton.width + 2 + } + if (virtualKeyboardButton.visible) { + margin += virtualKeyboardButton.width + } + if (revealButton.visible) { + margin += revealButton.width + } + return margin + } + opacity: 0 + focus: !demoMode + enabled: !demoMode + echoMode: parent.showPassword ? TextInput.Normal : TextInput.Password + onTextChanged: { + if (!demoMode) { + root.passwordBuffer = text + } + } + onAccepted: { + if (!demoMode && root.passwordBuffer.length > 0 && !pam.active) { + console.log("Enter pressed, starting PAM authentication") + pam.start() + } + } + Keys.onPressed: event => { + if (demoMode) { + return + } + + if (pam.active) { + console.log("PAM is active, ignoring input") + event.accepted = true + return + } + } + + Timer { + id: focusTimer + + interval: 100 + running: !demoMode + onTriggered: passwordField.forceActiveFocus() + } + } + + KeyboardController { + id: keyboardController + target: passwordField + rootObject: root + } + + StyledText { + id: placeholder + + anchors.left: lockIcon.right + anchors.leftMargin: Theme.spacingM + anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))) + anchors.rightMargin: 2 + anchors.verticalCenter: parent.verticalCenter + text: { + if (demoMode) { + return "" + } + if (LockScreenService.unlocking) { + return "Unlocking..." + } + if (pam.active) { + return "Authenticating..." + } + return "Password..." + } + color: LockScreenService.unlocking ? Theme.primary : (pam.active ? Theme.primary : Theme.outline) + font.pixelSize: Theme.fontSizeMedium + opacity: (demoMode || root.passwordBuffer.length === 0) ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + StyledText { + anchors.left: lockIcon.right + anchors.leftMargin: Theme.spacingM + anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))) + anchors.rightMargin: 2 + anchors.verticalCenter: parent.verticalCenter + text: { + if (demoMode) { + return "••••••••" + } + if (parent.showPassword) { + return root.passwordBuffer + } + return "•".repeat(Math.min(root.passwordBuffer.length, 25)) + } + color: Theme.surfaceText + font.pixelSize: parent.showPassword ? Theme.fontSizeMedium : Theme.fontSizeLarge + opacity: (demoMode || root.passwordBuffer.length > 0) ? 1 : 0 + elide: Text.ElideRight + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + } + + DankActionButton { + id: revealButton + + anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)) + anchors.rightMargin: 0 + anchors.verticalCenter: parent.verticalCenter + iconName: parent.showPassword ? "visibility_off" : "visibility" + buttonSize: 32 + visible: !demoMode && root.passwordBuffer.length > 0 && !pam.active && !LockScreenService.unlocking + enabled: visible + onClicked: parent.showPassword = !parent.showPassword + } + DankActionButton { + id: virtualKeyboardButton + + anchors.right: enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right) + anchors.rightMargin: enterButton.visible ? 0 : Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + iconName: "keyboard" + buttonSize: 32 + visible: !demoMode && !pam.active && !LockScreenService.unlocking + enabled: visible + onClicked: { + if (keyboardController.isKeyboardActive) { + keyboardController.hide() + } else { + keyboardController.show() + } + } + } + + Rectangle { + id: loadingSpinner + + anchors.right: enterButton.visible ? enterButton.left : parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + width: 24 + height: 24 + radius: 12 + color: "transparent" + visible: !demoMode && (pam.active || LockScreenService.unlocking) + + DankIcon { + anchors.centerIn: parent + name: "check_circle" + size: 20 + color: Theme.primary + visible: LockScreenService.unlocking + + SequentialAnimation on scale { + running: LockScreenService.unlocking + + NumberAnimation { + from: 0 + to: 1.2 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedDecel + } + + NumberAnimation { + from: 1.2 + to: 1 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedAccel + } + } + } + + Item { + anchors.fill: parent + visible: pam.active && !LockScreenService.unlocking + + Rectangle { + width: 20 + height: 20 + radius: 10 + anchors.centerIn: parent + color: "transparent" + border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) + border.width: 2 + } + + Rectangle { + width: 20 + height: 20 + radius: 10 + anchors.centerIn: parent + color: "transparent" + border.color: Theme.primary + border.width: 2 + + Rectangle { + width: parent.width + height: parent.height / 2 + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9) + } + + RotationAnimation on rotation { + running: pam.active && !LockScreenService.unlocking + loops: Animation.Infinite + duration: Anims.durLong + from: 0 + to: 360 + } + } + } + } + + DankActionButton { + id: enterButton + + anchors.right: parent.right + anchors.rightMargin: 2 + anchors.verticalCenter: parent.verticalCenter + iconName: "keyboard_return" + buttonSize: 36 + visible: (demoMode || (root.passwordBuffer.length > 0 && !pam.active && !LockScreenService.unlocking)) + enabled: !demoMode + onClicked: { + if (!demoMode) { + console.log("Enter button clicked, starting PAM authentication") + pam.start() + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + StyledText { + Layout.fillWidth: true + Layout.preferredHeight: LockScreenService.pamState ? 20 : 0 + text: { + if (LockScreenService.pamState === "error") { + return "Authentication error - try again" + } + if (LockScreenService.pamState === "max") { + return "Too many attempts - locked out" + } + if (LockScreenService.pamState === "fail") { + return "Incorrect password - try again" + } + return "" + } + color: Theme.error + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignHCenter + visible: LockScreenService.pamState !== "" + opacity: LockScreenService.pamState !== "" ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on Layout.preferredHeight { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + StyledText { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: Theme.spacingXL + text: "DEMO MODE - Click anywhere to exit" + font.pixelSize: Theme.fontSizeSmall + color: "white" + opacity: 0.7 + visible: demoMode + } + + Row { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacingXL + spacing: Theme.spacingL + + Row { + spacing: 6 + visible: WeatherService.weather.available + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: WeatherService.getWeatherIcon(WeatherService.weather.wCode) + size: Theme.iconSize + color: "white" + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Light + color: "white" + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: 1 + height: 24 + color: Qt.rgba(255, 255, 255, 0.2) + anchors.verticalCenter: parent.verticalCenter + visible: WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable) + } + + Row { + spacing: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + visible: NetworkService.networkStatus !== "disconnected" || (BluetoothService.available && BluetoothService.enabled) || (AudioService.sink && AudioService.sink.audio) + + DankIcon { + name: NetworkService.networkStatus === "ethernet" ? "lan" : NetworkService.wifiSignalIcon + size: Theme.iconSize - 2 + color: NetworkService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5) + anchors.verticalCenter: parent.verticalCenter + visible: NetworkService.networkStatus !== "disconnected" + } + + DankIcon { + name: "bluetooth" + size: Theme.iconSize - 2 + color: "white" + anchors.verticalCenter: parent.verticalCenter + visible: BluetoothService.available && BluetoothService.enabled + } + + DankIcon { + name: { + if (!AudioService.sink?.audio) { + return "volume_up" + } + if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) { + return "volume_off" + } + if (AudioService.sink.audio.volume * 100 < 33) { + return "volume_down" + } + return "volume_up" + } + size: Theme.iconSize - 2 + color: (AudioService.sink && AudioService.sink.audio && (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0)) ? Qt.rgba(255, 255, 255, 0.5) : "white" + anchors.verticalCenter: parent.verticalCenter + visible: AudioService.sink && AudioService.sink.audio + } + } + + Rectangle { + width: 1 + height: 24 + color: Qt.rgba(255, 255, 255, 0.2) + anchors.verticalCenter: parent.verticalCenter + visible: BatteryService.batteryAvailable && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio)) + } + + Row { + spacing: 4 + visible: BatteryService.batteryAvailable + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: { + if (BatteryService.isCharging) { + if (BatteryService.batteryLevel >= 90) { + return "battery_charging_full" + } + + if (BatteryService.batteryLevel >= 80) { + return "battery_charging_90" + } + + if (BatteryService.batteryLevel >= 60) { + return "battery_charging_80" + } + + if (BatteryService.batteryLevel >= 50) { + return "battery_charging_60" + } + + if (BatteryService.batteryLevel >= 30) { + return "battery_charging_50" + } + + if (BatteryService.batteryLevel >= 20) { + return "battery_charging_30" + } + + return "battery_charging_20" + } + if (BatteryService.isPluggedIn) { + if (BatteryService.batteryLevel >= 90) { + return "battery_charging_full" + } + + if (BatteryService.batteryLevel >= 80) { + return "battery_charging_90" + } + + if (BatteryService.batteryLevel >= 60) { + return "battery_charging_80" + } + + if (BatteryService.batteryLevel >= 50) { + return "battery_charging_60" + } + + if (BatteryService.batteryLevel >= 30) { + return "battery_charging_50" + } + + if (BatteryService.batteryLevel >= 20) { + return "battery_charging_30" + } + + return "battery_charging_20" + } + if (BatteryService.batteryLevel >= 95) { + return "battery_full" + } + + if (BatteryService.batteryLevel >= 85) { + return "battery_6_bar" + } + + if (BatteryService.batteryLevel >= 70) { + return "battery_5_bar" + } + + if (BatteryService.batteryLevel >= 55) { + return "battery_4_bar" + } + + if (BatteryService.batteryLevel >= 40) { + return "battery_3_bar" + } + + if (BatteryService.batteryLevel >= 25) { + return "battery_2_bar" + } + + return "battery_1_bar" + } + size: Theme.iconSize + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error + } + + if (BatteryService.isCharging || BatteryService.isPluggedIn) { + return Theme.primary + } + + return "white" + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: BatteryService.batteryLevel + "%" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Light + color: "white" + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Row { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: Theme.spacingXL + spacing: Theme.spacingL + visible: SettingsData.lockScreenShowPowerActions + + DankActionButton { + iconName: "power_settings_new" + iconColor: Theme.error + buttonSize: 40 + onClicked: { + if (demoMode) { + console.log("Demo: Power") + } else { + showPowerDialog("Power Off", "Power off this computer?", "Power Off", Theme.error, function () { + SessionService.poweroff() + }) + } + } + } + + DankActionButton { + iconName: "refresh" + buttonSize: 40 + onClicked: { + if (demoMode) { + console.log("Demo: Reboot") + } else { + showPowerDialog("Restart", "Restart this computer?", "Restart", Theme.primary, function () { + SessionService.reboot() + }) + } + } + } + + DankActionButton { + iconName: "logout" + buttonSize: 40 + onClicked: { + if (demoMode) { + console.log("Demo: Logout") + } else { + showPowerDialog("Log Out", "End this session?", "Log Out", Theme.primary, function () { + SessionService.logout() + }) + } + } + } + } + + StyledText { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: Theme.spacingL + width: Math.min(parent.width - Theme.spacingXL * 2, implicitWidth) + text: LockScreenService.randomFact + font.pixelSize: Theme.fontSizeSmall + color: "white" + opacity: 0.8 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.NoWrap + visible: LockScreenService.randomFact !== "" + } + } + + FileView { + id: pamConfigWatcher + + path: "/etc/pam.d/dankshell" + printErrors: false + } + + PamContext { + id: pam + + config: pamConfigWatcher.loaded ? "dankshell" : "login" + onResponseRequiredChanged: { + if (demoMode) + return + + console.log("PAM response required:", responseRequired) + if (!responseRequired) + return + + console.log("Responding to PAM with password buffer length:", root.passwordBuffer.length) + respond(root.passwordBuffer) + } + onCompleted: res => { + if (demoMode) + return + + console.log("PAM authentication completed with result:", res) + if (res === PamResult.Success) { + console.log("Authentication successful, unlocking") + LockScreenService.setUnlocking(true) + passwordField.text = "" + root.passwordBuffer = "" + root.unlockRequested() + return + } + console.log("Authentication failed:", res) + if (res === PamResult.Error) + LockScreenService.setPamState("error") + else if (res === PamResult.MaxTries) + LockScreenService.setPamState("max") + else if (res === PamResult.Failed) + LockScreenService.setPamState("fail") + placeholderDelay.restart() + } + } + + Timer { + id: placeholderDelay + + interval: 4000 + onTriggered: LockScreenService.setPamState("") + } + + MouseArea { + anchors.fill: parent + enabled: demoMode + onClicked: root.unlockRequested() + } + + // Internal power dialog + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.8) + visible: powerDialogVisible + z: 1000 + + Rectangle { + anchors.centerIn: parent + width: 320 + height: 180 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXL + + DankIcon { + anchors.horizontalCenter: parent.horizontalCenter + name: "power_settings_new" + size: 32 + color: powerDialogConfirmColor + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: powerDialogMessage + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + Rectangle { + width: 100 + height: 40 + radius: Theme.cornerRadius + color: Theme.surfaceVariant + + StyledText { + anchors.centerIn: parent + text: "Cancel" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: hidePowerDialog() + } + } + + Rectangle { + width: 100 + height: 40 + radius: Theme.cornerRadius + color: powerDialogConfirmColor + + StyledText { + anchors.centerIn: parent + text: powerDialogConfirmText + color: Theme.primaryText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + hidePowerDialog() + powerDialogOnConfirm() + } + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Lock/LockScreenDemo.qml b/quickshell/.config/quickshell/Modules/Lock/LockScreenDemo.qml new file mode 100644 index 0000000..63599fb --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Lock/LockScreenDemo.qml @@ -0,0 +1,45 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Common + +PanelWindow { + id: root + + property bool demoActive: false + + visible: demoActive + + anchors { + top: true + bottom: true + left: true + right: true + } + + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + color: "transparent" + + function showDemo(): void { + console.log("Showing lock screen demo") + demoActive = true + } + + function hideDemo(): void { + console.log("Hiding lock screen demo") + demoActive = false + } + + Loader { + anchors.fill: parent + active: demoActive + sourceComponent: LockScreenContent { + demoMode: true + screenName: root.screen?.name ?? "" + onUnlockRequested: root.hideDemo() + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Lock/LockSurface.qml b/quickshell/.config/quickshell/Modules/Lock/LockSurface.qml new file mode 100644 index 0000000..77ca4a9 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Lock/LockSurface.qml @@ -0,0 +1,37 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Common + +WlSessionLockSurface { + id: root + + required property WlSessionLock lock + required property string sharedPasswordBuffer + + signal passwordChanged(string newPassword) + + readonly property bool locked: lock && !lock.locked + + function unlock(): void { + lock.locked = false + } + + color: "transparent" + + Loader { + anchors.fill: parent + sourceComponent: LockScreenContent { + demoMode: false + passwordBuffer: root.sharedPasswordBuffer + screenName: root.screen?.name ?? "" + onUnlockRequested: root.unlock() + onPasswordBufferChanged: { + if (root.sharedPasswordBuffer !== passwordBuffer) { + root.passwordChanged(passwordBuffer) + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml b/quickshell/.config/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml new file mode 100644 index 0000000..1287a81 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml @@ -0,0 +1,137 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankListView { + id: listView + + property var keyboardController: null + property bool keyboardActive: false + property bool autoScrollDisabled: false + property alias count: listView.count + property alias listContentHeight: listView.contentHeight + + clip: true + model: NotificationService.groupedNotifications + spacing: Theme.spacingL + + onIsUserScrollingChanged: { + if (isUserScrolling && keyboardController && keyboardController.keyboardNavigationActive) { + autoScrollDisabled = true + } + } + + function enableAutoScroll() { + autoScrollDisabled = false + } + + Timer { + id: positionPreservationTimer + interval: 200 + running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled + repeat: true + onTriggered: { + if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) { + keyboardController.ensureVisible() + } + } + } + + NotificationEmptyState { + visible: listView.count === 0 + anchors.centerIn: parent + } + + onModelChanged: { + if (!keyboardController || !keyboardController.keyboardNavigationActive) { + return + } + keyboardController.rebuildFlatNavigation() + Qt.callLater(() => { + if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) { + keyboardController.ensureVisible() + } + }) + } + + delegate: Item { + required property var modelData + required property int index + + readonly property bool isExpanded: (NotificationService.expandedGroups[modelData && modelData.key] || false) + + width: ListView.view.width + height: notificationCard.height + + NotificationCard { + id: notificationCard + width: parent.width + notificationGroup: modelData + keyboardNavigationActive: listView.keyboardActive + + isGroupSelected: { + if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) { + return false + } + keyboardController.selectionVersion + const selection = keyboardController.getCurrentSelection() + return selection.type === "group" && selection.groupIndex === index + } + + selectedNotificationIndex: { + if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) { + return -1 + } + keyboardController.selectionVersion + const selection = keyboardController.getCurrentSelection() + return (selection.type === "notification" && selection.groupIndex === index) ? selection.notificationIndex : -1 + } + } + } + + Connections { + target: NotificationService + + function onGroupedNotificationsChanged() { + if (!keyboardController) { + return + } + + if (keyboardController.isTogglingGroup) { + keyboardController.rebuildFlatNavigation() + return + } + + keyboardController.rebuildFlatNavigation() + + if (keyboardController.keyboardNavigationActive) { + Qt.callLater(() => { + if (!autoScrollDisabled) { + keyboardController.ensureVisible() + } + }) + } + } + + function onExpandedGroupsChanged() { + if (keyboardController && keyboardController.keyboardNavigationActive) { + Qt.callLater(() => { + if (!autoScrollDisabled) { + keyboardController.ensureVisible() + } + }) + } + } + + function onExpandedMessagesChanged() { + if (keyboardController && keyboardController.keyboardNavigationActive) { + Qt.callLater(() => { + if (!autoScrollDisabled) { + keyboardController.ensureVisible() + } + }) + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationCard.qml b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationCard.qml new file mode 100644 index 0000000..22d411c --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationCard.qml @@ -0,0 +1,728 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var notificationGroup + property bool expanded: (NotificationService.expandedGroups[notificationGroup && notificationGroup.key] || false) + property bool descriptionExpanded: (NotificationService.expandedMessages[(notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification + && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""] || false) + property bool userInitiatedExpansion: false + + property bool isGroupSelected: false + property int selectedNotificationIndex: -1 + property bool keyboardNavigationActive: false + + width: parent ? parent.width : 400 + height: { + if (expanded) { + return expandedContent.height + 28 + } + const baseHeight = 116 + if (descriptionExpanded) { + return baseHeight + descriptionText.contentHeight - (descriptionText.font.pixelSize * 1.2 * 2) + } + return baseHeight + } + radius: Theme.cornerRadius + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + color: { + if (isGroupSelected && keyboardNavigationActive) { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2) + } + if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) + } + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1) + } + border.color: { + if (isGroupSelected && keyboardNavigationActive) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) + } + if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) + } + if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) + } + return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) + } + border.width: { + if (isGroupSelected && keyboardNavigationActive) { + return 1.5 + } + if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { + return 1 + } + if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) { + return 2 + } + return 1 + } + clip: true + + Rectangle { + anchors.fill: parent + radius: parent.radius + visible: notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { + position: 0.0 + color: Theme.primary + } + GradientStop { + position: 0.02 + color: Theme.primary + } + GradientStop { + position: 0.021 + color: "transparent" + } + } + opacity: 1.0 + } + + Item { + id: collapsedContent + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 12 + anchors.leftMargin: 16 + anchors.rightMargin: 56 + height: 92 + visible: !expanded + + Rectangle { + id: iconContainer + readonly property bool hasNotificationImage: notificationGroup?.latestNotification?.image && notificationGroup.latestNotification.image !== "" + + width: 55 + height: 55 + radius: 27.5 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + border.color: "transparent" + border.width: 0 + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 18 + + IconImage { + anchors.fill: parent + anchors.margins: 2 + source: { + if (parent.hasNotificationImage) + return notificationGroup.latestNotification.cleanImage + if (notificationGroup?.latestNotification?.appIcon) { + const appIcon = notificationGroup.latestNotification.appIcon + if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://")) + return appIcon + return Quickshell.iconPath(appIcon, true) + } + return "" + } + visible: status === Image.Ready + } + + StyledText { + anchors.centerIn: parent + visible: !parent.hasNotificationImage && (!notificationGroup?.latestNotification?.appIcon || notificationGroup.latestNotification.appIcon === "") + text: { + const appName = notificationGroup?.appName || "?" + return appName.charAt(0).toUpperCase() + } + font.pixelSize: 20 + font.weight: Font.Bold + color: Theme.primaryText + } + + Rectangle { + width: 18 + height: 18 + radius: 9 + color: Theme.primary + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: -2 + anchors.rightMargin: -2 + visible: (notificationGroup?.count || 0) > 1 + + StyledText { + anchors.centerIn: parent + text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString() + color: Theme.primaryText + font.pixelSize: 9 + font.weight: Font.Bold + } + } + } + + Rectangle { + id: textContainer + + anchors.left: iconContainer.right + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + color: "transparent" + + Item { + width: parent.width + height: parent.height + anchors.top: parent.top + anchors.topMargin: -2 + + Column { + width: parent.width + spacing: 2 + + StyledText { + width: parent.width + text: { + const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || "" + const appName = (notificationGroup && notificationGroup.appName) || "" + return timeStr.length > 0 ? `${appName} • ${timeStr}` : appName + } + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + } + + StyledText { + text: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.summary) || "" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + width: parent.width + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + + StyledText { + id: descriptionText + property string fullText: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.htmlBody) || "" + property bool hasMoreText: truncated + + text: fullText + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + width: parent.width + elide: Text.ElideRight + maximumLineCount: descriptionExpanded ? -1 : 2 + wrapMode: Text.WordWrap + visible: text.length > 0 + linkColor: Theme.primary + onLinkActivated: link => Qt.openUrlExternally(link) + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (parent.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor + + onClicked: mouse => { + if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) { + const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification + && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : "" + NotificationService.toggleMessageExpansion(messageId) + } + } + + propagateComposedEvents: true + onPressed: mouse => { + if (parent.hoveredLink) { + mouse.accepted = false + } + } + onReleased: mouse => { + if (parent.hoveredLink) { + mouse.accepted = false + } + } + } + } + } + } + } + } + + Column { + id: expandedContent + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 14 + anchors.bottomMargin: 14 + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + spacing: -1 + visible: expanded + + Item { + width: parent.width + height: 40 + + Row { + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 56 + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + StyledText { + text: notificationGroup?.appName || "" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + maximumLineCount: 1 + } + + Rectangle { + width: 18 + height: 18 + radius: 9 + color: Theme.primary + visible: (notificationGroup?.count || 0) > 1 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + anchors.centerIn: parent + text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString() + color: Theme.primaryText + font.pixelSize: 9 + font.weight: Font.Bold + } + } + } + } + + Column { + width: parent.width + spacing: 16 + + Repeater { + model: notificationGroup?.notifications?.slice(0, 10) || [] + + delegate: Rectangle { + required property var modelData + required property int index + readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false + readonly property bool isSelected: root.selectedNotificationIndex === index + + width: parent.width + height: { + const baseHeight = 120 + if (messageExpanded) { + const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2 + if (bodyText.implicitHeight > twoLineHeight + 2) { + const extraHeight = bodyText.implicitHeight - twoLineHeight + return baseHeight + extraHeight + } + } + return baseHeight + } + radius: Theme.cornerRadius + color: isSelected ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.25) : "transparent" + border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) + border.width: isSelected ? 1 : 1 + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on height { + enabled: false + } + + Item { + anchors.fill: parent + anchors.margins: 12 + anchors.bottomMargin: 8 + + Rectangle { + id: messageIcon + + readonly property bool hasNotificationImage: modelData?.image && modelData.image !== "" + + width: 32 + height: 32 + radius: 16 + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 32 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) + border.width: 1 + + IconImage { + anchors.fill: parent + anchors.margins: 1 + source: { + if (parent.hasNotificationImage) + return modelData.cleanImage + + if (modelData?.appIcon) { + const appIcon = modelData.appIcon + if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://")) + return appIcon + + return Quickshell.iconPath(appIcon, true) + } + return "" + } + visible: status === Image.Ready + } + + StyledText { + anchors.centerIn: parent + visible: !parent.hasNotificationImage && (!modelData?.appIcon || modelData.appIcon === "") + text: { + const appName = modelData?.appName || "?" + return appName.charAt(0).toUpperCase() + } + font.pixelSize: 12 + font.weight: Font.Bold + color: Theme.primaryText + } + } + + Item { + anchors.left: messageIcon.right + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.top: parent.top + anchors.bottom: parent.bottom + + Column { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: buttonArea.top + anchors.bottomMargin: 4 + spacing: 2 + + StyledText { + width: parent.width + text: modelData?.timeStr || "" + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + + StyledText { + width: parent.width + text: modelData?.summary || "" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + + StyledText { + id: bodyText + property bool hasMoreText: truncated + + text: modelData?.htmlBody || "" + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + width: parent.width + elide: messageExpanded ? Text.ElideNone : Text.ElideRight + maximumLineCount: messageExpanded ? -1 : 2 + wrapMode: Text.WordWrap + visible: text.length > 0 + linkColor: Theme.primary + onLinkActivated: link => Qt.openUrlExternally(link) + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor + + onClicked: mouse => { + if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) { + NotificationService.toggleMessageExpansion(modelData?.notification?.id || "") + } + } + + propagateComposedEvents: true + onPressed: mouse => { + if (parent.hoveredLink) { + mouse.accepted = false + } + } + onReleased: mouse => { + if (parent.hoveredLink) { + mouse.accepted = false + } + } + } + } + } + + Item { + id: buttonArea + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 30 + + Row { + anchors.right: parent.right + anchors.bottom: parent.bottom + spacing: 8 + + Repeater { + model: modelData?.actions || [] + + Rectangle { + property bool isHovered: false + + width: Math.max(actionText.implicitWidth + 12, 50) + height: 24 + radius: 4 + color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + + StyledText { + id: actionText + text: { + const baseText = modelData.text || "View" + if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) { + return `${baseText} (${index + 1})` + } + return baseText + } + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: { + if (modelData && modelData.invoke) { + modelData.invoke() + } + } + } + } + } + + Rectangle { + property bool isHovered: false + + width: Math.max(clearText.implicitWidth + 12, 50) + height: 24 + radius: 4 + color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + + StyledText { + id: clearText + text: "Clear" + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: NotificationService.dismissNotification(modelData) + } + } + } + } + } + } + } + } + } + } + + Row { + visible: !expanded + anchors.right: clearButton.left + anchors.rightMargin: 8 + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + spacing: 8 + + Repeater { + model: notificationGroup?.latestNotification?.actions || [] + + Rectangle { + property bool isHovered: false + + width: Math.max(actionText.implicitWidth + 12, 50) + height: 24 + radius: 4 + color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + + StyledText { + id: actionText + text: { + const baseText = modelData.text || "View" + if (keyboardNavigationActive && isGroupSelected) { + return `${baseText} (${index + 1})` + } + return baseText + } + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: { + if (modelData && modelData.invoke) { + modelData.invoke() + } + } + } + } + } + } + + Rectangle { + id: clearButton + + property bool isHovered: false + + visible: !expanded + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + width: clearText.width + 16 + height: clearText.height + 8 + radius: 6 + color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + + StyledText { + id: clearText + text: "Clear" + color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: clearButton.isHovered = true + onExited: clearButton.isHovered = false + onClicked: NotificationService.dismissGroup(notificationGroup?.key || "") + } + } + + MouseArea { + anchors.fill: parent + visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded + onClicked: { + root.userInitiatedExpansion = true + NotificationService.toggleGroupExpansion(notificationGroup?.key || "") + } + z: -1 + } + + Item { + id: fixedControls + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 12 + anchors.rightMargin: 16 + width: 60 + height: 28 + + DankActionButton { + anchors.left: parent.left + anchors.top: parent.top + visible: (notificationGroup?.count || 0) > 1 + iconName: expanded ? "expand_less" : "expand_more" + iconSize: 18 + buttonSize: 28 + onClicked: { + root.userInitiatedExpansion = true + NotificationService.toggleGroupExpansion(notificationGroup?.key || "") + } + } + + DankActionButton { + anchors.right: parent.right + anchors.top: parent.top + iconName: "close" + iconSize: 18 + buttonSize: 28 + onClicked: NotificationService.dismissGroup(notificationGroup?.key || "") + } + } + + Behavior on height { + enabled: root.userInitiatedExpansion + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + onFinished: root.userInitiatedExpansion = false + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml new file mode 100644 index 0000000..3ea923c --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml @@ -0,0 +1,202 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Notifications.Center + +DankPopout { + id: root + + property bool notificationHistoryVisible: false + property string triggerSection: "right" + property var triggerScreen: null + + NotificationKeyboardController { + id: keyboardController + listView: null + isOpen: notificationHistoryVisible + onClose: () => { + notificationHistoryVisible = false + } + } + + function setTriggerPosition(x, y, width, section, screen) { + triggerX = x + triggerY = y + triggerWidth = width + triggerSection = section + triggerScreen = screen + } + + popupWidth: 400 + popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400 + triggerX: Screen.width - 400 - Theme.spacingL + triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS + triggerWidth: 40 + positioning: "center" + screen: triggerScreen + shouldBeVisible: notificationHistoryVisible + visible: shouldBeVisible + + onNotificationHistoryVisibleChanged: { + if (notificationHistoryVisible) { + open() + } else { + close() + } + } + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + NotificationService.onOverlayOpen() + Qt.callLater(() => { + if (contentLoader.item) { + contentLoader.item.externalKeyboardController = keyboardController + + const notificationList = findChild(contentLoader.item, "notificationList") + const notificationHeader = findChild(contentLoader.item, "notificationHeader") + + if (notificationList) { + keyboardController.listView = notificationList + notificationList.keyboardController = keyboardController + } + if (notificationHeader) { + notificationHeader.keyboardController = keyboardController + } + + keyboardController.reset() + keyboardController.rebuildFlatNavigation() + } + }) + } else { + NotificationService.onOverlayClose() + keyboardController.keyboardNavigationActive = false + } + } + + function findChild(parent, objectName) { + if (parent.objectName === objectName) { + return parent + } + for (let i = 0; i < parent.children.length; i++) { + const child = parent.children[i] + const result = findChild(child, objectName) + if (result) { + return result + } + } + return null + } + + content: Component { + Rectangle { + id: notificationContent + + property var externalKeyboardController: null + property real cachedHeaderHeight: 32 + + implicitHeight: { + let baseHeight = Theme.spacingL * 2 + baseHeight += cachedHeaderHeight + baseHeight += (notificationSettings.expanded ? notificationSettings.contentHeight : 0) + baseHeight += Theme.spacingM * 2 + let listHeight = notificationList.listContentHeight + if (NotificationService.groupedNotifications.length === 0) { + listHeight = 200 + } + baseHeight += Math.min(listHeight, 600) + const maxHeight = root.screen ? root.screen.height * 0.8 : Screen.height * 0.8 + return Math.max(300, Math.min(baseHeight, maxHeight)) + } + + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + focus: true + + Component.onCompleted: { + if (root.shouldBeVisible) { + forceActiveFocus() + } + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + root.close() + event.accepted = true + } else if (externalKeyboardController) { + externalKeyboardController.handleKey(event) + } + } + + Connections { + function onShouldBeVisibleChanged() { + if (root.shouldBeVisible) { + Qt.callLater(() => { + notificationContent.forceActiveFocus() + }) + } else { + notificationContent.focus = false + } + } + target: root + } + + FocusScope { + id: contentColumn + + anchors.fill: parent + anchors.margins: Theme.spacingL + focus: true + + Column { + id: contentColumnInner + anchors.fill: parent + spacing: Theme.spacingM + + NotificationHeader { + id: notificationHeader + objectName: "notificationHeader" + onHeightChanged: notificationContent.cachedHeaderHeight = height + } + + NotificationSettings { + id: notificationSettings + expanded: notificationHeader.showSettings + } + + KeyboardNavigatedNotificationList { + id: notificationList + objectName: "notificationList" + + width: parent.width + height: parent.height - notificationContent.cachedHeaderHeight - notificationSettings.height - contentColumnInner.spacing * 2 + } + } + } + + NotificationKeyboardHints { + id: keyboardHints + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + showHints: (externalKeyboardController && externalKeyboardController.showKeyboardHints) || false + z: 200 + } + + Behavior on implicitHeight { + NumberAnimation { + duration: 180 + easing.type: Easing.OutQuart + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationEmptyState.qml b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationEmptyState.qml new file mode 100644 index 0000000..569bfa2 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationEmptyState.qml @@ -0,0 +1,34 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + width: parent.width + height: 200 + visible: NotificationService.notifications.length === 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + width: parent.width * 0.8 + + DankIcon { + anchors.horizontalCenter: parent.horizontalCenter + name: "notifications_none" + size: Theme.iconSizeLarge + 16 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: "Nothing to see here" + font.pixelSize: Theme.fontSizeLarge + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) + font.weight: Font.Medium + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationHeader.qml b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationHeader.qml new file mode 100644 index 0000000..b93051a --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationHeader.qml @@ -0,0 +1,157 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property var keyboardController: null + property bool showSettings: false + + width: parent.width + height: 32 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + StyledText { + text: "Notifications" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + DankActionButton { + id: doNotDisturbButton + + iconName: SessionData.doNotDisturb ? "notifications_off" : "notifications" + iconColor: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText + buttonSize: 28 + anchors.verticalCenter: parent.verticalCenter + onClicked: SessionData.setDoNotDisturb(!SessionData.doNotDisturb) + + Rectangle { + id: doNotDisturbTooltip + + width: tooltipText.contentWidth + Theme.spacingS * 2 + height: tooltipText.contentHeight + Theme.spacingXS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + visible: doNotDisturbButton.children[1].containsMouse + opacity: visible ? 1 : 0 + + StyledText { + id: tooltipText + + text: "Do Not Disturb" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.centerIn: parent + font.hintingPreference: Font.PreferFullHinting + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankActionButton { + id: helpButton + iconName: "info" + iconColor: (keyboardController && keyboardController.showKeyboardHints) ? Theme.primary : Theme.surfaceText + buttonSize: 28 + visible: keyboardController !== null + anchors.verticalCenter: parent.verticalCenter + onClicked: { + if (keyboardController) { + keyboardController.showKeyboardHints = !keyboardController.showKeyboardHints + } + } + } + + DankActionButton { + id: settingsButton + iconName: "settings" + iconColor: root.showSettings ? Theme.primary : Theme.surfaceText + buttonSize: 28 + anchors.verticalCenter: parent.verticalCenter + onClicked: root.showSettings = !root.showSettings + } + + Rectangle { + id: clearAllButton + + width: 120 + height: 28 + radius: Theme.cornerRadius + visible: NotificationService.notifications.length > 0 + color: clearArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: clearArea.containsMouse ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "delete_sweep" + size: Theme.iconSizeSmall + color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Clear All" + font.pixelSize: Theme.fontSizeSmall + color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: clearArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NotificationService.clearAllNotifications() + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationKeyboardController.qml b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationKeyboardController.qml new file mode 100644 index 0000000..928ffd7 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationKeyboardController.qml @@ -0,0 +1,459 @@ +import QtQuick +import qs.Common +import qs.Services + +QtObject { + id: controller + + property var listView: null + property bool isOpen: false + property var onClose: null + + property int selectionVersion: 0 + + property bool keyboardNavigationActive: false + property int selectedFlatIndex: 0 + property var flatNavigation: [] + property bool showKeyboardHints: false + + property string selectedNotificationId: "" + property string selectedGroupKey: "" + property string selectedItemType: "" + property bool isTogglingGroup: false + property bool isRebuilding: false + + function rebuildFlatNavigation() { + isRebuilding = true + + const nav = [] + const groups = NotificationService.groupedNotifications + + for (let i = 0; i < groups.length; i++) { + const group = groups[i] + const isExpanded = NotificationService.expandedGroups[group.key] || false + + nav.push({ + "type": "group", + "groupIndex": i, + "notificationIndex": -1, + "groupKey": group.key, + "notificationId": "" + }) + + if (isExpanded) { + const notifications = group.notifications || [] + for (let j = 0; j < notifications.length; j++) { + const notifId = String(notifications[j] && notifications[j].notification && notifications[j].notification.id ? notifications[j].notification.id : "") + nav.push({ + "type": "notification", + "groupIndex": i, + "notificationIndex": j, + "groupKey": group.key, + "notificationId": notifId + }) + } + } + } + + flatNavigation = nav + updateSelectedIndexFromId() + isRebuilding = false + } + + function updateSelectedIndexFromId() { + if (!keyboardNavigationActive) { + return + } + + for (let i = 0; i < flatNavigation.length; i++) { + const item = flatNavigation[i] + + if (selectedItemType === "group" && item.type === "group" && item.groupKey === selectedGroupKey) { + selectedFlatIndex = i + selectionVersion++ // Trigger UI update + return + } else if (selectedItemType === "notification" && item.type === "notification" && String(item.notificationId) === String(selectedNotificationId)) { + selectedFlatIndex = i + selectionVersion++ // Trigger UI update + return + } + } + + // If not found, try to find the same group but select the group header instead + if (selectedItemType === "notification") { + for (let j = 0; j < flatNavigation.length; j++) { + const groupItem = flatNavigation[j] + if (groupItem.type === "group" && groupItem.groupKey === selectedGroupKey) { + selectedFlatIndex = j + selectedItemType = "group" + selectedNotificationId = "" + selectionVersion++ // Trigger UI update + return + } + } + } + + // If still not found, clamp to valid range and update + if (flatNavigation.length > 0) { + selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1) + selectedFlatIndex = Math.max(selectedFlatIndex, 0) + updateSelectedIdFromIndex() + selectionVersion++ // Trigger UI update + } + } + + function updateSelectedIdFromIndex() { + if (selectedFlatIndex >= 0 && selectedFlatIndex < flatNavigation.length) { + const item = flatNavigation[selectedFlatIndex] + selectedItemType = item.type + selectedGroupKey = item.groupKey + selectedNotificationId = item.notificationId + } + } + + function reset() { + selectedFlatIndex = 0 + keyboardNavigationActive = false + showKeyboardHints = false + // Reset keyboardActive when modal is reset + if (listView) { + listView.keyboardActive = false + } + rebuildFlatNavigation() + } + + function selectNext() { + keyboardNavigationActive = true + if (flatNavigation.length === 0) + return + + // Re-enable auto-scrolling when arrow keys are used + if (listView && listView.enableAutoScroll) { + listView.enableAutoScroll() + } + + selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1) + updateSelectedIdFromIndex() + selectionVersion++ + ensureVisible() + } + + function selectNextWrapping() { + keyboardNavigationActive = true + if (flatNavigation.length === 0) + return + + // Re-enable auto-scrolling when arrow keys are used + if (listView && listView.enableAutoScroll) { + listView.enableAutoScroll() + } + + selectedFlatIndex = (selectedFlatIndex + 1) % flatNavigation.length + updateSelectedIdFromIndex() + selectionVersion++ + ensureVisible() + } + + function selectPrevious() { + keyboardNavigationActive = true + if (flatNavigation.length === 0) + return + + // Re-enable auto-scrolling when arrow keys are used + if (listView && listView.enableAutoScroll) { + listView.enableAutoScroll() + } + + selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0) + updateSelectedIdFromIndex() + selectionVersion++ + ensureVisible() + } + + function toggleGroupExpanded() { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) + return + + const currentItem = flatNavigation[selectedFlatIndex] + const groups = NotificationService.groupedNotifications + const group = groups[currentItem.groupIndex] + if (!group) + return + + // Prevent expanding groups with < 2 notifications + const notificationCount = group.notifications ? group.notifications.length : 0 + if (notificationCount < 2) + return + + const wasExpanded = NotificationService.expandedGroups[group.key] || false + const groupIndex = currentItem.groupIndex + + isTogglingGroup = true + NotificationService.toggleGroupExpansion(group.key) + rebuildFlatNavigation() + + // Smart selection after toggle + if (!wasExpanded) { + // Just expanded - move to first notification in the group + for (let i = 0; i < flatNavigation.length; i++) { + if (flatNavigation[i].type === "notification" && flatNavigation[i].groupIndex === groupIndex) { + selectedFlatIndex = i + break + } + } + } else { + // Just collapsed - stay on the group header + for (let i = 0; i < flatNavigation.length; i++) { + if (flatNavigation[i].type === "group" && flatNavigation[i].groupIndex === groupIndex) { + selectedFlatIndex = i + break + } + } + } + + isTogglingGroup = false + ensureVisible() + } + + function handleEnterKey() { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) + return + + const currentItem = flatNavigation[selectedFlatIndex] + const groups = NotificationService.groupedNotifications + const group = groups[currentItem.groupIndex] + if (!group) + return + + if (currentItem.type === "group") { + const notificationCount = group.notifications ? group.notifications.length : 0 + if (notificationCount >= 2) { + toggleGroupExpanded() + } else { + executeAction(0) + } + } else if (currentItem.type === "notification") { + executeAction(0) + } + } + + function toggleTextExpanded() { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) + return + + const currentItem = flatNavigation[selectedFlatIndex] + const groups = NotificationService.groupedNotifications + const group = groups[currentItem.groupIndex] + if (!group) + return + + let messageId = "" + + if (currentItem.type === "group") { + messageId = group.latestNotification?.notification?.id + "_desc" + } else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) { + messageId = group.notifications[currentItem.notificationIndex]?.notification?.id + "_desc" + } + + if (messageId) { + NotificationService.toggleMessageExpansion(messageId) + } + } + + function executeAction(actionIndex) { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) + return + + const currentItem = flatNavigation[selectedFlatIndex] + const groups = NotificationService.groupedNotifications + const group = groups[currentItem.groupIndex] + if (!group) + return + + let actions = [] + + if (currentItem.type === "group") { + actions = group.latestNotification?.actions || [] + } else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) { + actions = group.notifications[currentItem.notificationIndex]?.actions || [] + } + + if (actionIndex >= 0 && actionIndex < actions.length) { + const action = actions[actionIndex] + if (action.invoke) { + action.invoke() + if (onClose) + onClose() + } + } + } + + function clearSelected() { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) + return + + const currentItem = flatNavigation[selectedFlatIndex] + const groups = NotificationService.groupedNotifications + const group = groups[currentItem.groupIndex] + if (!group) + return + + if (currentItem.type === "group") { + NotificationService.dismissGroup(group.key) + } else if (currentItem.type === "notification") { + const notification = group.notifications[currentItem.notificationIndex] + NotificationService.dismissNotification(notification) + } + + rebuildFlatNavigation() + + if (flatNavigation.length === 0) { + keyboardNavigationActive = false + if (listView) { + listView.keyboardActive = false + } + } else { + selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1) + updateSelectedIdFromIndex() + ensureVisible() + } + } + + function ensureVisible() { + if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length || !listView) + return + + const currentItem = flatNavigation[selectedFlatIndex] + + if (keyboardNavigationActive && currentItem && currentItem.groupIndex >= 0) { + // Always center the selected item for better visibility + // This ensures the selected item stays in view even when new notifications arrive + if (currentItem.type === "notification") { + // For individual notifications, center on the group but bias towards the notification + listView.positionViewAtIndex(currentItem.groupIndex, ListView.Center) + } else { + // For group headers, center on the group + listView.positionViewAtIndex(currentItem.groupIndex, ListView.Center) + } + + // Force immediate update + listView.forceLayout() + } + } + + function handleKey(event) { + if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) && (event.modifiers & Qt.ShiftModifier)) { + NotificationService.clearAllNotifications() + rebuildFlatNavigation() + if (flatNavigation.length === 0) { + keyboardNavigationActive = false + if (listView) { + listView.keyboardActive = false + } + } else { + selectedFlatIndex = 0 + updateSelectedIdFromIndex() + } + selectionVersion++ + event.accepted = true + return + } + + if (event.key === Qt.Key_Escape) { + if (keyboardNavigationActive) { + keyboardNavigationActive = false + event.accepted = true + } else { + if (onClose) + onClose() + event.accepted = true + } + } else if (event.key === Qt.Key_Down || event.key === 16777237) { + if (!keyboardNavigationActive) { + keyboardNavigationActive = true + rebuildFlatNavigation() // Ensure we have fresh navigation data + selectedFlatIndex = 0 + updateSelectedIdFromIndex() + // Set keyboardActive on listView to show highlight + if (listView) { + listView.keyboardActive = true + } + selectionVersion++ + ensureVisible() + event.accepted = true + } else { + selectNext() + event.accepted = true + } + } else if (event.key === Qt.Key_Up || event.key === 16777235) { + if (!keyboardNavigationActive) { + keyboardNavigationActive = true + rebuildFlatNavigation() // Ensure we have fresh navigation data + selectedFlatIndex = 0 + updateSelectedIdFromIndex() + // Set keyboardActive on listView to show highlight + if (listView) { + listView.keyboardActive = true + } + selectionVersion++ + ensureVisible() + event.accepted = true + } else if (selectedFlatIndex === 0) { + keyboardNavigationActive = false + // Reset keyboardActive when navigation is disabled + if (listView) { + listView.keyboardActive = false + } + selectionVersion++ + event.accepted = true + return + } else { + selectPrevious() + event.accepted = true + } + } else if (keyboardNavigationActive) { + if (event.key === Qt.Key_Space) { + toggleGroupExpanded() + event.accepted = true + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + handleEnterKey() + event.accepted = true + } else if (event.key === Qt.Key_E) { + toggleTextExpanded() + event.accepted = true + } else if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) { + clearSelected() + event.accepted = true + } else if (event.key === Qt.Key_Tab) { + selectNextWrapping() + event.accepted = true + } else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) { + const actionIndex = event.key - Qt.Key_1 + executeAction(actionIndex) + event.accepted = true + } + } + + if (event.key === Qt.Key_F10) { + showKeyboardHints = !showKeyboardHints + event.accepted = true + } + } + + // Get current selection info for UI + function getCurrentSelection() { + if (!keyboardNavigationActive || selectedFlatIndex < 0 || selectedFlatIndex >= flatNavigation.length) { + return { + "type": "", + "groupIndex": -1, + "notificationIndex": -1 + } + } + const result = flatNavigation[selectedFlatIndex] || { + "type": "", + "groupIndex": -1, + "notificationIndex": -1 + } + return result + } +} diff --git a/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationKeyboardHints.qml b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationKeyboardHints.qml new file mode 100644 index 0000000..8c9c7ff --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationKeyboardHints.qml @@ -0,0 +1,50 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool showHints: false + + height: 80 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) + border.color: Theme.primary + border.width: 2 + opacity: showHints ? 1 : 0 + z: 100 + + Column { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingS + spacing: 2 + + StyledText { + text: "↑/↓: Nav • Space: Expand • Enter: Action/Expand • E: Text" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: "Del: Clear • Shift+Del: Clear All • 1-9: Actions • F10: Help • Esc: Close" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationSettings.qml b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationSettings.qml new file mode 100644 index 0000000..414b9e2 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Notifications/Center/NotificationSettings.qml @@ -0,0 +1,255 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property bool expanded: false + readonly property real contentHeight: contentColumn.height + Theme.spacingL * 2 + + width: parent.width + height: expanded ? Math.min(contentHeight, 400) : 0 + visible: expanded + clip: true + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + border.width: 1 + + Behavior on height { + NumberAnimation { + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasized + } + } + + opacity: expanded ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasized + } + } + + readonly property var timeoutOptions: [{ + "text": "Never", + "value": 0 + }, { + "text": "1 second", + "value": 1000 + }, { + "text": "3 seconds", + "value": 3000 + }, { + "text": "5 seconds", + "value": 5000 + }, { + "text": "8 seconds", + "value": 8000 + }, { + "text": "10 seconds", + "value": 10000 + }, { + "text": "15 seconds", + "value": 15000 + }, { + "text": "30 seconds", + "value": 30000 + }, { + "text": "1 minute", + "value": 60000 + }, { + "text": "2 minutes", + "value": 120000 + }, { + "text": "5 minutes", + "value": 300000 + }, { + "text": "10 minutes", + "value": 600000 + }] + + function getTimeoutText(value) { + if (value === undefined || value === null || isNaN(value)) { + return "5 seconds" + } + + for (let i = 0; i < timeoutOptions.length; i++) { + if (timeoutOptions[i].value === value) { + return timeoutOptions[i].text + } + } + if (value === 0) { + return "Never" + } + if (value < 1000) { + return value + "ms" + } + if (value < 60000) { + return Math.round(value / 1000) + " seconds" + } + return Math.round(value / 60000) + " minutes" + } + + Column { + id: contentColumn + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + StyledText { + text: "Notification Settings" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Bold + color: Theme.surfaceText + } + + Item { + width: parent.width + height: 36 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: SessionData.doNotDisturb ? "notifications_off" : "notifications" + size: Theme.iconSizeSmall + color: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Do Not Disturb" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: SessionData.doNotDisturb + onToggled: SessionData.setDoNotDisturb(!SessionData.doNotDisturb) + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + } + + StyledText { + text: "Notification Timeouts" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceVariantText + } + + DankDropdown { + width: parent.width + text: "Low Priority" + description: "Timeout for low priority notifications" + currentValue: getTimeoutText(SettingsData.notificationTimeoutLow) + options: timeoutOptions.map(opt => opt.text) + onValueChanged: value => { + for (let i = 0; i < timeoutOptions.length; i++) { + if (timeoutOptions[i].text === value) { + SettingsData.setNotificationTimeoutLow(timeoutOptions[i].value) + break + } + } + } + } + + DankDropdown { + width: parent.width + text: "Normal Priority" + description: "Timeout for normal priority notifications" + currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal) + options: timeoutOptions.map(opt => opt.text) + onValueChanged: value => { + for (let i = 0; i < timeoutOptions.length; i++) { + if (timeoutOptions[i].text === value) { + SettingsData.setNotificationTimeoutNormal(timeoutOptions[i].value) + break + } + } + } + } + + DankDropdown { + width: parent.width + text: "Critical Priority" + description: "Timeout for critical priority notifications" + currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical) + options: timeoutOptions.map(opt => opt.text) + onValueChanged: value => { + for (let i = 0; i < timeoutOptions.length; i++) { + if (timeoutOptions[i].text === value) { + SettingsData.setNotificationTimeoutCritical(timeoutOptions[i].value) + break + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + } + + Item { + width: parent.width + height: 36 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "notifications_active" + size: Theme.iconSizeSmall + color: SettingsData.notificationOverlayEnabled ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Notification Overlay" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + + StyledText { + text: "Display all priorities over fullscreen apps" + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + } + } + + DankToggle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.notificationOverlayEnabled + onToggled: toggled => SettingsData.setNotificationOverlayEnabled(toggled) + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/.config/quickshell/Modules/Notifications/Popup/NotificationPopup.qml new file mode 100644 index 0000000..79a2aeb --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -0,0 +1,586 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import Quickshell.Services.Notifications +import qs.Common +import qs.Services +import qs.Widgets + +PanelWindow { + id: win + + required property var notificationData + required property string notificationId + readonly property bool hasValidData: notificationData && notificationData.notification + property int screenY: 0 + property bool exiting: false + property bool _isDestroying: false + property bool _finalized: false + + signal entered + signal exitFinished + + function startExit() { + if (exiting || _isDestroying) { + return + } + exiting = true + exitAnim.restart() + exitWatchdog.restart() + if (NotificationService.removeFromVisibleNotifications) + NotificationService.removeFromVisibleNotifications(win.notificationData) + } + + function forceExit() { + if (_isDestroying) { + return + } + _isDestroying = true + exiting = true + visible = false + exitWatchdog.stop() + finalizeExit("forced") + } + + function finalizeExit(reason) { + if (_finalized) { + return + } + + _finalized = true + _isDestroying = true + exitWatchdog.stop() + wrapperConn.enabled = false + wrapperConn.target = null + win.exitFinished() + } + + visible: hasValidData + WlrLayershell.layer: { + if (!notificationData) + return WlrLayershell.Top + + SettingsData.notificationOverlayEnabled + + const shouldUseOverlay = (SettingsData.notificationOverlayEnabled) || (notificationData.urgency === NotificationUrgency.Critical) + + return shouldUseOverlay ? WlrLayershell.Overlay : WlrLayershell.Top + } + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + implicitWidth: 400 + implicitHeight: 122 + onScreenYChanged: margins.top = Theme.barHeight - 4 + SettingsData.topBarSpacing + 4 + screenY + onHasValidDataChanged: { + if (!hasValidData && !exiting && !_isDestroying) { + forceExit() + } + } + Component.onCompleted: { + if (hasValidData) { + Qt.callLater(() => enterX.restart()) + } else { + forceExit() + } + } + onNotificationDataChanged: { + if (!_isDestroying) { + wrapperConn.target = win.notificationData || null + notificationConn.target = (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null + } + } + onEntered: { + if (!_isDestroying) { + enterDelay.start() + } + } + Component.onDestruction: { + _isDestroying = true + exitWatchdog.stop() + if (notificationData && notificationData.timer) { + notificationData.timer.stop() + } + } + + anchors { + top: true + right: true + } + + margins { + top: Theme.barHeight - 4 + SettingsData.topBarSpacing + 4 + right: 12 + } + + Item { + id: content + + anchors.fill: parent + visible: win.hasValidData + layer.enabled: (enterX.running || exitAnim.running) + layer.smooth: true + + Rectangle { + property var shadowLayers: [shadowLayer1, shadowLayer2, shadowLayer3] + + anchors.fill: parent + anchors.margins: 4 + radius: Theme.cornerRadius + color: Theme.popupBackground() + border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 1 + clip: true + + Rectangle { + id: shadowLayer1 + + anchors.fill: parent + anchors.margins: -3 + color: "transparent" + radius: parent.radius + 3 + border.color: Qt.rgba(0, 0, 0, 0.05) + border.width: 1 + z: -3 + } + + Rectangle { + id: shadowLayer2 + + anchors.fill: parent + anchors.margins: -2 + color: "transparent" + radius: parent.radius + 2 + border.color: Qt.rgba(0, 0, 0, 0.08) + border.width: 1 + z: -2 + } + + Rectangle { + id: shadowLayer3 + + anchors.fill: parent + color: "transparent" + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + radius: parent.radius + z: -1 + } + + Rectangle { + anchors.fill: parent + radius: parent.radius + visible: notificationData && notificationData.urgency === NotificationUrgency.Critical + opacity: 1 + + gradient: Gradient { + orientation: Gradient.Horizontal + + GradientStop { + position: 0 + color: Theme.primary + } + + GradientStop { + position: 0.02 + color: Theme.primary + } + + GradientStop { + position: 0.021 + color: "transparent" + } + } + } + + Item { + id: notificationContent + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 12 + anchors.leftMargin: 16 + anchors.rightMargin: 56 + height: 98 + + Rectangle { + id: iconContainer + + readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== "" + + width: 55 + height: 55 + radius: 27.5 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + border.color: "transparent" + border.width: 0 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + IconImage { + id: iconImage + + anchors.fill: parent + anchors.margins: 2 + asynchronous: true + source: { + if (!notificationData) + return "" + + if (parent.hasNotificationImage) + return notificationData.cleanImage || "" + + if (notificationData.appIcon) { + const appIcon = notificationData.appIcon + if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://")) + return appIcon + + return Quickshell.iconPath(appIcon, true) + } + return "" + } + visible: status === Image.Ready + } + + StyledText { + anchors.centerIn: parent + visible: !parent.hasNotificationImage && (!notificationData || !notificationData.appIcon || notificationData.appIcon === "") + text: { + const appName = notificationData && notificationData.appName ? notificationData.appName : "?" + return appName.charAt(0).toUpperCase() + } + font.pixelSize: 20 + font.weight: Font.Bold + color: Theme.primaryText + } + } + + Rectangle { + id: textContainer + + anchors.left: iconContainer.right + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + color: "transparent" + + Item { + width: parent.width + height: parent.height + anchors.top: parent.top + anchors.topMargin: -2 + + Column { + width: parent.width + spacing: 2 + + StyledText { + width: parent.width + text: { + if (!notificationData) + return "" + + const appName = notificationData.appName || "" + const timeStr = notificationData.timeStr || "" + if (timeStr.length > 0) + return appName + " • " + timeStr + else + return appName + } + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + } + + StyledText { + text: notificationData ? (notificationData.summary || "") : "" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + width: parent.width + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + + StyledText { + text: notificationData ? (notificationData.htmlBody || "") : "" + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + width: parent.width + elide: Text.ElideRight + maximumLineCount: 2 + wrapMode: Text.WordWrap + visible: text.length > 0 + linkColor: Theme.primary + onLinkActivated: link => { + return Qt.openUrlExternally(link) + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + } + } + } + } + + DankActionButton { + id: closeButton + + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 12 + anchors.rightMargin: 16 + iconName: "close" + iconSize: 18 + buttonSize: 28 + z: 15 + onClicked: { + if (notificationData && !win.exiting) + notificationData.popup = false + } + } + + Row { + anchors.right: clearButton.left + anchors.rightMargin: 8 + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + spacing: 8 + z: 20 + + Repeater { + model: notificationData ? (notificationData.actions || []) : [] + + Rectangle { + property bool isHovered: false + + width: Math.max(actionText.implicitWidth + 12, 50) + height: 24 + radius: 4 + color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + + StyledText { + id: actionText + + text: modelData.text || "View" + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: { + if (modelData && modelData.invoke) + modelData.invoke() + + if (notificationData && !win.exiting) + notificationData.popup = false + } + } + } + } + } + + Rectangle { + id: clearButton + + property bool isHovered: false + + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + width: Math.max(clearText.implicitWidth + 12, 50) + height: 24 + radius: 4 + color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + z: 20 + + StyledText { + id: clearText + + text: "Clear" + color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onEntered: clearButton.isHovered = true + onExited: clearButton.isHovered = false + onClicked: { + if (notificationData && !win.exiting) + NotificationService.dismissNotification(notificationData) + } + } + } + + MouseArea { + id: cardHoverArea + + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + propagateComposedEvents: true + z: -1 + onEntered: { + if (notificationData && notificationData.timer) + notificationData.timer.stop() + } + onExited: { + if (notificationData && notificationData.popup && notificationData.timer) + notificationData.timer.restart() + } + onClicked: { + if (notificationData && !win.exiting) + notificationData.popup = false + } + } + } + + transform: Translate { + id: tx + + x: Anims.slidePx + } + } + + NumberAnimation { + id: enterX + + target: tx + property: "x" + from: Anims.slidePx + to: 0 + duration: Anims.durMed + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedDecel + onStopped: { + if (!win.exiting && !win._isDestroying && Math.abs(tx.x) < 0.5) { + win.entered() + } + } + } + + ParallelAnimation { + id: exitAnim + + onStopped: finalizeExit("animStopped") + + PropertyAnimation { + target: tx + property: "x" + from: 0 + to: Anims.slidePx + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedAccel + } + + NumberAnimation { + target: content + property: "opacity" + from: 1 + to: 0 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.standardAccel + } + + NumberAnimation { + target: content + property: "scale" + from: 1 + to: 0.98 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedAccel + } + } + + Connections { + id: wrapperConn + + function onPopupChanged() { + if (!win.notificationData || win._isDestroying) + return + + if (!win.notificationData.popup && !win.exiting) + startExit() + } + + target: win.notificationData || null + ignoreUnknownSignals: true + enabled: !win._isDestroying + } + + Connections { + id: notificationConn + + function onDropped() { + if (!win._isDestroying && !win.exiting) + forceExit() + } + + target: (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null + ignoreUnknownSignals: true + enabled: !win._isDestroying + } + + Timer { + id: enterDelay + + interval: 160 + repeat: false + onTriggered: { + if (notificationData && notificationData.timer && !exiting && !_isDestroying) + notificationData.timer.start() + } + } + + Timer { + id: exitWatchdog + + interval: 600 + repeat: false + onTriggered: finalizeExit("watchdog") + } + + Behavior on screenY { + id: screenYAnim + + enabled: !exiting && !_isDestroying + + NumberAnimation { + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.standardDecel + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/.config/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml new file mode 100644 index 0000000..aad4f7c --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -0,0 +1,271 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Services + +QtObject { + id: manager + + property var modelData + property int topMargin: 0 + property int baseNotificationHeight: 120 + property int maxTargetNotifications: 4 + property var popupWindows: [] // strong refs to windows (live until exitFinished) + property var destroyingWindows: new Set() + property Component popupComponent + + popupComponent: Component { + NotificationPopup { + onEntered: manager._onPopupEntered(this) + onExitFinished: manager._onPopupExitFinished(this) + } + } + + property Connections notificationConnections + + notificationConnections: Connections { + function onVisibleNotificationsChanged() { + manager._sync(NotificationService.visibleNotifications) + } + + target: NotificationService + } + + property Timer sweeper + + sweeper: Timer { + interval: 500 + running: false + repeat: true + onTriggered: { + const toRemove = [] + for (const p of popupWindows) { + if (!p) { + toRemove.push(p) + continue + } + const isZombie = p.status === Component.Null || (!p.visible && !p.exiting) || (!p.notificationData && !p._isDestroying) || (!p.hasValidData && !p._isDestroying) + if (isZombie) { + toRemove.push(p) + if (p.forceExit) { + p.forceExit() + } else if (p.destroy) { + try { + p.destroy() + } catch (e) { + + } + } + } + } + if (toRemove.length) { + popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1) + const survivors = _active().sort((a, b) => a.screenY - b.screenY) + for (let k = 0; k < survivors.length; ++k) { + survivors[k].screenY = topMargin + k * baseNotificationHeight + } + } + if (popupWindows.length === 0) { + sweeper.stop() + } + } + } + + function _hasWindowFor(w) { + return popupWindows.some(p => p && p.notificationData === w && !p._isDestroying && p.status !== Component.Null) + } + + function _isValidWindow(p) { + return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData + } + + function _canMakeRoomFor(wrapper) { + const activeWindows = _active() + if (activeWindows.length < maxTargetNotifications) { + return true + } + if (!wrapper || !wrapper.notification) { + return false + } + const incomingUrgency = wrapper.notification.urgency || 0 + for (const p of activeWindows) { + if (!p.notificationData || !p.notificationData.notification) { + continue + } + const existingUrgency = p.notificationData.notification.urgency || 0 + if (existingUrgency < incomingUrgency) { + return true + } + if (existingUrgency === incomingUrgency) { + const timer = p.notificationData.timer + if (timer && !timer.running) { + return true + } + } + } + return false + } + + function _makeRoomForNew(wrapper) { + const activeWindows = _active() + if (activeWindows.length < maxTargetNotifications) { + return + } + const toRemove = _selectPopupToRemove(activeWindows, wrapper) + if (toRemove && !toRemove.exiting) { + toRemove.notificationData.removedByLimit = true + toRemove.notificationData.popup = false + if (toRemove.notificationData.timer) { + toRemove.notificationData.timer.stop() + } + } + } + + function _selectPopupToRemove(activeWindows, incomingWrapper) { + const incomingUrgency = (incomingWrapper && incomingWrapper.notification) ? incomingWrapper.notification.urgency || 0 : 0 + const sortedWindows = activeWindows.slice().sort((a, b) => { + const aUrgency = (a.notificationData && a.notificationData.notification) ? a.notificationData.notification.urgency || 0 : 0 + const bUrgency = (b.notificationData && b.notificationData.notification) ? b.notificationData.notification.urgency || 0 : 0 + if (aUrgency !== bUrgency) { + return aUrgency - bUrgency + } + const aTimer = a.notificationData && a.notificationData.timer + const bTimer = b.notificationData && b.notificationData.timer + const aRunning = aTimer && aTimer.running + const bRunning = bTimer && bTimer.running + if (aRunning !== bRunning) { + return aRunning ? 1 : -1 + } + return b.screenY - a.screenY + }) + return sortedWindows[0] + } + + function _sync(newWrappers) { + for (const w of newWrappers) { + if (w && !_hasWindowFor(w)) { + insertNewestAtTop(w) + } + } + for (const p of popupWindows.slice()) { + if (!_isValidWindow(p)) { + continue + } + if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1 && !p.exiting) { + p.notificationData.removedByLimit = true + p.notificationData.popup = false + } + } + } + + function insertNewestAtTop(wrapper) { + if (!wrapper) { + return + } + for (const p of popupWindows) { + if (!_isValidWindow(p)) { + continue + } + if (p.exiting) { + continue + } + p.screenY = p.screenY + baseNotificationHeight + } + const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : "" + const win = popupComponent.createObject(null, { + "notificationData": wrapper, + "notificationId": notificationId, + "screenY": topMargin, + "screen": manager.modelData + }) + if (!win) { + return + } + if (!win.hasValidData) { + win.destroy() + return + } + popupWindows.push(win) + if (!sweeper.running) { + sweeper.start() + } + } + + function _active() { + return popupWindows.filter(p => _isValidWindow(p) && p.notificationData && p.notificationData.popup && !p.exiting) + } + + function _bottom() { + let b = null + let maxY = -1 + for (const p of _active()) { + if (p.screenY > maxY) { + maxY = p.screenY + b = p + } + } + return b + } + + function _onPopupEntered(p) {} + + function _onPopupExitFinished(p) { + if (!p) { + return + } + const windowId = p.toString() + if (destroyingWindows.has(windowId)) { + return + } + destroyingWindows.add(windowId) + const i = popupWindows.indexOf(p) + if (i !== -1) { + popupWindows.splice(i, 1) + popupWindows = popupWindows.slice() + } + if (NotificationService.releaseWrapper && p.notificationData) { + NotificationService.releaseWrapper(p.notificationData) + } + Qt.callLater(() => { + if (p && p.destroy) { + try { + p.destroy() + } catch (e) { + + } + } + Qt.callLater(() => destroyingWindows.delete(windowId)) + }) + const survivors = _active().sort((a, b) => a.screenY - b.screenY) + for (let k = 0; k < survivors.length; ++k) { + survivors[k].screenY = topMargin + k * baseNotificationHeight + } + } + + function cleanupAllWindows() { + sweeper.stop() + for (const p of popupWindows.slice()) { + if (p) { + try { + if (p.forceExit) { + p.forceExit() + } else if (p.destroy) { + p.destroy() + } + } catch (e) { + + } + } + } + popupWindows = [] + destroyingWindows.clear() + } + + onPopupWindowsChanged: { + if (popupWindows.length > 0 && !sweeper.running) { + sweeper.start() + } else if (popupWindows.length === 0 && sweeper.running) { + sweeper.stop() + } + } +} diff --git a/quickshell/.config/quickshell/Modules/OSD/BrightnessOSD.qml b/quickshell/.config/quickshell/Modules/OSD/BrightnessOSD.qml new file mode 100644 index 0000000..8e32f0d --- /dev/null +++ b/quickshell/.config/quickshell/Modules/OSD/BrightnessOSD.qml @@ -0,0 +1,134 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankOSD { + id: root + + osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2) + osdHeight: 40 + Theme.spacingS * 2 + autoHideInterval: 3000 + enableMouseInteraction: true + + property var brightnessDebounceTimer: Timer { + property int pendingValue: 0 + + interval: { + const deviceInfo = DisplayService.getCurrentDeviceInfo() + return (deviceInfo && deviceInfo.class === "ddc") ? 200 : 50 + } + repeat: false + onTriggered: { + DisplayService.setBrightnessInternal(pendingValue, DisplayService.lastIpcDevice) + } + } + + Connections { + target: DisplayService + function onBrightnessChanged() { + root.show() + } + } + + content: Item { + anchors.fill: parent + + Item { + property int gap: Theme.spacingS + + anchors.centerIn: parent + width: parent.width - Theme.spacingS * 2 + height: 40 + + Rectangle { + width: Theme.iconSize + height: Theme.iconSize + radius: Theme.iconSize / 2 + color: "transparent" + x: parent.gap + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: { + const deviceInfo = DisplayService.getCurrentDeviceInfo() + if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") { + return "brightness_medium" + } else if (deviceInfo.name.includes("kbd")) { + return "keyboard" + } else { + return "lightbulb" + } + } + size: Theme.iconSize + color: Theme.primary + } + } + + DankSlider { + id: brightnessSlider + + width: parent.width - Theme.iconSize - parent.gap * 3 + height: 40 + x: parent.gap * 2 + Theme.iconSize + anchors.verticalCenter: parent.verticalCenter + minimum: 1 + maximum: 100 + enabled: DisplayService.brightnessAvailable + showValue: true + unit: "%" + + Component.onCompleted: { + if (DisplayService.brightnessAvailable) { + value = DisplayService.brightnessLevel + } + } + + onSliderValueChanged: newValue => { + if (DisplayService.brightnessAvailable) { + root.brightnessDebounceTimer.pendingValue = newValue + root.brightnessDebounceTimer.restart() + resetHideTimer() + } + } + + onContainsMouseChanged: { + setChildHovered(containsMouse) + } + + onSliderDragFinished: finalValue => { + if (DisplayService.brightnessAvailable) { + root.brightnessDebounceTimer.stop() + DisplayService.setBrightnessInternal(finalValue, DisplayService.lastIpcDevice) + } + } + + Connections { + target: DisplayService + + function onBrightnessChanged() { + if (!brightnessSlider.pressed) { + brightnessSlider.value = DisplayService.brightnessLevel + } + } + + function onDeviceSwitched() { + if (!brightnessSlider.pressed) { + brightnessSlider.value = DisplayService.brightnessLevel + } + } + } + } + } + } + + onOsdShown: { + if (DisplayService.brightnessAvailable && contentLoader.item) { + const slider = contentLoader.item.children[0].children[1] + if (slider) { + slider.value = DisplayService.brightnessLevel + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/OSD/IdleInhibitorOSD.qml b/quickshell/.config/quickshell/Modules/OSD/IdleInhibitorOSD.qml new file mode 100644 index 0000000..cbf4ab4 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/OSD/IdleInhibitorOSD.qml @@ -0,0 +1,27 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankOSD { + id: root + + osdWidth: Theme.iconSize + Theme.spacingS * 2 + osdHeight: Theme.iconSize + Theme.spacingS * 2 + autoHideInterval: 2000 + enableMouseInteraction: false + + Connections { + target: SessionService + function onInhibitorChanged() { + root.show() + } + } + + content: DankIcon { + anchors.centerIn: parent + name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" + size: Theme.iconSize + color: SessionService.idleInhibited ? Theme.primary : Theme.outline + } +} diff --git a/quickshell/.config/quickshell/Modules/OSD/MicMuteOSD.qml b/quickshell/.config/quickshell/Modules/OSD/MicMuteOSD.qml new file mode 100644 index 0000000..d60db81 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/OSD/MicMuteOSD.qml @@ -0,0 +1,27 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankOSD { + id: root + + osdWidth: Theme.iconSize + Theme.spacingS * 2 + osdHeight: Theme.iconSize + Theme.spacingS * 2 + autoHideInterval: 2000 + enableMouseInteraction: false + + Connections { + target: AudioService + function onMicMuteChanged() { + root.show() + } + } + + content: DankIcon { + anchors.centerIn: parent + name: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? "mic_off" : "mic" + size: Theme.iconSize + color: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? Theme.error : Theme.primary + } +} diff --git a/quickshell/.config/quickshell/Modules/OSD/VolumeOSD.qml b/quickshell/.config/quickshell/Modules/OSD/VolumeOSD.qml new file mode 100644 index 0000000..9550595 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/OSD/VolumeOSD.qml @@ -0,0 +1,120 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankOSD { + id: root + + osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2) + osdHeight: 40 + Theme.spacingS * 2 + autoHideInterval: 3000 + enableMouseInteraction: true + + Connections { + target: AudioService + + function onVolumeChanged() { + root.show() + } + + function onSinkChanged() { + if (root.shouldBeVisible) { + root.show() + } + } + } + + content: Item { + anchors.fill: parent + + Item { + property int gap: Theme.spacingS + + anchors.centerIn: parent + width: parent.width - Theme.spacingS * 2 + height: 40 + + Rectangle { + width: Theme.iconSize + height: Theme.iconSize + radius: Theme.iconSize / 2 + color: "transparent" + x: parent.gap + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted ? "volume_off" : "volume_up" + size: Theme.iconSize + color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText + } + + MouseArea { + id: muteButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + AudioService.toggleMute() + resetHideTimer() + } + onContainsMouseChanged: { + setChildHovered(containsMouse || volumeSlider.containsMouse) + } + } + } + + DankSlider { + id: volumeSlider + + width: parent.width - Theme.iconSize - parent.gap * 3 + height: 40 + x: parent.gap * 2 + Theme.iconSize + anchors.verticalCenter: parent.verticalCenter + minimum: 0 + maximum: 100 + enabled: AudioService.sink && AudioService.sink.audio + showValue: true + unit: "%" + + Component.onCompleted: { + if (AudioService.sink && AudioService.sink.audio) { + value = Math.round(AudioService.sink.audio.volume * 100) + } + } + + onSliderValueChanged: newValue => { + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.volume = newValue / 100 + resetHideTimer() + } + } + + onContainsMouseChanged: { + setChildHovered(containsMouse || muteButton.containsMouse) + } + + Connections { + target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null + + function onVolumeChanged() { + if (volumeSlider && !volumeSlider.pressed) { + volumeSlider.value = Math.round(AudioService.sink.audio.volume * 100) + } + } + } + } + } + } + + onOsdShown: { + if (AudioService.sink && AudioService.sink.audio && contentLoader.item) { + const slider = contentLoader.item.children[0].children[1] + if (slider) { + slider.value = Math.round(AudioService.sink.audio.volume * 100) + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/ProcessList/PerformanceTab.qml b/quickshell/.config/quickshell/Modules/ProcessList/PerformanceTab.qml new file mode 100644 index 0000000..e18c314 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ProcessList/PerformanceTab.qml @@ -0,0 +1,483 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Column { + function formatNetworkSpeed(bytesPerSec) { + if (bytesPerSec < 1024) { + return bytesPerSec.toFixed(0) + " B/s"; + } else if (bytesPerSec < 1024 * 1024) { + return (bytesPerSec / 1024).toFixed(1) + " KB/s"; + } else if (bytesPerSec < 1024 * 1024 * 1024) { + return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"; + } else { + return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"; + } + } + + function formatDiskSpeed(bytesPerSec) { + if (bytesPerSec < 1024 * 1024) { + return (bytesPerSec / 1024).toFixed(1) + " KB/s"; + } else if (bytesPerSec < 1024 * 1024 * 1024) { + return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"; + } else { + return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"; + } + } + + anchors.fill: parent + spacing: Theme.spacingM + Component.onCompleted: { + DgopService.addRef(["cpu", "memory", "network", "disk"]); + } + Component.onDestruction: { + DgopService.removeRef(["cpu", "memory", "network", "disk"]); + } + + Rectangle { + width: parent.width + height: 200 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) + border.width: 1 + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Row { + width: parent.width + height: 32 + spacing: Theme.spacingM + + StyledText { + text: "CPU" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: 80 + height: 24 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: `${DgopService.cpuUsage.toFixed(1)}%` + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.primary + anchors.centerIn: parent + } + + } + + Item { + width: parent.width - 280 + height: 1 + } + + StyledText { + text: `${DgopService.cpuCores} cores` + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + } + + DankFlickable { + clip: true + width: parent.width + height: parent.height - 40 + contentHeight: coreUsageColumn.implicitHeight + + Column { + id: coreUsageColumn + + width: parent.width + spacing: 6 + + Repeater { + model: DgopService.perCoreCpuUsage + + Row { + width: parent.width + height: 20 + spacing: Theme.spacingS + + StyledText { + text: `C${index}` + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 24 + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: parent.width - 80 + height: 6 + radius: 3 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: parent.width * Math.min(1, modelData / 100) + height: parent.height + radius: parent.radius + color: { + const usage = modelData; + if (usage > 80) { + return Theme.error; + } + if (usage > 60) { + return Theme.warning; + } + return Theme.primary; + } + + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + } + + } + + } + + } + + StyledText { + text: modelData ? `${modelData.toFixed(0)}%` : "0%" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + width: 32 + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + + } + + } + + } + + } + + } + + } + + Rectangle { + width: parent.width + height: 80 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) + border.width: 1 + + Row { + anchors.centerIn: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + StyledText { + text: "Memory" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + } + + StyledText { + text: `${DgopService.formatSystemMemory(DgopService.usedMemoryKB)} / ${DgopService.formatSystemMemory(DgopService.totalMemoryKB)}` + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + } + + Item { + width: Theme.spacingL + height: 1 + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + width: 200 + + Rectangle { + width: parent.width + height: 16 + radius: 8 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + + Rectangle { + width: DgopService.totalMemoryKB > 0 ? parent.width * (DgopService.usedMemoryKB / DgopService.totalMemoryKB) : 0 + height: parent.height + radius: parent.radius + color: { + const usage = DgopService.totalMemoryKB > 0 ? (DgopService.usedMemoryKB / DgopService.totalMemoryKB) : 0; + if (usage > 0.9) { + return Theme.error; + } + if (usage > 0.7) { + return Theme.warning; + } + return Theme.secondary; + } + + Behavior on width { + NumberAnimation { + duration: Theme.mediumDuration + } + + } + + } + + } + + StyledText { + text: DgopService.totalMemoryKB > 0 ? `${((DgopService.usedMemoryKB / DgopService.totalMemoryKB) * 100).toFixed(1)}% used` : "No data" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + + } + + Item { + width: Theme.spacingL + height: 1 + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + StyledText { + text: "Swap" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + } + + StyledText { + text: DgopService.totalSwapKB > 0 ? `${DgopService.formatSystemMemory(DgopService.usedSwapKB)} / ${DgopService.formatSystemMemory(DgopService.totalSwapKB)}` : "No swap configured" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + } + + Item { + width: Theme.spacingL + height: 1 + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + width: 200 + + Rectangle { + width: parent.width + height: 16 + radius: 8 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + + Rectangle { + width: DgopService.totalSwapKB > 0 ? parent.width * (DgopService.usedSwapKB / DgopService.totalSwapKB) : 0 + height: parent.height + radius: parent.radius + color: { + if (!DgopService.totalSwapKB) { + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3); + } + const usage = DgopService.usedSwapKB / DgopService.totalSwapKB; + if (usage > 0.9) { + return Theme.error; + } + if (usage > 0.7) { + return Theme.warning; + } + return Theme.info; + } + + Behavior on width { + NumberAnimation { + duration: Theme.mediumDuration + } + + } + + } + + } + + StyledText { + text: DgopService.totalSwapKB > 0 ? `${((DgopService.usedSwapKB / DgopService.totalSwapKB) * 100).toFixed(1)}% used` : "Not available" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + + } + + } + + } + + Row { + width: parent.width + height: 80 + spacing: Theme.spacingM + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: 80 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) + border.width: 1 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: "Network" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Bold + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + Row { + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + Row { + spacing: 4 + + StyledText { + text: "↓" + font.pixelSize: Theme.fontSizeSmall + color: Theme.info + } + + StyledText { + text: DgopService.networkRxRate > 0 ? formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + + } + + Row { + spacing: 4 + + StyledText { + text: "↑" + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + } + + StyledText { + text: DgopService.networkTxRate > 0 ? formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + + } + + } + + } + + } + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: 80 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) + border.width: 1 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: "Disk" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Bold + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + Row { + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + Row { + spacing: 4 + + StyledText { + text: "R" + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + } + + StyledText { + text: formatDiskSpeed(DgopService.diskReadRate) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + + } + + Row { + spacing: 4 + + StyledText { + text: "W" + font.pixelSize: Theme.fontSizeSmall + color: Theme.warning + } + + StyledText { + text: formatDiskSpeed(DgopService.diskWriteRate) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/ProcessList/ProcessContextMenu.qml b/quickshell/.config/quickshell/Modules/ProcessList/ProcessContextMenu.qml new file mode 100644 index 0000000..14b8f1a --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ProcessList/ProcessContextMenu.qml @@ -0,0 +1,235 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets + +Popup { + id: processContextMenu + + property var processData: null + + function show(x, y) { + if (!processContextMenu.parent && typeof Overlay !== "undefined" && Overlay.overlay) { + processContextMenu.parent = Overlay.overlay; + } + + const menuWidth = 180; + const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2; + const screenWidth = Screen.width; + const screenHeight = Screen.height; + let finalX = x; + let finalY = y; + if (x + menuWidth > screenWidth - 20) { + finalX = x - menuWidth; + } + + if (y + menuHeight > screenHeight - 20) { + finalY = y - menuHeight; + } + + processContextMenu.x = Math.max(20, finalX); + processContextMenu.y = Math.max(20, finalY); + open(); + } + + width: 180 + height: menuColumn.implicitHeight + Theme.spacingS * 2 + padding: 0 + modal: false + closePolicy: Popup.CloseOnEscape + onClosed: { + closePolicy = Popup.CloseOnEscape; + } + onOpened: { + outsideClickTimer.start(); + } + + Timer { + id: outsideClickTimer + + interval: 100 + onTriggered: { + processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside; + } + } + + background: Rectangle { + color: "transparent" + } + + contentItem: Rectangle { + id: menuContent + + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + Column { + id: menuColumn + + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 1 + + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: copyPidArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: "Copy PID" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + } + + MouseArea { + id: copyPidArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (processContextMenu.processData) { + Quickshell.execDetached(["wl-copy", processContextMenu.processData.pid.toString()]); + } + + processContextMenu.close(); + } + } + + } + + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: copyNameArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: "Copy Process Name" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + } + + MouseArea { + id: copyNameArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (processContextMenu.processData) { + const processName = processContextMenu.processData.displayName || processContextMenu.processData.command; + Quickshell.execDetached(["wl-copy", processName]); + } + processContextMenu.close(); + } + } + + } + + Rectangle { + width: parent.width - Theme.spacingS * 2 + height: 5 + anchors.horizontalCenter: parent.horizontalCenter + color: "transparent" + + Rectangle { + anchors.centerIn: parent + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + } + + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: killArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" + enabled: processContextMenu.processData + opacity: enabled ? 1 : 0.5 + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: "Kill Process" + font.pixelSize: Theme.fontSizeSmall + color: parent.enabled ? (killArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + font.weight: Font.Normal + } + + MouseArea { + id: killArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: parent.enabled + onClicked: { + if (processContextMenu.processData) { + Quickshell.execDetached(["kill", processContextMenu.processData.pid.toString()]); + } + + processContextMenu.close(); + } + } + + } + + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: forceKillArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" + enabled: processContextMenu.processData && processContextMenu.processData.pid > 1000 + opacity: enabled ? 1 : 0.5 + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: "Force Kill Process" + font.pixelSize: Theme.fontSizeSmall + color: parent.enabled ? (forceKillArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + font.weight: Font.Normal + } + + MouseArea { + id: forceKillArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: parent.enabled + onClicked: { + if (processContextMenu.processData) { + Quickshell.execDetached(["kill", "-9", processContextMenu.processData.pid.toString()]); + } + + processContextMenu.close(); + } + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/ProcessList/ProcessListItem.qml b/quickshell/.config/quickshell/Modules/ProcessList/ProcessListItem.qml new file mode 100644 index 0000000..fbfb472 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ProcessList/ProcessListItem.qml @@ -0,0 +1,200 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: processItem + + property var process: null + property var contextMenu: null + + width: parent ? parent.width : 0 + height: 40 + radius: Theme.cornerRadius + color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + border.width: 1 + + MouseArea { + id: processMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: (mouse) => { + if (mouse.button === Qt.RightButton) { + if (process && process.pid > 0 && contextMenu) { + contextMenu.processData = process; + const globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y); + const localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos; + contextMenu.show(localPos.x, localPos.y); + } + } + } + onPressAndHold: { + if (process && process.pid > 0 && contextMenu) { + contextMenu.processData = process; + const globalPos = processMouseArea.mapToGlobal(processMouseArea.width / 2, processMouseArea.height / 2); + contextMenu.show(globalPos.x, globalPos.y); + } + } + } + + Item { + anchors.fill: parent + anchors.margins: 8 + + DankIcon { + id: processIcon + + name: DgopService.getProcessIcon(process ? process.command : "") + size: Theme.iconSize - 4 + color: { + if (process && process.cpu > 80) { + return Theme.error; + } + return Theme.surfaceText; + } + opacity: 0.8 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: process ? process.displayName : "" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Medium + color: Theme.surfaceText + width: 250 + elide: Text.ElideRight + anchors.left: processIcon.right + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + id: cpuBadge + + width: 80 + height: 20 + radius: Theme.cornerRadius + color: { + if (process && process.cpu > 80) { + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12); + } + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08); + } + anchors.right: parent.right + anchors.rightMargin: 194 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: DgopService.formatCpuUsage(process ? process.cpu : 0) + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: { + if (process && process.cpu > 80) { + return Theme.error; + } + return Theme.surfaceText; + } + anchors.centerIn: parent + } + + } + + Rectangle { + id: memoryBadge + + width: 80 + height: 20 + radius: Theme.cornerRadius + color: { + if (process && process.memoryKB > 1024 * 1024) { + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12); + } + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08); + } + anchors.right: parent.right + anchors.rightMargin: 102 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: DgopService.formatMemoryUsage(process ? process.memoryKB : 0) + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: { + if (process && process.memoryKB > 1024 * 1024) { + return Theme.error; + } + return Theme.surfaceText; + } + anchors.centerIn: parent + } + + } + + StyledText { + text: process ? process.pid.toString() : "" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + opacity: 0.7 + width: 50 + horizontalAlignment: Text.AlignRight + anchors.right: parent.right + anchors.rightMargin: 40 + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + id: menuButton + + width: 28 + height: 28 + radius: Theme.cornerRadius + color: menuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "more_vert" + size: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.6 + anchors.centerIn: parent + } + + MouseArea { + id: menuButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (process && process.pid > 0 && contextMenu) { + contextMenu.processData = process; + const globalPos = menuButtonArea.mapToGlobal(menuButtonArea.width / 2, menuButtonArea.height); + const localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos; + contextMenu.show(localPos.x, localPos.y); + } + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/ProcessList/ProcessListPopout.qml b/quickshell/.config/quickshell/Modules/ProcessList/ProcessListPopout.qml new file mode 100644 index 0000000..0ae5770 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ProcessList/ProcessListPopout.qml @@ -0,0 +1,138 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Modules.ProcessList +import qs.Services +import qs.Widgets + +DankPopout { + id: processListPopout + + property var parentWidget: null + property string triggerSection: "right" + property var triggerScreen: null + + function setTriggerPosition(x, y, width, section, screen) { + triggerX = x; + triggerY = y; + triggerWidth = width; + triggerSection = section; + triggerScreen = screen; + } + + function hide() { + close(); + if (processContextMenu.visible) { + processContextMenu.close(); + } + } + + function show() { + open(); + } + + popupWidth: 600 + popupHeight: 600 + triggerX: Screen.width - 600 - Theme.spacingL + triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS + triggerWidth: 55 + positioning: "center" + screen: triggerScreen + visible: shouldBeVisible + shouldBeVisible: false + + Ref { + service: DgopService + } + + ProcessContextMenu { + id: processContextMenu + } + + content: Component { + Rectangle { + id: processListContent + + radius: Theme.cornerRadius + color: Theme.popupBackground() + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + clip: true + antialiasing: true + smooth: true + focus: true + Component.onCompleted: { + if (processListPopout.shouldBeVisible) { + forceActiveFocus(); + } + } + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + processListPopout.close(); + event.accepted = true; + } + } + + Connections { + function onShouldBeVisibleChanged() { + if (processListPopout.shouldBeVisible) { + Qt.callLater(() => { + processListContent.forceActiveFocus(); + }); + } + } + + target: processListPopout + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + Rectangle { + Layout.fillWidth: true + height: systemOverview.height + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + SystemOverview { + id: systemOverview + + anchors.centerIn: parent + width: parent.width - Theme.spacingM * 2 + } + + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) + border.width: 1 + + ProcessListView { + anchors.fill: parent + anchors.margins: Theme.spacingS + contextMenu: processContextMenu + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/ProcessList/ProcessListView.qml b/quickshell/.config/quickshell/Modules/ProcessList/ProcessListView.qml new file mode 100644 index 0000000..063fd6c --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ProcessList/ProcessListView.qml @@ -0,0 +1,260 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Column { + id: root + + property var contextMenu: null + + Component.onCompleted: { + DgopService.addRef(["processes"]); + } + Component.onDestruction: { + DgopService.removeRef(["processes"]); + } + + Item { + id: columnHeaders + + width: parent.width + anchors.leftMargin: 8 + height: 24 + + Rectangle { + width: 60 + height: 20 + color: { + if (DgopService.currentSort === "name") { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); + } + return processHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"; + } + radius: Theme.cornerRadius + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Process" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: DgopService.currentSort === "name" ? Font.Bold : Font.Medium + color: Theme.surfaceText + opacity: DgopService.currentSort === "name" ? 1 : 0.7 + anchors.centerIn: parent + } + + MouseArea { + id: processHeaderArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + DgopService.setSortBy("name"); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + Rectangle { + width: 80 + height: 20 + color: { + if (DgopService.currentSort === "cpu") { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); + } + return cpuHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"; + } + radius: Theme.cornerRadius + anchors.right: parent.right + anchors.rightMargin: 200 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "CPU" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: DgopService.currentSort === "cpu" ? Font.Bold : Font.Medium + color: Theme.surfaceText + opacity: DgopService.currentSort === "cpu" ? 1 : 0.7 + anchors.centerIn: parent + } + + MouseArea { + id: cpuHeaderArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + DgopService.setSortBy("cpu"); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + Rectangle { + width: 80 + height: 20 + color: { + if (DgopService.currentSort === "memory") { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); + } + return memoryHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"; + } + radius: Theme.cornerRadius + anchors.right: parent.right + anchors.rightMargin: 112 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "RAM" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: DgopService.currentSort === "memory" ? Font.Bold : Font.Medium + color: Theme.surfaceText + opacity: DgopService.currentSort === "memory" ? 1 : 0.7 + anchors.centerIn: parent + } + + MouseArea { + id: memoryHeaderArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + DgopService.setSortBy("memory"); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + Rectangle { + width: 50 + height: 20 + color: { + if (DgopService.currentSort === "pid") { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); + } + return pidHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"; + } + radius: Theme.cornerRadius + anchors.right: parent.right + anchors.rightMargin: 53 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "PID" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: DgopService.currentSort === "pid" ? Font.Bold : Font.Medium + color: Theme.surfaceText + opacity: DgopService.currentSort === "pid" ? 1 : 0.7 + horizontalAlignment: Text.AlignHCenter + anchors.centerIn: parent + } + + MouseArea { + id: pidHeaderArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + DgopService.setSortBy("pid"); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + Rectangle { + width: 28 + height: 28 + radius: Theme.cornerRadius + color: sortOrderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: DgopService.sortDescending ? "↓" : "↑" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.centerIn: parent + } + + MouseArea { + // TODO: Re-implement sort order toggle + + id: sortOrderArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + } + + DankListView { + id: processListView + + property string keyRoleName: "pid" + + width: parent.width + height: parent.height - columnHeaders.height + clip: true + spacing: 4 + model: DgopService.processes + + delegate: ProcessListItem { + process: modelData + contextMenu: root.contextMenu + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/ProcessList/ProcessesTab.qml b/quickshell/.config/quickshell/Modules/ProcessList/ProcessesTab.qml new file mode 100644 index 0000000..987dd90 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ProcessList/ProcessesTab.qml @@ -0,0 +1,28 @@ +import QtQuick +import QtQuick.Layouts +import qs.Common +import qs.Modules.ProcessList +import qs.Services + +ColumnLayout { + id: processesTab + + property var contextMenu: null + + anchors.fill: parent + spacing: Theme.spacingM + + SystemOverview { + Layout.fillWidth: true + } + + ProcessListView { + Layout.fillWidth: true + Layout.fillHeight: true + contextMenu: processesTab.contextMenu + } + + ProcessContextMenu { + id: localContextMenu + } +} diff --git a/quickshell/.config/quickshell/Modules/ProcessList/SystemOverview.qml b/quickshell/.config/quickshell/Modules/ProcessList/SystemOverview.qml new file mode 100644 index 0000000..cf813a9 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ProcessList/SystemOverview.qml @@ -0,0 +1,435 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Row { + width: parent.width + spacing: Theme.spacingM + Component.onCompleted: { + DgopService.addRef(["cpu", "memory", "system"]); + } + Component.onDestruction: { + DgopService.removeRef(["cpu", "memory", "system"]); + } + + Rectangle { + width: (parent.width - Theme.spacingM * 2) / 3 + height: 80 + radius: Theme.cornerRadius + color: { + if (DgopService.sortBy === "cpu") { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16); + } else if (cpuCardMouseArea.containsMouse) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); + } else { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08); + } + } + border.color: DgopService.sortBy === "cpu" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) + border.width: DgopService.sortBy === "cpu" ? 2 : 1 + + MouseArea { + id: cpuCardMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + DgopService.setSortBy("cpu"); + } + } + + Column { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: "CPU" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: DgopService.sortBy === "cpu" ? Theme.primary : Theme.secondary + opacity: DgopService.sortBy === "cpu" ? 1 : 0.8 + } + + Row { + spacing: Theme.spacingS + + StyledText { + text: { + if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null) { + return "--%"; + } + return DgopService.cpuUsage.toFixed(1) + "%"; + } + font.pixelSize: Theme.fontSizeLarge + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: 1 + height: 20 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature <= 0) { + return "--°"; + } + return Math.round(DgopService.cpuTemperature) + "°"; + } + font.pixelSize: Theme.fontSizeMedium + font.family: SettingsData.monoFontFamily + font.weight: Font.Medium + color: { + if (DgopService.cpuTemperature > 80) { + return Theme.error; + } + if (DgopService.cpuTemperature > 60) { + return Theme.warning; + } + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + } + + StyledText { + text: `${DgopService.cpuCores} cores` + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + opacity: 0.7 + } + + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + Rectangle { + width: (parent.width - Theme.spacingM * 2) / 3 + height: 80 + radius: Theme.cornerRadius + color: { + if (DgopService.sortBy === "memory") { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16); + } else if (memoryCardMouseArea.containsMouse) { + return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12); + } else { + return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08); + } + } + border.color: DgopService.sortBy === "memory" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.2) + border.width: DgopService.sortBy === "memory" ? 2 : 1 + + MouseArea { + id: memoryCardMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + DgopService.setSortBy("memory"); + } + } + + Column { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: "Memory" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: DgopService.sortBy === "memory" ? Theme.primary : Theme.secondary + opacity: DgopService.sortBy === "memory" ? 1 : 0.8 + } + + Row { + spacing: Theme.spacingS + + StyledText { + text: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + font.pixelSize: Theme.fontSizeLarge + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: 1 + height: 20 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) + anchors.verticalCenter: parent.verticalCenter + visible: DgopService.totalSwapKB > 0 + } + + StyledText { + text: DgopService.totalSwapKB > 0 ? DgopService.formatSystemMemory(DgopService.usedSwapKB) : "" + font.pixelSize: Theme.fontSizeMedium + font.family: SettingsData.monoFontFamily + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + visible: DgopService.totalSwapKB > 0 + } + + } + + StyledText { + text: { + if (DgopService.totalSwapKB > 0) { + return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB) + " + swap"; + } + return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB); + } + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + opacity: 0.7 + } + + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + Rectangle { + width: (parent.width - Theme.spacingM * 2) / 3 + height: 80 + radius: Theme.cornerRadius + color: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16); + } else { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08); + } + } + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + const vendor = gpu.vendor.toLowerCase(); + if (vendor.includes("nvidia")) { + if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { + return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.2); + } else { + return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.12); + } + } else if (vendor.includes("amd")) { + if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2); + } else { + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12); + } + } else if (vendor.includes("intel")) { + if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { + return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.2); + } else { + return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.12); + } + } + if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16); + } else { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08); + } + } + border.color: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2); + } + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + const vendor = gpu.vendor.toLowerCase(); + if (vendor.includes("nvidia")) { + return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3); + } else if (vendor.includes("amd")) { + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3); + } else if (vendor.includes("intel")) { + return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3); + } + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2); + } + border.width: 1 + + MouseArea { + id: gpuCardMouseArea + + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: (mouse) => { + if (mouse.button === Qt.LeftButton) { + if (DgopService.availableGpus.length > 1) { + const nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length; + SessionData.setSelectedGpuIndex(nextIndex); + } + } else if (mouse.button === Qt.RightButton) { + gpuContextMenu.popup(); + } + } + } + + Column { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: "GPU" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.secondary + opacity: 0.8 + } + + StyledText { + text: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return "No GPU"; + } + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + // Check if temperature monitoring is enabled for this GPU + const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1; + const temp = gpu.temperature; + const hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0; + if (hasTemp) { + return Math.round(temp) + "°"; + } else { + return gpu.vendor; + } + } + font.pixelSize: Theme.fontSizeLarge + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return Theme.surfaceText; + } + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1; + const temp = gpu.temperature || 0; + if (tempEnabled && temp > 80) { + return Theme.error; + } + if (tempEnabled && temp > 60) { + return Theme.warning; + } + return Theme.surfaceText; + } + } + + StyledText { + text: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return "No GPUs detected"; + } + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1; + const temp = gpu.temperature; + const hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0; + if (hasTemp) { + return gpu.vendor + " " + gpu.displayName; + } else { + return gpu.displayName; + } + } + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + opacity: 0.7 + width: parent.parent.width - Theme.spacingM * 2 + elide: Text.ElideRight + maximumLineCount: 1 + } + + } + + Menu { + id: gpuContextMenu + + MenuItem { + text: "Enable GPU Temperature" + checkable: true + checked: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return false; + } + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + if (!gpu.pciId) { + return false; + } + return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1 : false; + } + onTriggered: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return; + } + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + if (!gpu.pciId) { + return; + } + const enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice() : []; + const index = enabledIds.indexOf(gpu.pciId); + if (checked && index === -1) { + enabledIds.push(gpu.pciId); + DgopService.addGpuPciId(gpu.pciId); + } else if (!checked && index !== -1) { + enabledIds.splice(index, 1); + DgopService.removeGpuPciId(gpu.pciId); + } + SessionData.setEnabledGpuPciIds(enabledIds); + } + } + + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/ProcessList/SystemTab.qml b/quickshell/.config/quickshell/Modules/ProcessList/SystemTab.qml new file mode 100644 index 0000000..6c1c7a2 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/ProcessList/SystemTab.qml @@ -0,0 +1,613 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +DankFlickable { + anchors.fill: parent + contentHeight: systemColumn.implicitHeight + Component.onCompleted: { + DgopService.addRef(["system", "hardware", "diskmounts"]); + } + Component.onDestruction: { + DgopService.removeRef(["system", "hardware", "diskmounts"]); + } + + Column { + id: systemColumn + + width: parent.width + spacing: Theme.spacingM + + Rectangle { + width: parent.width + height: systemInfoColumn.implicitHeight + 2 * Theme.spacingL + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.6) + border.width: 0 + + Column { + id: systemInfoColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + Row { + width: parent.width + spacing: Theme.spacingL + + SystemLogo { + width: 80 + height: 80 + } + + Column { + width: parent.width - 80 - Theme.spacingL + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + StyledText { + text: DgopService.hostname + font.pixelSize: Theme.fontSizeXLarge + font.family: SettingsData.monoFontFamily + font.weight: Font.Light + color: Theme.surfaceText + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: `${DgopService.distribution} • ${DgopService.architecture} • ${DgopService.kernelVersion}` + font.pixelSize: Theme.fontSizeMedium + font.family: SettingsData.monoFontFamily + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: `Up ${UserInfoService.uptime} • Boot: ${DgopService.bootTime}` + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: `Load: ${DgopService.loadAverage} • ${DgopService.processCount} processes, ${DgopService.threadCount} threads` + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + verticalAlignment: Text.AlignVCenter + } + + } + + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + Row { + width: parent.width + spacing: Theme.spacingXL + + Rectangle { + width: (parent.width - Theme.spacingXL) / 2 + height: hardwareColumn.implicitHeight + Theme.spacingL + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.4) + border.width: 1 + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + + Column { + id: hardwareColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: "memory" + size: Theme.iconSizeSmall + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "System" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + } + + StyledText { + text: DgopService.cpuModel + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + wrapMode: Text.NoWrap + maximumLineCount: 1 + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: DgopService.motherboard + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) + width: parent.width + elide: Text.ElideRight + wrapMode: Text.NoWrap + maximumLineCount: 1 + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: `BIOS ${DgopService.biosVersion}` + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + width: parent.width + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: `${DgopService.formatSystemMemory(DgopService.totalMemoryKB)} RAM` + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) + width: parent.width + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + } + + } + + Rectangle { + width: (parent.width - Theme.spacingXL) / 2 + height: gpuColumn.implicitHeight + Theme.spacingL + radius: Theme.cornerRadius + color: { + const baseColor = Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.4); + const hoverColor = Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.6); + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1 ? hoverColor : baseColor; + } + + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + const vendor = gpu.fullName.split(' ')[0].toLowerCase(); + let tintColor; + if (vendor.includes("nvidia")) { + tintColor = Theme.success; + } else if (vendor.includes("amd")) { + tintColor = Theme.error; + } else if (vendor.includes("intel")) { + tintColor = Theme.info; + } else { + return gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1 ? hoverColor : baseColor; + } + if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { + return Qt.rgba((hoverColor.r + tintColor.r * 0.1) / 1.1, (hoverColor.g + tintColor.g * 0.1) / 1.1, (hoverColor.b + tintColor.b * 0.1) / 1.1, 0.6); + } else { + return Qt.rgba((baseColor.r + tintColor.r * 0.08) / 1.08, (baseColor.g + tintColor.g * 0.08) / 1.08, (baseColor.b + tintColor.b * 0.08) / 1.08, 0.4); + } + } + border.width: 1 + border.color: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1); + } + + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + const vendor = gpu.fullName.split(' ')[0].toLowerCase(); + if (vendor.includes("nvidia")) { + return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3); + } else if (vendor.includes("amd")) { + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3); + } else if (vendor.includes("intel")) { + return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3); + } + return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1); + } + + MouseArea { + id: gpuCardMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + if (DgopService.availableGpus.length > 1) { + const nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length; + SessionData.setSelectedGpuIndex(nextIndex); + } + } + } + + Column { + id: gpuColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: "auto_awesome_mosaic" + size: Theme.iconSizeSmall + color: Theme.secondary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "GPU" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.secondary + anchors.verticalCenter: parent.verticalCenter + } + + } + + StyledText { + text: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return "No GPUs detected"; + } + + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + return gpu.fullName; + } + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + maximumLineCount: 1 + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return "Device: N/A"; + } + + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + return `Device: ${gpu.pciId}`; + } + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) + width: parent.width + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + textFormat: Text.RichText + } + + StyledText { + text: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return "Driver: N/A"; + } + + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + return `Driver: ${gpu.driver}`; + } + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) + width: parent.width + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return "Temp: --°"; + } + + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + const temp = gpu.temperature; + return `Temp: ${(temp === undefined || temp === null || temp === 0) ? '--°' : `${Math.round(temp)}°C`}`; + } + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7); + } + + const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; + const temp = gpu.temperature || 0; + if (temp > 80) { + return Theme.error; + } + + if (temp > 60) { + return Theme.warning; + } + + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7); + } + width: parent.width + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + + } + + } + + } + + } + + } + + Rectangle { + width: parent.width + height: storageColumn.implicitHeight + 2 * Theme.spacingL + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.6) + border.width: 0 + + Column { + id: storageColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: "storage" + size: Theme.iconSize + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Storage & Disks" + font.pixelSize: Theme.fontSizeLarge + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + } + + Column { + width: parent.width + spacing: 2 + + Row { + width: parent.width + height: 24 + spacing: Theme.spacingS + + StyledText { + text: "Device" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + width: parent.width * 0.25 + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: "Mount" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + width: parent.width * 0.2 + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: "Size" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + width: parent.width * 0.15 + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: "Used" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + width: parent.width * 0.15 + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: "Available" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + width: parent.width * 0.15 + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: "Use%" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + width: parent.width * 0.1 + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + } + + Repeater { + id: diskMountRepeater + + model: DgopService.diskMounts + + Rectangle { + width: parent.width + height: 24 + radius: Theme.cornerRadius + color: diskMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) : "transparent" + + MouseArea { + id: diskMouseArea + + anchors.fill: parent + hoverEnabled: true + } + + Row { + anchors.fill: parent + spacing: Theme.spacingS + + StyledText { + text: modelData.device + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + width: parent.width * 0.25 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: modelData.mount + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + width: parent.width * 0.2 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: modelData.size + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + width: parent.width * 0.15 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: modelData.used + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + width: parent.width * 0.15 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: modelData.avail + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + width: parent.width * 0.15 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + } + + StyledText { + text: modelData.percent + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: { + const percent = parseInt(modelData.percent); + if (percent > 90) { + return Theme.error; + } + + if (percent > 75) { + return Theme.warning; + } + + return Theme.surfaceText; + } + width: parent.width * 0.1 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + } + + } + + } + + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/Settings/AboutTab.qml b/quickshell/.config/quickshell/Modules/Settings/AboutTab.qml new file mode 100644 index 0000000..feff20a --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/AboutTab.qml @@ -0,0 +1,535 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: aboutTab + + property bool isHyprland: CompositorService.isHyprland + + DankFlickable { + anchors.fill: parent + anchors.topMargin: Theme.spacingL + clip: true + contentHeight: mainColumn.height + contentWidth: width + + Column { + id: mainColumn + + width: parent.width + spacing: Theme.spacingXL + + // ASCII Art Header + StyledRect { + width: parent.width + height: asciiSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: asciiSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Item { + width: parent.width + height: asciiText.implicitHeight + + StyledText { + id: asciiText + + text: "██████╗ █████╗ ███╗ ██╗██╗ ██╗\n██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝\n██║ ██║███████║██╔██╗ ██║█████╔╝ \n██║ ██║██╔══██║██║╚██╗██║██╔═██╗ \n██████╔╝██║ ██║██║ ╚████║██║ ██╗\n╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝" + isMonospace: true + font.pixelSize: Theme.fontSizeMedium + color: Theme.primary + anchors.centerIn: parent + } + } + + StyledText { + text: "DankMaterialShell" + font.pixelSize: Theme.fontSizeXLarge + font.weight: Font.Bold + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + width: parent.width + } + + Item { + id: communityIcons + anchors.horizontalCenter: parent.horizontalCenter + height: 24 + width: { + if (isHyprland) { + return compositorButton.width + discordButton.width + Theme.spacingM + redditButton.width + Theme.spacingM + } else { + return compositorButton.width + matrixButton.width + 4 + discordButton.width + Theme.spacingM + redditButton.width + Theme.spacingM + } + } + + // Compositor logo (Niri or Hyprland) + Item { + id: compositorButton + width: 24 + height: 24 + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: -2 + x: 0 + + property bool hovered: false + property string tooltipText: isHyprland ? "Hyprland Website" : "niri GitHub" + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(".").toString().replace( + "file://", "").replace( + "/Modules/Settings/", + "") + (isHyprland ? "/assets/hyprland.svg" : "/assets/niri.svg") + sourceSize: Qt.size(24, 24) + smooth: true + fillMode: Image.PreserveAspectFit + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally( + isHyprland ? "https://hypr.land" : "https://github.com/YaLTeR/niri") + } + } + + // Matrix button (only for Niri) + Item { + id: matrixButton + width: 30 + height: 24 + x: compositorButton.x + compositorButton.width + 4 + visible: !isHyprland + + property bool hovered: false + property string tooltipText: "niri Matrix Chat" + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(".").toString().replace( + "file://", "").replace( + "/Modules/Settings/", + "") + "/assets/matrix-logo-white.svg" + sourceSize: Qt.size(28, 18) + smooth: true + fillMode: Image.PreserveAspectFit + layer.enabled: true + + layer.effect: MultiEffect { + colorization: 1 + colorizationColor: Theme.surfaceText + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally( + "https://matrix.to/#/#niri:matrix.org") + } + } + + // Discord button + Item { + id: discordButton + width: 20 + height: 20 + x: isHyprland ? compositorButton.x + compositorButton.width + Theme.spacingM : matrixButton.x + matrixButton.width + Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + + property bool hovered: false + property string tooltipText: isHyprland ? "Hyprland Discord Server" : "niri Discord Server" + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(".").toString().replace( + "file://", "").replace( + "/Modules/Settings/", + "") + "/assets/discord.svg" + sourceSize: Qt.size(20, 20) + smooth: true + fillMode: Image.PreserveAspectFit + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally( + isHyprland ? "https://discord.com/invite/hQ9XvMUjjr" : "https://discord.gg/vT8Sfjy7sx") + } + } + + // Reddit button + Item { + id: redditButton + width: 20 + height: 20 + x: discordButton.x + discordButton.width + Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + + property bool hovered: false + property string tooltipText: isHyprland ? "r/hyprland Subreddit" : "r/niri Subreddit" + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(".").toString().replace( + "file://", "").replace( + "/Modules/Settings/", + "") + "/assets/reddit.svg" + sourceSize: Qt.size(20, 20) + smooth: true + fillMode: Image.PreserveAspectFit + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally( + isHyprland ? "https://reddit.com/r/hyprland" : "https://reddit.com/r/niri") + } + } + } + } + } + + + // Project Information + StyledRect { + width: parent.width + height: projectSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: projectSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "info" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "About" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: `DankMaterialShell is a modern desktop inspired by MUI 3. +

        The goal is to provide a high level of functionality and customization so that it can be a suitable replacement for complete desktop environments like Gnome, KDE, or Cosmic. + ` + textFormat: Text.RichText + font.pixelSize: Theme.fontSizeMedium + linkColor: Theme.primary + onLinkActivated: url => Qt.openUrlExternally(url) + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + } + } + + // Technical Details + StyledRect { + width: parent.width + height: techSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: techSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "code" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Technical Details" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Grid { + width: parent.width + columns: 2 + columnSpacing: Theme.spacingL + rowSpacing: Theme.spacingS + + StyledText { + text: "Framework:" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: `Quickshell` + linkColor: Theme.primary + textFormat: Text.RichText + onLinkActivated: url => Qt.openUrlExternally(url) + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + + StyledText { + text: "Language:" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "QML (Qt Modeling Language)" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + + StyledText { + text: "Compositor:" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + Row { + spacing: 4 + + StyledText { + text: `niri` + font.pixelSize: Theme.fontSizeMedium + linkColor: Theme.primary + textFormat: Text.RichText + color: Theme.surfaceVariantText + onLinkActivated: url => Qt.openUrlExternally(url) + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + + StyledText { + text: "&" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: `hyprland` + font.pixelSize: Theme.fontSizeMedium + linkColor: Theme.primary + textFormat: Text.RichText + color: Theme.surfaceVariantText + onLinkActivated: url => Qt.openUrlExternally(url) + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + } + + StyledText { + text: "Github:" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + Row { + spacing: 4 + + StyledText { + text: `DankMaterialShell` + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + linkColor: Theme.primary + textFormat: Text.RichText + onLinkActivated: url => Qt.openUrlExternally(url) + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + + StyledText { + text: "- Support Us With a Star ⭐" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: "System Monitoring:" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + Row { + spacing: 4 + + StyledText { + text: `dgop` + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + linkColor: Theme.primary + textFormat: Text.RichText + onLinkActivated: url => Qt.openUrlExternally(url) + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + + StyledText { + text: "- Stateless System Monitoring" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + } + } + + // Community tooltip - positioned absolutely above everything + Rectangle { + id: communityTooltip + parent: aboutTab + z: 1000 + + property var hoveredButton: { + if (compositorButton.hovered) return compositorButton + if (matrixButton.visible && matrixButton.hovered) return matrixButton + if (discordButton.hovered) return discordButton + if (redditButton.hovered) return redditButton + return null + } + + property string tooltipText: hoveredButton ? hoveredButton.tooltipText : "" + + visible: hoveredButton !== null && tooltipText !== "" + width: tooltipLabel.implicitWidth + 24 + height: tooltipLabel.implicitHeight + 12 + + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.outlineMedium + + x: hoveredButton ? hoveredButton.mapToItem(aboutTab, hoveredButton.width / 2, 0).x - width / 2 : 0 + y: hoveredButton ? communityIcons.mapToItem(aboutTab, 0, 0).y - height - 8 : 0 + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowOpacity: 0.15 + shadowVerticalOffset: 2 + shadowBlur: 0.5 + } + + StyledText { + id: tooltipLabel + anchors.centerIn: parent + text: communityTooltip.tooltipText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Settings/DisplaysTab.qml b/quickshell/.config/quickshell/Modules/Settings/DisplaysTab.qml new file mode 100644 index 0000000..add096f --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/DisplaysTab.qml @@ -0,0 +1,369 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: displaysTab + + property var variantComponents: [{ + "id": "topBar", + "name": "Top Bar", + "description": "System bar with widgets and system information", + "icon": "toolbar" + }, { + "id": "dock", + "name": "Application Dock", + "description": "Bottom dock for pinned and running applications", + "icon": "dock" + }, { + "id": "notifications", + "name": "Notification Popups", + "description": "Notification toast popups", + "icon": "notifications" + }, { + "id": "wallpaper", + "name": "Wallpaper", + "description": "Desktop background images", + "icon": "wallpaper" + }, { + "id": "osd", + "name": "On-Screen Displays", + "description": "Volume, brightness, and other system OSDs", + "icon": "picture_in_picture" + }, { + "id": "toast", + "name": "Toast Messages", + "description": "System toast notifications", + "icon": "campaign" + }, { + "id": "notepad", + "name": "Notepad Slideout", + "description": "Quick note-taking slideout panel", + "icon": "sticky_note_2" + }, { + "id": "systemTray", + "name": "System Tray", + "description": "System tray icons", + "icon": "notifications" + }] + + function getScreenPreferences(componentId) { + return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"]; + } + + function setScreenPreferences(componentId, screenNames) { + var prefs = SettingsData.screenPreferences || { + }; + prefs[componentId] = screenNames; + SettingsData.setScreenPreferences(prefs); + } + + DankFlickable { + anchors.fill: parent + anchors.topMargin: Theme.spacingL + anchors.bottomMargin: Theme.spacingS + clip: true + contentHeight: mainColumn.height + contentWidth: width + + Column { + id: mainColumn + + width: parent.width + spacing: Theme.spacingXL + + StyledRect { + width: parent.width + height: screensInfoSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: screensInfoSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "monitor" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Connected Displays" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Configure which displays show shell components" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + + } + + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Available Screens (" + Quickshell.screens.length + ")" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + Repeater { + model: Quickshell.screens + + delegate: Rectangle { + width: parent.width + height: screenRow.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + border.width: 1 + + Row { + id: screenRow + + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingM + + DankIcon { + name: "desktop_windows" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM * 2 + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS / 2 + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + Row { + spacing: Theme.spacingS + + StyledText { + text: modelData.width + "×" + modelData.height + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: modelData.model || "Unknown Model" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + } + + } + + } + + } + + } + + } + + } + + } + + Column { + width: parent.width + spacing: Theme.spacingL + + Repeater { + model: displaysTab.variantComponents + + delegate: StyledRect { + width: parent.width + height: componentSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: componentSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: modelData.icon + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: modelData.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + + } + + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Show on screens:" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + Column { + property string componentId: modelData.id + property var selectedScreens: displaysTab.getScreenPreferences(componentId) + + width: parent.width + spacing: Theme.spacingXS + + DankToggle { + width: parent.width + text: "All displays" + description: "Show on all connected displays" + checked: parent.selectedScreens.includes("all") + onToggled: (checked) => { + if (checked) + displaysTab.setScreenPreferences(parent.componentId, ["all"]); + else + displaysTab.setScreenPreferences(parent.componentId, []); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: !parent.selectedScreens.includes("all") + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: !parent.selectedScreens.includes("all") + + Repeater { + model: Quickshell.screens + + delegate: DankToggle { + property string screenName: modelData.name + property string componentId: parent.parent.componentId + + width: parent.width + text: screenName + description: modelData.width + "×" + modelData.height + " • " + (modelData.model || "Unknown Model") + checked: { + var prefs = displaysTab.getScreenPreferences(componentId); + return !prefs.includes("all") && prefs.includes(screenName); + } + onToggled: (checked) => { + var currentPrefs = displaysTab.getScreenPreferences(componentId); + if (currentPrefs.includes("all")) + currentPrefs = []; + + var newPrefs = currentPrefs.slice(); + if (checked) { + if (!newPrefs.includes(screenName)) + newPrefs.push(screenName); + + } else { + var index = newPrefs.indexOf(screenName); + if (index > -1) + newPrefs.splice(index, 1); + + } + displaysTab.setScreenPreferences(componentId, newPrefs); + } + } + + } + + } + + } + + } + + } + + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/Settings/PersonalizationTab.qml b/quickshell/.config/quickshell/Modules/Settings/PersonalizationTab.qml new file mode 100644 index 0000000..0559671 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/PersonalizationTab.qml @@ -0,0 +1,1439 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import qs.Common +import qs.Modals +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets + +Item { + id: personalizationTab + + property alias wallpaperBrowser: wallpaperBrowser + property var parentModal: null + property var cachedFontFamilies: [] + property var cachedMonoFamilies: [] + property bool fontsEnumerated: false + property string selectedMonitorName: { + var screens = Quickshell.screens + return screens.length > 0 ? screens[0].name : "" + } + + function enumerateFonts() { + var fonts = ["Default"] + var availableFonts = Qt.fontFamilies() + var rootFamilies = [] + var seenFamilies = new Set() + for (var i = 0; i < availableFonts.length; i++) { + var fontName = availableFonts[i] + if (fontName.startsWith(".")) + continue + + if (fontName === SettingsData.defaultFontFamily) + continue + + var rootName = fontName.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, + "").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i, function (match, suffix) { + return match + }).trim() + if (!seenFamilies.has(rootName) && rootName !== "") { + seenFamilies.add(rootName) + rootFamilies.push(rootName) + } + } + cachedFontFamilies = fonts.concat(rootFamilies.sort()) + var monoFonts = ["Default"] + var monoFamilies = [] + var seenMonoFamilies = new Set() + for (var j = 0; j < availableFonts.length; j++) { + var fontName2 = availableFonts[j] + if (fontName2.startsWith(".")) + continue + + if (fontName2 === SettingsData.defaultMonoFontFamily) + continue + + var lowerName = fontName2.toLowerCase() + if (lowerName.includes("mono") || lowerName.includes("code") || lowerName.includes("console") || lowerName.includes("terminal") || lowerName.includes("courier") || lowerName.includes("dejavu sans mono") || lowerName.includes( + "jetbrains") || lowerName.includes("fira") || lowerName.includes("hack") || lowerName.includes("source code") || lowerName.includes("ubuntu mono") || lowerName.includes("cascadia")) { + var rootName2 = fontName2.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").trim() + if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") { + seenMonoFamilies.add(rootName2) + monoFamilies.push(rootName2) + } + } + } + cachedMonoFamilies = monoFonts.concat(monoFamilies.sort()) + } + + Component.onCompleted: { + // Access WallpaperCyclingService to ensure it's initialized + WallpaperCyclingService.cyclingActive + if (!fontsEnumerated) { + enumerateFonts() + fontsEnumerated = true + } + } + + DankFlickable { + anchors.fill: parent + anchors.topMargin: Theme.spacingL + clip: true + contentHeight: mainColumn.height + contentWidth: width + + Column { + id: mainColumn + + width: parent.width + spacing: Theme.spacingXL + + // Wallpaper Section + StyledRect { + width: parent.width + height: wallpaperSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: wallpaperSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "wallpaper" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Wallpaper" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + spacing: Theme.spacingL + + StyledRect { + width: 160 + height: 90 + radius: Theme.cornerRadius + color: Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + + CachingImage { + anchors.fill: parent + anchors.margins: 1 + source: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return (currentWallpaper !== "" && !currentWallpaper.startsWith("#")) ? "file://" + currentWallpaper : "" + } + fillMode: Image.PreserveAspectCrop + visible: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper !== "" && !currentWallpaper.startsWith("#") + } + maxCacheSize: 160 + layer.enabled: true + + layer.effect: MultiEffect { + maskEnabled: true + maskSource: wallpaperMask + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper.startsWith("#") ? currentWallpaper : "transparent" + } + visible: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper !== "" && currentWallpaper.startsWith("#") + } + } + + Rectangle { + id: wallpaperMask + + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: "black" + visible: false + layer.enabled: true + } + + DankIcon { + anchors.centerIn: parent + name: "image" + size: Theme.iconSizeLarge + 8 + color: Theme.surfaceVariantText + visible: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper === "" + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: Qt.rgba(0, 0, 0, 0.7) + visible: wallpaperMouseArea.containsMouse + + Row { + anchors.centerIn: parent + spacing: 4 + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(255, 255, 255, 0.9) + + DankIcon { + anchors.centerIn: parent + name: "folder_open" + size: 18 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (parentModal) { + parentModal.allowFocusOverride = true + parentModal.shouldHaveFocus = false + } + wallpaperBrowser.open() + } + } + } + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(255, 255, 255, 0.9) + + DankIcon { + anchors.centerIn: parent + name: "palette" + size: 18 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + colorPicker.open() + } + } + } + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(255, 255, 255, 0.9) + visible: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper !== "" + } + + DankIcon { + anchors.centerIn: parent + name: "clear" + size: 18 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorWallpaper(selectedMonitorName, "") + } else { + if (Theme.currentTheme === Theme.dynamic) + Theme.switchTheme("blue") + SessionData.clearWallpaper() + } + } + } + } + } + } + + MouseArea { + id: wallpaperMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + } + } + + Column { + width: parent.width - 160 - Theme.spacingL + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper ? currentWallpaper.split('/').pop() : "No wallpaper selected" + } + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + } + + StyledText { + text: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper ? currentWallpaper : "" + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + visible: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper !== "" + } + } + + Row { + spacing: Theme.spacingS + visible: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper !== "" + } + + DankActionButton { + buttonSize: 32 + iconName: "skip_previous" + iconSize: Theme.iconSizeSmall + enabled: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper && !currentWallpaper.startsWith("#") + } + opacity: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return (currentWallpaper && !currentWallpaper.startsWith("#")) ? 1 : 0.5 + } + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + iconColor: Theme.surfaceText + onClicked: { + if (SessionData.perMonitorWallpaper) { + WallpaperCyclingService.cyclePrevForMonitor(selectedMonitorName) + } else { + WallpaperCyclingService.cyclePrevManually() + } + } + } + + DankActionButton { + buttonSize: 32 + iconName: "skip_next" + iconSize: Theme.iconSizeSmall + enabled: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return currentWallpaper && !currentWallpaper.startsWith("#") + } + opacity: { + var currentWallpaper = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaper(selectedMonitorName) : SessionData.wallpaperPath + return (currentWallpaper && !currentWallpaper.startsWith("#")) ? 1 : 0.5 + } + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + iconColor: Theme.surfaceText + onClicked: { + if (SessionData.perMonitorWallpaper) { + WallpaperCyclingService.cycleNextForMonitor(selectedMonitorName) + } else { + WallpaperCyclingService.cycleNextManually() + } + } + } + } + } + } + + // Per-Monitor Wallpaper Section - Full Width + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: SessionData.wallpaperPath !== "" + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.wallpaperPath !== "" + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "monitor" + size: Theme.iconSize + color: SessionData.perMonitorWallpaper ? Theme.primary : Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - perMonitorToggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Per-Monitor Wallpapers" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Set different wallpapers for each connected monitor" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + } + } + + DankToggle { + id: perMonitorToggle + + anchors.verticalCenter: parent.verticalCenter + checked: SessionData.perMonitorWallpaper + onToggled: toggled => { + return SessionData.setPerMonitorWallpaper(toggled) + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: SessionData.perMonitorWallpaper + leftPadding: Theme.iconSize + Theme.spacingM + + StyledText { + text: "Monitor Selection:" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankDropdown { + id: monitorDropdown + + width: parent.width - parent.leftPadding + text: "Monitor" + description: "Select monitor to configure wallpaper" + currentValue: selectedMonitorName || "No monitors" + options: { + var screenNames = [] + var screens = Quickshell.screens + for (var i = 0; i < screens.length; i++) { + screenNames.push(screens[i].name) + } + return screenNames + } + onValueChanged: value => { + selectedMonitorName = value + } + } + } + } + + // Wallpaper Cycling Section - Full Width + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: SessionData.wallpaperPath !== "" && !SessionData.perMonitorWallpaper + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.wallpaperPath !== "" && !SessionData.perMonitorWallpaper + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "schedule" + size: Theme.iconSize + color: SessionData.wallpaperCyclingEnabled ? Theme.primary : Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - cyclingToggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Automatic Cycling" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Automatically cycle through wallpapers in the same folder" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + } + } + + DankToggle { + id: cyclingToggle + + anchors.verticalCenter: parent.verticalCenter + checked: SessionData.wallpaperCyclingEnabled + enabled: !SessionData.perMonitorWallpaper + onToggled: toggled => { + return SessionData.setWallpaperCyclingEnabled(toggled) + } + } + } + + // Cycling mode and settings + Column { + width: parent.width + spacing: Theme.spacingS + visible: SessionData.wallpaperCyclingEnabled + leftPadding: Theme.iconSize + Theme.spacingM + + Row { + spacing: Theme.spacingL + width: parent.width - parent.leftPadding + + StyledText { + text: "Mode:" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: 200 + height: 45 + Theme.spacingM + + DankTabBar { + id: modeTabBar + + width: 200 + height: 45 + model: [{ + "text": "Interval", + "icon": "schedule" + }, { + "text": "Time", + "icon": "access_time" + }] + currentIndex: SessionData.wallpaperCyclingMode === "time" ? 1 : 0 + onTabClicked: index => { + SessionData.setWallpaperCyclingMode(index === 1 ? "time" : "interval") + } + } + } + } + + // Interval settings + DankDropdown { + property var intervalOptions: ["1 minute", "5 minutes", "15 minutes", "30 minutes", "1 hour", "1.5 hours", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours"] + property var intervalValues: [60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200] + + width: parent.width - parent.leftPadding + visible: SessionData.wallpaperCyclingMode === "interval" + text: "Interval" + description: "How often to change wallpaper" + options: intervalOptions + currentValue: { + const currentSeconds = SessionData.wallpaperCyclingInterval + const index = intervalValues.indexOf(currentSeconds) + return index >= 0 ? intervalOptions[index] : "5 minutes" + } + onValueChanged: value => { + const index = intervalOptions.indexOf(value) + if (index >= 0) + SessionData.setWallpaperCyclingInterval(intervalValues[index]) + } + } + + // Time settings + Row { + spacing: Theme.spacingM + visible: SessionData.wallpaperCyclingMode === "time" + width: parent.width - parent.leftPadding + + StyledText { + text: "Daily at:" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + width: 100 + height: 40 + text: SessionData.wallpaperCyclingTime + placeholderText: "00:00" + maximumLength: 5 + topPadding: Theme.spacingS + bottomPadding: Theme.spacingS + onAccepted: { + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text) + if (isValid) + SessionData.setWallpaperCyclingTime(text) + else + text = SessionData.wallpaperCyclingTime + } + onEditingFinished: { + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text) + if (isValid) + SessionData.setWallpaperCyclingTime(text) + else + text = SessionData.wallpaperCyclingTime + } + anchors.verticalCenter: parent.verticalCenter + + validator: RegularExpressionValidator { + regularExpression: /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/ + } + } + + StyledText { + text: "24-hour format" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + } + + // Dynamic Theme Section + StyledRect { + width: parent.width + height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: dynamicThemeSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "palette" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - toggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Dynamic Theming" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Automatically extract colors from wallpaper" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + + DankToggle { + id: toggle + + anchors.verticalCenter: parent.verticalCenter + checked: Theme.wallpaperPath !== "" && Theme.currentTheme === Theme.dynamic + enabled: ToastService.wallpaperErrorStatus !== "matugen_missing" && Theme.wallpaperPath !== "" + onToggled: toggled => { + if (toggled) + Theme.switchTheme(Theme.dynamic) + else + Theme.switchTheme("blue") + } + } + } + + StyledText { + text: "matugen not detected - dynamic theming unavailable" + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + visible: ToastService.wallpaperErrorStatus === "matugen_missing" + width: parent.width + leftPadding: Theme.iconSize + Theme.spacingM + } + } + } + + // Display Settings + StyledRect { + width: parent.width + height: displaySection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: displaySection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "monitor" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Display Settings" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + width: parent.width + text: "Light Mode" + description: "Use light theme instead of dark theme" + checked: SessionData.isLightMode + onToggled: checked => { + Theme.setLightMode(checked) + } + } + + DankToggle { + width: parent.width + text: "Hide Brightness Slider" + description: "Hide the brightness slider in Control Center and make audio slider full width" + checked: SettingsData.hideBrightnessSlider + onToggled: checked => { + SettingsData.setHideBrightnessSlider(checked) + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + } + + DankToggle { + id: nightModeToggle + + width: parent.width + text: "Night Mode" + description: "Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates." + checked: DisplayService.nightModeEnabled + onToggled: checked => { + DisplayService.toggleNightMode() + } + + Connections { + function onNightModeEnabledChanged() { + nightModeToggle.checked = DisplayService.nightModeEnabled + } + + target: DisplayService + } + } + + DankDropdown { + width: parent.width + text: "Temperature" + description: "Color temperature for night mode" + currentValue: SessionData.nightModeTemperature + "K" + options: { + var temps = [] + for (var i = 2500; i <= 6000; i += 500) { + temps.push(i + "K") + } + return temps + } + onValueChanged: value => { + var temp = parseInt(value.replace("K", "")) + SessionData.setNightModeTemperature(temp) + } + } + + DankToggle { + id: automaticToggle + width: parent.width + text: "Automatic Control" + description: "Only adjust gamma based on time or location rules." + checked: SessionData.nightModeAutoEnabled + onToggled: checked => { + if (checked && !DisplayService.nightModeEnabled) { + DisplayService.toggleNightMode() + } else if (!checked && DisplayService.nightModeEnabled) { + DisplayService.toggleNightMode() + } + SessionData.setNightModeAutoEnabled(checked) + } + + Connections { + target: SessionData + function onNightModeAutoEnabledChanged() { + automaticToggle.checked = SessionData.nightModeAutoEnabled + } + } + } + + Column { + id: automaticSettings + width: parent.width + spacing: Theme.spacingS + visible: SessionData.nightModeAutoEnabled + leftPadding: Theme.spacingM + + Connections { + target: SessionData + function onNightModeAutoEnabledChanged() { + automaticSettings.visible = SessionData.nightModeAutoEnabled + } + } + + Item { + width: 200 + height: 45 + Theme.spacingM + + DankTabBar { + id: modeTabBarNight + width: 200 + height: 45 + model: [{ + "text": "Time", + "icon": "access_time" + }, { + "text": "Location", + "icon": "place" + }] + + Component.onCompleted: { + currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0 + Qt.callLater(updateIndicator) + } + + onTabClicked: index => { + console.log("Tab clicked:", index, "Setting mode to:", index === 1 ? "location" : "time") + DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time") + currentIndex = index + } + + Connections { + target: SessionData + function onNightModeAutoModeChanged() { + modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0 + Qt.callLater(modeTabBarNight.updateIndicator) + } + } + } + } + + Column { + property bool isTimeMode: SessionData.nightModeAutoMode === "time" + visible: isTimeMode + spacing: Theme.spacingM + + // Header row + Row { + spacing: Theme.spacingM + height: 20 + leftPadding: 45 + + StyledText { + text: "Hour" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 50 + horizontalAlignment: Text.AlignHCenter + anchors.bottom: parent.bottom + } + + StyledText { + text: "Minute" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 50 + horizontalAlignment: Text.AlignHCenter + anchors.bottom: parent.bottom + } + } + + // Start time row + Row { + spacing: Theme.spacingM + height: 32 + + StyledText { + id: startLabel + text: "Start" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: 50 + anchors.verticalCenter: parent.verticalCenter + } + + DankDropdown { + width: 60 + height: 32 + text: "" + currentValue: SessionData.nightModeStartHour.toString() + options: { + var hours = [] + for (var i = 0; i < 24; i++) { + hours.push(i.toString()) + } + return hours + } + onValueChanged: value => { + SessionData.setNightModeStartHour(parseInt(value)) + } + } + + DankDropdown { + width: 60 + height: 32 + text: "" + currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0') + options: { + var minutes = [] + for (var i = 0; i < 60; i += 5) { + minutes.push(i.toString().padStart(2, '0')) + } + return minutes + } + onValueChanged: value => { + SessionData.setNightModeStartMinute(parseInt(value)) + } + } + } + + // End time row + Row { + spacing: Theme.spacingM + height: 32 + + StyledText { + text: "End" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: startLabel.width + anchors.verticalCenter: parent.verticalCenter + } + + DankDropdown { + width: 60 + height: 32 + text: "" + currentValue: SessionData.nightModeEndHour.toString() + options: { + var hours = [] + for (var i = 0; i < 24; i++) { + hours.push(i.toString()) + } + return hours + } + onValueChanged: value => { + SessionData.setNightModeEndHour(parseInt(value)) + } + } + + DankDropdown { + width: 60 + height: 32 + text: "" + currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0') + options: { + var minutes = [] + for (var i = 0; i < 60; i += 5) { + minutes.push(i.toString().padStart(2, '0')) + } + return minutes + } + onValueChanged: value => { + SessionData.setNightModeEndMinute(parseInt(value)) + } + } + } + } + + Column { + property bool isLocationMode: SessionData.nightModeAutoMode === "location" + visible: isLocationMode + spacing: Theme.spacingM + width: parent.width + + DankToggle { + width: parent.width + text: "Auto-location" + description: DisplayService.geoclueAvailable ? "Use automatic location detection (geoclue2)" : "Geoclue service not running - cannot auto-detect location" + checked: SessionData.nightModeLocationProvider === "geoclue2" + enabled: DisplayService.geoclueAvailable + onToggled: checked => { + if (checked && DisplayService.geoclueAvailable) { + SessionData.setNightModeLocationProvider("geoclue2") + SessionData.setLatitude(0.0) + SessionData.setLongitude(0.0) + } else { + SessionData.setNightModeLocationProvider("") + } + } + } + + StyledText { + text: "Manual Coordinates" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + visible: SessionData.nightModeLocationProvider !== "geoclue2" + } + + Row { + spacing: Theme.spacingM + visible: SessionData.nightModeLocationProvider !== "geoclue2" + + Column { + spacing: Theme.spacingXS + + StyledText { + text: "Latitude" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: 120 + height: 40 + text: SessionData.latitude.toString() + placeholderText: "0.0" + onTextChanged: { + const lat = parseFloat(text) || 0.0 + if (lat >= -90 && lat <= 90) { + SessionData.setLatitude(lat) + } + } + } + } + + Column { + spacing: Theme.spacingXS + + StyledText { + text: "Longitude" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: 120 + height: 40 + text: SessionData.longitude.toString() + placeholderText: "0.0" + onTextChanged: { + const lon = parseFloat(text) || 0.0 + if (lon >= -180 && lon <= 180) { + SessionData.setLongitude(lon) + } + } + } + } + } + + StyledText { + text: "Uses sunrise/sunset times to automatically adjust night mode based on your location." + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + } + } + } + } + } + + // Lock Screen Settings + StyledRect { + width: parent.width + height: lockScreenSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: lockScreenSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "lock" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Lock Screen" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + width: parent.width + text: "Show Power Actions" + description: "Show power, restart, and logout buttons on the lock screen" + checked: SettingsData.lockScreenShowPowerActions + onToggled: checked => { + SettingsData.setLockScreenShowPowerActions(checked) + } + } + } + } + + // Font Settings + StyledRect { + width: parent.width + height: fontSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: fontSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "font_download" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Font Settings" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankDropdown { + width: parent.width + text: "Font Family" + description: "Select system font family" + currentValue: { + if (SettingsData.fontFamily === SettingsData.defaultFontFamily) + return "Default" + else + return SettingsData.fontFamily || "Default" + } + enableFuzzySearch: true + popupWidthOffset: 100 + maxPopupHeight: 400 + options: cachedFontFamilies + onValueChanged: value => { + if (value.startsWith("Default")) + SettingsData.setFontFamily(SettingsData.defaultFontFamily) + else + SettingsData.setFontFamily(value) + } + } + + DankDropdown { + width: parent.width + text: "Font Weight" + description: "Select font weight" + currentValue: { + switch (SettingsData.fontWeight) { + case Font.Thin: + return "Thin" + case Font.ExtraLight: + return "Extra Light" + case Font.Light: + return "Light" + case Font.Normal: + return "Regular" + case Font.Medium: + return "Medium" + case Font.DemiBold: + return "Demi Bold" + case Font.Bold: + return "Bold" + case Font.ExtraBold: + return "Extra Bold" + case Font.Black: + return "Black" + default: + return "Regular" + } + } + options: ["Thin", "Extra Light", "Light", "Regular", "Medium", "Demi Bold", "Bold", "Extra Bold", "Black"] + onValueChanged: value => { + var weight + switch (value) { + case "Thin": + weight = Font.Thin + break + case "Extra Light": + weight = Font.ExtraLight + break + case "Light": + weight = Font.Light + break + case "Regular": + weight = Font.Normal + break + case "Medium": + weight = Font.Medium + break + case "Demi Bold": + weight = Font.DemiBold + break + case "Bold": + weight = Font.Bold + break + case "Extra Bold": + weight = Font.ExtraBold + break + case "Black": + weight = Font.Black + break + default: + weight = Font.Normal + break + } + SettingsData.setFontWeight(weight) + } + } + + DankDropdown { + width: parent.width + text: "Monospace Font" + description: "Select monospace font for process list and technical displays" + currentValue: { + if (SettingsData.monoFontFamily === SettingsData.defaultMonoFontFamily) + return "Default" + + return SettingsData.monoFontFamily || "Default" + } + enableFuzzySearch: true + popupWidthOffset: 100 + maxPopupHeight: 400 + options: cachedMonoFamilies + onValueChanged: value => { + if (value === "Default") + SettingsData.setMonoFontFamily(SettingsData.defaultMonoFontFamily) + else + SettingsData.setMonoFontFamily(value) + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + Column { + width: parent.width - fontScaleControls.width - Theme.spacingM + spacing: Theme.spacingXS + + StyledText { + text: "Font Scale" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Scale all font sizes (" + (SettingsData.fontScale * 100).toFixed(0) + "%)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + } + } + + Row { + id: fontScaleControls + + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + buttonSize: 32 + iconName: "remove" + iconSize: Theme.iconSizeSmall + enabled: SettingsData.fontScale > 1.0 + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + iconColor: Theme.surfaceText + onClicked: { + var newScale = Math.max(1.0, SettingsData.fontScale - 0.05) + SettingsData.setFontScale(newScale) + } + } + + StyledRect { + width: 60 + height: 32 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + StyledText { + anchors.centerIn: parent + text: (SettingsData.fontScale * 100).toFixed(0) + "%" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + } + } + + DankActionButton { + buttonSize: 32 + iconName: "add" + iconSize: Theme.iconSizeSmall + enabled: SettingsData.fontScale < 2.0 + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + iconColor: Theme.surfaceText + onClicked: { + var newScale = Math.min(2.0, SettingsData.fontScale + 0.05) + SettingsData.setFontScale(newScale) + } + } + } + } + } + } + } + } + + FileBrowserModal { + id: wallpaperBrowser + + browserTitle: "Select Wallpaper" + browserIcon: "wallpaper" + browserType: "wallpaper" + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + onFileSelected: path => { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorWallpaper(selectedMonitorName, path) + } else { + SessionData.setWallpaper(path) + } + close() + } + onDialogClosed: { + if (parentModal) { + parentModal.allowFocusOverride = false + parentModal.shouldHaveFocus = Qt.binding(() => { + return parentModal.shouldBeVisible + }) + } + } + } + + DankColorPicker { + id: colorPicker + + pickerTitle: "Choose Wallpaper Color" + onColorSelected: selectedColor => { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorWallpaper(selectedMonitorName, selectedColor) + } else { + SessionData.setWallpaperColor(selectedColor) + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Settings/RecentAppsTab.qml b/quickshell/.config/quickshell/Modules/Settings/RecentAppsTab.qml new file mode 100644 index 0000000..daa2e39 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/RecentAppsTab.qml @@ -0,0 +1,246 @@ +import QtQuick +import QtQuick.Controls +import Quickshell.Widgets +import qs.Common +import qs.Widgets + +Item { + id: recentAppsTab + + DankFlickable { + anchors.fill: parent + anchors.topMargin: Theme.spacingL + clip: true + contentHeight: mainColumn.height + contentWidth: width + + Column { + id: mainColumn + width: parent.width + spacing: Theme.spacingXL + + StyledRect { + width: parent.width + height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: recentlyUsedSection + + property var rankedAppsModel: { + var apps = [] + for (var appId in (AppUsageHistoryData.appUsageRanking + || {})) { + var appData = (AppUsageHistoryData.appUsageRanking + || {})[appId] + apps.push({ + "id": appId, + "name": appData.name, + "exec": appData.exec, + "icon": appData.icon, + "comment": appData.comment, + "usageCount": appData.usageCount, + "lastUsed": appData.lastUsed + }) + } + apps.sort(function (a, b) { + if (a.usageCount !== b.usageCount) + return b.usageCount - a.usageCount + + return a.name.localeCompare(b.name) + }) + return apps.slice(0, 20) + } + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "history" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Recently Used Apps" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - parent.children[0].width + - parent.children[1].width + - clearAllButton.width - Theme.spacingM * 3 + height: 1 + } + + DankActionButton { + id: clearAllButton + + iconName: "delete_sweep" + iconSize: Theme.iconSize - 2 + iconColor: Theme.error + anchors.verticalCenter: parent.verticalCenter + onClicked: { + AppUsageHistoryData.appUsageRanking = {} + SettingsData.saveSettings() + } + } + } + + StyledText { + width: parent.width + text: "Apps are ordered by usage frequency, then last used, then alphabetically." + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + Column { + id: rankedAppsList + + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: recentlyUsedSection.rankedAppsModel + + delegate: Rectangle { + width: rankedAppsList.width + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, + Theme.surfaceContainer.g, + Theme.surfaceContainer.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, + Theme.outline.g, + Theme.outline.b, 0.1) + border.width: 1 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + StyledText { + text: (index + 1).toString() + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.primary + width: 20 + anchors.verticalCenter: parent.verticalCenter + } + + Image { + width: 24 + height: 24 + source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable" + sourceSize.width: 24 + sourceSize.height: 24 + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + onStatusChanged: { + if (status === Image.Error) + source = "image://icon/application-x-executable" + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.name + || "Unknown App" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: { + if (!modelData.lastUsed) + return "Never used" + + var date = new Date(modelData.lastUsed) + var now = new Date() + var diffMs = now - date + var diffMins = Math.floor( + diffMs / (1000 * 60)) + var diffHours = Math.floor( + diffMs / (1000 * 60 * 60)) + var diffDays = Math.floor( + diffMs / (1000 * 60 * 60 * 24)) + if (diffMins < 1) + return "Last launched just now" + + if (diffMins < 60) + return "Last launched " + diffMins + " minute" + + (diffMins === 1 ? "" : "s") + " ago" + + if (diffHours < 24) + return "Last launched " + diffHours + " hour" + + (diffHours === 1 ? "" : "s") + " ago" + + if (diffDays < 7) + return "Last launched " + diffDays + " day" + + (diffDays === 1 ? "" : "s") + " ago" + + return "Last launched " + date.toLocaleDateString() + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + DankActionButton { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + circular: true + iconName: "close" + iconSize: 16 + iconColor: Theme.error + onClicked: { + var currentRanking = Object.assign( + {}, + AppUsageHistoryData.appUsageRanking + || {}) + delete currentRanking[modelData.id] + AppUsageHistoryData.appUsageRanking = currentRanking + SettingsData.saveSettings() + } + } + } + } + + StyledText { + width: parent.width + text: recentlyUsedSection.rankedAppsModel.length + === 0 ? "No apps have been launched yet." : "" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + visible: recentlyUsedSection.rankedAppsModel.length === 0 + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Settings/SettingsSection.qml b/quickshell/.config/quickshell/Modules/Settings/SettingsSection.qml new file mode 100644 index 0000000..2e180cd --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/SettingsSection.qml @@ -0,0 +1,106 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + id: root + + property string title: "" + property string iconName: "" + property alias content: contentLoader.sourceComponent + property bool expanded: false + property bool collapsible: true + property bool lazyLoad: true + + width: parent.width + spacing: expanded ? Theme.spacingM : 0 + Component.onCompleted: { + if (!collapsible) + expanded = true + } + + MouseArea { + width: parent.width + height: headerRow.height + enabled: collapsible + hoverEnabled: collapsible + onClicked: { + if (collapsible) + expanded = !expanded + } + + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(Theme.primary.r, + Theme.primary.g, + Theme.primary.b, + 0.08) : "transparent" + radius: Theme.radiusS + } + + Row { + id: headerRow + + width: parent.width + spacing: Theme.spacingS + topPadding: Theme.spacingS + bottomPadding: Theme.spacingS + + DankIcon { + name: root.collapsible ? (root.expanded ? "expand_less" : "expand_more") : root.iconName + size: Theme.iconSize - 2 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + + Behavior on rotation { + NumberAnimation { + duration: Appearance.anim.durations.fast + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + DankIcon { + name: root.iconName + size: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + visible: root.collapsible + } + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + visible: expanded || !collapsible + } + + Loader { + id: contentLoader + + width: parent.width + active: lazyLoad ? expanded || !collapsible : true + visible: expanded || !collapsible + asynchronous: true + opacity: visible ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Settings/ThemeColorsTab.qml b/quickshell/.config/quickshell/Modules/Settings/ThemeColorsTab.qml new file mode 100644 index 0000000..f64175d --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -0,0 +1,942 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Modals +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets + +Item { + id: themeColorsTab + + property var cachedFontFamilies: [] + property var cachedMonoFamilies: [] + property bool fontsEnumerated: false + + function enumerateFonts() { + var fonts = ["Default"] + var availableFonts = Qt.fontFamilies() + var rootFamilies = [] + var seenFamilies = new Set() + for (var i = 0; i < availableFonts.length; i++) { + var fontName = availableFonts[i] + if (fontName.startsWith(".")) + continue + + if (fontName === SettingsData.defaultFontFamily) + continue + + var rootName = fontName.replace( + / (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, + "").replace( + / (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, + "").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i, + function (match, suffix) { + return match + }).trim() + if (!seenFamilies.has(rootName) && rootName !== "") { + seenFamilies.add(rootName) + rootFamilies.push(rootName) + } + } + cachedFontFamilies = fonts.concat(rootFamilies.sort()) + var monoFonts = ["Default"] + var monoFamilies = [] + var seenMonoFamilies = new Set() + for (var j = 0; j < availableFonts.length; j++) { + var fontName2 = availableFonts[j] + if (fontName2.startsWith(".")) + continue + + if (fontName2 === SettingsData.defaultMonoFontFamily) + continue + + var lowerName = fontName2.toLowerCase() + if (lowerName.includes("mono") || lowerName.includes( + "code") || lowerName.includes( + "console") || lowerName.includes( + "terminal") || lowerName.includes( + "courier") || lowerName.includes( + "dejavu sans mono") || lowerName.includes( + "jetbrains") || lowerName.includes( + "fira") || lowerName.includes( + "hack") || lowerName.includes( + "source code") || lowerName.includes( + "ubuntu mono") || lowerName.includes("cascadia")) { + var rootName2 = fontName2.replace( + / (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, + "").replace( + / (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, + "").trim() + if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") { + seenMonoFamilies.add(rootName2) + monoFamilies.push(rootName2) + } + } + } + cachedMonoFamilies = monoFonts.concat(monoFamilies.sort()) + } + + Component.onCompleted: { + if (!fontsEnumerated) { + enumerateFonts() + fontsEnumerated = true + } + } + + DankFlickable { + anchors.fill: parent + anchors.topMargin: Theme.spacingL + clip: true + contentHeight: mainColumn.height + contentWidth: width + + Column { + id: mainColumn + + width: parent.width + spacing: Theme.spacingXL + + // Theme Color + StyledRect { + width: parent.width + height: themeSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: themeSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "palette" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Theme Color" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Current Theme: " + (Theme.currentTheme === Theme.dynamic ? "Dynamic" : Theme.getThemeColors(Theme.currentThemeName).name) + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + if (Theme.currentTheme === Theme.dynamic) + return "Wallpaper-based dynamic colors" + + var descriptions = { + "blue": "Material blue inspired by modern interfaces", + "deepBlue": "Deep blue inspired by material 3", + "purple": "Rich purple tones for elegance", + "green": "Natural green for productivity", + "orange": "Energetic orange for creativity", + "red": "Bold red for impact", + "cyan": "Cool cyan for tranquility", + "pink": "Vibrant pink for expression", + "amber": "Warm amber for comfort", + "coral": "Soft coral for gentle warmth" + } + return descriptions[Theme.currentThemeName] || "Select a theme" + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + wrapMode: Text.WordWrap + width: Math.min(parent.width, 400) + horizontalAlignment: Text.AlignHCenter + } + } + + Column { + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + Row { + spacing: Theme.spacingM + + Repeater { + model: Theme.availableThemeNames.slice(0, 5) + + Rectangle { + property string themeName: modelData + width: 32 + height: 32 + radius: 16 + color: Theme.getThemeColors(themeName).primary + border.color: Theme.outline + border.width: (Theme.currentThemeName === themeName + && Theme.currentTheme !== Theme.dynamic) ? 2 : 1 + scale: (Theme.currentThemeName === themeName + && Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1 + + Rectangle { + width: nameText.contentWidth + Theme.spacingS * 2 + height: nameText.contentHeight + Theme.spacingXS * 2 + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + visible: mouseArea.containsMouse + + StyledText { + id: nameText + + text: Theme.getThemeColors(themeName).name + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.centerIn: parent + } + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Theme.switchTheme(themeName) + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } + + Row { + spacing: Theme.spacingM + + Repeater { + model: Theme.availableThemeNames.slice(5, 10) + + Rectangle { + property string themeName: modelData + + width: 32 + height: 32 + radius: 16 + color: Theme.getThemeColors(themeName).primary + border.color: Theme.outline + border.width: Theme.currentThemeName === themeName ? 2 : 1 + visible: true + scale: Theme.currentThemeName === themeName ? 1.1 : 1 + + Rectangle { + width: nameText2.contentWidth + Theme.spacingS * 2 + height: nameText2.contentHeight + Theme.spacingXS * 2 + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + visible: mouseArea2.containsMouse + + StyledText { + id: nameText2 + + text: Theme.getThemeColors(themeName).name + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.centerIn: parent + } + } + + MouseArea { + id: mouseArea2 + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Theme.switchTheme(themeName) + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } + + Item { + width: 1 + height: Theme.spacingM + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingL + + Rectangle { + width: 120 + height: 40 + radius: 20 + color: { + if (ToastService.wallpaperErrorStatus === "error" + || ToastService.wallpaperErrorStatus === "matugen_missing") + return Qt.rgba(Theme.error.r, + Theme.error.g, + Theme.error.b, 0.12) + else + return Qt.rgba(Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + } + border.color: { + if (ToastService.wallpaperErrorStatus === "error" + || ToastService.wallpaperErrorStatus === "matugen_missing") + return Qt.rgba(Theme.error.r, + Theme.error.g, + Theme.error.b, 0.5) + else if (Theme.currentThemeName === "dynamic") + return Theme.primary + else + return Theme.outline + } + border.width: (Theme.currentThemeName === "dynamic") ? 2 : 1 + scale: (Theme.currentThemeName === "dynamic") ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: { + if (ToastService.wallpaperErrorStatus === "error" + || ToastService.wallpaperErrorStatus + === "matugen_missing") + return "error" + else + return "palette" + } + size: 16 + color: { + if (ToastService.wallpaperErrorStatus === "error" + || ToastService.wallpaperErrorStatus + === "matugen_missing") + return Theme.error + else + return Theme.surfaceText + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (ToastService.wallpaperErrorStatus === "error") + return "Error" + else if (ToastService.wallpaperErrorStatus + === "matugen_missing") + return "No matugen" + else + return "Auto" + } + font.pixelSize: Theme.fontSizeMedium + color: { + if (ToastService.wallpaperErrorStatus === "error" + || ToastService.wallpaperErrorStatus + === "matugen_missing") + return Theme.error + else + return Theme.surfaceText + } + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: autoMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (ToastService.wallpaperErrorStatus === "matugen_missing") + ToastService.showError( + "matugen not found - install matugen package for dynamic theming") + else if (ToastService.wallpaperErrorStatus === "error") + ToastService.showError( + "Wallpaper processing failed - check wallpaper path") + else + Theme.switchTheme(Theme.dynamic) + } + } + + Rectangle { + width: autoTooltipText.contentWidth + Theme.spacingM * 2 + height: autoTooltipText.contentHeight + Theme.spacingS * 2 + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + visible: autoMouseArea.containsMouse + && (Theme.currentTheme !== Theme.dynamic + || ToastService.wallpaperErrorStatus === "error" + || ToastService.wallpaperErrorStatus + === "matugen_missing") + + StyledText { + id: autoTooltipText + + text: { + if (ToastService.wallpaperErrorStatus === "matugen_missing") + return "Install matugen package for dynamic themes" + else + return "Dynamic wallpaper-based colors" + } + font.pixelSize: Theme.fontSizeSmall + color: (ToastService.wallpaperErrorStatus === "error" + || ToastService.wallpaperErrorStatus + === "matugen_missing") ? Theme.error : Theme.surfaceText + anchors.centerIn: parent + wrapMode: Text.WordWrap + width: Math.min(implicitWidth, 250) + horizontalAlignment: Text.AlignHCenter + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + } + + Rectangle { + width: 120 + height: 40 + radius: 20 + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: (Theme.currentThemeName === "custom") ? Theme.primary : Theme.outline + border.width: (Theme.currentThemeName === "custom") ? 2 : 1 + scale: (Theme.currentThemeName === "custom") ? 1.1 : (customMouseArea.containsMouse ? 1.02 : 1) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "folder_open" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Custom" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: customMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + fileBrowserModal.open() + } + } + + Rectangle { + width: customTooltipText.contentWidth + Theme.spacingM * 2 + height: customTooltipText.contentHeight + Theme.spacingS * 2 + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + visible: customMouseArea.containsMouse + + StyledText { + id: customTooltipText + text: { + if (Theme.currentThemeName === "custom") + return SettingsData.customThemeFile || "Custom theme loaded" + else + return "Load custom theme from JSON file" + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.centerIn: parent + wrapMode: Text.WordWrap + width: Math.min(implicitWidth, 250) + horizontalAlignment: Text.AlignHCenter + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + } // Close Row + } + } + } + + // Transparency Settings + StyledRect { + width: parent.width + height: transparencySection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: transparencySection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "opacity" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Transparency Settings" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Top Bar Transparency" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + height: 24 + value: Math.round( + SettingsData.topBarTransparency * 100) + minimum: 0 + maximum: 100 + unit: "" + showValue: true + wheelEnabled: false + onSliderValueChanged: newValue => { + SettingsData.setTopBarTransparency( + newValue / 100) + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Top Bar Widget Transparency" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + height: 24 + value: Math.round( + SettingsData.topBarWidgetTransparency * 100) + minimum: 0 + maximum: 100 + unit: "" + showValue: true + wheelEnabled: false + onSliderValueChanged: newValue => { + SettingsData.setTopBarWidgetTransparency( + newValue / 100) + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Popup Transparency" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + height: 24 + value: Math.round( + SettingsData.popupTransparency * 100) + minimum: 0 + maximum: 100 + unit: "" + showValue: true + wheelEnabled: false + onSliderValueChanged: newValue => { + SettingsData.setPopupTransparency( + newValue / 100) + } + } + } + } + } + + // System Configuration Warning + Rectangle { + width: parent.width + height: warningText.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.warning.r, Theme.warning.g, + Theme.warning.b, 0.12) + border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, + Theme.warning.b, 0.3) + border.width: 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: "info" + size: Theme.iconSizeSmall + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: warningText + font.pixelSize: Theme.fontSizeSmall + text: "The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf and ~/.config/gtk-3.0|gtk-4.0)." + wrapMode: Text.WordWrap + width: parent.width - Theme.iconSizeSmall - Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + } + } + } + + // Icon Theme + StyledRect { + width: parent.width + height: iconThemeSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: iconThemeSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingXS + + DankIcon { + name: "image" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + DankDropdown { + width: parent.width - Theme.iconSize - Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + text: "Icon Theme" + description: "DankShell & System Icons\n(requires restart)" + currentValue: SettingsData.iconTheme + enableFuzzySearch: true + popupWidthOffset: 100 + maxPopupHeight: 236 + options: { + SettingsData.detectAvailableIconThemes() + return SettingsData.availableIconThemes + } + onValueChanged: value => { + SettingsData.setIconTheme(value) + if (Quickshell.env("QT_QPA_PLATFORMTHEME") != "gtk3" && + Quickshell.env("QT_QPA_PLATFORMTHEME") != "qt6ct" && + Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") != "qt6ct") { + ToastService.showError("Missing Environment Variables", "You need to set either:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct\nas environment variables, and then restart the shell.\n\nqt6ct requires qt6ct-kde to be installed.") + } + } + } + } + } + } + + // System App Theming + StyledRect { + width: parent.width + height: systemThemingSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + visible: Theme.matugenAvailable + + Column { + id: systemThemingSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "extension" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "System App Theming" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + border.color: Theme.primary + border.width: 1 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "folder" + size: 16 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Apply GTK Colors" + font.pixelSize: Theme.fontSizeMedium + color: Theme.primary + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Theme.applyGtkColors() + } + } + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + border.color: Theme.primary + border.width: 1 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "settings" + size: 16 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Apply Qt Colors" + font.pixelSize: Theme.fontSizeMedium + color: Theme.primary + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Theme.applyQtColors() + } + } + } + + StyledText { + text: `Generate baseline GTK3/4 or QT5/QT6 (requires qt6ct-kde) configurations to follow DMS colors. Only needed once.

        It is recommended to install Colloid GTK theme prior to applying GTK themes.` + textFormat: Text.RichText + linkColor: Theme.primary + onLinkActivated: url => Qt.openUrlExternally(url) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignHCenter + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + } + } + } + } + + FileBrowserModal { + id: fileBrowserModal + browserTitle: "Select Custom Theme" + filterExtensions: ["*.json"] + showHiddenFiles: true + + function selectCustomTheme() { + shouldBeVisible = true + } + + onFileSelected: function(filePath) { + // Save the custom theme file path and switch to custom theme + if (filePath.endsWith(".json")) { + SettingsData.setCustomThemeFile(filePath) + Theme.switchTheme("custom") + close() + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Settings/TimeTab.qml b/quickshell/.config/quickshell/Modules/Settings/TimeTab.qml new file mode 100644 index 0000000..bdf017a --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/TimeTab.qml @@ -0,0 +1,384 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +Item { + id: timeTab + + DankFlickable { + anchors.fill: parent + anchors.topMargin: Theme.spacingL + clip: true + contentHeight: mainColumn.height + contentWidth: width + + Column { + id: mainColumn + + width: parent.width + spacing: Theme.spacingXL + + // Time Format + StyledRect { + width: parent.width + height: timeSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: timeSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "schedule" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + - toggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "24-Hour Format" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Use 24-hour time format instead of 12-hour AM/PM" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + + DankToggle { + id: toggle + + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.use24HourClock + onToggled: checked => { + return SettingsData.setClockFormat( + checked) + } + } + } + } + } + + // Date Format Section + StyledRect { + width: parent.width + height: dateSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: dateSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "calendar_today" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Date Format" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankDropdown { + width: parent.width + height: 50 + text: "Top Bar Format" + description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d")) + currentValue: { + if (!SettingsData.clockDateFormat || SettingsData.clockDateFormat.length === 0) { + return "System Default" + } + // Find matching preset or show "Custom" + const presets = [{ + "format": "ddd d", + "label": "Day Date" + }, { + "format": "ddd MMM d", + "label": "Day Month Date" + }, { + "format": "MMM d", + "label": "Month Date" + }, { + "format": "M/d", + "label": "Numeric (M/D)" + }, { + "format": "d/M", + "label": "Numeric (D/M)" + }, { + "format": "ddd d MMM yyyy", + "label": "Full with Year" + }, { + "format": "yyyy-MM-dd", + "label": "ISO Date" + }, { + "format": "dddd, MMMM d", + "label": "Full Day & Month" + }] + const match = presets.find(p => { + return p.format + === SettingsData.clockDateFormat + }) + return match ? match.label : "Custom: " + SettingsData.clockDateFormat + } + options: ["System Default", "Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."] + onValueChanged: value => { + const formatMap = { + "System Default": "", + "Day Date": "ddd d", + "Day Month Date": "ddd MMM d", + "Month Date": "MMM d", + "Numeric (M/D)": "M/d", + "Numeric (D/M)": "d/M", + "Full with Year": "ddd d MMM yyyy", + "ISO Date": "yyyy-MM-dd", + "Full Day & Month": "dddd, MMMM d" + } + if (value === "Custom...") { + customFormatInput.visible = true + } else { + customFormatInput.visible = false + SettingsData.setClockDateFormat( + formatMap[value]) + } + } + } + + DankDropdown { + width: parent.width + height: 50 + text: "Lock Screen Format" + description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat)) + currentValue: { + if (!SettingsData.lockDateFormat || SettingsData.lockDateFormat.length === 0) { + return "System Default" + } + // Find matching preset or show "Custom" + const presets = [{ + "format": "ddd d", + "label": "Day Date" + }, { + "format": "ddd MMM d", + "label": "Day Month Date" + }, { + "format": "MMM d", + "label": "Month Date" + }, { + "format": "M/d", + "label": "Numeric (M/D)" + }, { + "format": "d/M", + "label": "Numeric (D/M)" + }, { + "format": "ddd d MMM yyyy", + "label": "Full with Year" + }, { + "format": "yyyy-MM-dd", + "label": "ISO Date" + }, { + "format": "dddd, MMMM d", + "label": "Full Day & Month" + }] + const match = presets.find(p => { + return p.format + === SettingsData.lockDateFormat + }) + return match ? match.label : "Custom: " + SettingsData.lockDateFormat + } + options: ["System Default", "Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."] + onValueChanged: value => { + const formatMap = { + "System Default": "", + "Day Date": "ddd d", + "Day Month Date": "ddd MMM d", + "Month Date": "MMM d", + "Numeric (M/D)": "M/d", + "Numeric (D/M)": "d/M", + "Full with Year": "ddd d MMM yyyy", + "ISO Date": "yyyy-MM-dd", + "Full Day & Month": "dddd, MMMM d" + } + if (value === "Custom...") { + customLockFormatInput.visible = true + } else { + customLockFormatInput.visible = false + SettingsData.setLockDateFormat( + formatMap[value]) + } + } + } + + DankTextField { + id: customFormatInput + + width: parent.width + visible: false + placeholderText: "Enter custom top bar format (e.g., ddd MMM d)" + text: SettingsData.clockDateFormat + onTextChanged: { + if (visible && text) + SettingsData.setClockDateFormat(text) + } + } + + DankTextField { + id: customLockFormatInput + + width: parent.width + visible: false + placeholderText: "Enter custom lock screen format (e.g., dddd, MMMM d)" + text: SettingsData.lockDateFormat + onTextChanged: { + if (visible && text) + SettingsData.setLockDateFormat(text) + } + } + + Rectangle { + width: parent.width + height: formatHelp.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.2) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.1) + border.width: 1 + + Column { + id: formatHelp + + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + StyledText { + text: "Format Legend" + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + } + + Row { + width: parent.width + spacing: Theme.spacingL + + Column { + width: (parent.width - Theme.spacingL) / 2 + spacing: 2 + + StyledText { + text: "• d - Day (1-31)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "• dd - Day (01-31)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "• ddd - Day name (Mon)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "• dddd - Day name (Monday)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "• M - Month (1-12)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + + Column { + width: (parent.width - Theme.spacingL) / 2 + spacing: 2 + + StyledText { + text: "• MM - Month (01-12)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "• MMM - Month (Jan)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "• MMMM - Month (January)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "• yy - Year (24)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "• yyyy - Year (2024)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Settings/TopBarTab.qml b/quickshell/.config/quickshell/Modules/Settings/TopBarTab.qml new file mode 100644 index 0000000..8921d49 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/TopBarTab.qml @@ -0,0 +1,1206 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: topBarTab + + property var baseWidgetDefinitions: [{ + "id": "launcherButton", + "text": "App Launcher", + "description": "Quick access to application launcher", + "icon": "apps", + "enabled": true + }, { + "id": "workspaceSwitcher", + "text": "Workspace Switcher", + "description": "Shows current workspace and allows switching", + "icon": "view_module", + "enabled": true + }, { + "id": "focusedWindow", + "text": "Focused Window", + "description": "Display currently focused application title", + "icon": "window", + "enabled": true + }, { + "id": "runningApps", + "text": "Running Apps", + "description": "Shows all running applications with focus indication", + "icon": "apps", + "enabled": true + }, { + "id": "clock", + "text": "Clock", + "description": "Current time and date display", + "icon": "schedule", + "enabled": true + }, { + "id": "weather", + "text": "Weather Widget", + "description": "Current weather conditions and temperature", + "icon": "wb_sunny", + "enabled": true + }, { + "id": "music", + "text": "Media Controls", + "description": "Control currently playing media", + "icon": "music_note", + "enabled": true + }, { + "id": "clipboard", + "text": "Clipboard Manager", + "description": "Access clipboard history", + "icon": "content_paste", + "enabled": true + }, { + "id": "cpuUsage", + "text": "CPU Usage", + "description": "CPU usage indicator", + "icon": "memory", + "enabled": DgopService.dgopAvailable, + "warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined + }, { + "id": "memUsage", + "text": "Memory Usage", + "description": "Memory usage indicator", + "icon": "storage", + "enabled": DgopService.dgopAvailable, + "warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined + }, { + "id": "cpuTemp", + "text": "CPU Temperature", + "description": "CPU temperature display", + "icon": "device_thermostat", + "enabled": DgopService.dgopAvailable, + "warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined + }, { + "id": "gpuTemp", + "text": "GPU Temperature", + "description": "GPU temperature display", + "icon": "auto_awesome_mosaic", + "warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : "This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.", + "enabled": DgopService.dgopAvailable + }, { + "id": "systemTray", + "text": "System Tray", + "description": "System notification area icons", + "icon": "notifications", + "enabled": true + }, { + "id": "privacyIndicator", + "text": "Privacy Indicator", + "description": "Shows when microphone, camera, or screen sharing is active", + "icon": "privacy_tip", + "enabled": true + }, { + "id": "controlCenterButton", + "text": "Control Center", + "description": "Access to system controls and settings", + "icon": "settings", + "enabled": true + }, { + "id": "notificationButton", + "text": "Notification Center", + "description": "Access to notifications and do not disturb", + "icon": "notifications", + "enabled": true + }, { + "id": "battery", + "text": "Battery", + "description": "Battery level and power management", + "icon": "battery_std", + "enabled": true + }, { + "id": "vpn", + "text": "VPN", + "description": "VPN status and quick connect", + "icon": "vpn_lock", + "enabled": true + }, { + "id": "idleInhibitor", + "text": "Idle Inhibitor", + "description": "Prevent screen timeout", + "icon": "motion_sensor_active", + "enabled": true + }, { + "id": "spacer", + "text": "Spacer", + "description": "Customizable empty space", + "icon": "more_horiz", + "enabled": true + }, { + "id": "separator", + "text": "Separator", + "description": "Visual divider between widgets", + "icon": "remove", + "enabled": true + }, + { + "id": "network_speed_monitor", + "text": "Network Speed Monitor", + "description": "Network download and upload speed display", + "icon": "network_check", + "warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined, + "enabled": DgopService.dgopAvailable + }, { + "id": "keyboard_layout_name", + "text": "Keyboard Layout Name", + "description": "Displays the active keyboard layout and allows switching", + "icon": "keyboard", + }, { + "id": "notepadButton", + "text": "Notepad", + "description": "Quick access to notepad", + "icon": "assignment", + "enabled": true + }] + property var defaultLeftWidgets: [{ + "id": "launcherButton", + "enabled": true + }, { + "id": "workspaceSwitcher", + "enabled": true + }, { + "id": "focusedWindow", + "enabled": true + }] + property var defaultCenterWidgets: [{ + "id": "music", + "enabled": true + }, { + "id": "clock", + "enabled": true + }, { + "id": "weather", + "enabled": true + }] + property var defaultRightWidgets: [{ + "id": "privacyIndicator", + "enabled": true + }, { + "id": "systemTray", + "enabled": true + }, { + "id": "clipboard", + "enabled": true + }, { + "id": "notificationButton", + "enabled": true + }, { + "id": "battery", + "enabled": true + }, { + "id": "controlCenterButton", + "enabled": true + }] + + function addWidgetToSection(widgetId, targetSection) { + var widgetObj = { + "id": widgetId, + "enabled": true + } + if (widgetId === "spacer") + widgetObj.size = 20 + if (widgetId === "gpuTemp") { + widgetObj.selectedGpuIndex = 0 + widgetObj.pciId = "" + } + if (widgetId === "controlCenterButton") { + widgetObj.showNetworkIcon = true + widgetObj.showBluetoothIcon = true + widgetObj.showAudioIcon = true + } + + var widgets = [] + if (targetSection === "left") { + widgets = SettingsData.topBarLeftWidgets.slice() + widgets.push(widgetObj) + SettingsData.setTopBarLeftWidgets(widgets) + } else if (targetSection === "center") { + widgets = SettingsData.topBarCenterWidgets.slice() + widgets.push(widgetObj) + SettingsData.setTopBarCenterWidgets(widgets) + } else if (targetSection === "right") { + widgets = SettingsData.topBarRightWidgets.slice() + widgets.push(widgetObj) + SettingsData.setTopBarRightWidgets(widgets) + } + } + + function removeWidgetFromSection(sectionId, widgetIndex) { + var widgets = [] + if (sectionId === "left") { + widgets = SettingsData.topBarLeftWidgets.slice() + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + widgets.splice(widgetIndex, 1) + } + SettingsData.setTopBarLeftWidgets(widgets) + } else if (sectionId === "center") { + widgets = SettingsData.topBarCenterWidgets.slice() + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + widgets.splice(widgetIndex, 1) + } + SettingsData.setTopBarCenterWidgets(widgets) + } else if (sectionId === "right") { + widgets = SettingsData.topBarRightWidgets.slice() + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + widgets.splice(widgetIndex, 1) + } + SettingsData.setTopBarRightWidgets(widgets) + } + } + + function handleItemEnabledChanged(sectionId, itemId, enabled) { + var widgets = [] + if (sectionId === "left") + widgets = SettingsData.topBarLeftWidgets.slice() + else if (sectionId === "center") + widgets = SettingsData.topBarCenterWidgets.slice() + else if (sectionId === "right") + widgets = SettingsData.topBarRightWidgets.slice() + for (var i = 0; i < widgets.length; i++) { + var widget = widgets[i] + var widgetId = typeof widget === "string" ? widget : widget.id + if (widgetId === itemId) { + if (typeof widget === "string") { + widgets[i] = { + "id": widget, + "enabled": enabled + } + } else { + var newWidget = { + "id": widget.id, + "enabled": enabled + } + if (widget.size !== undefined) + newWidget.size = widget.size + if (widget.selectedGpuIndex !== undefined) + newWidget.selectedGpuIndex = widget.selectedGpuIndex + else if (widget.id === "gpuTemp") + newWidget.selectedGpuIndex = 0 + if (widget.pciId !== undefined) + newWidget.pciId = widget.pciId + else if (widget.id === "gpuTemp") + newWidget.pciId = "" + if (widget.id === "controlCenterButton") { + newWidget.showNetworkIcon = widget.showNetworkIcon !== undefined ? widget.showNetworkIcon : true + newWidget.showBluetoothIcon = widget.showBluetoothIcon !== undefined ? widget.showBluetoothIcon : true + newWidget.showAudioIcon = widget.showAudioIcon !== undefined ? widget.showAudioIcon : true + } + widgets[i] = newWidget + } + break + } + } + if (sectionId === "left") + SettingsData.setTopBarLeftWidgets(widgets) + else if (sectionId === "center") + SettingsData.setTopBarCenterWidgets(widgets) + else if (sectionId === "right") + SettingsData.setTopBarRightWidgets(widgets) + } + + function handleItemOrderChanged(sectionId, newOrder) { + if (sectionId === "left") + SettingsData.setTopBarLeftWidgets(newOrder) + else if (sectionId === "center") + SettingsData.setTopBarCenterWidgets(newOrder) + else if (sectionId === "right") + SettingsData.setTopBarRightWidgets(newOrder) + } + + function handleSpacerSizeChanged(sectionId, widgetIndex, newSize) { + var widgets = [] + if (sectionId === "left") + widgets = SettingsData.topBarLeftWidgets.slice() + else if (sectionId === "center") + widgets = SettingsData.topBarCenterWidgets.slice() + else if (sectionId === "right") + widgets = SettingsData.topBarRightWidgets.slice() + + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + var widget = widgets[widgetIndex] + var widgetId = typeof widget === "string" ? widget : widget.id + if (widgetId === "spacer") { + if (typeof widget === "string") { + widgets[widgetIndex] = { + "id": widget, + "enabled": true, + "size": newSize + } + } else { + var newWidget = { + "id": widget.id, + "enabled": widget.enabled, + "size": newSize + } + if (widget.selectedGpuIndex !== undefined) + newWidget.selectedGpuIndex = widget.selectedGpuIndex + if (widget.pciId !== undefined) + newWidget.pciId = widget.pciId + if (widget.id === "controlCenterButton") { + newWidget.showNetworkIcon = widget.showNetworkIcon !== undefined ? widget.showNetworkIcon : true + newWidget.showBluetoothIcon = widget.showBluetoothIcon !== undefined ? widget.showBluetoothIcon : true + newWidget.showAudioIcon = widget.showAudioIcon !== undefined ? widget.showAudioIcon : true + } + widgets[widgetIndex] = newWidget + } + } + } + + if (sectionId === "left") + SettingsData.setTopBarLeftWidgets(widgets) + else if (sectionId === "center") + SettingsData.setTopBarCenterWidgets(widgets) + else if (sectionId === "right") + SettingsData.setTopBarRightWidgets(widgets) + } + + function handleGpuSelectionChanged(sectionId, widgetIndex, selectedGpuIndex) { + var widgets = [] + if (sectionId === "left") + widgets = SettingsData.topBarLeftWidgets.slice() + else if (sectionId === "center") + widgets = SettingsData.topBarCenterWidgets.slice() + else if (sectionId === "right") + widgets = SettingsData.topBarRightWidgets.slice() + + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + var widget = widgets[widgetIndex] + if (typeof widget === "string") { + widgets[widgetIndex] = { + "id": widget, + "enabled": true, + "selectedGpuIndex": selectedGpuIndex, + "pciId": DgopService.availableGpus + && DgopService.availableGpus.length + > selectedGpuIndex ? DgopService.availableGpus[selectedGpuIndex].pciId : "" + } + } else { + var newWidget = { + "id": widget.id, + "enabled": widget.enabled, + "selectedGpuIndex": selectedGpuIndex, + "pciId": DgopService.availableGpus + && DgopService.availableGpus.length + > selectedGpuIndex ? DgopService.availableGpus[selectedGpuIndex].pciId : "" + } + if (widget.size !== undefined) + newWidget.size = widget.size + widgets[widgetIndex] = newWidget + } + } + + if (sectionId === "left") + SettingsData.setTopBarLeftWidgets(widgets) + else if (sectionId === "center") + SettingsData.setTopBarCenterWidgets(widgets) + else if (sectionId === "right") + SettingsData.setTopBarRightWidgets(widgets) + } + + function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) { + // Control Center settings are global, not per-widget instance + if (settingName === "showNetworkIcon") { + SettingsData.setControlCenterShowNetworkIcon(value) + } else if (settingName === "showBluetoothIcon") { + SettingsData.setControlCenterShowBluetoothIcon(value) + } else if (settingName === "showAudioIcon") { + SettingsData.setControlCenterShowAudioIcon(value) + } + } + + function getItemsForSection(sectionId) { + var widgets = [] + var widgetData = [] + if (sectionId === "left") + widgetData = SettingsData.topBarLeftWidgets || [] + else if (sectionId === "center") + widgetData = SettingsData.topBarCenterWidgets || [] + else if (sectionId === "right") + widgetData = SettingsData.topBarRightWidgets || [] + widgetData.forEach(widget => { + var widgetId = typeof widget === "string" ? widget : widget.id + var widgetEnabled = typeof widget + === "string" ? true : widget.enabled + var widgetSize = typeof widget === "string" ? undefined : widget.size + var widgetSelectedGpuIndex = typeof widget + === "string" ? undefined : widget.selectedGpuIndex + var widgetPciId = typeof widget + === "string" ? undefined : widget.pciId + var widgetShowNetworkIcon = typeof widget === "string" ? undefined : widget.showNetworkIcon + var widgetShowBluetoothIcon = typeof widget === "string" ? undefined : widget.showBluetoothIcon + var widgetShowAudioIcon = typeof widget === "string" ? undefined : widget.showAudioIcon + var widgetDef = baseWidgetDefinitions.find(w => { + return w.id === widgetId + }) + if (widgetDef) { + var item = Object.assign({}, widgetDef) + item.enabled = widgetEnabled + if (widgetSize !== undefined) + item.size = widgetSize + if (widgetSelectedGpuIndex !== undefined) + item.selectedGpuIndex = widgetSelectedGpuIndex + if (widgetPciId !== undefined) + item.pciId = widgetPciId + if (widgetShowNetworkIcon !== undefined) + item.showNetworkIcon = widgetShowNetworkIcon + if (widgetShowBluetoothIcon !== undefined) + item.showBluetoothIcon = widgetShowBluetoothIcon + if (widgetShowAudioIcon !== undefined) + item.showAudioIcon = widgetShowAudioIcon + + widgets.push(item) + } + }) + return widgets + } + + Component.onCompleted: { + // Only set defaults if widgets have never been configured (null/undefined, not empty array) + if (!SettingsData.topBarLeftWidgets) + SettingsData.setTopBarLeftWidgets(defaultLeftWidgets) + + if (!SettingsData.topBarCenterWidgets) + SettingsData.setTopBarCenterWidgets(defaultCenterWidgets) + + if (!SettingsData.topBarRightWidgets) + SettingsData.setTopBarRightWidgets(defaultRightWidgets) + const sections = ["left", "center", "right"] + sections.forEach(sectionId => { + var widgets = [] + if (sectionId === "left") + widgets = SettingsData.topBarLeftWidgets.slice() + else if (sectionId === "center") + widgets = SettingsData.topBarCenterWidgets.slice() + else if (sectionId === "right") + widgets = SettingsData.topBarRightWidgets.slice() + var updated = false + for (var i = 0; i < widgets.length; i++) { + var widget = widgets[i] + if (typeof widget === "object" + && widget.id === "spacer" + && !widget.size) { + widgets[i] = Object.assign({}, widget, { + "size": 20 + }) + updated = true + } + } + if (updated) { + if (sectionId === "left") + SettingsData.setTopBarLeftWidgets(widgets) + else if (sectionId === "center") + SettingsData.setTopBarCenterWidgets(widgets) + else if (sectionId === "right") + SettingsData.setTopBarRightWidgets(widgets) + } + }) + } + + DankFlickable { + anchors.fill: parent + anchors.topMargin: Theme.spacingL + anchors.bottomMargin: Theme.spacingS + clip: true + contentHeight: mainColumn.height + contentWidth: width + + Column { + id: mainColumn + width: parent.width + spacing: Theme.spacingXL + + // TopBar Auto-hide Section + StyledRect { + width: parent.width + height: topBarAutoHideSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: topBarAutoHideSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "visibility_off" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + - autoHideToggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Auto-hide" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Automatically hide the top bar to expand screen real estate" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + + DankToggle { + id: autoHideToggle + + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.topBarAutoHide + onToggled: toggled => { + return SettingsData.setTopBarAutoHide( + toggled) + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + } + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "visibility" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + - visibilityToggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Manual Show/Hide" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Toggle top bar visibility manually (can be controlled via IPC)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + + DankToggle { + id: visibilityToggle + + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.topBarVisible + onToggled: toggled => { + return SettingsData.setTopBarVisible( + toggled) + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: CompositorService.isNiri + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: CompositorService.isNiri + + DankIcon { + name: "fullscreen" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + - overviewToggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Show on Overview" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Always show the top bar when niri's overview is open" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + + DankToggle { + id: overviewToggle + + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.topBarOpenOnOverview + onToggled: toggled => { + return SettingsData.setTopBarOpenOnOverview( + toggled) + } + } + } + } + } + + + // Spacing + StyledRect { + width: parent.width + height: topBarSpacingSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: topBarSpacingSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "space_bar" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Spacing" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Top/Left/Right Gaps (0 = edge-to-edge)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + height: 24 + value: SettingsData.topBarSpacing + minimum: 0 + maximum: 32 + unit: "" + showValue: true + wheelEnabled: false + onSliderValueChanged: newValue => { + SettingsData.setTopBarSpacing( + newValue) + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Bottom Gap (Exclusive Zone)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + height: 24 + value: SettingsData.topBarBottomGap + minimum: -100 + maximum: 100 + unit: "" + showValue: true + wheelEnabled: false + onSliderValueChanged: newValue => { + SettingsData.setTopBarBottomGap( + newValue) + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Size" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + height: 24 + value: SettingsData.topBarInnerPadding + minimum: 0 + maximum: 24 + unit: "" + showValue: true + wheelEnabled: false + onSliderValueChanged: newValue => { + SettingsData.setTopBarInnerPadding( + newValue) + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Corner Radius (0 = square corners)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + height: 24 + value: SettingsData.cornerRadius + minimum: 0 + maximum: 32 + unit: "" + showValue: true + wheelEnabled: false + onSliderValueChanged: newValue => { + SettingsData.setCornerRadius( + newValue) + } + } + } + + DankToggle { + width: parent.width + text: "Square Corners" + description: "Removes rounded corners from bar container." + checked: SettingsData.topBarSquareCorners + onToggled: checked => { + SettingsData.setTopBarSquareCorners( + checked) + } + } + + DankToggle { + width: parent.width + text: "No Background" + description: "Remove widget backgrounds for a minimal look with tighter spacing." + checked: SettingsData.topBarNoBackground + onToggled: checked => { + SettingsData.setTopBarNoBackground( + checked) + } + } + } + } + + // Widget Management Section + StyledRect { + width: parent.width + height: widgetManagementSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: widgetManagementSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + RowLayout { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + id: widgetIcon + name: "widgets" + size: Theme.iconSize + color: Theme.primary + Layout.alignment: Qt.AlignVCenter + } + + StyledText { + id: widgetTitle + text: "Widget Management" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + Layout.alignment: Qt.AlignVCenter + } + + Item { + height: 1 + Layout.fillWidth: true + } + + Rectangle { + id: resetButton + width: 80 + height: 28 + radius: Theme.cornerRadius + color: resetArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariant + Layout.alignment: Qt.AlignVCenter + border.width: 1 + border.color: resetArea.containsMouse ? Theme.outline : Qt.rgba( + Theme.outline.r, + Theme.outline.g, + Theme.outline.b, + 0.5) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "refresh" + size: 14 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Reset" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: resetArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + SettingsData.setTopBarLeftWidgets( + defaultLeftWidgets) + SettingsData.setTopBarCenterWidgets( + defaultCenterWidgets) + SettingsData.setTopBarRightWidgets( + defaultRightWidgets) + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + StyledText { + width: parent.width + text: "Drag widgets to reorder within sections. Use the eye icon to hide/show widgets (maintains spacing), or X to remove them completely." + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingL + + // Left Section + StyledRect { + width: parent.width + height: leftSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + WidgetsTabSection { + id: leftSection + anchors.fill: parent + anchors.margins: Theme.spacingL + title: "Left Section" + titleIcon: "format_align_left" + sectionId: "left" + allWidgets: topBarTab.baseWidgetDefinitions + items: topBarTab.getItemsForSection("left") + onItemEnabledChanged: (sectionId, itemId, enabled) => { + topBarTab.handleItemEnabledChanged( + sectionId, + itemId, enabled) + } + onItemOrderChanged: newOrder => { + topBarTab.handleItemOrderChanged( + "left", newOrder) + } + onAddWidget: sectionId => { + widgetSelectionPopup.allWidgets + = topBarTab.baseWidgetDefinitions + widgetSelectionPopup.targetSection = sectionId + widgetSelectionPopup.safeOpen() + } + onRemoveWidget: (sectionId, widgetIndex) => { + topBarTab.removeWidgetFromSection( + sectionId, widgetIndex) + } + onSpacerSizeChanged: (sectionId, widgetIndex, newSize) => { + topBarTab.handleSpacerSizeChanged( + sectionId, widgetIndex, newSize) + } + onCompactModeChanged: (widgetId, value) => { + if (widgetId === "clock") { + SettingsData.setClockCompactMode( + value) + } else if (widgetId === "music") { + SettingsData.setMediaSize( + value) + } else if (widgetId === "focusedWindow") { + SettingsData.setFocusedWindowCompactMode( + value) + } else if (widgetId === "runningApps") { + SettingsData.setRunningAppsCompactMode( + value) + } + } + onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => { + handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) + } + onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => { + topBarTab.handleGpuSelectionChanged( + sectionId, widgetIndex, + selectedIndex) + } + } + } + + // Center Section + StyledRect { + width: parent.width + height: centerSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + WidgetsTabSection { + id: centerSection + anchors.fill: parent + anchors.margins: Theme.spacingL + title: "Center Section" + titleIcon: "format_align_center" + sectionId: "center" + allWidgets: topBarTab.baseWidgetDefinitions + items: topBarTab.getItemsForSection("center") + onItemEnabledChanged: (sectionId, itemId, enabled) => { + topBarTab.handleItemEnabledChanged( + sectionId, + itemId, enabled) + } + onItemOrderChanged: newOrder => { + topBarTab.handleItemOrderChanged( + "center", newOrder) + } + onAddWidget: sectionId => { + widgetSelectionPopup.allWidgets + = topBarTab.baseWidgetDefinitions + widgetSelectionPopup.targetSection = sectionId + widgetSelectionPopup.safeOpen() + } + onRemoveWidget: (sectionId, widgetIndex) => { + topBarTab.removeWidgetFromSection( + sectionId, widgetIndex) + } + onSpacerSizeChanged: (sectionId, widgetIndex, newSize) => { + topBarTab.handleSpacerSizeChanged( + sectionId, widgetIndex, newSize) + } + onCompactModeChanged: (widgetId, value) => { + if (widgetId === "clock") { + SettingsData.setClockCompactMode( + value) + } else if (widgetId === "music") { + SettingsData.setMediaSize( + value) + } else if (widgetId === "focusedWindow") { + SettingsData.setFocusedWindowCompactMode( + value) + } else if (widgetId === "runningApps") { + SettingsData.setRunningAppsCompactMode( + value) + } + } + onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => { + handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) + } + onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => { + topBarTab.handleGpuSelectionChanged( + sectionId, widgetIndex, + selectedIndex) + } + } + } + + // Right Section + StyledRect { + width: parent.width + height: rightSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + WidgetsTabSection { + id: rightSection + anchors.fill: parent + anchors.margins: Theme.spacingL + title: "Right Section" + titleIcon: "format_align_right" + sectionId: "right" + allWidgets: topBarTab.baseWidgetDefinitions + items: topBarTab.getItemsForSection("right") + onItemEnabledChanged: (sectionId, itemId, enabled) => { + topBarTab.handleItemEnabledChanged( + sectionId, + itemId, enabled) + } + onItemOrderChanged: newOrder => { + topBarTab.handleItemOrderChanged( + "right", newOrder) + } + onAddWidget: sectionId => { + widgetSelectionPopup.allWidgets + = topBarTab.baseWidgetDefinitions + widgetSelectionPopup.targetSection = sectionId + widgetSelectionPopup.safeOpen() + } + onRemoveWidget: (sectionId, widgetIndex) => { + topBarTab.removeWidgetFromSection( + sectionId, widgetIndex) + } + onSpacerSizeChanged: (sectionId, widgetIndex, newSize) => { + topBarTab.handleSpacerSizeChanged( + sectionId, widgetIndex, newSize) + } + onCompactModeChanged: (widgetId, value) => { + if (widgetId === "clock") { + SettingsData.setClockCompactMode( + value) + } else if (widgetId === "music") { + SettingsData.setMediaSize( + value) + } else if (widgetId === "focusedWindow") { + SettingsData.setFocusedWindowCompactMode( + value) + } else if (widgetId === "runningApps") { + SettingsData.setRunningAppsCompactMode( + value) + } + } + onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => { + handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) + } + onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => { + topBarTab.handleGpuSelectionChanged( + sectionId, widgetIndex, + selectedIndex) + } + } + } + } + } + } + + WidgetSelectionPopup { + id: widgetSelectionPopup + + anchors.centerIn: parent + onWidgetSelected: (widgetId, targetSection) => { + topBarTab.addWidgetToSection(widgetId, + targetSection) + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Settings/WeatherTab.qml b/quickshell/.config/quickshell/Modules/Settings/WeatherTab.qml new file mode 100644 index 0000000..eb9c2f2 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/WeatherTab.qml @@ -0,0 +1,308 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +Item { + id: weatherTab + + DankFlickable { + anchors.fill: parent + anchors.topMargin: Theme.spacingL + clip: true + contentHeight: mainColumn.height + contentWidth: width + + Column { + id: mainColumn + + width: parent.width + spacing: Theme.spacingXL + + // Enable Weather + StyledRect { + width: parent.width + height: enableWeatherSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: enableWeatherSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "cloud" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + - enableToggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Enable Weather" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Show weather information in top bar and control center" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + + DankToggle { + id: enableToggle + + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.weatherEnabled + onToggled: checked => { + return SettingsData.setWeatherEnabled( + checked) + } + } + } + } + } + + // Temperature Unit + StyledRect { + width: parent.width + height: temperatureSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + visible: SettingsData.weatherEnabled + opacity: visible ? 1 : 0 + + Column { + id: temperatureSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "thermostat" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + - temperatureToggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Use Fahrenheit" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Use Fahrenheit instead of Celsius for temperature" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + + DankToggle { + id: temperatureToggle + + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.useFahrenheit + onToggled: checked => { + return SettingsData.setTemperatureUnit( + checked) + } + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + // Location Settings + StyledRect { + width: parent.width + height: locationSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + visible: SettingsData.weatherEnabled + opacity: visible ? 1 : 0 + + Column { + id: locationSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "location_on" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + - autoLocationToggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Auto Location" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: "Automatically determine your location using your IP address" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + + DankToggle { + id: autoLocationToggle + + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.useAutoLocation + onToggled: checked => { + return SettingsData.setAutoLocation( + checked) + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: !SettingsData.useAutoLocation + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + } + + StyledText { + text: "Custom Location" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + Row { + width: parent.width + spacing: Theme.spacingM + + DankTextField { + id: latitudeInput + width: (parent.width - Theme.spacingM) / 2 + height: 48 + placeholderText: "Latitude" + text: SettingsData.weatherCoordinates ? SettingsData.weatherCoordinates.split(',')[0] : "" + backgroundColor: Theme.surfaceVariant + normalBorderColor: Theme.primarySelected + focusedBorderColor: Theme.primary + onTextEdited: { + if (text && longitudeInput.text) { + const coords = text + "," + longitudeInput.text + const displayName = `${text}, ${longitudeInput.text}` + SettingsData.setWeatherLocation(displayName, coords) + } + } + } + + DankTextField { + id: longitudeInput + width: (parent.width - Theme.spacingM) / 2 + height: 48 + placeholderText: "Longitude" + text: SettingsData.weatherCoordinates ? SettingsData.weatherCoordinates.split(',')[1] : "" + backgroundColor: Theme.surfaceVariant + normalBorderColor: Theme.primarySelected + focusedBorderColor: Theme.primary + onTextEdited: { + if (text && latitudeInput.text) { + const coords = latitudeInput.text + "," + text + const displayName = `${latitudeInput.text}, ${text}` + SettingsData.setWeatherLocation(displayName, coords) + } + } + } + } + + DankLocationSearch { + width: parent.width + currentLocation: SettingsData.weatherLocation + placeholderText: "New York, NY" + onLocationSelected: (displayName, coordinates) => { + SettingsData.setWeatherLocation( + displayName, + coordinates) + } + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Settings/WidgetSelectionPopup.qml b/quickshell/.config/quickshell/Modules/Settings/WidgetSelectionPopup.qml new file mode 100644 index 0000000..2b3b4d7 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/WidgetSelectionPopup.qml @@ -0,0 +1,310 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +Popup { + id: root + + property var allWidgets: [] + property string targetSection: "" + property bool isOpening: false + property string searchQuery: "" + property var filteredWidgets: [] + property int selectedIndex: -1 + property bool keyboardNavigationActive: false + + signal widgetSelected(string widgetId, string targetSection) + + function updateFilteredWidgets() { + if (!searchQuery || searchQuery.length === 0) { + filteredWidgets = allWidgets.slice() + return + } + + var filtered = [] + var query = searchQuery.toLowerCase() + + for (var i = 0; i < allWidgets.length; i++) { + var widget = allWidgets[i] + var text = widget.text ? widget.text.toLowerCase() : "" + var description = widget.description ? widget.description.toLowerCase() : "" + var id = widget.id ? widget.id.toLowerCase() : "" + + if (text.indexOf(query) !== -1 || + description.indexOf(query) !== -1 || + id.indexOf(query) !== -1) { + filtered.push(widget) + } + } + + filteredWidgets = filtered + selectedIndex = -1 + keyboardNavigationActive = false + } + + onAllWidgetsChanged: { + updateFilteredWidgets() + } + + function selectNext() { + if (filteredWidgets.length === 0) return + keyboardNavigationActive = true + selectedIndex = Math.min(selectedIndex + 1, filteredWidgets.length - 1) + } + + function selectPrevious() { + if (filteredWidgets.length === 0) return + keyboardNavigationActive = true + selectedIndex = Math.max(selectedIndex - 1, -1) + if (selectedIndex === -1) { + keyboardNavigationActive = false + } + } + + function selectWidget() { + if (selectedIndex >= 0 && selectedIndex < filteredWidgets.length) { + var widget = filteredWidgets[selectedIndex] + root.widgetSelected(widget.id, root.targetSection) + root.close() + } + } + + function safeOpen() { + if (!isOpening && !visible) { + isOpening = true + open() + } + } + + width: 500 + height: 550 + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + onOpened: { + isOpening = false + Qt.callLater(() => { + searchField.forceActiveFocus() + }) + } + onClosed: { + isOpening = false + allWidgets = [] + targetSection = "" + searchQuery = "" + filteredWidgets = [] + selectedIndex = -1 + keyboardNavigationActive = false + } + + background: Rectangle { + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, + Theme.surfaceContainer.b, 1) + border.color: Theme.primarySelected + border.width: 1 + radius: Theme.cornerRadius + } + + contentItem: Item { + anchors.fill: parent + focus: true + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + root.close() + event.accepted = true + } else if (event.key === Qt.Key_Down) { + root.selectNext() + event.accepted = true + } else if (event.key === Qt.Key_Up) { + root.selectPrevious() + event.accepted = true + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (root.keyboardNavigationActive) { + root.selectWidget() + } else if (root.filteredWidgets.length > 0) { + var firstWidget = root.filteredWidgets[0] + root.widgetSelected(firstWidget.id, root.targetSection) + root.close() + } + event.accepted = true + } else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) { + searchField.forceActiveFocus() + searchField.insertText(event.text) + event.accepted = true + } + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 2 + iconColor: Theme.outline + anchors.top: parent.top + anchors.topMargin: Theme.spacingM + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + onClicked: root.close() + } + + Column { + id: contentColumn + + spacing: Theme.spacingM + anchors.fill: parent + anchors.margins: Theme.spacingL + anchors.topMargin: Theme.spacingL + 30 // Space for close button + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "add_circle" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Add Widget to " + root.targetSection + " Section" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: "Select a widget to add to the " + root.targetSection.toLowerCase( + ) + " section of the top bar. You can add multiple instances of the same widget if needed." + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + width: parent.width + wrapMode: Text.WordWrap + } + + DankTextField { + id: searchField + width: parent.width + height: 48 + cornerRadius: Theme.cornerRadius + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + focusedBorderColor: Theme.primary + leftIconName: "search" + leftIconSize: Theme.iconSize - 2 + leftIconColor: Theme.outline + leftIconFocusedColor: Theme.primary + showClearButton: true + textColor: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + placeholderText: "Search widgets..." + text: root.searchQuery + ignoreLeftRightKeys: true + keyForwardTargets: [root.contentItem] + onTextEdited: { + root.searchQuery = text + updateFilteredWidgets() + } + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + root.close() + event.accepted = true + } else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || + ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) { + event.accepted = false + } + } + } + + DankListView { + id: widgetList + + width: parent.width + height: parent.height - y + spacing: Theme.spacingS + model: root.filteredWidgets + clip: true + + delegate: Rectangle { + width: widgetList.width + height: 60 + radius: Theme.cornerRadius + property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex + color: isSelected ? Theme.primarySelected : + widgetArea.containsMouse ? Theme.primaryHover : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.3) + border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: isSelected ? 2 : 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: modelData.icon + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - Theme.iconSize - Theme.spacingM * 3 + + StyledText { + text: modelData.text + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: modelData.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + elide: Text.ElideRight + width: parent.width + wrapMode: Text.WordWrap + } + } + + DankIcon { + name: "add" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: widgetArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.widgetSelected(modelData.id, + root.targetSection) + root.close() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Settings/WidgetTweaksTab.qml b/quickshell/.config/quickshell/Modules/Settings/WidgetTweaksTab.qml new file mode 100644 index 0000000..61ebe10 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/WidgetTweaksTab.qml @@ -0,0 +1,510 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: widgetTweaksTab + + DankFlickable { + anchors.fill: parent + anchors.topMargin: Theme.spacingL + clip: true + contentHeight: mainColumn.height + contentWidth: width + + Column { + id: mainColumn + width: parent.width + spacing: Theme.spacingXL + + // Launcher Button Section + StyledRect { + width: parent.width + height: launcherButtonSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: launcherButtonSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "apps" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Launcher Button" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + width: parent.width + text: "Use OS Logo" + description: "Display operating system logo instead of apps icon" + checked: SettingsData.useOSLogo + onToggled: checked => { + return SettingsData.setUseOSLogo(checked) + } + } + + Row { + width: parent.width - Theme.spacingL + spacing: Theme.spacingL + visible: SettingsData.useOSLogo + opacity: visible ? 1 : 0 + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL + + Column { + width: 120 + spacing: Theme.spacingS + + StyledText { + text: "Color Override" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankTextField { + width: 100 + height: 28 + placeholderText: "#ffffff" + text: SettingsData.osLogoColorOverride + maximumLength: 7 + font.pixelSize: Theme.fontSizeSmall + topPadding: Theme.spacingXS + bottomPadding: Theme.spacingXS + onEditingFinished: { + var color = text.trim() + if (color === "" + || /^#[0-9A-Fa-f]{6}$/.test(color)) + SettingsData.setOSLogoColorOverride( + color) + else + text = SettingsData.osLogoColorOverride + } + } + } + + Column { + width: 120 + spacing: Theme.spacingS + + StyledText { + text: "Brightness" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: 100 + height: 20 + minimum: 0 + maximum: 100 + value: Math.round( + SettingsData.osLogoBrightness * 100) + unit: "%" + showValue: true + wheelEnabled: false + onSliderValueChanged: newValue => { + SettingsData.setOSLogoBrightness( + newValue / 100) + } + } + } + + Column { + width: 120 + spacing: Theme.spacingS + + StyledText { + text: "Contrast" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: 100 + height: 20 + minimum: 0 + maximum: 200 + value: Math.round( + SettingsData.osLogoContrast * 100) + unit: "%" + showValue: true + wheelEnabled: false + onSliderValueChanged: newValue => { + SettingsData.setOSLogoContrast( + newValue / 100) + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } + + StyledRect { + width: parent.width + height: workspaceSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: workspaceSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "view_module" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Workspace Settings" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + width: parent.width + text: "Workspace Index Numbers" + description: "Show workspace index numbers in the top bar workspace switcher" + checked: SettingsData.showWorkspaceIndex + onToggled: checked => { + return SettingsData.setShowWorkspaceIndex( + checked) + } + } + + DankToggle { + width: parent.width + text: "Workspace Padding" + description: "Always show a minimum of 3 workspaces, even if fewer are available" + checked: SettingsData.showWorkspacePadding + onToggled: checked => { + return SettingsData.setShowWorkspacePadding( + checked) + } + } + + DankToggle { + width: parent.width + text: "Show Workspace Apps" + description: "Display application icons in workspace indicators" + checked: SettingsData.showWorkspaceApps + onToggled: checked => { + return SettingsData.setShowWorkspaceApps( + checked) + } + } + + Row { + width: parent.width - Theme.spacingL + spacing: Theme.spacingL + visible: SettingsData.showWorkspaceApps + opacity: visible ? 1 : 0 + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL + + Column { + width: 120 + spacing: Theme.spacingS + + StyledText { + text: "Max apps to show" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankTextField { + width: 100 + height: 28 + placeholderText: "#ffffff" + text: SettingsData.maxWorkspaceIcons + maximumLength: 7 + font.pixelSize: Theme.fontSizeSmall + topPadding: Theme.spacingXS + bottomPadding: Theme.spacingXS + onEditingFinished: { + SettingsData.setMaxWorkspaceIcons(parseInt(text, 10)) + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + DankToggle { + width: parent.width + text: "Per-Monitor Workspaces" + description: "Show only workspaces belonging to each specific monitor." + checked: SettingsData.workspacesPerMonitor + onToggled: checked => { + return SettingsData.setWorkspacesPerMonitor(checked); + } + } + } + } + + StyledRect { + width: parent.width + height: runningAppsSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: runningAppsSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "apps" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Running Apps Settings" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + width: parent.width + text: "Running Apps Only In Current Workspace" + description: "Show only apps running in current workspace" + checked: SettingsData.runningAppsCurrentWorkspace + onToggled: checked => { + return SettingsData.setRunningAppsCurrentWorkspace( + checked) + } + } + } + } + + StyledRect { + width: parent.width + height: workspaceIconsSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + visible: SettingsData.hasNamedWorkspaces() + + Column { + id: workspaceIconsSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "label" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Named Workspace Icons" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + width: parent.width + text: "Configure icons for named workspaces. Icons take priority over numbers when both are enabled." + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + wrapMode: Text.WordWrap + } + + Repeater { + model: SettingsData.getNamedWorkspaces() + + Rectangle { + width: parent.width + height: workspaceIconRow.implicitHeight + Theme.spacingM + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, + Theme.surfaceContainer.g, + Theme.surfaceContainer.b, 0.5) + border.color: Qt.rgba(Theme.outline.r, + Theme.outline.g, + Theme.outline.b, 0.3) + border.width: 1 + + Row { + id: workspaceIconRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingM + + StyledText { + text: "\"" + modelData + "\"" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: 150 + elide: Text.ElideRight + } + + DankIconPicker { + id: iconPicker + anchors.verticalCenter: parent.verticalCenter + + Component.onCompleted: { + var iconData = SettingsData.getWorkspaceNameIcon( + modelData) + if (iconData) { + setIcon(iconData.value, + iconData.type) + } + } + + onIconSelected: (iconName, iconType) => { + SettingsData.setWorkspaceNameIcon( + modelData, { + "type": iconType, + "value": iconName + }) + setIcon(iconName, + iconType) + } + + Connections { + target: SettingsData + function onWorkspaceIconsUpdated() { + var iconData = SettingsData.getWorkspaceNameIcon( + modelData) + if (iconData) { + iconPicker.setIcon( + iconData.value, + iconData.type) + } else { + iconPicker.setIcon("", "icon") + } + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: Theme.cornerRadius + color: clearMouseArea.containsMouse ? Theme.errorHover : Theme.surfaceContainer + border.color: clearMouseArea.containsMouse ? Theme.error : Theme.outline + border.width: 1 + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "close" + size: 16 + color: clearMouseArea.containsMouse ? Theme.error : Theme.outline + anchors.centerIn: parent + } + + MouseArea { + id: clearMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + SettingsData.removeWorkspaceNameIcon( + modelData) + } + } + } + + Item { + width: parent.width - 150 - 240 - 28 - Theme.spacingM * 4 + height: 1 + } + } + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Settings/WidgetsTabSection.qml b/quickshell/.config/quickshell/Modules/Settings/WidgetsTabSection.qml new file mode 100644 index 0000000..e7340d0 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Settings/WidgetsTabSection.qml @@ -0,0 +1,761 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets +import qs.Services + +Column { + id: root + + property var items: [] + property var allWidgets: [] + property string title: "" + property string titleIcon: "widgets" + property string sectionId: "" + + signal itemEnabledChanged(string sectionId, string itemId, bool enabled) + signal itemOrderChanged(var newOrder) + signal addWidget(string sectionId) + signal removeWidget(string sectionId, int widgetIndex) + signal spacerSizeChanged(string sectionId, int widgetIndex, int newSize) + signal compactModeChanged(string widgetId, var value) + signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex) + signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value) + + width: parent.width + height: implicitHeight + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: root.titleIcon + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - 60 + height: 1 + } + } + + Column { + id: itemsList + + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: root.items + + delegate: Item { + id: delegateItem + + property bool held: dragArea.pressed + property real originalY: y + + width: itemsList.width + height: 70 + z: held ? 2 : 1 + + Rectangle { + id: itemBackground + + anchors.fill: parent + anchors.margins: 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, + Theme.surfaceContainer.g, + Theme.surfaceContainer.b, 0.8) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + + DankIcon { + name: "drag_indicator" + size: Theme.iconSize - 4 + color: Theme.outline + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + 8 + anchors.verticalCenter: parent.verticalCenter + opacity: 0.8 + } + + DankIcon { + name: modelData.icon + size: Theme.iconSize + color: modelData.enabled ? Theme.primary : Theme.outline + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM * 2 + 40 + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize + anchors.right: actionButtons.left + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.text + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: modelData.enabled ? Theme.surfaceText : Theme.outline + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: modelData.description + font.pixelSize: Theme.fontSizeSmall + color: modelData.enabled ? Theme.outline : Qt.rgba( + Theme.outline.r, + Theme.outline.g, + Theme.outline.b, 0.6) + elide: Text.ElideRight + width: parent.width + wrapMode: Text.WordWrap + } + } + + Row { + id: actionButtons + + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Item { + width: 120 + height: 32 + visible: modelData.id === "gpuTemp" + + DankDropdown { + id: gpuDropdown + anchors.fill: parent + currentValue: { + var selectedIndex = modelData.selectedGpuIndex + !== undefined ? modelData.selectedGpuIndex : 0 + if (DgopService.availableGpus + && DgopService.availableGpus.length > selectedIndex + && selectedIndex >= 0) { + var gpu = DgopService.availableGpus[selectedIndex] + return gpu.driver.toUpperCase() + } + return DgopService.availableGpus + && DgopService.availableGpus.length + > 0 ? DgopService.availableGpus[0].driver.toUpperCase( + ) : "" + } + options: { + var gpuOptions = [] + if (DgopService.availableGpus + && DgopService.availableGpus.length > 0) { + for (var i = 0; i < DgopService.availableGpus.length; i++) { + var gpu = DgopService.availableGpus[i] + gpuOptions.push( + gpu.driver.toUpperCase( + )) + } + } + return gpuOptions + } + onValueChanged: value => { + var gpuIndex = options.indexOf( + value) + if (gpuIndex >= 0) { + root.gpuSelectionChanged( + root.sectionId, + index, gpuIndex) + } + } + } + } + + Item { + width: 32 + height: 32 + visible: (modelData.warning !== undefined + && modelData.warning !== "") + && (modelData.id === "cpuUsage" + || modelData.id === "memUsage" + || modelData.id === "cpuTemp" + || modelData.id === "gpuTemp") + + DankIcon { + name: "warning" + size: 20 + color: Theme.error + anchors.centerIn: parent + opacity: warningArea.containsMouse ? 1.0 : 0.8 + } + + MouseArea { + id: warningArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + + Rectangle { + id: warningTooltip + + property string warningText: (modelData.warning !== undefined + && modelData.warning + !== "") ? modelData.warning : "" + + width: Math.min( + 250, + warningTooltipText.implicitWidth) + Theme.spacingM * 2 + height: warningTooltipText.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + visible: warningArea.containsMouse + && warningText !== "" + opacity: visible ? 1 : 0 + x: -width - Theme.spacingS + y: (parent.height - height) / 2 + z: 100 + + StyledText { + id: warningTooltipText + anchors.centerIn: parent + anchors.margins: Theme.spacingS + text: warningTooltip.warningText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: Math.min(250, implicitWidth) + wrapMode: Text.WordWrap + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + Row { + spacing: Theme.spacingXS + visible: modelData.id === "clock" + || modelData.id === "music" + || modelData.id === "focusedWindow" + || modelData.id === "runningApps" + + DankActionButton { + id: smallSizeButton + buttonSize: 28 + visible: modelData.id === "music" + iconName: "photo_size_select_small" + iconSize: 16 + iconColor: SettingsData.mediaSize + === 0 ? Theme.primary : Theme.outline + onClicked: { + root.compactModeChanged("music", 0) + } + } + + DankActionButton { + id: mediumSizeButton + buttonSize: 28 + visible: modelData.id === "music" + iconName: "photo_size_select_actual" + iconSize: 16 + iconColor: SettingsData.mediaSize + === 1 ? Theme.primary : Theme.outline + onClicked: { + root.compactModeChanged("music", 1) + } + } + + DankActionButton { + id: largeSizeButton + buttonSize: 28 + visible: modelData.id === "music" + iconName: "photo_size_select_large" + iconSize: 16 + iconColor: SettingsData.mediaSize + === 2 ? Theme.primary : Theme.outline + onClicked: { + root.compactModeChanged("music", 2) + } + } + + DankActionButton { + id: compactModeButton + buttonSize: 28 + visible: modelData.id === "clock" + || modelData.id === "focusedWindow" + || modelData.id === "runningApps" + iconName: { + if (modelData.id === "clock") + return SettingsData.clockCompactMode ? "zoom_out" : "zoom_in" + if (modelData.id === "focusedWindow") + return SettingsData.focusedWindowCompactMode ? "zoom_out" : "zoom_in" + if (modelData.id === "runningApps") + return SettingsData.runningAppsCompactMode ? "zoom_out" : "zoom_in" + return "zoom_in" + } + iconSize: 16 + iconColor: { + if (modelData.id === "clock") + return SettingsData.clockCompactMode ? Theme.primary : Theme.outline + if (modelData.id === "focusedWindow") + return SettingsData.focusedWindowCompactMode ? Theme.primary : Theme.outline + if (modelData.id === "runningApps") + return SettingsData.runningAppsCompactMode ? Theme.primary : Theme.outline + return Theme.outline + } + onClicked: { + if (modelData.id === "clock") { + root.compactModeChanged( + "clock", + !SettingsData.clockCompactMode) + } else if (modelData.id === "focusedWindow") { + root.compactModeChanged( + "focusedWindow", + !SettingsData.focusedWindowCompactMode) + } else if (modelData.id === "runningApps") { + root.compactModeChanged( + "runningApps", + !SettingsData.runningAppsCompactMode) + } + } + } + + Rectangle { + id: compactModeTooltip + width: tooltipText.contentWidth + Theme.spacingM * 2 + height: tooltipText.contentHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + visible: false + opacity: visible ? 1 : 0 + x: -width - Theme.spacingS + y: (parent.height - height) / 2 + z: 100 + + StyledText { + id: tooltipText + anchors.centerIn: parent + text: "Compact Mode" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + DankActionButton { + visible: modelData.id === "controlCenterButton" + buttonSize: 32 + iconName: "more_vert" + iconSize: 18 + iconColor: Theme.outline + onClicked: { + console.log("Control Center three-dot button clicked for widget:", modelData.id) + controlCenterContextMenu.widgetData = modelData + controlCenterContextMenu.sectionId = root.sectionId + controlCenterContextMenu.widgetIndex = index + // Position relative to the action buttons row, not the specific button + var parentPos = parent.mapToItem(root, 0, 0) + controlCenterContextMenu.x = parentPos.x - 210 // Position to the left with margin + controlCenterContextMenu.y = parentPos.y - 10 // Slightly above + controlCenterContextMenu.open() + } + } + + DankActionButton { + visible: modelData.id !== "spacer" + buttonSize: 32 + iconName: modelData.enabled ? "visibility" : "visibility_off" + iconSize: 18 + iconColor: modelData.enabled ? Theme.primary : Theme.outline + onClicked: { + root.itemEnabledChanged(root.sectionId, + modelData.id, + !modelData.enabled) + } + } + + Row { + visible: modelData.id === "spacer" + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + buttonSize: 24 + iconName: "remove" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var currentSize = modelData.size || 20 + var newSize = Math.max(5, currentSize - 5) + root.spacerSizeChanged(root.sectionId, + index, + newSize) + } + } + + StyledText { + text: (modelData.size || 20).toString() + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankActionButton { + buttonSize: 24 + iconName: "add" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var currentSize = modelData.size || 20 + var newSize = Math.min(5000, + currentSize + 5) + root.spacerSizeChanged(root.sectionId, + index, + newSize) + } + } + } + + DankActionButton { + buttonSize: 32 + iconName: "close" + iconSize: 18 + iconColor: Theme.error + onClicked: { + root.removeWidget(root.sectionId, index) + } + } + } + + MouseArea { + id: dragArea + + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 60 + hoverEnabled: true + cursorShape: Qt.SizeVerCursor + drag.target: held ? delegateItem : undefined + drag.axis: Drag.YAxis + drag.minimumY: -delegateItem.height + drag.maximumY: itemsList.height + preventStealing: true + onPressed: { + delegateItem.z = 2 + delegateItem.originalY = delegateItem.y + } + onReleased: { + delegateItem.z = 1 + if (drag.active) { + var newIndex = Math.round( + delegateItem.y / (delegateItem.height + + itemsList.spacing)) + newIndex = Math.max( + 0, Math.min(newIndex, + root.items.length - 1)) + if (newIndex !== index) { + var newItems = root.items.slice() + var draggedItem = newItems.splice(index, + 1)[0] + newItems.splice(newIndex, 0, draggedItem) + root.itemOrderChanged(newItems.map(item => { + return ({ + "id": item.id, + "enabled": item.enabled, + "size": item.size + }) + })) + } + } + delegateItem.x = 0 + delegateItem.y = delegateItem.originalY + } + } + + Behavior on y { + enabled: !dragArea.held && !dragArea.drag.active + + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + + Rectangle { + width: 200 + height: 40 + radius: Theme.cornerRadius + color: addButtonArea.containsMouse ? Theme.primaryContainer : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: "Add Widget" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + anchors.centerIn: parent + } + + MouseArea { + id: addButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.addWidget(root.sectionId) + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + Popup { + id: controlCenterContextMenu + + property var widgetData: null + property string sectionId: "" + property int widgetIndex: -1 + + + width: 200 + height: 120 + padding: 0 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + onOpened: { + console.log("Control Center context menu opened") + } + + onClosed: { + console.log("Control Center context menu closed") + } + + background: Rectangle { + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + } + + contentItem: Item { + + Column { + id: menuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 2 + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: networkToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "lan" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Network Icon" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: networkToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: SettingsData.controlCenterShowNetworkIcon + onToggled: { + root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showNetworkIcon", toggled) + } + } + + MouseArea { + id: networkToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + networkToggle.checked = !networkToggle.checked + root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showNetworkIcon", networkToggle.checked) + } + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: bluetoothToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "bluetooth" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Bluetooth Icon" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: bluetoothToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: SettingsData.controlCenterShowBluetoothIcon + onToggled: { + root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showBluetoothIcon", toggled) + } + } + + MouseArea { + id: bluetoothToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + bluetoothToggle.checked = !bluetoothToggle.checked + root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showBluetoothIcon", bluetoothToggle.checked) + } + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: audioToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "volume_up" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Audio Icon" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: audioToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: SettingsData.controlCenterShowAudioIcon + onToggled: { + root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showAudioIcon", toggled) + } + } + + MouseArea { + id: audioToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + audioToggle.checked = !audioToggle.checked + root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showAudioIcon", audioToggle.checked) + } + } + } + } + + } + } +} diff --git a/quickshell/.config/quickshell/Modules/Toast.qml b/quickshell/.config/quickshell/Modules/Toast.qml new file mode 100644 index 0000000..e7bf33d --- /dev/null +++ b/quickshell/.config/quickshell/Modules/Toast.qml @@ -0,0 +1,336 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets + +PanelWindow { + id: root + + property var modelData + property bool shouldBeVisible: false + property real frozenWidth: 0 + + Connections { + target: ToastService + function onToastVisibleChanged() { + if (ToastService.toastVisible) { + shouldBeVisible = true + visible = true + } else { + // Freeze the width before starting exit animation + frozenWidth = toast.width + shouldBeVisible = false + closeTimer.restart() + } + } + } + + Timer { + id: closeTimer + interval: Theme.mediumDuration + 50 + onTriggered: { + if (!shouldBeVisible) { + visible = false + } + } + } + + screen: modelData + visible: shouldBeVisible + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + Rectangle { + id: toast + + property bool expanded: false + + Connections { + target: ToastService + function onResetToastState() { + toast.expanded = false + } + } + + width: shouldBeVisible ? (ToastService.hasDetails ? 380 : 350) : frozenWidth + height: toastContent.height + Theme.spacingL * 2 + anchors.horizontalCenter: parent.horizontalCenter + y: Theme.barHeight - 4 + SettingsData.topBarSpacing + 2 + color: { + switch (ToastService.currentLevel) { + case ToastService.levelError: + return Theme.error + case ToastService.levelWarn: + return Theme.warning + case ToastService.levelInfo: + return Theme.surfaceContainer + default: + return Theme.surfaceContainer + } + } + radius: Theme.cornerRadius + layer.enabled: true + opacity: shouldBeVisible ? 1 : 0 + + Column { + id: toastContent + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: Theme.spacingL + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + spacing: Theme.spacingS + + Item { + width: parent.width + height: Theme.iconSize + 8 + + DankIcon { + id: statusIcon + name: { + switch (ToastService.currentLevel) { + case ToastService.levelError: + return "error" + case ToastService.levelWarn: + return "warning" + case ToastService.levelInfo: + return "info" + default: + return "info" + } + } + size: Theme.iconSize + color: { + switch (ToastService.currentLevel) { + case ToastService.levelError: + case ToastService.levelWarn: + return SessionData.isLightMode ? Theme.surfaceText : Theme.background + default: + return Theme.surfaceText + } + } + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: messageText + text: ToastService.currentMessage + font.pixelSize: Theme.fontSizeMedium + color: { + switch (ToastService.currentLevel) { + case ToastService.levelError: + case ToastService.levelWarn: + return SessionData.isLightMode ? Theme.surfaceText : Theme.background + default: + return Theme.surfaceText + } + } + font.weight: Font.Medium + anchors.left: statusIcon.right + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + anchors.right: ToastService.hasDetails ? expandButton.left : closeButton.left + anchors.rightMargin: Theme.spacingM + wrapMode: Text.NoWrap + elide: Text.ElideRight + } + + DankActionButton { + id: expandButton + iconName: toast.expanded ? "expand_less" : "expand_more" + iconSize: Theme.iconSize + iconColor: { + switch (ToastService.currentLevel) { + case ToastService.levelError: + case ToastService.levelWarn: + return SessionData.isLightMode ? Theme.surfaceText : Theme.background + default: + return Theme.surfaceText + } + } + buttonSize: Theme.iconSize + 8 + anchors.right: closeButton.left + anchors.rightMargin: 2 + anchors.verticalCenter: parent.verticalCenter + visible: ToastService.hasDetails + + onClicked: { + toast.expanded = !toast.expanded + if (toast.expanded) { + ToastService.stopTimer() + } else { + ToastService.restartTimer() + } + } + } + + DankActionButton { + id: closeButton + iconName: "close" + iconSize: Theme.iconSize + iconColor: { + switch (ToastService.currentLevel) { + case ToastService.levelError: + case ToastService.levelWarn: + return SessionData.isLightMode ? Theme.surfaceText : Theme.background + default: + return Theme.surfaceText + } + } + buttonSize: Theme.iconSize + 8 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + visible: ToastService.hasDetails + + onClicked: { + ToastService.hideToast() + } + } + } + + Rectangle { + width: parent.width + height: detailsText.height + Theme.spacingS * 2 + color: Qt.rgba(0, 0, 0, 0.2) + radius: Theme.cornerRadius / 2 + visible: toast.expanded && ToastService.hasDetails + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + id: detailsText + text: ToastService.currentDetails + font.pixelSize: Theme.fontSizeSmall + color: { + switch (ToastService.currentLevel) { + case ToastService.levelError: + case ToastService.levelWarn: + return SessionData.isLightMode ? Theme.surfaceText : Theme.background + default: + return Theme.surfaceText + } + } + isMonospace: true + anchors.left: parent.left + anchors.right: copyButton.left + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Theme.spacingS + anchors.rightMargin: Theme.spacingS + wrapMode: Text.Wrap + } + + DankActionButton { + id: copyButton + iconName: "content_copy" + iconSize: Theme.iconSizeSmall + iconColor: { + switch (ToastService.currentLevel) { + case ToastService.levelError: + case ToastService.levelWarn: + return SessionData.isLightMode ? Theme.surfaceText : Theme.background + default: + return Theme.surfaceText + } + } + buttonSize: Theme.iconSizeSmall + 8 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: Theme.spacingS + + property bool showTooltip: false + + onClicked: { + Quickshell.execDetached( + ["wl-copy", ToastService.currentDetails]) + showTooltip = true + tooltipTimer.start() + } + + Timer { + id: tooltipTimer + interval: 1500 + onTriggered: copyButton.showTooltip = false + } + + Rectangle { + visible: copyButton.showTooltip + width: tooltipLabel.implicitWidth + 16 + height: tooltipLabel.implicitHeight + 8 + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.outlineMedium + y: -height - 4 + x: -width / 2 + copyButton.width / 2 + + StyledText { + id: tooltipLabel + anchors.centerIn: parent + text: "Copied!" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + } + } + } + } + + MouseArea { + anchors.fill: parent + visible: !ToastService.hasDetails + onClicked: ToastService.hideToast() + } + + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 4 + shadowBlur: 0.8 + shadowColor: Qt.rgba(0, 0, 0, 0.3) + shadowOpacity: 0.3 + } + + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on height { + enabled: false + } + + Behavior on width { + enabled: false + } + } + + mask: Region { + item: toast + } +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/AudioVisualization.qml b/quickshell/.config/quickshell/Modules/TopBar/AudioVisualization.qml new file mode 100644 index 0000000..8c471d4 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/AudioVisualization.qml @@ -0,0 +1,77 @@ +import QtQuick +import Quickshell.Services.Mpris +import qs.Common +import qs.Services + +Item { + id: root + + readonly property MprisPlayer activePlayer: MprisController.activePlayer + readonly property bool hasActiveMedia: activePlayer !== null + readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing + + width: 20 + height: Theme.iconSize + + Loader { + active: isPlaying + + sourceComponent: Component { + Ref { + service: CavaService + } + + } + + } + + Timer { + id: fallbackTimer + + running: !CavaService.cavaAvailable && isPlaying + interval: 256 + repeat: true + onTriggered: { + CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25]; + } + } + + Row { + anchors.centerIn: parent + spacing: 1.5 + + Repeater { + model: 6 + + Rectangle { + width: 2 + height: { + if (root.isPlaying && CavaService.values.length > index) { + const rawLevel = CavaService.values[index] || 0; + const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100; + const maxHeight = Theme.iconSize - 2; + const minHeight = 3; + return minHeight + (scaledLevel / 100) * (maxHeight - minHeight); + } + return 3; + } + radius: 1.5 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + + Behavior on height { + NumberAnimation { + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.standardDecel + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/Battery.qml b/quickshell/.config/quickshell/Modules/TopBar/Battery.qml new file mode 100644 index 0000000..98e5d66 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/Battery.qml @@ -0,0 +1,259 @@ +import QtQuick +import Quickshell.Services.UPower +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: battery + + property bool batteryPopupVisible: false + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property real widgetHeight: 30 + property real barHeight: 48 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + signal toggleBatteryPopup() + + width: batteryContent.implicitWidth + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) + return "transparent"; + + const baseColor = batteryArea.containsMouse || batteryPopupVisible ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + visible: true + + Row { + id: batteryContent + + anchors.centerIn: parent + spacing: SettingsData.topBarNoBackground ? 1 : 2 + + DankIcon { + name: { + if (!BatteryService.batteryAvailable) { + return "power"; + } + + if (BatteryService.isCharging) { + if (BatteryService.batteryLevel >= 90) { + return "battery_charging_full"; + } + + if (BatteryService.batteryLevel >= 80) { + return "battery_charging_90"; + } + + if (BatteryService.batteryLevel >= 60) { + return "battery_charging_80"; + } + + if (BatteryService.batteryLevel >= 50) { + return "battery_charging_60"; + } + + if (BatteryService.batteryLevel >= 30) { + return "battery_charging_50"; + } + + if (BatteryService.batteryLevel >= 20) { + return "battery_charging_30"; + } + + return "battery_charging_20"; + } + // Check if plugged in but not charging (like at 80% charge limit) + if (BatteryService.isPluggedIn) { + if (BatteryService.batteryLevel >= 90) { + return "battery_charging_full"; + } + + if (BatteryService.batteryLevel >= 80) { + return "battery_charging_90"; + } + + if (BatteryService.batteryLevel >= 60) { + return "battery_charging_80"; + } + + if (BatteryService.batteryLevel >= 50) { + return "battery_charging_60"; + } + + if (BatteryService.batteryLevel >= 30) { + return "battery_charging_50"; + } + + if (BatteryService.batteryLevel >= 20) { + return "battery_charging_30"; + } + + return "battery_charging_20"; + } + // On battery power + if (BatteryService.batteryLevel >= 95) { + return "battery_full"; + } + + if (BatteryService.batteryLevel >= 85) { + return "battery_6_bar"; + } + + if (BatteryService.batteryLevel >= 70) { + return "battery_5_bar"; + } + + if (BatteryService.batteryLevel >= 55) { + return "battery_4_bar"; + } + + if (BatteryService.batteryLevel >= 40) { + return "battery_3_bar"; + } + + if (BatteryService.batteryLevel >= 25) { + return "battery_2_bar"; + } + + return "battery_1_bar"; + } + size: Theme.iconSize - 6 + color: { + if (!BatteryService.batteryAvailable) { + return Theme.surfaceText; + } + + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error; + } + + if (BatteryService.isCharging || BatteryService.isPluggedIn) { + return Theme.primary; + } + + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: `${BatteryService.batteryLevel}%` + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: { + if (!BatteryService.batteryAvailable) { + return Theme.surfaceText; + } + + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error; + } + + if (BatteryService.isCharging) { + return Theme.primary; + } + + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + visible: BatteryService.batteryAvailable + } + + } + + MouseArea { + id: batteryArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen); + } + toggleBatteryPopup(); + } + } + + Rectangle { + id: batteryTooltip + + width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2) + height: tooltipText.contentHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.surfaceVariantAlpha + border.width: 1 + visible: batteryArea.containsMouse && !batteryPopupVisible + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + opacity: batteryArea.containsMouse ? 1 : 0 + + Column { + anchors.centerIn: parent + spacing: 2 + + StyledText { + id: tooltipText + + text: { + if (!BatteryService.batteryAvailable) { + if (typeof PowerProfiles === "undefined") { + return "Power Management"; + } + + switch (PowerProfiles.profile) { + case PowerProfile.PowerSaver: + return "Power Profile: Power Saver"; + case PowerProfile.Performance: + return "Power Profile: Performance"; + default: + return "Power Profile: Balanced"; + } + } + const status = BatteryService.batteryStatus; + const level = `${BatteryService.batteryLevel}%`; + const time = BatteryService.formatTimeRemaining(); + if (time !== "Unknown") { + return `${status} • ${level} • ${time}`; + } else { + return `${status} • ${level}`; + } + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + } + + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/BatteryPopout.qml b/quickshell/.config/quickshell/Modules/TopBar/BatteryPopout.qml new file mode 100644 index 0000000..a7ecafd --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/BatteryPopout.qml @@ -0,0 +1,591 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Services.UPower +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +DankPopout { + id: root + + property string triggerSection: "right" + property var triggerScreen: null + + function setTriggerPosition(x, y, width, section, screen) { + triggerX = x; + triggerY = y; + triggerWidth = width; + triggerSection = section; + triggerScreen = screen; + } + + function isActiveProfile(profile) { + if (typeof PowerProfiles === "undefined") { + return false; + } + + return PowerProfiles.profile === profile; + } + + function setProfile(profile) { + if (typeof PowerProfiles === "undefined") { + ToastService.showError("power-profiles-daemon not available"); + return ; + } + PowerProfiles.profile = profile; + if (PowerProfiles.profile !== profile) { + ToastService.showError("Failed to set power profile"); + } + + } + + popupWidth: 400 + popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400 + triggerX: Screen.width - 380 - Theme.spacingL + triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS + triggerWidth: 70 + positioning: "center" + screen: triggerScreen + shouldBeVisible: false + visible: shouldBeVisible + + content: Component { + Rectangle { + id: batteryContent + + implicitHeight: contentColumn.height + Theme.spacingL * 2 + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.color: Theme.outlineMedium + border.width: 1 + antialiasing: true + smooth: true + focus: true + Component.onCompleted: { + if (root.shouldBeVisible) { + forceActiveFocus(); + } + + } + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Escape) { + root.close(); + event.accepted = true; + } + } + + Connections { + function onShouldBeVisibleChanged() { + if (root.shouldBeVisible) { + Qt.callLater(function() { + batteryContent.forceActiveFocus(); + }); + } + + } + + target: root + } + + Rectangle { + anchors.fill: parent + anchors.margins: -3 + color: "transparent" + radius: parent.radius + 3 + border.color: Qt.rgba(0, 0, 0, 0.05) + border.width: 1 + z: -3 + } + + Rectangle { + anchors.fill: parent + anchors.margins: -2 + color: "transparent" + radius: parent.radius + 2 + border.color: Theme.shadowMedium + border.width: 1 + z: -2 + } + + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: Theme.outlineStrong + border.width: 1 + radius: parent.radius + z: -1 + } + + Column { + id: contentColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + Item { + width: parent.width + height: 32 + + StyledText { + text: BatteryService.batteryAvailable ? "Battery Information" : "Power Management" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: closeBatteryArea.containsMouse ? Theme.errorHover : "transparent" + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "close" + size: Theme.iconSize - 4 + color: closeBatteryArea.containsMouse ? Theme.error : Theme.surfaceText + } + + MouseArea { + id: closeBatteryArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.close(); + } + } + + } + + } + + Rectangle { + width: parent.width + height: 80 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4) + border.color: BatteryService.isCharging ? Theme.primary : (BatteryService.isLowBattery ? Theme.error : Theme.outlineMedium) + border.width: BatteryService.isCharging || BatteryService.isLowBattery ? 2 : 1 + visible: BatteryService.batteryAvailable + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingL + + DankIcon { + name: { + if (!BatteryService.batteryAvailable) + return "power"; + + // Check if plugged in but not charging (like at 80% charge limit) + if (!BatteryService.isCharging && BatteryService.isPluggedIn) { + if (BatteryService.batteryLevel >= 90) { + return "battery_charging_full"; + } + + if (BatteryService.batteryLevel >= 80) { + return "battery_charging_90"; + } + + if (BatteryService.batteryLevel >= 60) { + return "battery_charging_80"; + } + + if (BatteryService.batteryLevel >= 50) { + return "battery_charging_60"; + } + + if (BatteryService.batteryLevel >= 30) { + return "battery_charging_50"; + } + + if (BatteryService.batteryLevel >= 20) { + return "battery_charging_30"; + } + + return "battery_charging_20"; + } + if (BatteryService.isCharging) { + if (BatteryService.batteryLevel >= 90) { + return "battery_charging_full"; + } + + if (BatteryService.batteryLevel >= 80) { + return "battery_charging_90"; + } + + if (BatteryService.batteryLevel >= 60) { + return "battery_charging_80"; + } + + if (BatteryService.batteryLevel >= 50) { + return "battery_charging_60"; + } + + if (BatteryService.batteryLevel >= 30) { + return "battery_charging_50"; + } + + if (BatteryService.batteryLevel >= 20) { + return "battery_charging_30"; + } + + return "battery_charging_20"; + } else { + if (BatteryService.batteryLevel >= 95) { + return "battery_full"; + } + + if (BatteryService.batteryLevel >= 85) { + return "battery_6_bar"; + } + + if (BatteryService.batteryLevel >= 70) { + return "battery_5_bar"; + } + + if (BatteryService.batteryLevel >= 55) { + return "battery_4_bar"; + } + + if (BatteryService.batteryLevel >= 40) { + return "battery_3_bar"; + } + + if (BatteryService.batteryLevel >= 25) { + return "battery_2_bar"; + } + + return "battery_1_bar"; + } + } + size: Theme.iconSizeLarge + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) + return Theme.error; + + if (BatteryService.isCharging || BatteryService.isPluggedIn) + return Theme.primary; + + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + + Row { + spacing: Theme.spacingM + + StyledText { + text: `${BatteryService.batteryLevel}%` + font.pixelSize: Theme.fontSizeLarge + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error; + } + + if (BatteryService.isCharging) { + return Theme.primary; + } + + return Theme.surfaceText; + } + font.weight: Font.Bold + } + + StyledText { + text: BatteryService.batteryStatus + font.pixelSize: Theme.fontSizeMedium + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error; + } + + if (BatteryService.isCharging) { + return Theme.primary; + } + + return Theme.surfaceText; + } + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + } + + StyledText { + text: { + const time = BatteryService.formatTimeRemaining(); + if (time !== "Unknown") { + return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`; + } + + return ""; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + visible: text.length > 0 + } + + } + + } + + } + + Rectangle { + width: parent.width + height: 80 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4) + border.color: Theme.outlineMedium + border.width: 1 + visible: !BatteryService.batteryAvailable + + Row { + anchors.centerIn: parent + spacing: Theme.spacingL + + DankIcon { + name: "power" + size: 36 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "No Battery Detected" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: "Power profile management is available" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceTextMedium + } + + } + + } + + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: BatteryService.batteryAvailable + + StyledText { + text: "Battery Details" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + Row { + width: parent.width + spacing: Theme.spacingXL + + Column { + spacing: 2 + width: (parent.width - Theme.spacingXL) / 2 + + StyledText { + text: "Health" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + font.weight: Font.Medium + } + + StyledText { + text: BatteryService.batteryHealth + font.pixelSize: Theme.fontSizeMedium + color: { + if (BatteryService.batteryHealth === "N/A") { + return Theme.surfaceText; + } + + const healthNum = parseInt(BatteryService.batteryHealth); + return healthNum < 80 ? Theme.error : Theme.surfaceText; + } + } + + } + + Column { + spacing: 2 + width: (parent.width - Theme.spacingXL) / 2 + + StyledText { + text: "Capacity" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + font.weight: Font.Medium + } + + StyledText { + text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : "Unknown" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + } + + } + + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: true + + StyledText { + text: "Power Profile" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] + + Rectangle { + width: parent.width + height: 50 + radius: Theme.cornerRadius + color: profileArea.containsMouse ? Theme.primaryHoverLight : (root.isActiveProfile(modelData) ? Theme.primaryPressed : Theme.surfaceLight) + border.color: root.isActiveProfile(modelData) ? Theme.primary : Theme.outlineLight + border.width: root.isActiveProfile(modelData) ? 2 : 1 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: Theme.getPowerProfileIcon(modelData) + size: Theme.iconSize + color: root.isActiveProfile(modelData) ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: Theme.getPowerProfileLabel(modelData) + font.pixelSize: Theme.fontSizeMedium + color: root.isActiveProfile(modelData) ? Theme.primary : Theme.surfaceText + font.weight: root.isActiveProfile(modelData) ? Font.Medium : Font.Normal + } + + StyledText { + text: Theme.getPowerProfileDescription(modelData) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + + } + + } + + MouseArea { + id: profileArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.setProfile(modelData); + } + } + + } + + } + + } + + } + + Rectangle { + width: parent.width + height: 60 + radius: Theme.cornerRadius + color: Theme.errorHover + border.color: Theme.primarySelected + border.width: 1 + visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "warning" + size: Theme.iconSize + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Power Profile Degradation" + font.pixelSize: Theme.fontSizeMedium + color: Theme.error + font.weight: Font.Medium + } + + StyledText { + text: (typeof PowerProfiles !== "undefined") ? PerformanceDegradationReason.toString(PowerProfiles.degradationReason) : "" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8) + } + + } + + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/Clock.qml b/quickshell/.config/quickshell/Modules/TopBar/Clock.qml new file mode 100644 index 0000000..ac64b71 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/Clock.qml @@ -0,0 +1,99 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool compactMode: false + property string section: "center" + property var popupTarget: null + property var parentScreen: null + property real barHeight: 48 + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS + + signal clockClicked + + width: clockRow.implicitWidth + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent" + } + + const baseColor = clockMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency) + } + + Row { + id: clockRow + + anchors.centerIn: parent + spacing: Theme.spacingS + + StyledText { + text: { + const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP" + return systemClock?.date?.toLocaleTimeString(Qt.locale(), format) + } + font.pixelSize: Theme.fontSizeMedium - 1 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.outlineButton + anchors.verticalCenter: parent.verticalCenter + visible: !SettingsData.clockCompactMode + } + + StyledText { + text: { + if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) { + return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) + } + + return systemClock?.date?.toLocaleDateString(Qt.locale(), "ddd d") + } + font.pixelSize: Theme.fontSizeMedium - 1 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + visible: !SettingsData.clockCompactMode + } + } + + SystemClock { + id: systemClock + precision: SystemClock.Seconds + } + + MouseArea { + id: clockMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0) + const currentScreen = parentScreen || Screen + const screenX = currentScreen.x || 0 + const relativeX = globalPos.x - screenX + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen) + } + root.clockClicked() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/ControlCenterButton.qml b/quickshell/.config/quickshell/Modules/TopBar/ControlCenterButton.qml new file mode 100644 index 0000000..2645645 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/ControlCenterButton.qml @@ -0,0 +1,184 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property bool isActive: false + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property var widgetData: null + property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon + property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon + property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon + property real widgetHeight: 30 + property real barHeight: 48 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + signal clicked() + + width: controlIndicators.implicitWidth + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = controlCenterArea.containsMouse || root.isActive ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + + Row { + id: controlIndicators + + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + id: networkIcon + + name: { + if (NetworkService.wifiToggling) { + return "sync"; + } + + if (NetworkService.networkStatus === "ethernet") { + return "lan"; + } + + return NetworkService.wifiSignalIcon; + } + size: Theme.iconSize - 8 + color: { + if (NetworkService.wifiToggling) { + return Theme.primary; + } + + return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton; + } + anchors.verticalCenter: parent.verticalCenter + visible: root.showNetworkIcon + + RotationAnimation on rotation { + running: NetworkService.wifiToggling + loops: Animation.Infinite + from: 0 + to: 360 + duration: 1000 + } + + } + + DankIcon { + id: bluetoothIcon + + name: "bluetooth" + size: Theme.iconSize - 8 + color: BluetoothService.enabled ? Theme.primary : Theme.outlineButton + anchors.verticalCenter: parent.verticalCenter + visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled + } + + Rectangle { + width: audioIcon.implicitWidth + 4 + height: audioIcon.implicitHeight + 4 + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + visible: root.showAudioIcon + + DankIcon { + id: audioIcon + + name: { + if (AudioService.sink && AudioService.sink.audio) { + if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) { + return "volume_off"; + } else if (AudioService.sink.audio.volume * 100 < 33) { + return "volume_down"; + } else { + return "volume_up"; + } + } + return "volume_up"; + } + size: Theme.iconSize - 8 + color: Theme.surfaceText + anchors.centerIn: parent + } + + MouseArea { + id: audioWheelArea + + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onWheel: function(wheelEvent) { + let delta = wheelEvent.angleDelta.y; + let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0; + let newVolume; + if (delta > 0) { + newVolume = Math.min(100, currentVolume + 5); + } else { + newVolume = Math.max(0, currentVolume - 5); + } + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.muted = false; + AudioService.sink.audio.volume = newVolume / 100; + AudioService.volumeChanged(); + } + wheelEvent.accepted = true; + } + } + + } + + DankIcon { + name: "mic" + size: Theme.iconSize - 8 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + visible: false // TODO: Add mic detection + } + + // Fallback settings icon when all other icons are hidden + DankIcon { + name: "settings" + size: Theme.iconSize - 8 + color: controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon + } + + } + + MouseArea { + id: controlCenterArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen); + } + root.clicked(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/CpuMonitor.qml b/quickshell/.config/quickshell/Modules/TopBar/CpuMonitor.qml new file mode 100644 index 0000000..e854333 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/CpuMonitor.qml @@ -0,0 +1,99 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property bool showPercentage: true + property bool showIcon: true + property var toggleProcessList + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property real barHeight: 48 + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + width: cpuContent.implicitWidth + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = cpuArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + Component.onCompleted: { + DgopService.addRef(["cpu"]); + } + Component.onDestruction: { + DgopService.removeRef(["cpu"]); + } + + MouseArea { + id: cpuArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen); + } + DgopService.setSortBy("cpu"); + if (root.toggleProcessList) { + root.toggleProcessList(); + } + + } + } + + Row { + id: cpuContent + + anchors.centerIn: parent + spacing: 3 + + DankIcon { + name: "memory" + size: Theme.iconSize - 8 + color: { + if (DgopService.cpuUsage > 80) { + return Theme.tempDanger; + } + + if (DgopService.cpuUsage > 60) { + return Theme.tempWarning; + } + + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null || DgopService.cpuUsage === 0) { + return "--%"; + } + + return DgopService.cpuUsage.toFixed(0) + "%"; + } + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/CpuTemperature.qml b/quickshell/.config/quickshell/Modules/TopBar/CpuTemperature.qml new file mode 100644 index 0000000..19a0beb --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/CpuTemperature.qml @@ -0,0 +1,107 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property bool showPercentage: true + property bool showIcon: true + property var toggleProcessList + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property real barHeight: 48 + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + width: cpuTempContent.implicitWidth + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = cpuTempArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + Component.onCompleted: { + DgopService.addRef(["cpu"]); + } + Component.onDestruction: { + DgopService.removeRef(["cpu"]); + } + + MouseArea { + id: cpuTempArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen); + } + DgopService.setSortBy("cpu"); + if (root.toggleProcessList) { + root.toggleProcessList(); + } + + } + } + + Row { + id: cpuTempContent + + anchors.centerIn: parent + spacing: 3 + + DankIcon { + name: "memory" + size: Theme.iconSize - 8 + color: { + if (DgopService.cpuTemperature > 85) { + return Theme.tempDanger; + } + + if (DgopService.cpuTemperature > 69) { + return Theme.tempWarning; + } + + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature < 0) { + return "--°"; + } + + return Math.round(DgopService.cpuTemperature) + "°"; + } + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/FocusedApp.qml b/quickshell/.config/quickshell/Modules/TopBar/FocusedApp.qml new file mode 100644 index 0000000..eb44ed6 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/FocusedApp.qml @@ -0,0 +1,129 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property bool compactMode: SettingsData.focusedWindowCompactMode + property int availableWidth: 400 + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS + readonly property int baseWidth: contentRow.implicitWidth + horizontalPadding * 2 + readonly property int maxNormalWidth: 456 + readonly property int maxCompactWidth: 288 + readonly property Toplevel activeWindow: ToplevelManager.activeToplevel + + width: compactMode ? Math.min(baseWidth, maxCompactWidth) : Math.min(baseWidth, maxNormalWidth) + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (!activeWindow || !activeWindow.title) { + return "transparent"; + } + + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + clip: true + visible: activeWindow && activeWindow.title + + Row { + id: contentRow + + anchors.centerIn: parent + spacing: Theme.spacingS + + StyledText { + id: appText + + text: { + if (!activeWindow || !activeWindow.appId) { + return ""; + } + + const desktopEntry = DesktopEntries.heuristicLookup(activeWindow.appId); + return desktopEntry && desktopEntry.name ? desktopEntry.name : activeWindow.appId; + } + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, compactMode ? 80 : 180) + visible: !compactMode && text.length > 0 + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.outlineButton + anchors.verticalCenter: parent.verticalCenter + visible: !compactMode && appText.text && titleText.text + } + + StyledText { + id: titleText + + text: { + const title = activeWindow && activeWindow.title ? activeWindow.title : ""; + const appName = appText.text; + if (!title || !appName) { + return title; + } + + // Remove app name from end of title if it exists there + if (title.endsWith(" - " + appName)) { + return title.substring(0, title.length - (" - " + appName).length); + } + + if (title.endsWith(appName)) { + return title.substring(0, title.length - appName.length).replace(/ - $/, ""); + } + + return title; + } + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, compactMode ? 280 : 250) + visible: text.length > 0 + } + + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/GpuTemperature.qml b/quickshell/.config/quickshell/Modules/TopBar/GpuTemperature.qml new file mode 100644 index 0000000..0c00322 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/GpuTemperature.qml @@ -0,0 +1,198 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property bool showPercentage: true + property bool showIcon: true + property var toggleProcessList + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property var widgetData: null + property real barHeight: 48 + property real widgetHeight: 30 + property int selectedGpuIndex: (widgetData && widgetData.selectedGpuIndex !== undefined) ? widgetData.selectedGpuIndex : 0 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + property real displayTemp: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return 0; + } + + if (selectedGpuIndex >= 0 && selectedGpuIndex < DgopService.availableGpus.length) { + return DgopService.availableGpus[selectedGpuIndex].temperature || 0; + } + + return 0; + } + + function updateWidgetPciId(pciId) { + // Find and update this widget's pciId in the settings + const sections = ["left", "center", "right"]; + for (let s = 0; s < sections.length; s++) { + const sectionId = sections[s]; + let widgets = []; + if (sectionId === "left") { + widgets = SettingsData.topBarLeftWidgets.slice(); + } else if (sectionId === "center") { + widgets = SettingsData.topBarCenterWidgets.slice(); + } else if (sectionId === "right") { + widgets = SettingsData.topBarRightWidgets.slice(); + } + for (let i = 0; i < widgets.length; i++) { + const widget = widgets[i]; + if (typeof widget === "object" && widget.id === "gpuTemp" && (!widget.pciId || widget.pciId === "")) { + widgets[i] = { + "id": widget.id, + "enabled": widget.enabled !== undefined ? widget.enabled : true, + "selectedGpuIndex": 0, + "pciId": pciId + }; + if (sectionId === "left") { + SettingsData.setTopBarLeftWidgets(widgets); + } else if (sectionId === "center") { + SettingsData.setTopBarCenterWidgets(widgets); + } else if (sectionId === "right") { + SettingsData.setTopBarRightWidgets(widgets); + } + return ; + } + } + } + } + + width: gpuTempContent.implicitWidth + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = gpuArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + Component.onCompleted: { + DgopService.addRef(["gpu"]); + console.log("GpuTemperature widget - pciId:", widgetData ? widgetData.pciId : "no widgetData", "selectedGpuIndex:", widgetData ? widgetData.selectedGpuIndex : "no widgetData"); + // Add this widget's PCI ID to the service + if (widgetData && widgetData.pciId) { + console.log("Adding GPU PCI ID to service:", widgetData.pciId); + DgopService.addGpuPciId(widgetData.pciId); + } else { + console.log("No PCI ID in widget data, starting auto-detection"); + // No PCI ID saved, auto-detect and save the first GPU + autoSaveTimer.running = true; + } + } + Component.onDestruction: { + DgopService.removeRef(["gpu"]); + // Remove this widget's PCI ID from the service + if (widgetData && widgetData.pciId) { + DgopService.removeGpuPciId(widgetData.pciId); + } + + } + + Connections { + function onWidgetDataChanged() { + // Force property re-evaluation by triggering change detection + root.selectedGpuIndex = Qt.binding(() => { + return (root.widgetData && root.widgetData.selectedGpuIndex !== undefined) ? root.widgetData.selectedGpuIndex : 0; + }); + } + + target: SettingsData + } + + MouseArea { + id: gpuArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen); + } + DgopService.setSortBy("cpu"); + if (root.toggleProcessList) { + root.toggleProcessList(); + } + + } + } + + Row { + id: gpuTempContent + + anchors.centerIn: parent + spacing: 3 + + DankIcon { + name: "auto_awesome_mosaic" + size: Theme.iconSize - 8 + color: { + if (root.displayTemp > 80) { + return Theme.tempDanger; + } + + if (root.displayTemp > 65) { + return Theme.tempWarning; + } + + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (root.displayTemp === undefined || root.displayTemp === null || root.displayTemp === 0) { + return "--°"; + } + + return Math.round(root.displayTemp) + "°"; + } + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + } + + Timer { + id: autoSaveTimer + + interval: 100 + running: false + onTriggered: { + if (DgopService.availableGpus && DgopService.availableGpus.length > 0) { + const firstGpu = DgopService.availableGpus[0]; + if (firstGpu && firstGpu.pciId) { + // Save the first GPU's PCI ID to this widget's settings + updateWidgetPciId(firstGpu.pciId); + DgopService.addGpuPciId(firstGpu.pciId); + } + } + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/IdleInhibitor.qml b/quickshell/.config/quickshell/Modules/TopBar/IdleInhibitor.qml new file mode 100644 index 0000000..8d073a0 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/IdleInhibitor.qml @@ -0,0 +1,57 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + width: idleIcon.width + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : (SessionService.idleInhibited ? Theme.primaryHover : Theme.secondaryHover); + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + + DankIcon { + id: idleIcon + + anchors.centerIn: parent + name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" + size: Theme.iconSize - 6 + color: Theme.surfaceText + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + SessionService.toggleIdleInhibit(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/KeyboardLayoutName.qml b/quickshell/.config/quickshell/Modules/TopBar/KeyboardLayoutName.qml new file mode 100644 index 0000000..f45f152 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/KeyboardLayoutName.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Modules.ProcessList +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + width: contentRow.implicitWidth + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + NiriService.cycleKeyboardLayout(); + } + } + + Row { + id: contentRow + + anchors.centerIn: parent + spacing: Theme.spacingS + + StyledText { + text: NiriService.getCurrentKeyboardLayoutName() + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/LauncherButton.qml b/quickshell/.config/quickshell/Modules/TopBar/LauncherButton.qml new file mode 100644 index 0000000..d872136 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/LauncherButton.qml @@ -0,0 +1,83 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property bool isActive: false + property string section: "left" + property var popupTarget: null + property var parentScreen: null + property real widgetHeight: 30 + property real barHeight: 48 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + signal clicked() + + width: Theme.iconSize + horizontalPadding * 2 + height: widgetHeight + + MouseArea { + id: launcherArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen); + } + root.clicked(); + } + } + + Rectangle { + id: launcherContent + + anchors.fill: parent + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = launcherArea.containsMouse ? Theme.primaryPressed : (SessionService.idleInhibited ? Theme.primaryHover : Theme.secondaryHover); + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + + SystemLogo { + visible: SettingsData.useOSLogo + anchors.centerIn: parent + width: Theme.iconSize - 3 + height: Theme.iconSize - 3 + colorOverride: SettingsData.osLogoColorOverride + brightnessOverride: SettingsData.osLogoBrightness + contrastOverride: SettingsData.osLogoContrast + } + + DankIcon { + visible: !SettingsData.useOSLogo + anchors.centerIn: parent + name: "apps" + size: Theme.iconSize - 6 + color: Theme.surfaceText + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/Media.qml b/quickshell/.config/quickshell/Modules/TopBar/Media.qml new file mode 100644 index 0000000..266266f --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/Media.qml @@ -0,0 +1,343 @@ +import QtQuick +import Quickshell.Services.Mpris +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + readonly property MprisPlayer activePlayer: MprisController.activePlayer + readonly property bool playerAvailable: activePlayer !== null + property bool compactMode: false + readonly property int textWidth: { + switch (SettingsData.mediaSize) { + case 0: + return 0; // No text in small mode + case 2: + return 180; // Large text area + default: + return 120; // Medium text area + } + } + readonly property int currentContentWidth: { + // Calculate actual content width: + // AudioViz (20) + spacing + [text + spacing] + controls (prev:20 + spacing + play:24 + spacing + next:20) + padding + const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20; + // ~72px total + const audioVizWidth = 20; + const contentWidth = audioVizWidth + Theme.spacingXS + controlsWidth; + return contentWidth + (textWidth > 0 ? textWidth + Theme.spacingXS : 0) + horizontalPadding * 2; + } + property string section: "center" + property var popupTarget: null + property var parentScreen: null + property real barHeight: 48 + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + signal clicked() + + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = Theme.surfaceTextHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + states: [ + State { + name: "shown" + when: playerAvailable + + PropertyChanges { + target: root + opacity: 1 + width: currentContentWidth + } + + }, + State { + name: "hidden" + when: !playerAvailable + + PropertyChanges { + target: root + opacity: 0 + width: 0 + } + + } + ] + transitions: [ + Transition { + from: "shown" + to: "hidden" + + SequentialAnimation { + PauseAnimation { + duration: 500 + } + + NumberAnimation { + properties: "opacity,width" + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + }, + Transition { + from: "hidden" + to: "shown" + + NumberAnimation { + properties: "opacity,width" + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + ] + + Row { + id: mediaRow + + anchors.centerIn: parent + spacing: Theme.spacingXS + + Row { + id: mediaInfo + + spacing: Theme.spacingXS + + AudioVisualization { + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + id: textContainer + + property string displayText: { + if (!activePlayer || !activePlayer.trackTitle) { + return ""; + } + + let identity = activePlayer.identity || ""; + let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium") || identity.toLowerCase().includes("edge") || identity.toLowerCase().includes("safari"); + let title = ""; + let subtitle = ""; + if (isWebMedia && activePlayer.trackTitle) { + title = activePlayer.trackTitle; + subtitle = activePlayer.trackArtist || identity; + } else { + title = activePlayer.trackTitle || "Unknown Track"; + subtitle = activePlayer.trackArtist || ""; + } + return subtitle.length > 0 ? title + " • " + subtitle : title; + } + + anchors.verticalCenter: parent.verticalCenter + width: textWidth + height: 20 + visible: SettingsData.mediaSize > 0 + clip: true + color: "transparent" + + StyledText { + id: mediaText + + property bool needsScrolling: implicitWidth > textContainer.width + property real scrollOffset: 0 + + anchors.verticalCenter: parent.verticalCenter + text: textContainer.displayText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + wrapMode: Text.NoWrap + x: needsScrolling ? -scrollOffset : 0 + onTextChanged: { + scrollOffset = 0; + scrollAnimation.restart(); + } + + SequentialAnimation { + id: scrollAnimation + + running: mediaText.needsScrolling && textContainer.visible + loops: Animation.Infinite + + PauseAnimation { + duration: 2000 + } + + NumberAnimation { + target: mediaText + property: "scrollOffset" + from: 0 + to: mediaText.implicitWidth - textContainer.width + 5 + duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60) + easing.type: Easing.Linear + } + + PauseAnimation { + duration: 2000 + } + + NumberAnimation { + target: mediaText + property: "scrollOffset" + to: 0 + duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60) + easing.type: Easing.Linear + } + + } + + } + + MouseArea { + anchors.fill: parent + enabled: root.playerAvailable && root.opacity > 0 && root.width > 0 && textContainer.visible + hoverEnabled: enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onPressed: { + if (root.popupTarget && root.popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = root.parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + root.popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, root.width, root.section, currentScreen); + } + root.clicked(); + } + } + + } + + } + + Row { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: 20 + height: 20 + radius: 10 + anchors.verticalCenter: parent.verticalCenter + color: prevArea.containsMouse ? Theme.primaryHover : "transparent" + visible: root.playerAvailable + opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3 + + DankIcon { + anchors.centerIn: parent + name: "skip_previous" + size: 12 + color: Theme.surfaceText + } + + MouseArea { + id: prevArea + + anchors.fill: parent + enabled: root.playerAvailable && root.width > 0 + hoverEnabled: enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + if (activePlayer) { + activePlayer.previous(); + } + } + } + + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + anchors.verticalCenter: parent.verticalCenter + color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover + visible: root.playerAvailable + opacity: activePlayer ? 1 : 0.3 + + DankIcon { + anchors.centerIn: parent + name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow" + size: 14 + color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary + } + + MouseArea { + anchors.fill: parent + enabled: root.playerAvailable && root.width > 0 + hoverEnabled: enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + if (activePlayer) { + activePlayer.togglePlaying(); + } + } + } + + } + + Rectangle { + width: 20 + height: 20 + radius: 10 + anchors.verticalCenter: parent.verticalCenter + color: nextArea.containsMouse ? Theme.primaryHover : "transparent" + visible: playerAvailable + opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3 + + DankIcon { + anchors.centerIn: parent + name: "skip_next" + size: 12 + color: Theme.surfaceText + } + + MouseArea { + id: nextArea + + anchors.fill: parent + enabled: root.playerAvailable && root.width > 0 + hoverEnabled: enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + if (activePlayer) { + activePlayer.next(); + } + } + } + + } + + } + + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/NetworkMonitor.qml b/quickshell/.config/quickshell/Modules/TopBar/NetworkMonitor.qml new file mode 100644 index 0000000..a10b9be --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/NetworkMonitor.qml @@ -0,0 +1,117 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Modules.ProcessList +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property int availableWidth: 400 + readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2 + readonly property int maxNormalWidth: 456 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + function formatNetworkSpeed(bytesPerSec) { + if (bytesPerSec < 1024) { + return bytesPerSec.toFixed(0) + " B/s"; + } else if (bytesPerSec < 1024 * 1024) { + return (bytesPerSec / 1024).toFixed(1) + " KB/s"; + } else if (bytesPerSec < 1024 * 1024 * 1024) { + return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"; + } else { + return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"; + } + } + + width: contentRow.implicitWidth + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = networkArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + Component.onCompleted: { + DgopService.addRef(["network"]); + } + Component.onDestruction: { + DgopService.removeRef(["network"]); + } + + MouseArea { + id: networkArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + + Row { + id: contentRow + + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "network_check" + size: Theme.iconSize - 8 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Row { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + StyledText { + text: "↓" + font.pixelSize: Theme.fontSizeSmall + color: Theme.info + } + + StyledText { + text: DgopService.networkRxRate > 0 ? formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + } + + Row { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + StyledText { + text: "↑" + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + } + + StyledText { + text: DgopService.networkTxRate > 0 ? formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + } + + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/NotepadButton.qml b/quickshell/.config/quickshell/Modules/TopBar/NotepadButton.qml new file mode 100644 index 0000000..25ae8b4 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/NotepadButton.qml @@ -0,0 +1,71 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool isActive: false + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property real widgetHeight: 30 + property real barHeight: 48 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + signal clicked() + + width: notepadIcon.width + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = notepadArea.containsMouse || root.isActive ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + + DankIcon { + id: notepadIcon + + anchors.centerIn: parent + name: "assignment" + size: Theme.iconSize - 6 + color: notepadArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText + } + + Rectangle { + width: 6 + height: 6 + radius: 3 + color: Theme.primary + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: SettingsData.topBarNoBackground ? 0 : 4 + anchors.topMargin: SettingsData.topBarNoBackground ? 0 : 4 + visible: SessionData.notepadContent.length > 0 + opacity: 0.8 + } + + MouseArea { + id: notepadArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.clicked(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/NotificationCenterButton.qml b/quickshell/.config/quickshell/Modules/TopBar/NotificationCenterButton.qml new file mode 100644 index 0000000..c70c8f6 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/NotificationCenterButton.qml @@ -0,0 +1,78 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool hasUnread: false + property bool isActive: false + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property real widgetHeight: 30 + property real barHeight: 48 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + signal clicked() + + width: notificationIcon.width + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = notificationArea.containsMouse || root.isActive ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + + DankIcon { + id: notificationIcon + + anchors.centerIn: parent + name: SessionData.doNotDisturb ? "notifications_off" : "notifications" + size: Theme.iconSize - 6 + color: SessionData.doNotDisturb ? Theme.error : (notificationArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText) + } + + Rectangle { + width: 8 + height: 8 + radius: 4 + color: Theme.error + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: SettingsData.topBarNoBackground ? 0 : 6 + anchors.topMargin: SettingsData.topBarNoBackground ? 0 : 6 + visible: root.hasUnread + } + + MouseArea { + id: notificationArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen); + } + root.clicked(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/PrivacyIndicator.qml b/quickshell/.config/quickshell/Modules/TopBar/PrivacyIndicator.qml new file mode 100644 index 0000000..1029692 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/PrivacyIndicator.qml @@ -0,0 +1,170 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS + readonly property bool hasActivePrivacy: PrivacyService.anyPrivacyActive + readonly property int activeCount: PrivacyService.microphoneActive + PrivacyService.cameraActive + PrivacyService.screensharingActive + readonly property real contentWidth: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0 + + width: hasActivePrivacy ? (contentWidth + horizontalPadding * 2) : 0 + height: hasActivePrivacy ? widgetHeight : 0 + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + visible: hasActivePrivacy + opacity: hasActivePrivacy ? 1 : 0 + enabled: hasActivePrivacy + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + return Qt.rgba(privacyArea.containsMouse ? Theme.errorPressed.r : Theme.errorHover.r, privacyArea.containsMouse ? Theme.errorPressed.g : Theme.errorHover.g, privacyArea.containsMouse ? Theme.errorPressed.b : Theme.errorHover.b, (privacyArea.containsMouse ? Theme.errorPressed.a : Theme.errorHover.a) * Theme.widgetTransparency); + } + + MouseArea { + // Privacy indicator click handler + + id: privacyArea + + anchors.fill: parent + hoverEnabled: hasActivePrivacy + enabled: hasActivePrivacy + cursorShape: Qt.PointingHandCursor + onClicked: { + } + } + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + visible: hasActivePrivacy + + Item { + width: 18 + height: 18 + visible: PrivacyService.microphoneActive + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "mic" + size: Theme.iconSizeSmall + color: Theme.error + filled: true + anchors.centerIn: parent + } + + } + + Item { + width: 18 + height: 18 + visible: PrivacyService.cameraActive + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "camera_video" + size: Theme.iconSizeSmall + color: Theme.surfaceText + filled: true + anchors.centerIn: parent + } + + Rectangle { + width: 6 + height: 6 + radius: 3 + color: Theme.error + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: -2 + anchors.topMargin: -1 + } + + } + + Item { + width: 18 + height: 18 + visible: PrivacyService.screensharingActive + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "screen_share" + size: Theme.iconSizeSmall + color: Theme.warning + filled: true + anchors.centerIn: parent + } + + } + + } + + Rectangle { + id: tooltip + + width: tooltipText.contentWidth + Theme.spacingM * 2 + height: tooltipText.contentHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.popupBackground() + border.color: Theme.outlineMedium + border.width: 1 + visible: false + opacity: privacyArea.containsMouse && hasActivePrivacy ? 1 : 0 + z: 100 + x: (parent.width - width) / 2 + y: -height - Theme.spacingXS + + StyledText { + id: tooltipText + + anchors.centerIn: parent + text: PrivacyService.getPrivacySummary() + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + + Rectangle { + width: 8 + height: 8 + color: parent.color + border.color: parent.border.color + border.width: parent.border.width + rotation: 45 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.bottom + anchors.topMargin: -4 + } + + Behavior on opacity { + enabled: hasActivePrivacy && root.visible + + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + } + + Behavior on width { + enabled: hasActivePrivacy && visible + + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/RamMonitor.qml b/quickshell/.config/quickshell/Modules/TopBar/RamMonitor.qml new file mode 100644 index 0000000..89bf7ae --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/RamMonitor.qml @@ -0,0 +1,99 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property bool showPercentage: true + property bool showIcon: true + property var toggleProcessList + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property real barHeight: 48 + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + width: ramContent.implicitWidth + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = ramArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + Component.onCompleted: { + DgopService.addRef(["memory"]); + } + Component.onDestruction: { + DgopService.removeRef(["memory"]); + } + + MouseArea { + id: ramArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen); + } + DgopService.setSortBy("memory"); + if (root.toggleProcessList) { + root.toggleProcessList(); + } + + } + } + + Row { + id: ramContent + + anchors.centerIn: parent + spacing: 3 + + DankIcon { + name: "developer_board" + size: Theme.iconSize - 8 + color: { + if (DgopService.memoryUsage > 90) { + return Theme.tempDanger; + } + + if (DgopService.memoryUsage > 75) { + return Theme.tempWarning; + } + + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (DgopService.memoryUsage === undefined || DgopService.memoryUsage === null || DgopService.memoryUsage === 0) { + return "--%"; + } + + return DgopService.memoryUsage.toFixed(0) + "%"; + } + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/RunningApps.qml b/quickshell/.config/quickshell/Modules/TopBar/RunningApps.qml new file mode 100644 index 0000000..9cb47e6 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/RunningApps.qml @@ -0,0 +1,437 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property string section: "left" + property var parentScreen + property var hoveredItem: null + property var topBar: null + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS + // The visual root for this window + property Item windowRoot: (Window.window ? Window.window.contentItem : null) + readonly property var sortedToplevels: { + if (SettingsData.runningAppsCurrentWorkspace) { + return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen.name); + } + return CompositorService.sortedToplevels; + } + readonly property int windowCount: sortedToplevels.length + readonly property int calculatedWidth: { + if (windowCount === 0) { + return 0; + } + if (SettingsData.runningAppsCompactMode) { + return windowCount * 24 + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2; + } else { + return windowCount * (24 + Theme.spacingXS + 120) + + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2; + } + } + + width: calculatedWidth + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + visible: windowCount > 0 + clip: false + color: { + if (windowCount === 0) { + return "transparent"; + } + + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + const baseColor = Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, + baseColor.a * Theme.widgetTransparency); + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + + property real scrollAccumulator: 0 + property real touchpadThreshold: 500 + + onWheel: (wheel) => { + const deltaY = wheel.angleDelta.y; + const isMouseWheel = Math.abs(deltaY) >= 120 + && (Math.abs(deltaY) % 120) === 0; + + const windows = root.sortedToplevels; + if (windows.length < 2) { + return; + } + + if (isMouseWheel) { + // Direct mouse wheel action + let currentIndex = -1; + for (let i = 0; i < windows.length; i++) { + if (windows[i].activated) { + currentIndex = i; + break; + } + } + + let nextIndex; + if (deltaY < 0) { + if (currentIndex === -1) { + nextIndex = 0; + } else { + nextIndex = (currentIndex + 1) % windows.length; + } + } else { + if (currentIndex === -1) { + nextIndex = windows.length - 1; + } else { + nextIndex = (currentIndex - 1 + windows.length) % windows.length; + } + } + + const nextWindow = windows[nextIndex]; + if (nextWindow) { + nextWindow.activate(); + } + } else { + // Touchpad - accumulate small deltas + scrollAccumulator += deltaY; + + if (Math.abs(scrollAccumulator) >= touchpadThreshold) { + let currentIndex = -1; + for (let i = 0; i < windows.length; i++) { + if (windows[i].activated) { + currentIndex = i; + break; + } + } + + let nextIndex; + if (scrollAccumulator < 0) { + if (currentIndex === -1) { + nextIndex = 0; + } else { + nextIndex = (currentIndex + 1) % windows.length; + } + } else { + if (currentIndex === -1) { + nextIndex = windows.length - 1; + } else { + nextIndex = (currentIndex - 1 + windows.length) % windows.length; + } + } + + const nextWindow = windows[nextIndex]; + if (nextWindow) { + nextWindow.activate(); + } + + scrollAccumulator = 0; + } + } + + wheel.accepted = true; + } + } + + Row { + id: windowRow + + anchors.centerIn: parent + spacing: Theme.spacingXS + + Repeater { + id: windowRepeater + + model: sortedToplevels + + delegate: Item { + id: delegateItem + + property bool isFocused: modelData.activated + property string appId: modelData.appId || "" + property string windowTitle: modelData.title || "(Unnamed)" + property var toplevelObject: modelData + property string tooltipText: { + let appName = "Unknown"; + if (appId) { + const desktopEntry = DesktopEntries.heuristicLookup(appId); + appName = desktopEntry + && desktopEntry.name ? desktopEntry.name : appId; + } + return appName + (windowTitle ? " • " + windowTitle : "") + } + + width: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120) + height: 24 + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: { + if (isFocused) { + return mouseArea.containsMouse ? Qt.rgba( + Theme.primary.r, + Theme.primary.g, + Theme.primary.b, + 0.3) : Qt.rgba( + Theme.primary.r, + Theme.primary.g, + Theme.primary.b, + 0.2); + } else { + return mouseArea.containsMouse ? Qt.rgba( + Theme.primaryHover.r, + Theme.primaryHover.g, + Theme.primaryHover.b, + 0.1) : "transparent"; + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + // App icon + IconImage { + id: iconImg + anchors.left: parent.left + anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - 18) / 2 : Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + width: 18 + height: 18 + source: { + const moddedId = Paths.moddedAppId(appId) + if (moddedId.toLowerCase().includes("steam_app")) { + return "" + } + return Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true) + } + smooth: true + mipmap: true + asynchronous: true + visible: status === Image.Ready + } + + DankIcon { + anchors.left: parent.left + anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - 18) / 2 : Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + size: 18 + name: "sports_esports" + color: Theme.surfaceText + visible: { + const moddedId = Paths.moddedAppId(appId) + return moddedId.toLowerCase().includes("steam_app") + } + } + + // Fallback text if no icon found + Text { + anchors.centerIn: parent + visible: { + const moddedId = Paths.moddedAppId(appId) + const isSteamApp = moddedId.toLowerCase().includes("steam_app") + return !iconImg.visible && !isSteamApp + } + text: { + if (!appId) { + return "?"; + } + + const desktopEntry = DesktopEntries.heuristicLookup(appId); + if (desktopEntry && desktopEntry.name) { + return desktopEntry.name.charAt(0).toUpperCase(); + } + + return appId.charAt(0).toUpperCase(); + } + font.pixelSize: 10 + color: Theme.surfaceText + font.weight: Font.Medium + } + + // Window title text (only visible in expanded mode) + StyledText { + anchors.left: iconImg.right + anchors.leftMargin: Theme.spacingXS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + visible: !SettingsData.runningAppsCompactMode + text: windowTitle + font.pixelSize: Theme.fontSizeMedium - 1 + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: (mouse) => { + if (mouse.button === Qt.LeftButton) { + if (toplevelObject) { + toplevelObject.activate(); + } + } else if (mouse.button === Qt.RightButton) { + if (tooltipLoader.item) { + tooltipLoader.item.hideTooltip(); + } + tooltipLoader.active = false; + + windowContextMenuLoader.active = true; + if (windowContextMenuLoader.item) { + windowContextMenuLoader.item.currentWindow = toplevelObject; + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0); + const screenX = root.parentScreen ? root.parentScreen.x : 0; + const screenY = root.parentScreen ? root.parentScreen.y : 0; + const relativeX = globalPos.x - screenX; + const yPos = Theme.barHeight + SettingsData.topBarSpacing - 7; + windowContextMenuLoader.item.showAt(relativeX, yPos); + } + } + } + onEntered: { + root.hoveredItem = delegateItem; + const globalPos = delegateItem.mapToGlobal( + delegateItem.width / 2, delegateItem.height); + tooltipLoader.active = true; + if (tooltipLoader.item) { + const tooltipY = Theme.barHeight + + SettingsData.topBarSpacing + Theme.spacingXS; + tooltipLoader.item.showTooltip( + delegateItem.tooltipText, globalPos.x, + tooltipY, root.parentScreen); + } + } + onExited: { + if (root.hoveredItem === delegateItem) { + root.hoveredItem = null; + if (tooltipLoader.item) { + tooltipLoader.item.hideTooltip(); + } + + tooltipLoader.active = false; + } + } + } + } + } + } + + Loader { + id: tooltipLoader + + active: false + + sourceComponent: RunningAppsTooltip {} + } + + Loader { + id: windowContextMenuLoader + active: false + sourceComponent: PanelWindow { + id: contextMenuWindow + + property var currentWindow: null + property bool isVisible: false + property point anchorPos: Qt.point(0, 0) + + function showAt(x, y) { + screen = root.parentScreen; + anchorPos = Qt.point(x, y); + isVisible = true; + visible = true; + } + + function close() { + isVisible = false; + visible = false; + windowContextMenuLoader.active = false; + } + + implicitWidth: 100 + implicitHeight: 40 + visible: false + color: "transparent" + + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + + anchors { + top: true + left: true + right: true + bottom: true + } + + MouseArea { + anchors.fill: parent + onClicked: contextMenuWindow.close(); + } + + Rectangle { + x: { + const left = 10; + const right = contextMenuWindow.width - width - 10; + const want = contextMenuWindow.anchorPos.x - width / 2; + return Math.max(left, Math.min(right, want)); + } + y: contextMenuWindow.anchorPos.y + width: 100 + height: 32 + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.width: 1 + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: closeMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + } + + StyledText { + anchors.centerIn: parent + text: "Close" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (contextMenuWindow.currentWindow) { + contextMenuWindow.currentWindow.close(); + } + contextMenuWindow.close(); + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/RunningAppsTooltip.qml b/quickshell/.config/quickshell/Modules/TopBar/RunningAppsTooltip.qml new file mode 100644 index 0000000..f16e047 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/RunningAppsTooltip.qml @@ -0,0 +1,67 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Common + +PanelWindow { + id: root + + property string tooltipText: "" + property real targetX: 0 + property real targetY: 0 + property var targetScreen: null + + function showTooltip(text, x, y, screen) { + tooltipText = text; + targetScreen = screen; + const screenX = screen ? screen.x : 0; + targetX = x - screenX; + targetY = y; + visible = true; + } + + function hideTooltip() { + visible = false; + } + + screen: targetScreen + implicitWidth: Math.min(300, Math.max(120, textContent.implicitWidth + Theme.spacingM * 2)) + implicitHeight: textContent.implicitHeight + Theme.spacingS * 2 + color: "transparent" + visible: false + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + + anchors { + top: true + left: true + } + + margins { + left: Math.round(targetX - implicitWidth / 2) + top: Math.round(targetY) + } + + Rectangle { + anchors.fill: parent + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.outlineMedium + + Text { + id: textContent + + anchors.centerIn: parent + text: root.tooltipText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + wrapMode: Text.NoWrap + maximumLineCount: 1 + elide: Text.ElideRight + width: parent.width - Theme.spacingM * 2 + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/SystemTrayBar.qml b/quickshell/.config/quickshell/Modules/TopBar/SystemTrayBar.qml new file mode 100644 index 0000000..b31e438 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/SystemTrayBar.qml @@ -0,0 +1,132 @@ +import QtQuick +import Quickshell +import Quickshell.Services.SystemTray +import Quickshell.Widgets +import qs.Common + +Rectangle { + id: root + + property var parentWindow: null + property var parentScreen: null + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS + readonly property int calculatedWidth: SystemTray.items.values.length > 0 ? SystemTray.items.values.length * 24 + horizontalPadding * 2 : 0 + + width: calculatedWidth + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SystemTray.items.values.length === 0) { + return "transparent"; + } + + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + visible: SystemTray.items.values.length > 0 + + Row { + id: systemTrayRow + + anchors.centerIn: parent + spacing: 0 + + Repeater { + model: SystemTray.items.values + + delegate: Item { + property var trayItem: modelData + property string iconSource: { + let icon = trayItem && trayItem.icon; + if (typeof icon === 'string' || icon instanceof String) { + if (icon.includes("?path=")) { + const split = icon.split("?path="); + if (split.length !== 2) { + return icon; + } + + const name = split[0]; + const path = split[1]; + const fileName = name.substring(name.lastIndexOf("/") + 1); + return `file://${path}/${fileName}`; + } + return icon; + } + return ""; + } + + width: 24 + height: 24 + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent" + + Behavior on color { + enabled: trayItemArea.containsMouse !== undefined + + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + } + + IconImage { + anchors.centerIn: parent + width: 16 + height: 16 + source: parent.iconSource + asynchronous: true + smooth: true + mipmap: true + } + + MouseArea { + id: trayItemArea + + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: (mouse) => { + if (!trayItem) { + return; + } + + if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) { + trayItem.activate(); + return ; + } + if (trayItem.hasMenu) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + menuAnchor.menu = trayItem.menu; + menuAnchor.anchor.window = parentWindow; + menuAnchor.anchor.rect = Qt.rect(relativeX, parentWindow.effectiveBarHeight + SettingsData.topBarSpacing, parent.width, 1); + menuAnchor.open(); + } + } + } + + } + + } + + } + + QsMenuAnchor { + id: menuAnchor + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/TopBar.qml b/quickshell/.config/quickshell/Modules/TopBar/TopBar.qml new file mode 100644 index 0000000..e214b62 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/TopBar.qml @@ -0,0 +1,1007 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris +import Quickshell.Services.Notifications +import Quickshell.Services.SystemTray +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Modules +import qs.Modules.TopBar +import qs.Services +import qs.Widgets + +PanelWindow { + id: root + + WlrLayershell.namespace: "quickshell:bar" + + property var modelData + property var notepadVariants: null + + function getNotepadInstanceForScreen() { + if (!notepadVariants || !notepadVariants.instances) return null + + for (var i = 0; i < notepadVariants.instances.length; i++) { + var loader = notepadVariants.instances[i] + if (loader.modelData && loader.modelData.name === root.screen?.name) { + return loader.ensureLoaded() + } + } + return null + } + property string screenName: modelData.name + readonly property int notificationCount: NotificationService.notifications.length + readonly property real effectiveBarHeight: Math.max(root.widgetHeight + SettingsData.topBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.topBarInnerPadding)) + readonly property real widgetHeight: Math.max(20, 26 + SettingsData.topBarInnerPadding * 0.6) + + screen: modelData + implicitHeight: effectiveBarHeight + SettingsData.topBarSpacing + color: "transparent" + Component.onCompleted: { + const fonts = Qt.fontFamilies() + if (fonts.indexOf("Material Symbols Rounded") === -1) { + ToastService.showError("Please install Material Symbols Rounded and Restart your Shell. See README.md for instructions") + } + + SettingsData.forceTopBarLayoutRefresh.connect(() => { + Qt.callLater(() => { + leftSection.visible = false + centerSection.visible = false + rightSection.visible = false + Qt.callLater(() => { + leftSection.visible = true + centerSection.visible = true + rightSection.visible = true + }) + }) + }) + + updateGpuTempConfig() + Qt.callLater(() => Qt.callLater(forceWidgetRefresh)) + } + + function forceWidgetRefresh() { + const sections = [leftSection, centerSection, rightSection] + sections.forEach(section => section && (section.visible = false)) + Qt.callLater(() => sections.forEach(section => section && (section.visible = true))) + } + + function updateGpuTempConfig() { + const allWidgets = [...(SettingsData.topBarLeftWidgets || []), ...(SettingsData.topBarCenterWidgets || []), ...(SettingsData.topBarRightWidgets || [])] + + const hasGpuTempWidget = allWidgets.some(widget => { + const widgetId = typeof widget === "string" ? widget : widget.id + const widgetEnabled = typeof widget === "string" ? true : (widget.enabled !== false) + return widgetId === "gpuTemp" && widgetEnabled + }) + + DgopService.gpuTempEnabled = hasGpuTempWidget || SessionData.nvidiaGpuTempEnabled || SessionData.nonNvidiaGpuTempEnabled + DgopService.nvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nvidiaGpuTempEnabled + DgopService.nonNvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nonNvidiaGpuTempEnabled + } + + Connections { + function onTopBarLeftWidgetsChanged() { + root.updateGpuTempConfig() + } + + function onTopBarCenterWidgetsChanged() { + root.updateGpuTempConfig() + } + + function onTopBarRightWidgetsChanged() { + root.updateGpuTempConfig() + } + + target: SettingsData + } + + Connections { + function onNvidiaGpuTempEnabledChanged() { + root.updateGpuTempConfig() + } + + function onNonNvidiaGpuTempEnabledChanged() { + root.updateGpuTempConfig() + } + + target: SessionData + } + + Connections { + target: root.screen + function onGeometryChanged() { + if (centerSection?.width > 0) { + Qt.callLater(centerSection.updateLayout) + } + } + } + + anchors { + top: true + left: true + right: true + } + + exclusiveZone: (!SettingsData.topBarVisible || topBarCore.autoHide) ? -1 : root.effectiveBarHeight + SettingsData.topBarSpacing - 2 + SettingsData.topBarBottomGap + + mask: Region { + item: topBarMouseArea + } + + Item { + id: topBarCore + anchors.fill: parent + + property real backgroundTransparency: SettingsData.topBarTransparency + property bool autoHide: SettingsData.topBarAutoHide + property bool reveal: { + // Handle Niri overview state first + if (CompositorService.isNiri && NiriService.inOverview) { + // If Show on Overview is enabled, show the bar + if (SettingsData.topBarOpenOnOverview) { + return true + } + // If Show on Overview is disabled, hide the bar + return false + } + // Normal visibility logic when not in overview + return SettingsData.topBarVisible && (!autoHide || topBarMouseArea.containsMouse || hasActivePopout) + } + + property var notepadInstance: null + property bool notepadInstanceVisible: notepadInstance?.notepadVisible ?? false + + readonly property bool hasActivePopout: { + const loaders = [{ + "loader": appDrawerLoader, + "prop": "shouldBeVisible" + }, { + "loader": dankDashPopoutLoader, + "prop": "shouldBeVisible" + }, { + "loader": processListPopoutLoader, + "prop": "shouldBeVisible" + }, { + "loader": notificationCenterLoader, + "prop": "shouldBeVisible" + }, { + "loader": batteryPopoutLoader, + "prop": "shouldBeVisible" + }, { + "loader": vpnPopoutLoader, + "prop": "shouldBeVisible" + }, { + "loader": controlCenterLoader, + "prop": "shouldBeVisible" + }, { + "loader": clipboardHistoryModalPopup, + "prop": "visible" + }] + return notepadInstanceVisible || loaders.some(item => { + if (item.loader) { + return item.loader?.item?.[item.prop] + } + return false + }) + } + + Component.onCompleted: { + notepadInstance = root.getNotepadInstanceForScreen() + } + + Connections { + function onTopBarTransparencyChanged() { + topBarCore.backgroundTransparency = SettingsData.topBarTransparency + } + + target: SettingsData + } + + MouseArea { + id: topBarMouseArea + height: parent.reveal ? root.effectiveBarHeight + SettingsData.topBarSpacing : 4 + anchors { + top: parent.top + left: parent.left + right: parent.right + } + hoverEnabled: true + + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + Item { + id: topBarContainer + anchors.fill: parent + + transform: Translate { + id: topBarSlide + y: topBarCore.reveal ? 0 : -(root.effectiveBarHeight - 4) + + Behavior on y { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + Item { + anchors.fill: parent + anchors.topMargin: SettingsData.topBarSpacing + anchors.bottomMargin: 0 + anchors.leftMargin: SettingsData.topBarSpacing + anchors.rightMargin: SettingsData.topBarSpacing + + Rectangle { + anchors.fill: parent + radius: SettingsData.topBarSquareCorners ? 0 : Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, topBarCore.backgroundTransparency) + layer.enabled: true + + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: Theme.outlineMedium + border.width: 1 + radius: parent.radius + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04) + radius: parent.radius + + SequentialAnimation on opacity { + running: false + loops: Animation.Infinite + + NumberAnimation { + to: 0.08 + duration: Theme.extraLongDuration + easing.type: Theme.standardEasing + } + + NumberAnimation { + to: 0.02 + duration: Theme.extraLongDuration + easing.type: Theme.standardEasing + } + } + } + + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 4 + shadowBlur: 0.5 // radius/32, adjusted for visual match + shadowColor: Qt.rgba(0, 0, 0, 0.15) + shadowOpacity: 0.15 + } + } + + Item { + id: topBarContent + + readonly property int availableWidth: width + readonly property int launcherButtonWidth: 40 + readonly property int workspaceSwitcherWidth: 120 // Approximate + readonly property int focusedAppMaxWidth: 456 // Fixed width since we don't have focusedApp reference + readonly property int estimatedLeftSectionWidth: launcherButtonWidth + workspaceSwitcherWidth + focusedAppMaxWidth + (Theme.spacingXS * 2) + readonly property int rightSectionWidth: rightSection.width + readonly property int clockWidth: 120 // Approximate clock width + readonly property int mediaMaxWidth: 280 // Normal max width + readonly property int weatherWidth: 80 // Approximate weather width + readonly property bool validLayout: availableWidth > 100 && estimatedLeftSectionWidth > 0 && rightSectionWidth > 0 + readonly property int clockLeftEdge: (availableWidth - clockWidth) / 2 + readonly property int clockRightEdge: clockLeftEdge + clockWidth + readonly property int leftSectionRightEdge: estimatedLeftSectionWidth + readonly property int mediaLeftEdge: clockLeftEdge - mediaMaxWidth - Theme.spacingS + readonly property int rightSectionLeftEdge: availableWidth - rightSectionWidth + readonly property int leftToClockGap: Math.max(0, clockLeftEdge - leftSectionRightEdge) + readonly property int leftToMediaGap: mediaMaxWidth > 0 ? Math.max(0, mediaLeftEdge - leftSectionRightEdge) : leftToClockGap + readonly property int mediaToClockGap: mediaMaxWidth > 0 ? Theme.spacingS : 0 + readonly property int clockToRightGap: validLayout ? Math.max(0, rightSectionLeftEdge - clockRightEdge) : 1000 + readonly property bool spacingTight: validLayout && (leftToMediaGap < 150 || clockToRightGap < 100) + readonly property bool overlapping: validLayout && (leftToMediaGap < 100 || clockToRightGap < 50) + + function getWidgetEnabled(enabled) { + return enabled !== false + } + + function getWidgetSection(parentItem) { + if (!parentItem?.parent) { + return "left" + } + if (parentItem.parent === leftSection) { + return "left" + } + if (parentItem.parent === rightSection) { + return "right" + } + if (parentItem.parent === centerSection) { + return "center" + } + return "left" + } + + readonly property var widgetVisibility: ({ + "cpuUsage": DgopService.dgopAvailable, + "memUsage": DgopService.dgopAvailable, + "cpuTemp": DgopService.dgopAvailable, + "gpuTemp": DgopService.dgopAvailable, + "network_speed_monitor": DgopService.dgopAvailable + }) + + function getWidgetVisible(widgetId) { + return widgetVisibility[widgetId] ?? true + } + + readonly property var componentMap: ({ + "launcherButton": launcherButtonComponent, + "workspaceSwitcher": workspaceSwitcherComponent, + "focusedWindow": focusedWindowComponent, + "runningApps": runningAppsComponent, + "clock": clockComponent, + "music": mediaComponent, + "weather": weatherComponent, + "systemTray": systemTrayComponent, + "privacyIndicator": privacyIndicatorComponent, + "clipboard": clipboardComponent, + "cpuUsage": cpuUsageComponent, + "memUsage": memUsageComponent, + "cpuTemp": cpuTempComponent, + "gpuTemp": gpuTempComponent, + "notificationButton": notificationButtonComponent, + "battery": batteryComponent, + "controlCenterButton": controlCenterButtonComponent, + "idleInhibitor": idleInhibitorComponent, + "spacer": spacerComponent, + "separator": separatorComponent, + "network_speed_monitor": networkComponent, + "keyboard_layout_name": keyboardLayoutNameComponent, + "vpn": vpnComponent, + "notepadButton": notepadButtonComponent + }) + + function getWidgetComponent(widgetId) { + return componentMap[widgetId] || null + } + + anchors.fill: parent + anchors.leftMargin: Math.max(Theme.spacingXS, SettingsData.topBarInnerPadding * 0.8) + anchors.rightMargin: Math.max(Theme.spacingXS, SettingsData.topBarInnerPadding * 0.8) + anchors.topMargin: SettingsData.topBarInnerPadding / 2 + anchors.bottomMargin: SettingsData.topBarInnerPadding / 2 + clip: true + + Row { + id: leftSection + + height: parent.height + spacing: SettingsData.topBarNoBackground ? 2 : Theme.spacingXS + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + Repeater { + model: SettingsData.topBarLeftWidgetsModel + + Loader { + property string widgetId: model.widgetId + property var widgetData: model + property int spacerSize: model.size || 20 + + anchors.verticalCenter: parent ? parent.verticalCenter : undefined + active: topBarContent.getWidgetVisible(model.widgetId) + sourceComponent: topBarContent.getWidgetComponent(model.widgetId) + opacity: topBarContent.getWidgetEnabled(model.enabled) ? 1 : 0 + asynchronous: false + } + } + } + + Item { + id: centerSection + + property var centerWidgets: [] + property int totalWidgets: 0 + property real totalWidth: 0 + property real spacing: SettingsData.topBarNoBackground ? 2 : Theme.spacingXS + + function updateLayout() { + if (width <= 0 || height <= 0 || !visible) { + Qt.callLater(updateLayout) + return + } + + centerWidgets = [] + totalWidgets = 0 + totalWidth = 0 + + for (var i = 0; i < centerRepeater.count; i++) { + const item = centerRepeater.itemAt(i) + if (item?.active && item.item) { + centerWidgets.push(item.item) + totalWidgets++ + totalWidth += item.item.width + } + } + + if (totalWidgets > 1) { + totalWidth += spacing * (totalWidgets - 1) + } + positionWidgets() + } + + function positionWidgets() { + if (totalWidgets === 0 || width <= 0) { + return + } + + const parentCenterX = width / 2 + const isOdd = totalWidgets % 2 === 1 + + centerWidgets.forEach(widget => widget.anchors.horizontalCenter = undefined) + + if (isOdd) { + const middleIndex = Math.floor(totalWidgets / 2) + const middleWidget = centerWidgets[middleIndex] + middleWidget.x = parentCenterX - (middleWidget.width / 2) + + let currentX = middleWidget.x + for (var i = middleIndex - 1; i >= 0; i--) { + currentX -= (spacing + centerWidgets[i].width) + centerWidgets[i].x = currentX + } + + currentX = middleWidget.x + middleWidget.width + for (var i = middleIndex + 1; i < totalWidgets; i++) { + currentX += spacing + centerWidgets[i].x = currentX + currentX += centerWidgets[i].width + } + } else { + const leftIndex = (totalWidgets / 2) - 1 + const rightIndex = totalWidgets / 2 + const halfSpacing = spacing / 2 + + centerWidgets[leftIndex].x = parentCenterX - halfSpacing - centerWidgets[leftIndex].width + centerWidgets[rightIndex].x = parentCenterX + halfSpacing + + let currentX = centerWidgets[leftIndex].x + for (var i = leftIndex - 1; i >= 0; i--) { + currentX -= (spacing + centerWidgets[i].width) + centerWidgets[i].x = currentX + } + + currentX = centerWidgets[rightIndex].x + centerWidgets[rightIndex].width + for (var i = rightIndex + 1; i < totalWidgets; i++) { + currentX += spacing + centerWidgets[i].x = currentX + currentX += centerWidgets[i].width + } + } + } + + height: parent.height + width: parent.width + anchors.centerIn: parent + Component.onCompleted: { + Qt.callLater(() => { + Qt.callLater(updateLayout) + }) + } + + onWidthChanged: { + if (width > 0) { + Qt.callLater(updateLayout) + } + } + + onVisibleChanged: { + if (visible && width > 0) { + Qt.callLater(updateLayout) + } + } + + Repeater { + id: centerRepeater + + model: SettingsData.topBarCenterWidgetsModel + + Loader { + property string widgetId: model.widgetId + property var widgetData: model + property int spacerSize: model.size || 20 + + anchors.verticalCenter: parent ? parent.verticalCenter : undefined + active: topBarContent.getWidgetVisible(model.widgetId) + sourceComponent: topBarContent.getWidgetComponent(model.widgetId) + opacity: topBarContent.getWidgetEnabled(model.enabled) ? 1 : 0 + asynchronous: false + + onLoaded: { + if (!item) { + return + } + item.onWidthChanged.connect(centerSection.updateLayout) + if (model.widgetId === "spacer") { + item.spacerSize = Qt.binding(() => model.size || 20) + } + Qt.callLater(centerSection.updateLayout) + } + onActiveChanged: { + Qt.callLater(centerSection.updateLayout) + } + } + } + + Connections { + function onCountChanged() { + Qt.callLater(centerSection.updateLayout) + } + + target: SettingsData.topBarCenterWidgetsModel + } + } + + Row { + id: rightSection + + height: parent.height + spacing: SettingsData.topBarNoBackground ? 2 : Theme.spacingXS + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Repeater { + model: SettingsData.topBarRightWidgetsModel + + Loader { + property string widgetId: model.widgetId + property var widgetData: model + property int spacerSize: model.size || 20 + + anchors.verticalCenter: parent ? parent.verticalCenter : undefined + active: topBarContent.getWidgetVisible(model.widgetId) + sourceComponent: topBarContent.getWidgetComponent(model.widgetId) + opacity: topBarContent.getWidgetEnabled(model.enabled) ? 1 : 0 + asynchronous: false + } + } + } + + Component { + id: clipboardComponent + + Rectangle { + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (root.widgetHeight / 30)) + width: clipboardIcon.width + horizontalPadding * 2 + height: root.widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent" + } + const baseColor = clipboardArea.containsMouse ? Theme.primaryHover : Theme.secondaryHover + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency) + } + + DankIcon { + id: clipboardIcon + anchors.centerIn: parent + name: "content_paste" + size: Theme.iconSize - 6 + color: Theme.surfaceText + } + + MouseArea { + id: clipboardArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + clipboardHistoryModalPopup.toggle() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + Component { + id: launcherButtonComponent + + LauncherButton { + isActive: false + widgetHeight: root.widgetHeight + barHeight: root.effectiveBarHeight + section: topBarContent.getWidgetSection(parent) + popupTarget: appDrawerLoader.item + parentScreen: root.screen + onClicked: { + appDrawerLoader.active = true + appDrawerLoader.item?.toggle() + } + } + } + + Component { + id: workspaceSwitcherComponent + + WorkspaceSwitcher { + screenName: root.screenName + widgetHeight: root.widgetHeight + } + } + + Component { + id: focusedWindowComponent + + FocusedApp { + availableWidth: topBarContent.leftToMediaGap + widgetHeight: root.widgetHeight + } + } + + Component { + id: runningAppsComponent + + RunningApps { + widgetHeight: root.widgetHeight + section: topBarContent.getWidgetSection(parent) + parentScreen: root.screen + topBar: topBarContent + } + } + + Component { + id: clockComponent + + Clock { + compactMode: topBarContent.overlapping + barHeight: root.effectiveBarHeight + widgetHeight: root.widgetHeight + section: topBarContent.getWidgetSection(parent) || "center" + popupTarget: { + dankDashPopoutLoader.active = true + return dankDashPopoutLoader.item + } + parentScreen: root.screen + onClockClicked: { + dankDashPopoutLoader.active = true + if (dankDashPopoutLoader.item) { + dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible + dankDashPopoutLoader.item.currentTabIndex = 0 // Overview tab + } + } + } + } + + Component { + id: mediaComponent + + Media { + compactMode: topBarContent.spacingTight || topBarContent.overlapping + barHeight: root.effectiveBarHeight + widgetHeight: root.widgetHeight + section: topBarContent.getWidgetSection(parent) || "center" + popupTarget: { + dankDashPopoutLoader.active = true + return dankDashPopoutLoader.item + } + parentScreen: root.screen + onClicked: { + dankDashPopoutLoader.active = true + if (dankDashPopoutLoader.item) { + dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible + dankDashPopoutLoader.item.currentTabIndex = 1 // Media tab + } + } + } + } + + Component { + id: weatherComponent + + Weather { + barHeight: root.effectiveBarHeight + widgetHeight: root.widgetHeight + section: topBarContent.getWidgetSection(parent) || "center" + popupTarget: { + dankDashPopoutLoader.active = true + return dankDashPopoutLoader.item + } + parentScreen: root.screen + onClicked: { + dankDashPopoutLoader.active = true + if (dankDashPopoutLoader.item) { + dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible + dankDashPopoutLoader.item.currentTabIndex = 2 // Weather tab + } + } + } + } + + Component { + id: systemTrayComponent + + SystemTrayBar { + parentWindow: root + parentScreen: root.screen + widgetHeight: root.widgetHeight + visible: SettingsData.getFilteredScreens("systemTray").includes(root.screen) + } + } + + Component { + id: privacyIndicatorComponent + + PrivacyIndicator { + widgetHeight: root.widgetHeight + section: topBarContent.getWidgetSection(parent) || "right" + parentScreen: root.screen + } + } + + Component { + id: cpuUsageComponent + + CpuMonitor { + barHeight: root.effectiveBarHeight + widgetHeight: root.widgetHeight + section: topBarContent.getWidgetSection(parent) || "right" + popupTarget: { + processListPopoutLoader.active = true + return processListPopoutLoader.item + } + parentScreen: root.screen + toggleProcessList: () => { + processListPopoutLoader.active = true + return processListPopoutLoader.item?.toggle() + } + } + } + + Component { + id: memUsageComponent + + RamMonitor { + barHeight: root.effectiveBarHeight + widgetHeight: root.widgetHeight + section: topBarContent.getWidgetSection(parent) || "right" + popupTarget: { + processListPopoutLoader.active = true + return processListPopoutLoader.item + } + parentScreen: root.screen + toggleProcessList: () => { + processListPopoutLoader.active = true + return processListPopoutLoader.item?.toggle() + } + } + } + + Component { + id: cpuTempComponent + + CpuTemperature { + barHeight: root.effectiveBarHeight + widgetHeight: root.widgetHeight + section: topBarContent.getWidgetSection(parent) || "right" + popupTarget: { + processListPopoutLoader.active = true + return processListPopoutLoader.item + } + parentScreen: root.screen + toggleProcessList: () => { + processListPopoutLoader.active = true + return processListPopoutLoader.item?.toggle() + } + } + } + + Component { + id: gpuTempComponent + + GpuTemperature { + barHeight: root.effectiveBarHeight + widgetHeight: root.widgetHeight + section: topBarContent.getWidgetSection(parent) || "right" + popupTarget: { + processListPopoutLoader.active = true + return processListPopoutLoader.item + } + parentScreen: root.screen + widgetData: parent.widgetData + toggleProcessList: () => { + processListPopoutLoader.active = true + return processListPopoutLoader.item?.toggle() + } + } + } + + Component { + id: networkComponent + + NetworkMonitor {} + } + + Component { + id: notificationButtonComponent + + NotificationCenterButton { + hasUnread: root.notificationCount > 0 + isActive: notificationCenterLoader.item ? notificationCenterLoader.item.shouldBeVisible : false + widgetHeight: root.widgetHeight + barHeight: root.effectiveBarHeight + section: topBarContent.getWidgetSection(parent) || "right" + popupTarget: { + notificationCenterLoader.active = true + return notificationCenterLoader.item + } + parentScreen: root.screen + onClicked: { + notificationCenterLoader.active = true + notificationCenterLoader.item?.toggle() + } + } + } + + Component { + id: batteryComponent + + Battery { + batteryPopupVisible: batteryPopoutLoader.item ? batteryPopoutLoader.item.shouldBeVisible : false + widgetHeight: root.widgetHeight + barHeight: root.effectiveBarHeight + section: topBarContent.getWidgetSection(parent) || "right" + popupTarget: { + batteryPopoutLoader.active = true + return batteryPopoutLoader.item + } + parentScreen: root.screen + onToggleBatteryPopup: { + batteryPopoutLoader.active = true + batteryPopoutLoader.item?.toggle() + } + } + } + + Component { + id: vpnComponent + + Vpn { + widgetHeight: root.widgetHeight + barHeight: root.effectiveBarHeight + section: topBarContent.getWidgetSection(parent) || "right" + popupTarget: { + vpnPopoutLoader.active = true + return vpnPopoutLoader.item + } + parentScreen: root.screen + onToggleVpnPopup: { + vpnPopoutLoader.active = true + vpnPopoutLoader.item?.toggle() + } + } + } + + Component { + id: controlCenterButtonComponent + + ControlCenterButton { + isActive: controlCenterLoader.item ? controlCenterLoader.item.shouldBeVisible : false + widgetHeight: root.widgetHeight + barHeight: root.effectiveBarHeight + section: topBarContent.getWidgetSection(parent) || "right" + popupTarget: { + controlCenterLoader.active = true + return controlCenterLoader.item + } + parentScreen: root.screen + widgetData: parent.widgetData + onClicked: { + controlCenterLoader.active = true + if (!controlCenterLoader.item) { + return + } + controlCenterLoader.item.triggerScreen = root.screen + controlCenterLoader.item.toggle() + if (controlCenterLoader.item.shouldBeVisible && NetworkService.wifiEnabled) { + NetworkService.scanWifi() + } + } + } + } + + Component { + id: idleInhibitorComponent + + IdleInhibitor { + widgetHeight: root.widgetHeight + section: topBarContent.getWidgetSection(parent) || "right" + parentScreen: root.screen + } + } + + Component { + id: spacerComponent + + Item { + width: parent.spacerSize || 20 + height: root.widgetHeight + + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + border.width: 1 + radius: 2 + visible: false + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.visible = true + onExited: parent.visible = false + } + } + } + } + + Component { + id: separatorComponent + + Rectangle { + width: 1 + height: root.widgetHeight * 0.67 + color: Theme.outline + opacity: 0.3 + } + } + + Component { + id: keyboardLayoutNameComponent + + KeyboardLayoutName {} + } + + Component { + id: notepadButtonComponent + + NotepadButton { + property var notepadInstance: topBarCore.notepadInstance + isActive: notepadInstance?.notepadVisible ?? false + widgetHeight: root.widgetHeight + barHeight: root.effectiveBarHeight + section: topBarContent.getWidgetSection(parent) || "right" + popupTarget: notepadInstance + parentScreen: root.screen + onClicked: { + if (notepadInstance) { + notepadInstance.toggle() + } + } + } + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/Vpn.qml b/quickshell/.config/quickshell/Modules/TopBar/Vpn.qml new file mode 100644 index 0000000..4aa3626 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/Vpn.qml @@ -0,0 +1,113 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + // Passed in by TopBar + property int widgetHeight: 28 + property int barHeight: 32 + property string section: "right" + property var popupTarget: null + property var parentScreen: null + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + signal toggleVpnPopup() + + width: Theme.iconSize + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const base = clickArea.containsMouse || (popupTarget && popupTarget.shouldBeVisible) ? Theme.primaryPressed : Theme.secondaryHover; + return Qt.rgba(base.r, base.g, base.b, base.a * Theme.widgetTransparency); + } + + DankIcon { + id: icon + + name: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off") + size: Theme.iconSize - 6 + color: VpnService.connected ? Theme.primary : Theme.surfaceText + anchors.centerIn: parent + + RotationAnimation on rotation { + running: VpnService.isBusy + loops: Animation.Infinite + from: 0 + to: 360 + duration: 900 + } + + } + + MouseArea { + id: clickArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen); + } + root.toggleVpnPopup(); + } + } + + Rectangle { + id: tooltip + + width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2) + height: tooltipText.contentHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.surfaceVariantAlpha + border.width: 1 + visible: clickArea.containsMouse && !(popupTarget && popupTarget.shouldBeVisible) + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + opacity: clickArea.containsMouse ? 1 : 0 + + Text { + id: tooltipText + + anchors.centerIn: parent + text: { + if (!VpnService.connected) { + return "VPN Disconnected"; + } + + const names = VpnService.activeNames || []; + if (names.length <= 1) { + return "VPN Connected • " + (names[0] || ""); + } + + return "VPN Connected • " + names[0] + " +" + (names.length - 1); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/VpnPopout.qml b/quickshell/.config/quickshell/Modules/TopBar/VpnPopout.qml new file mode 100644 index 0000000..7145bf4 --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/VpnPopout.qml @@ -0,0 +1,418 @@ +// No external details import; content inlined for consistency + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +DankPopout { + id: root + + property string triggerSection: "right" + property var triggerScreen: null + + function setTriggerPosition(x, y, width, section, screen) { + triggerX = x; + triggerY = y; + triggerWidth = width; + triggerSection = section; + triggerScreen = screen; + } + + popupWidth: 360 + popupHeight: Math.min(Screen.height - 100, contentLoader.item ? contentLoader.item.implicitHeight : 260) + triggerX: Screen.width - 380 - Theme.spacingL + triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS + triggerWidth: 70 + positioning: "center" + screen: triggerScreen + shouldBeVisible: false + visible: shouldBeVisible + + content: Component { + Rectangle { + id: content + + implicitHeight: contentColumn.height + Theme.spacingL * 2 + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.color: Theme.outlineMedium + border.width: 1 + antialiasing: true + smooth: true + focus: true + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Escape) { + root.close(); + event.accepted = true; + } + } + + // Outer subtle shadow rings to match BatteryPopout + Rectangle { + anchors.fill: parent + anchors.margins: -3 + color: "transparent" + radius: parent.radius + 3 + border.color: Qt.rgba(0, 0, 0, 0.05) + border.width: 1 + z: -3 + } + + Rectangle { + anchors.fill: parent + anchors.margins: -2 + color: "transparent" + radius: parent.radius + 2 + border.color: Theme.shadowMedium + border.width: 1 + z: -2 + } + + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: Theme.outlineStrong + border.width: 1 + radius: parent.radius + z: -1 + } + + Column { + id: contentColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Item { + width: parent.width + height: 32 + + StyledText { + text: "VPN Connections" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + // Close button (matches BatteryPopout) + Rectangle { + width: 32 + height: 32 + radius: 16 + color: closeArea.containsMouse ? Theme.errorHover : "transparent" + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "close" + size: Theme.iconSize - 4 + color: closeArea.containsMouse ? Theme.error : Theme.surfaceText + } + + MouseArea { + id: closeArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: root.close() + } + + } + + } + + // Inlined VPN details + Rectangle { + id: vpnDetail + + width: parent.width + implicitHeight: detailsColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Theme.outlineStrong + border.width: 1 + clip: true + + Column { + id: detailsColumn + + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + RowLayout { + spacing: Theme.spacingS + width: parent.width + + StyledText { + text: { + if (!VpnService.connected) { + return "Active: None"; + } + + const names = VpnService.activeNames || []; + if (names.length <= 1) { + return "Active: " + (names[0] || "VPN"); + } + + return "Active: " + names[0] + " +" + (names.length - 1); + } + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + Item { + Layout.fillWidth: true + height: 1 + } + + // Removed Quick Connect for clarity + Item { + width: 1 + height: 1 + } + + // Disconnect all (shown only when any active) + Rectangle { + height: 28 + radius: 14 + color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight + visible: VpnService.connected + width: 130 + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + border.width: 1 + border.color: Theme.outlineLight + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "link_off" + size: Theme.fontSizeSmall + color: Theme.surfaceText + } + + StyledText { + text: "Disconnect" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + } + + MouseArea { + id: discAllArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: VpnService.disconnectAllActive() + } + + } + + } + + Rectangle { + height: 1 + width: parent.width + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + DankFlickable { + width: parent.width + height: 160 + contentHeight: listCol.height + clip: true + + Column { + id: listCol + + width: parent.width + spacing: Theme.spacingXS + + Item { + width: parent.width + height: VpnService.profiles.length === 0 ? 120 : 0 + visible: height > 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "playlist_remove" + size: 36 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: "No VPN profiles found" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: "Add a VPN in NetworkManager" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + } + + } + + Repeater { + model: VpnService.profiles + + delegate: Rectangle { + required property var modelData + + width: parent ? parent.width : 300 + height: 50 + radius: Theme.cornerRadius + color: rowArea.containsMouse ? Theme.primaryHoverLight : (VpnService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight) + border.width: VpnService.isActiveUuid(modelData.uuid) ? 2 : 1 + border.color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: VpnService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off" + size: Theme.iconSize - 4 + color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText + Layout.alignment: Qt.AlignVCenter + } + + Column { + spacing: 2 + Layout.alignment: Qt.AlignVCenter + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText + } + + StyledText { + text: { + if (modelData.type === "wireguard") { + return "WireGuard"; + } + + const svc = modelData.serviceType || ""; + if (svc.indexOf("openvpn") !== -1) { + return "OpenVPN"; + } + + if (svc.indexOf("wireguard") !== -1) { + return "WireGuard (plugin)"; + } + + if (svc.indexOf("openconnect") !== -1) { + return "OpenConnect"; + } + + if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1) { + return "Fortinet"; + } + + if (svc.indexOf("strongswan") !== -1) { + return "IPsec (strongSwan)"; + } + + if (svc.indexOf("libreswan") !== -1) { + return "IPsec (Libreswan)"; + } + + if (svc.indexOf("l2tp") !== -1) { + return "L2TP/IPsec"; + } + + if (svc.indexOf("pptp") !== -1) { + return "PPTP"; + } + + if (svc.indexOf("vpnc") !== -1) { + return "Cisco (vpnc)"; + } + + if (svc.indexOf("sstp") !== -1) { + return "SSTP"; + } + + if (svc) { + const parts = svc.split('.'); + return parts[parts.length - 1]; + } + return "VPN"; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + + } + + Item { + Layout.fillWidth: true + height: 1 + } + + } + + MouseArea { + id: rowArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: VpnService.toggle(modelData.uuid) + } + + } + + } + + Item { + height: 1 + width: 1 + } + + } + + } + + } + + } + + } + + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/Weather.qml b/quickshell/.config/quickshell/Modules/TopBar/Weather.qml new file mode 100644 index 0000000..afd348a --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/Weather.qml @@ -0,0 +1,98 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property string section: "center" + property var popupTarget: null + property var parentScreen: null + property real barHeight: 48 + property real widgetHeight: 30 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS + + signal clicked() + + visible: SettingsData.weatherEnabled + width: visible ? Math.min(100, weatherRow.implicitWidth + horizontalPadding * 2) : 0 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) { + return "transparent"; + } + + const baseColor = weatherArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + + Ref { + service: WeatherService + } + + Row { + id: weatherRow + + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: WeatherService.getWeatherIcon(WeatherService.weather.wCode) + size: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp; + if (temp === undefined || temp === null || temp === 0) { + return "--°" + (SettingsData.useFahrenheit ? "F" : "C"); + } + + return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C"); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + } + + MouseArea { + id: weatherArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popupTarget && popupTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0); + const currentScreen = parentScreen || Screen; + const screenX = currentScreen.x || 0; + const relativeX = globalPos.x - screenX; + popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen); + } + root.clicked(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + + } + +} diff --git a/quickshell/.config/quickshell/Modules/TopBar/WorkspaceSwitcher.qml b/quickshell/.config/quickshell/Modules/TopBar/WorkspaceSwitcher.qml new file mode 100644 index 0000000..59cabed --- /dev/null +++ b/quickshell/.config/quickshell/Modules/TopBar/WorkspaceSwitcher.qml @@ -0,0 +1,379 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property string screenName: "" + property real widgetHeight: 30 + property int currentWorkspace: { + if (CompositorService.isNiri) { + return getNiriActiveWorkspace() + } + return 1 + } + property var workspaceList: { + if (CompositorService.isNiri) { + const baseList = getNiriWorkspaces() + return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList + } + return [1] + } + + function getWorkspaceIcons(ws) { + if (!SettingsData.showWorkspaceApps || !ws) { + return [] + } + + let targetWorkspaceId + if (CompositorService.isNiri) { + const wsNumber = typeof ws === "number" ? ws : -1 + if (wsNumber <= 0) { + return [] + } + const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.screenName) + if (!workspace) { + return [] + } + targetWorkspaceId = workspace.id + } else { + return [] + } + + const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels + + + const byApp = {} + const isActiveWs = CompositorService.isNiri ? NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active) : targetWorkspaceId === root.currentWorkspace + + wins.forEach((w, i) => { + if (!w) { + return + } + + let winWs = null + if (CompositorService.isNiri) { + winWs = w.workspace_id + } + + + if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) { + return + } + + const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown") + const key = isActiveWs ? `${keyBase}_${i}` : keyBase + + if (!byApp[key]) { + const moddedId = Paths.moddedAppId(keyBase) + //console.log(`moddedId = ${moddedId}`) + const isSteamApp = moddedId.toLowerCase().includes("steam_app") + const icon = isSteamApp ? "" : DesktopService.resolveIconPath(moddedId) + byApp[key] = { + "type": "icon", + "icon": icon, + "isSteamApp": isSteamApp, + "active": !!(w.activated || (CompositorService.isNiri && w.is_focused)), + "count": 1, + "windowId": w.address || w.id, + "fallbackText": w.appId || w.class || w.title || "" + } + } else { + byApp[key].count++ + if (w.activated || (CompositorService.isNiri && w.is_focused)) { + byApp[key].active = true + } + } + }) + + return Object.values(byApp) + } + + function padWorkspaces(list) { + const padded = list.slice() + const placeholder = -1 + while (padded.length < 3) { + padded.push(placeholder) + } + return padded + } + + function getNiriWorkspaces() { + if (NiriService.allWorkspaces.length === 0) { + return [1, 2] + } + + if (!root.screenName || !SettingsData.workspacesPerMonitor) { + return NiriService.getCurrentOutputWorkspaceNumbers() + } + + const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName).map(ws => ws.idx + 1) + return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2] + } + + function getNiriActiveWorkspace() { + if (NiriService.allWorkspaces.length === 0) { + return 1 + } + + if (!root.screenName || !SettingsData.workspacesPerMonitor) { + return NiriService.getCurrentWorkspaceNumber() + } + + const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active) + return activeWs ? activeWs.idx + 1 : 1 + } + + readonly property real padding: (widgetHeight - workspaceRow.implicitHeight) / 2 + + function getRealWorkspaces() { + return root.workspaceList.filter(ws => { + return ws !== -1 + }) + } + + function switchWorkspace(direction) { + if (CompositorService.isNiri) { + const realWorkspaces = getRealWorkspaces() + if (realWorkspaces.length < 2) { + return + } + + const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace) + const validIndex = currentIndex === -1 ? 0 : currentIndex + const nextIndex = direction > 0 ? (validIndex + 1) % realWorkspaces.length : (validIndex - 1 + realWorkspaces.length) % realWorkspaces.length + + NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1) + } + } + + width: workspaceRow.implicitWidth + padding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) + return "transparent" + const baseColor = Theme.surfaceTextHover + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency) + } + visible: CompositorService.isNiri + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + + property real scrollAccumulator: 0 + property real touchpadThreshold: 500 + + onWheel: wheel => { + const deltaY = wheel.angleDelta.y + const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0 + const direction = deltaY < 0 ? 1 : -1 + + if (isMouseWheel) { + switchWorkspace(direction) + } else { + scrollAccumulator += deltaY + + if (Math.abs(scrollAccumulator) >= touchpadThreshold) { + const touchDirection = scrollAccumulator < 0 ? 1 : -1 + switchWorkspace(touchDirection) + scrollAccumulator = 0 + } + } + + wheel.accepted = true + } + } + + Row { + id: workspaceRow + + anchors.centerIn: parent + spacing: Theme.spacingS + + Repeater { + model: root.workspaceList + + Rectangle { + property bool isActive: { + return modelData === root.currentWorkspace + } + property bool isPlaceholder: { + return modelData === -1 + } + property bool isHovered: mouseArea.containsMouse + property var workspaceData: { + if (isPlaceholder) { + return null + } + + if (CompositorService.isNiri) { + return NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.screenName) || null + } + return null + } + property var iconData: workspaceData?.name ? SettingsData.getWorkspaceNameIcon(workspaceData.name) : null + property bool hasIcon: iconData !== null + property var icons: SettingsData.showWorkspaceApps ? root.getWorkspaceIcons((modelData === -1 ? null : modelData)) : [] + + width: { + if (SettingsData.showWorkspaceApps) { + if (icons.length > 0) { + return isActive ? widgetHeight * 1.0 + Theme.spacingXS + contentRow.implicitWidth : widgetHeight * 0.8 + contentRow.implicitWidth + } else { + return isActive ? widgetHeight * 1.0 + Theme.spacingXS : widgetHeight * 0.8 + } + } + return isActive ? widgetHeight * 1.2 + Theme.spacingXS : widgetHeight * 0.8 + } + height: SettingsData.showWorkspaceApps ? widgetHeight * 0.8 : widgetHeight * 0.6 + radius: height / 2 + color: isActive ? Theme.primary : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: !isPlaceholder + cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor + enabled: !isPlaceholder + onClicked: { + if (isPlaceholder) { + return + } + + if (CompositorService.isNiri) { + NiriService.switchToWorkspace(modelData - 1) + } + } + } + + Row { + id: contentRow + anchors.centerIn: parent + spacing: 4 + visible: SettingsData.showWorkspaceApps && icons.length > 0 + + Repeater { + model: icons.slice(0, SettingsData.maxWorkspaceIcons) + delegate: Item { + width: 18 + height: 18 + + IconImage { + id: appIcon + property var windowId: modelData.windowId + anchors.fill: parent + source: modelData.icon + opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6 + visible: !modelData.isSteamApp + } + + DankIcon { + anchors.centerIn: parent + size: 18 + name: "sports_esports" + color: Theme.surfaceText + opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isSteamApp + } + + MouseArea { + id: appMouseArea + hoverEnabled: true + anchors.fill: parent + enabled: isActive + cursorShape: Qt.PointingHandCursor + onClicked: { + if (CompositorService.isNiri) { + NiriService.focusWindow(appIcon.windowId) + } + } + } + + Rectangle { + visible: modelData.count > 1 && !isActive + width: 12 + height: 12 + radius: 6 + color: "black" + border.color: "white" + border.width: 1 + anchors.right: parent.right + anchors.bottom: parent.bottom + z: 2 + + Text { + anchors.centerIn: parent + text: modelData.count + font.pixelSize: 8 + color: "white" + } + } + } + } + } + + DankIcon { + visible: hasIcon && iconData.type === "icon" && (!SettingsData.showWorkspaceApps || icons.length === 0) + anchors.centerIn: parent + name: (hasIcon && iconData.type === "icon") ? iconData.value : "" + size: Theme.fontSizeSmall + color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium + weight: isActive && !isPlaceholder ? 500 : 400 + } + + StyledText { + visible: hasIcon && iconData.type === "text" && (!SettingsData.showWorkspaceApps || icons.length === 0) + anchors.centerIn: parent + text: (hasIcon && iconData.type === "text") ? iconData.value : "" + color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium + font.pixelSize: Theme.fontSizeSmall + font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal + } + + StyledText { + visible: (SettingsData.showWorkspaceIndex && !hasIcon && (!SettingsData.showWorkspaceApps || icons.length === 0)) + anchors.centerIn: parent + text: { + const isPlaceholder = (modelData === -1) + + if (isPlaceholder) { + return index + 1 + } + + return (modelData - 1) + } + color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + font.pixelSize: Theme.fontSizeSmall + font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal + } + + Behavior on width { + // When having more icons, animation becomes clunky + enabled: (!SettingsData.showWorkspaceApps || SettingsData.maxWorkspaceIcons <= 3) + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on color { + // When having more icons, animation becomes clunky + enabled: (!SettingsData.showWorkspaceApps || SettingsData.maxWorkspaceIcons <= 3) + ColorAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Modules/WallpaperBackground.qml b/quickshell/.config/quickshell/Modules/WallpaperBackground.qml new file mode 100644 index 0000000..83ceadf --- /dev/null +++ b/quickshell/.config/quickshell/Modules/WallpaperBackground.qml @@ -0,0 +1,135 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Widgets + +LazyLoader { + active: true + + Variants { + model: SettingsData.getFilteredScreens("wallpaper") + + PanelWindow { + id: wallpaperWindow + + required property var modelData + + screen: modelData + + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + + color: "black" + + Item { + id: root + anchors.fill: parent + + property string source: SessionData.getMonitorWallpaper(modelData.name) || "" + property bool isColorSource: source.startsWith("#") + property Image current: one + + onSourceChanged: { + if (!source) { + current = null + one.source = "" + two.source = "" + } else if (isColorSource) { + current = null + one.source = "" + two.source = "" + } else { + if (current === one) + two.update() + else + one.update() + } + } + + onIsColorSourceChanged: { + if (isColorSource) { + current = null + one.source = "" + two.source = "" + } else if (source) { + if (current === one) + two.update() + else + one.update() + } + } + + Loader { + anchors.fill: parent + active: !root.source || root.isColorSource + asynchronous: true + + sourceComponent: DankBackdrop { + screenName: modelData.name + } + } + + Img { + id: one + } + + Img { + id: two + } + + component Img: Image { + id: img + + function update(): void { + source = "" + source = root.source + } + + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + smooth: true + asynchronous: true + cache: false + + opacity: 0 + + onStatusChanged: { + if (status === Image.Ready) { + root.current = this + if (root.current === one && two.source) { + two.source = "" + } else if (root.current === two && one.source) { + one.source = "" + } + } + } + + states: State { + name: "visible" + when: root.current === img + + PropertyChanges { + img.opacity: 1 + } + } + + transitions: Transition { + NumberAnimation { + target: img + properties: "opacity" + duration: Theme.mediumDuration + easing.type: Easing.OutCubic + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/README.md b/quickshell/.config/quickshell/README.md new file mode 100644 index 0000000..0c3fcba --- /dev/null +++ b/quickshell/.config/quickshell/README.md @@ -0,0 +1,604 @@ +# DankMaterialShell (dms) + +

        + +[![GitHub stars](https://img.shields.io/github/stars/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/AvengeMedia/DankMaterialShell/stargazers) +[![GitHub License](https://img.shields.io/github/license/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE) +[![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases) +[![GitHub last commit](https://img.shields.io/github/last-commit/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/commits/master) +[![AUR version](https://img.shields.io/aur/version/dms-shell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell) +[![AUR version (git)](https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git))](https://aur.archlinux.org/packages/dms-shell-git) + +
        + +A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and designed for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors. Features Material 3 design principles with a heavy focus on functionality and customizability. + +## Screenshots + +
        +
        + +https://github.com/user-attachments/assets/5ad934bb-e7aa-4c04-8d40-149181bd2d29 + +
        +
        + +
        View More Screenshots + +
        + +
        + +### Desktop Overview + +DankMaterialShell Desktop + +### Application Launcher + +Spotlight Launcher + +### System Monitor + +System Monitor + +### Widget Customization + +Widget Customization + +### Lock Screen + +Lock Screen + +### Dynamic Theming + +Auto Theme + +### Notification Center + +Notification Center + +### Dock + +Dock + +
        + +
        + +## Quick start (full dotfiles, most distros) + +```bash +curl -fsSL https://install.danklinux.com | sh +``` +*Or skip to [Installation](https://github.com/AvengeMedia/DankMaterialShell?tab=readme-ov-file#installation)* + +
        Features + +**tl;dr** dms can serve as AIO replacement for lock screen, notification daemon, wallpaper service, app launchers, dock, and more. + +**Core Widgets:** +- **TopBar**: fully customizable bar where widgets can be added, removed, and re-arranged. + - **App Launcher** with fuzzy search, categories, and auto-sorting by most used apps. + - **Workspace Switcher** Configurable workspace switcher. + - **Focused Window** Displays the currently focused window app name and title. + - **Running Apps** A view of all running apps, sorted by monitor, workspace, then position on workspace. + - **Media Player** Short form media player with equalizer, song title, and controls. + - **Clock** Clock and date widget + - **Weather** Weather widget with customizable location + - **System Tray** System tray applets with context menus. + - **Process Monitor** CPU, RAM, and GPU usage percentages, temperatures. (requires [dgop](https://github.com/AvengeMedia/dgop)) + - **Power/Battery** Power/Battery widget for battery metrics and power profile changing. + - **Notifications** Notification bell with a notification center popup + - **Control Center** High-level view of network, bluetooth, and audio status + - **Privacy Indicator** Attempts to reveal if a microphone or screen recording session is active, relying on Pipewire data sources + - **Idle Inhibitor** Creates a systemd idle inhibitor to prevent sleep/locking from occuring. +- **Spotlight Launcher** A central app launcher/search that can be triggered via an IPC keybinding. +- **Central Command** A combined music, weather, calendar, and events PopUp. +- **Process List** A process list, with system metrics and information. More detailed modal available via IPC. +- **Notification Center** A center for notifications that has support for grouping. +- **Dock** A dock with pinned apps support, recent apps support, and currently running application support. +- **Control Center** A full control center with user profile information, network, bluetooth, audio input/output, display controls, and night mode automation. +- **Lock Screen** Using quickshell's WlSessionLock with embedded virtual keyboard for Niri (Niri doesn't support placing virtual keyboard above lockscreen natively: [issue](https://github.com/YaLTeR/niri/issues/2201)) +- **Notepad** A simple text notepad/scratchpad with auto-save to session data and file export/import functionality. + +**Highlights:** + +- Dynamic wallpaper-based theming with matugen integration +- Numerous IPCs to trigger actions and open various modals. +- Calendar integration with [khal](https://github.com/pimutils/khal) +- Audio/media controls +- Grouped notifications +- Brightness control for internal and external displays +- Automated night mode with time-based and location-based scheduling +- Qt and GTK app theming synchronization, as well as [Ghostty](https://ghostty.org/) auto-theme support. + +
        + +## Installation + +### Compositor Setup + +DankMaterialShell supports both **niri** and **Hyprland** compositors: + +**Niri**: +```bash +# Arch Linux +paru -S niri-git + +# Fedora +sudo dnf copr enable yalter/niri && sudo dnf install niri +``` + +For detailed niri installation instructions, see the [niri Getting Started guide](https://yalter.github.io/niri/Getting-Started.html). + +**Hyprland**: +```bash +# Arch Linux +sudo pacman -S hyprland + +# Or from AUR for latest +paru -S hyprland-git + +# Fedora +sudo dnf install hyprland + +# Or use Copr for latest builds +sudo dnf copr enable solopasha/hyprland && sudo dnf install hyprland +``` + +For detailed Hyprland installation instructions, see the [Hyprland wiki](https://wiki.hypr.land/Getting-Started/Installation/). + +### Dank Shell Installation + +*feel free to contribute steps for other distributions* + +#### Arch Linux - via AUR + +```bash +paru -S dms-shell-git +``` + +#### nixOS - via flake + +```bash +nix profile install github:AvengeMedia/DankMaterialShell +``` + +#### Other Distributions - via manual installation + +**1. Install Quickshell (Varies by Distribution)** +```bash +# Arch +paru -S quickshell-git +# Fedora +sudo dnf copr enable errornointernet/quickshell && sudo dnf install quickshell-git +# ! TODO - document other distros +``` + +**2. Install fonts** +*Inter Variable* and *Fira Code* are not strictly required, but they are the default fonts of dms. + +**2.1 Install Material Symbols** +```bash +mkdir -p ~/.local/share/fonts && +curl -L "https://github.com/google/material-design-icons/raw/master/variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.ttf" -o ~/.local/share/fonts/MaterialSymbolsRounded.ttf +``` +**2.2 Install Inter Variable** +```bash +curl -L "https://github.com/rsms/inter/raw/refs/tags/v4.1/docs/font-files/InterVariable.ttf" -o ~/.local/share/fonts/InterVariable.ttf +``` + +**2.3 Install Fira Code (monospace font)** +```bash +curl -L "https://github.com/tonsky/FiraCode/releases/latest/download/FiraCode-Regular.ttf" -o ~/.local/share/fonts/FiraCode-Regular.ttf +``` + +**2.4 Refresh font cache** +```bash +fc-cache -fv +``` + +**3. Install the shell** + +**3.1. Clone latest master** +```bash +mkdir ~/.config/quickshell && git clone https://github.com/AvengeMedia/DankMaterialShell.git ~/.config/quickshell/dms +``` + +**3.2. Install latest dms CLI** +```bash +sudo sh -c "curl -L https://github.com/AvengeMedia/danklinux/releases/latest/download/dms-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').gz | gunzip | tee /usr/local/bin/dms > /dev/null && chmod +x /usr/local/bin/dms" +``` + +**4. Optional Features (system monitoring, clipboard history, brightness controls, etc.)** + +**4.1 Core optional dependencies** +```bash +# Arch Linux +sudo pacman -S cava wl-clipboard cliphist brightnessctl +paru -S matugen-bin dgop + +# Fedora +sudo dnf install cava wl-clipboard brightnessctl +sudo dnf copr enable wef/cliphist && sudo dnf install cliphist +sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen +``` + +*Other distros will just need to find sources for the above packages* + +**4.2 - dgop manual installation** + +`dgop` is available via AUR and a nix flake, other distributions can install it manually. + +```bash +sudo sh -c "curl -L https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').gz | gunzip | tee /usr/local/bin/dgop > /dev/null && chmod +x /usr/local/bin/dgop" +``` + +**Optional Requirement Overview** + +- `dgop`: Ability to have system resource widgets, process list modal, and temperature monitoring. +- `matugen`: Wallpaper-based dynamic theming +- `brightnessctl`: Backlight and LED brightness control +- `wl-clipboard`: Required for copying various elements to clipboard. +- `cava`: Audio visualizer +- `cliphist`: Clipboard history +- `gammastep`: Night mode support + +## Compositor Configuration + +A lot of options are subject to personal preference, but the below sets a good starting point for most features. + +### Niri Integration + +Add to your niri config + +```kdl +// Required for clipboard history integration +spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &" + +// Recommended (must install polkit-mate before hand) for elevation prompts +spawn-at-startup "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" +// This may be a different path on different distributions, the above is for the arch linux mate-polkit package + +// Starts DankShell +spawn-at-startup "dms" "run" + +// If using niri newer than 271534e115e5915231c99df287bbfe396185924d (~aug 17 2025) +// you can add this to disable built in config load errors since dank shell provides this +config-notification { + disable-failed +} + +// Dank keybinds +// 1. These should not be in conflict with any pre-existing keybindings +// 2. You need to merge them with your existing config if you want to use these +// 3. You can change the keys to whatever you want, if you prefer something different +// 4. For the increment/decrement ones you can change the steps to whatever you like too +binds { + Mod+Space hotkey-overlay-title="Application Launcher" { + spawn "dms" "ipc" "call" "spotlight" "toggle"; + } + Mod+V hotkey-overlay-title="Clipboard Manager" { + spawn "dms" "ipc" "call" "clipboard" "toggle"; + } + Mod+M hotkey-overlay-title="Task Manager" { + spawn "dms" "ipc" "call" "processlist" "toggle"; + } + Mod+N hotkey-overlay-title="Notification Center" { + spawn "dms" "ipc" "call" "notifications" "toggle"; + } + Mod+Comma hotkey-overlay-title="Settings" { + spawn "dms" "ipc" "call" "settings" "toggle"; + } + Mod+P hotkey-overlay-title="Notepad" { + spawn "dms" "ipc" "call" "notepad" "toggle"; + } + Super+Alt+L hotkey-overlay-title="Lock Screen" { + spawn "dms" "ipc" "call" "lock" "lock"; + } + Mod+X hotkey-overlay-title="Power Menu" { + spawn "dms" "ipc" "call" "powermenu" "toggle"; + } + XF86AudioRaiseVolume allow-when-locked=true { + spawn "dms" "ipc" "call" "audio" "increment" "3"; + } + XF86AudioLowerVolume allow-when-locked=true { + spawn "dms" "ipc" "call" "audio" "decrement" "3"; + } + XF86AudioMute allow-when-locked=true { + spawn "dms" "ipc" "call" "audio" "mute"; + } + XF86AudioMicMute allow-when-locked=true { + spawn "dms" "ipc" "call" "audio" "micmute"; + } + XF86MonBrightnessUp allow-when-locked=true { + spawn "dms" "ipc" "call" "brightness" "increment" "5" ""; + } + // You can override the default device for e.g. keyboards by adding the device name to the last param + XF86MonBrightnessDown allow-when-locked=true { + spawn "dms" "ipc" "call" "brightness" "decrement" "5" ""; + } + // Night mode toggle + Mod+Shift+N allow-when-locked=true { + spawn "dms" "ipc" "call" "night" "toggle"; + } +} +``` + +### Hyprland Integration + +Add to your Hyprland config (`~/.config/hypr/hyprland.conf`): + +```bash +# Required for clipboard history integration +exec-once = bash -c "wl-paste --watch cliphist store &" + +# Recommended (must install polkit-mate beforehand) for elevation prompts +exec-once = /usr/lib/mate-polkit/polkit-mate-authentication-agent-1 +# This may be a different path on different distributions, the above is for the arch linux mate-polkit package + +# Starts DankShell +exec-once = dms run + +# Dank keybinds +# 1. These should not be in conflict with any pre-existing keybindings +# 2. You need to merge them with your existing config if you want to use these +# 3. You can change the keys to whatever you want, if you prefer something different +# 4. For the increment/decrement ones you can change the steps to whatever you like too + +# Application and system controls +bind = SUPER, Space, exec, dms ipc call spotlight toggle +bind = SUPER, V, exec, dms ipc call clipboard toggle +bind = SUPER, M, exec, dms ipc call processlist toggle +bind = SUPER, N, exec, dms ipc call notifications toggle +bind = SUPER, comma, exec, dms ipc call settings toggle +bind = SUPER, P, exec, dms ipc call notepad toggle +bind = SUPERALT, L, exec, dms ipc call lock lock +bind = SUPER, X, exec, dms ipc call powermenu toggle + +# Audio controls (function keys) +bindl = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3 +bindl = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3 +bindl = , XF86AudioMute, exec, dms ipc call audio mute +bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute + +# Brightness controls (function keys) +bindl = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 "" +# You can override the default device for e.g. keyboards by adding the device name to the last param +bindl = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 "" + +# Night mode toggle +bind = SUPERSHIFT, N, exec, dms ipc call night toggle +``` + +## IPC Commands + +Control everything from the command line, or via keybinds. For comprehensive documentation of all available IPC commands, see [docs/IPC.md](docs/IPC.md). + +### Audio control +```bash +dms ipc call audio setvolume 50 +dms ipc call audio mute +``` +### Launch applications +```bash +dms ipc call spotlight toggle +dms ipc call notepad toggle +dms ipc call processlist toggle +dms ipc call powermenu toggle +``` +### System control +``` +dms ipc call wallpaper set /path/to/image.jpg +dms ipc call theme toggle +dms ipc call night toggle +dms ipc call lock lock +``` +### Media control +``` +dms ipc call mpris playPause +dms ipc call mpris next +``` + +## Theming + +### Custom Themes + +DankMaterialShell supports custom color themes! You can create your own Material Design 3 color schemes or use pre-made themes like Cyberpunk Electric, Hotline Miami, and Miami Vice. + +For detailed instructions on creating and using custom themes, see [docs/CUSTOM_THEMES.md](docs/CUSTOM_THEMES.md). + +### System App Integration + +There's two toggles in the appearance section of settings, for GTK and QT apps. + +These settings will override some local GTK and QT configuration files, you can still integrate auto-theming if you do not wish DankShell to mess with your QTCT/GTK files. + +No matter what when matugen is enabled the files will be created on wallpaper changes: + +- ~/.config/gtk-3.0/dank-colors.css +- ~/.config/gtk-4.0/dank-colors.css +- ~/.config/qt6ct/colors/matugen.conf +- ~/.config/qt5ct/colors/matugen.conf + +If you do not like our theme path, you can integrate this with other GTK themes, matugen themes, etc. + +#### GTK Apps + +1. Install [Colloid](https://github.com/vinceliuice/Colloid-gtk-theme) + +Colloid is a hard requirement for the auto-theming because of how it integrates with colloid css files, however you can integrate auto-theming with other themes, you just have to do it manually (so leave the toggle OFF in settings) + +It will still create `~/.config/gtk-3.0/4.0/dank-colors.css` on theme updates, these you can import into other compatible GTK themes. + +```bash +# Some default install settings for colloid +./install.sh -s standard -l --tweaks normal +``` + +Configure in `~/.config/gtk-3.0/settings.ini` and `~/.config/gtk-4.0/settings.ini`: + +```ini +[Settings] +gtk-theme-name=Colloid +``` + +#### QT: basic gtk3 based theme (Option 1) + +If you mostly use gtk apps, you'll probably be happy to just set the QT platform theme to gtk3. + +```kdl +environment { + // Add to existing environment block + QT_QPA_PLATFORMTHEME "gtk3" + QT_QPA_PLATFORMTHEME_QT6 "gtk3" +} +``` + +#### QT: better theming (Option 2) + +1. Install qt6ct-kde + +```bash +# Arch +paru -S qt6ct-kde +``` + +*I'm not sure what it is on other distros, but you can manually install via instructions provides on [qt6ct-kde github](https://www.opencode.net/trialuser/qt6ct) + +2. **Configure Environment in niri** + +```kdl + // Add to existing environment block + QT_QPA_PLATFORMTHEME "qt6ct" + QT_QPA_PLATFORMTHEME_QT6 "qt6ct" +``` + +You'll have to restart your session for themes to take effect. + +Nevigate to dms settings -> themes & colors -> and click "Apply QT Themes" + +### Terminal Integration + +The matugen integration will automatically generate new colors for certain apps only if they are installed. + +You can enable the dynamic color schemes in supported terminal apps by modifying their configurations: + +**Ghostty**: + +```bash +echo "config-file = ./config-dankcolors" >> ~/.config/ghostty/config +``` + +**kitty**: + +```bash +echo "include dank-theme.conf" >> ~/.config/kitty/kitty.conf +``` + +### Calendar Setup + +Sync your caldev compatible calendar (Google, Office365, etc.) for dashboard integration: + +
        Configuration Steps + +**Install dependencies:** + +#### Arch +```bash +sudo pacman -S vdirsyncer khal python-aiohttp-oauthlib +``` + +#### Fedora +```bash +sudo dnf install python3-vdirsyncer khal python3-aiohttp-oauthlib +``` + +**Configure vdirsyncer** (`~/.vdirsyncer/config`): + +```ini +[general] +status_path = "~/.calendars/status" + +[pair personal_sync] +a = "personal" +b = "personallocal" +collections = ["from a", "from b"] +conflict_resolution = "a wins" +metadata = ["color"] + +[storage personal] +type = "google_calendar" +token_file = "~/.vdirsyncer/google_calendar_token" +client_id = "your_client_id" +client_secret = "your_client_secret" + +[storage personallocal] +type = "filesystem" +path = "~/.calendars/Personal" +fileext = ".ics" +``` + +**Setup sync:** + +```bash +vdirsyncer sync +khal configure +``` + +#### Auto-sync every 5 minutes +```bash +crontab -e +# Add: */5 * * * * /usr/bin/vdirsyncer sync +``` + +
        + +## Configuration + +All settings are configurable in +``` +~/.config/DankMaterialShell/settings.json`, or more intuitively the built-in settings modal. +``` + +**Key configuration areas:** + +- Widget positioning and behavior +- Theme and color preferences +- Time format, weather units and location +- Light/Dark modes +- Wallpaper and Profile picture +- Dock enable/disable and various tunes. + +## Troubleshooting + +**Common issues:** + +- **Missing icons:** Verify Material Symbols font installation with `fc-list | grep Material` +- **No dynamic theming:** Install matugen and enable in settings +- **Qt apps not themed:** Configure qt5ct/qt6ct and set QT_QPA_PLATFORMTHEME +- **Calendar not syncing:** Check vdirsyncer credentials and network connectivity + +**Getting help:** + +- Check the [issues](https://github.com/AvengeMedia/DankMaterialShell/issues) for known problems +- Re-run the shell with `dms kill && dms run` to capture logs. +- Join the niri community for compositor-specific questions + +## Contributing + +DankMaterialShell welcomes contributions! Whether it's bug fixes, new widgets, theme improvements, or documentation updates - all help is appreciated. + +**Areas that need attention:** + +- More widget options and customization +- Additional compositor compatibility +- Performance optimizations +- Documentation and examples + +## Credits + +- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible. +- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor. +- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets. +- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets. diff --git a/quickshell/.config/quickshell/Services/AppSearchService.qml b/quickshell/.config/quickshell/Services/AppSearchService.qml new file mode 100644 index 0000000..7f53e9a --- /dev/null +++ b/quickshell/.config/quickshell/Services/AppSearchService.qml @@ -0,0 +1,178 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import "../Common/fzf.js" as Fzf + +Singleton { + id: root + + property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal) + + function searchApplications(query) { + if (!query || query.length === 0) + return applications + if (applications.length === 0) + return [] + + const queryLower = query.toLowerCase().trim() + const scoredApps = [] + + for (const app of applications) { + const name = (app.name || "").toLowerCase() + const genericName = (app.genericName || "").toLowerCase() + const comment = (app.comment || "").toLowerCase() + const keywords = app.keywords ? app.keywords.map(k => k.toLowerCase()) : [] + + let score = 0 + let matched = false + + const nameWords = name.trim().split(/\s+/).filter(w => w.length > 0) + const containsAsWord = nameWords.includes(queryLower) + const startsWithAsWord = nameWords.some(word => word.startsWith(queryLower)) + + if (name === queryLower) { + score = 10000 + matched = true + } else if (containsAsWord) { + score = 9500 + (100 - Math.min(name.length, 100)) + matched = true + } else if (name.startsWith(queryLower)) { + score = 9000 + (100 - Math.min(name.length, 100)) + matched = true + } else if (startsWithAsWord) { + score = 8500 + (100 - Math.min(name.length, 100)) + matched = true + } else if (name.includes(queryLower)) { + score = 8000 + (100 - Math.min(name.length, 100)) + matched = true + } else if (keywords.length > 0) { + for (const keyword of keywords) { + if (keyword === queryLower) { + score = 6000 + matched = true + break + } else if (keyword.startsWith(queryLower)) { + score = 5500 + matched = true + break + } else if (keyword.includes(queryLower)) { + score = 5000 + matched = true + break + } + } + } + if (!matched && genericName.includes(queryLower)) { + score = 4000 + matched = true + } else if (!matched && comment.includes(queryLower)) { + score = 3000 + matched = true + } else if (!matched) { + const nameFinder = new Fzf.Finder([app], { + "selector": a => a.name || "", + "casing": "case-insensitive", + "fuzzy": "v2" + }) + const fuzzyResults = nameFinder.find(query) + if (fuzzyResults.length > 0 && fuzzyResults[0].score > 0) { + score = Math.min(fuzzyResults[0].score, 2000) + matched = true + } + } + + if (matched) { + scoredApps.push({ + "app": app, + "score": score + }) + } + } + + scoredApps.sort((a, b) => b.score - a.score) + return scoredApps.slice(0, 50).map(item => item.app) + } + + function getCategoriesForApp(app) { + if (!app?.categories) + return [] + + const categoryMap = { + "AudioVideo": "Media", + "Audio": "Media", + "Video": "Media", + "Development": "Development", + "TextEditor": "Development", + "IDE": "Development", + "Education": "Education", + "Game": "Games", + "Graphics": "Graphics", + "Photography": "Graphics", + "Network": "Internet", + "WebBrowser": "Internet", + "Email": "Internet", + "Office": "Office", + "WordProcessor": "Office", + "Spreadsheet": "Office", + "Presentation": "Office", + "Science": "Science", + "Settings": "Settings", + "System": "System", + "Utility": "Utilities", + "Accessories": "Utilities", + "FileManager": "Utilities", + "TerminalEmulator": "Utilities" + } + + const mappedCategories = new Set() + + for (const cat of app.categories) { + if (categoryMap[cat]) + mappedCategories.add(categoryMap[cat]) + } + + return Array.from(mappedCategories) + } + + property var categoryIcons: ({ + "All": "apps", + "Media": "music_video", + "Development": "code", + "Games": "sports_esports", + "Graphics": "photo_library", + "Internet": "web", + "Office": "content_paste", + "Settings": "settings", + "System": "host", + "Utilities": "build" + }) + + function getCategoryIcon(category) { + return categoryIcons[category] || "folder" + } + + function getAllCategories() { + const categories = new Set(["All"]) + + for (const app of applications) { + const appCategories = getCategoriesForApp(app) + appCategories.forEach(cat => categories.add(cat)) + } + + return Array.from(categories).sort() + } + + function getAppsInCategory(category) { + if (category === "All") { + return applications + } + + return applications.filter(app => { + const appCategories = getCategoriesForApp(app) + return appCategories.includes(category) + }) + } +} diff --git a/quickshell/.config/quickshell/Services/AudioService.qml b/quickshell/.config/quickshell/Services/AudioService.qml new file mode 100644 index 0000000..92d5c55 --- /dev/null +++ b/quickshell/.config/quickshell/Services/AudioService.qml @@ -0,0 +1,211 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire + +Singleton { + id: root + + readonly property PwNode sink: Pipewire.defaultAudioSink + readonly property PwNode source: Pipewire.defaultAudioSource + + signal volumeChanged + signal micMuteChanged + + function displayName(node) { + if (!node) { + return "" + } + + if (node.properties && node.properties["device.description"]) { + return node.properties["device.description"] + } + + if (node.description && node.description !== node.name) { + return node.description + } + + if (node.nickname && node.nickname !== node.name) { + return node.nickname + } + + if (node.name.includes("analog-stereo")) { + return "Built-in Speakers" + } + if (node.name.includes("bluez")) { + return "Bluetooth Audio" + } + if (node.name.includes("usb")) { + return "USB Audio" + } + if (node.name.includes("hdmi")) { + return "HDMI Audio" + } + + return node.name + } + + function subtitle(name) { + if (!name) { + return "" + } + + if (name.includes('usb-')) { + if (name.includes('SteelSeries')) { + return "USB Gaming Headset" + } + if (name.includes('Generic')) { + return "USB Audio Device" + } + return "USB Audio" + } + + if (name.includes('pci-')) { + if (name.includes('01_00.1') || name.includes('01:00.1')) { + return "NVIDIA GPU Audio" + } + return "PCI Audio" + } + + if (name.includes('bluez')) { + return "Bluetooth Audio" + } + if (name.includes('analog')) { + return "Built-in Audio" + } + if (name.includes('hdmi')) { + return "HDMI Audio" + } + + return "" + } + + PwObjectTracker { + objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream) + } + + function setVolume(percentage) { + if (!root.sink?.audio) { + return "No audio sink available" + } + + const clampedVolume = Math.max(0, Math.min(100, percentage)) + root.sink.audio.volume = clampedVolume / 100 + root.volumeChanged() + return `Volume set to ${clampedVolume}%` + } + + function toggleMute() { + if (!root.sink?.audio) { + return "No audio sink available" + } + + root.sink.audio.muted = !root.sink.audio.muted + return root.sink.audio.muted ? "Audio muted" : "Audio unmuted" + } + + function setMicVolume(percentage) { + if (!root.source?.audio) { + return "No audio source available" + } + + const clampedVolume = Math.max(0, Math.min(100, percentage)) + root.source.audio.volume = clampedVolume / 100 + return `Microphone volume set to ${clampedVolume}%` + } + + function toggleMicMute() { + if (!root.source?.audio) { + return "No audio source available" + } + + root.source.audio.muted = !root.source.audio.muted + return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted" + } + + IpcHandler { + target: "audio" + + function setvolume(percentage: string): string { + return root.setVolume(parseInt(percentage)) + } + + function increment(step: string): string { + if (!root.sink?.audio) { + return "No audio sink available" + } + + if (root.sink.audio.muted) { + root.sink.audio.muted = false + } + + const currentVolume = Math.round(root.sink.audio.volume * 100) + const stepValue = parseInt(step || "5") + const newVolume = Math.max(0, Math.min(100, currentVolume + stepValue)) + + root.sink.audio.volume = newVolume / 100 + root.volumeChanged() + return `Volume increased to ${newVolume}%` + } + + function decrement(step: string): string { + if (!root.sink?.audio) { + return "No audio sink available" + } + + if (root.sink.audio.muted) { + root.sink.audio.muted = false + } + + const currentVolume = Math.round(root.sink.audio.volume * 100) + const stepValue = parseInt(step || "5") + const newVolume = Math.max(0, Math.min(100, currentVolume - stepValue)) + + root.sink.audio.volume = newVolume / 100 + root.volumeChanged() + return `Volume decreased to ${newVolume}%` + } + + function mute(): string { + const result = root.toggleMute() + root.volumeChanged() + return result + } + + function setmic(percentage: string): string { + return root.setMicVolume(parseInt(percentage)) + } + + function micmute(): string { + const result = root.toggleMicMute() + root.micMuteChanged() + return result + } + + function status(): string { + let result = "Audio Status:\n" + + if (root.sink?.audio) { + const volume = Math.round(root.sink.audio.volume * 100) + const muteStatus = root.sink.audio.muted ? " (muted)" : "" + result += `Output: ${volume}%${muteStatus}\n` + } else { + result += "Output: No sink available\n" + } + + if (root.source?.audio) { + const micVolume = Math.round(root.source.audio.volume * 100) + const muteStatus = root.source.audio.muted ? " (muted)" : "" + result += `Input: ${micVolume}%${muteStatus}` + } else { + result += "Input: No source available" + } + + return result + } + } +} diff --git a/quickshell/.config/quickshell/Services/BatteryService.qml b/quickshell/.config/quickshell/Services/BatteryService.qml new file mode 100644 index 0000000..588fc2f --- /dev/null +++ b/quickshell/.config/quickshell/Services/BatteryService.qml @@ -0,0 +1,228 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.UPower +import qs.Common + +Singleton { + id: root + + property bool suppressSound: true + property bool previousPluggedState: false + + Timer { + id: startupTimer + interval: 500 + repeat: false + running: true + onTriggered: root.suppressSound = false + } + + readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY") + + // List of laptop batteries + readonly property var batteries: UPower.devices.values.filter(dev => dev.isLaptopBattery) + + readonly property bool usePreferred: preferredBatteryOverride && preferredBatteryOverride.length > 0 + + // Main battery (for backward compatibility) + readonly property UPowerDevice device: { + var preferredDev + if (usePreferred) { + preferredDev = batteries.find(dev => dev.nativePath.toLowerCase().includes(preferredBatteryOverride.toLowerCase())) + } + return preferredDev || batteries[0] || null + } + // Whether at least one battery is available + readonly property bool batteryAvailable: batteries.length > 0 + // Aggregated charge level (percentage) + readonly property real batteryLevel: { + if (!batteryAvailable) return 0 + if (batteryCapacity === 0) { + if (usePreferred && device && device.ready) return Math.round(device.percentage) + const validBatteries = batteries.filter(b => b.ready && b.percentage >= 0) + if (validBatteries.length === 0) return 0 + const avgPercentage = validBatteries.reduce((sum, b) => sum + b.percentage, 0) / validBatteries.length + return Math.round(avgPercentage) + } + return Math.round((batteryEnergy * 100) / batteryCapacity) + } + readonly property bool isCharging: batteryAvailable && batteries.some(b => b.state === UPowerDeviceState.Charging || b.state === UPowerDeviceState.FullyCharged) + + // Is the system plugged in (none of the batteries are discharging or empty) + readonly property bool isPluggedIn: batteryAvailable && batteries.every(b => b.state !== UPowerDeviceState.Discharging) + readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20 + + onIsPluggedInChanged: { + if (suppressSound || !batteryAvailable) { + previousPluggedState = isPluggedIn + return + } + + if (SettingsData.soundsEnabled && SettingsData.soundPluggedIn) { + if (isPluggedIn && !previousPluggedState) { + AudioService.playPowerPlugSound() + } else if (!isPluggedIn && previousPluggedState) { + AudioService.playPowerUnplugSound() + } + } + + previousPluggedState = isPluggedIn + } + + // Aggregated charge/discharge rate + readonly property real changeRate: { + if (!batteryAvailable) return 0 + if (usePreferred && device && device.ready) return device.changeRate + return batteries.length > 0 ? batteries.reduce((sum, b) => sum + b.changeRate, 0) : 0 + } + + // Aggregated battery health + readonly property string batteryHealth: { + if (!batteryAvailable) return "N/A" + + // If a preferred battery is selected and ready + if (usePreferred && device && device.ready && device.healthSupported) return `${Math.round(device.healthPercentage)}%` + + // Otherwise, calculate the average health of all laptop batteries + const validBatteries = batteries.filter(b => b.healthSupported && b.healthPercentage > 0) + if (validBatteries.length === 0) return "N/A" + + const avgHealth = validBatteries.reduce((sum, b) => sum + b.healthPercentage, 0) / validBatteries.length + return `${Math.round(avgHealth)}%` + } + + readonly property real batteryEnergy: { + if (!batteryAvailable) return 0 + if (usePreferred && device && device.ready) return device.energy + return batteries.length > 0 ? batteries.reduce((sum, b) => sum + b.energy, 0) : 0 + } + + // Total battery capacity (Wh) + readonly property real batteryCapacity: { + if (!batteryAvailable) return 0 + if (usePreferred && device && device.ready) return device.energyCapacity + return batteries.length > 0 ? batteries.reduce((sum, b) => sum + b.energyCapacity, 0) : 0 + } + + // Aggregated battery status + readonly property string batteryStatus: { + if (!batteryAvailable) { + return "No Battery" + } + + if (isCharging && !batteries.some(b => b.changeRate > 0)) return "Plugged In" + + const states = batteries.map(b => b.state) + if (states.every(s => s === states[0])) return UPowerDeviceState.toString(states[0]) + + return isCharging ? "Charging" : (isPluggedIn ? "Plugged In" : "Discharging") + } + + readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && (typeof PowerProfiles !== "undefined" && PowerProfiles.profile !== PowerProfile.PowerSaver) + + readonly property var bluetoothDevices: { + const btDevices = [] + const bluetoothTypes = [UPowerDeviceType.BluetoothGeneric, UPowerDeviceType.Headphones, UPowerDeviceType.Headset, UPowerDeviceType.Keyboard, UPowerDeviceType.Mouse, UPowerDeviceType.Speakers] + + for (var i = 0; i < UPower.devices.count; i++) { + const dev = UPower.devices.get(i) + if (dev && dev.ready && bluetoothTypes.includes(dev.type)) { + btDevices.push({ + "name": dev.model || UPowerDeviceType.toString(dev.type), + "percentage": Math.round(dev.percentage), + "type": dev.type + }) + } + } + return btDevices + } + + // Format time remaining for charge/discharge + function formatTimeRemaining() { + if (!batteryAvailable) { + return "Unknown" + } + + let totalTime = 0 + totalTime = (isCharging) ? ((batteryCapacity - batteryEnergy) / changeRate) : (batteryEnergy / changeRate) + const avgTime = Math.abs(totalTime * 3600) + if (!avgTime || avgTime <= 0 || avgTime > 86400) return "Unknown" + + const hours = Math.floor(avgTime / 3600) + const minutes = Math.floor((avgTime % 3600) / 60) + return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m` + } + + function getBatteryIcon() { + if (!batteryAvailable) { + return "power" + } + + if (isCharging) { + if (batteryLevel >= 90) { + return "battery_charging_full" + } + if (batteryLevel >= 80) { + return "battery_charging_90" + } + if (batteryLevel >= 60) { + return "battery_charging_80" + } + if (batteryLevel >= 50) { + return "battery_charging_60" + } + if (batteryLevel >= 30) { + return "battery_charging_50" + } + if (batteryLevel >= 20) { + return "battery_charging_30" + } + return "battery_charging_20" + } + if (isPluggedIn) { + if (batteryLevel >= 90) { + return "battery_charging_full" + } + if (batteryLevel >= 80) { + return "battery_charging_90" + } + if (batteryLevel >= 60) { + return "battery_charging_80" + } + if (batteryLevel >= 50) { + return "battery_charging_60" + } + if (batteryLevel >= 30) { + return "battery_charging_50" + } + if (batteryLevel >= 20) { + return "battery_charging_30" + } + return "battery_charging_20" + } + if (batteryLevel >= 95) { + return "battery_full" + } + if (batteryLevel >= 85) { + return "battery_6_bar" + } + if (batteryLevel >= 70) { + return "battery_5_bar" + } + if (batteryLevel >= 55) { + return "battery_4_bar" + } + if (batteryLevel >= 40) { + return "battery_3_bar" + } + if (batteryLevel >= 25) { + return "battery_2_bar" + } + return "battery_1_bar" + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Services/BluetoothService.qml b/quickshell/.config/quickshell/Services/BluetoothService.qml new file mode 100644 index 0000000..7422e54 --- /dev/null +++ b/quickshell/.config/quickshell/Services/BluetoothService.qml @@ -0,0 +1,485 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Bluetooth + +Singleton { + id: root + + readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter + readonly property bool available: adapter !== null + readonly property bool enabled: (adapter && adapter.enabled) ?? false + readonly property bool discovering: (adapter && adapter.discovering) ?? false + readonly property var devices: adapter ? adapter.devices : null + readonly property var pairedDevices: { + if (!adapter || !adapter.devices) { + return [] + } + + return adapter.devices.values.filter(dev => { + return dev && (dev.paired || dev.trusted) + }) + } + readonly property var allDevicesWithBattery: { + if (!adapter || !adapter.devices) { + return [] + } + + return adapter.devices.values.filter(dev => { + return dev && dev.batteryAvailable && dev.battery > 0 + }) + } + + function sortDevices(devices) { + return devices.sort((a, b) => { + const aName = a.name || a.deviceName || "" + const bName = b.name || b.deviceName || "" + + const aHasRealName = aName.includes(" ") && aName.length > 3 + const bHasRealName = bName.includes(" ") && bName.length > 3 + + if (aHasRealName && !bHasRealName) { + return -1 + } + if (!aHasRealName && bHasRealName) { + return 1 + } + + const aSignal = (a.signalStrength !== undefined && a.signalStrength > 0) ? a.signalStrength : 0 + const bSignal = (b.signalStrength !== undefined && b.signalStrength > 0) ? b.signalStrength : 0 + return bSignal - aSignal + }) + } + + function getDeviceIcon(device) { + if (!device) { + return "bluetooth" + } + + const name = (device.name || device.deviceName || "").toLowerCase() + const icon = (device.icon || "").toLowerCase() + + const audioKeywords = ["headset", "audio", "headphone", "airpod", "arctis"] + if (audioKeywords.some(keyword => icon.includes(keyword) || name.includes(keyword))) { + return "headset" + } + + if (icon.includes("mouse") || name.includes("mouse")) { + return "mouse" + } + + if (icon.includes("keyboard") || name.includes("keyboard")) { + return "keyboard" + } + + const phoneKeywords = ["phone", "iphone", "android", "samsung"] + if (phoneKeywords.some(keyword => icon.includes(keyword) || name.includes(keyword))) { + return "smartphone" + } + + if (icon.includes("watch") || name.includes("watch")) { + return "watch" + } + + if (icon.includes("speaker") || name.includes("speaker")) { + return "speaker" + } + + if (icon.includes("display") || name.includes("tv")) { + return "tv" + } + + return "bluetooth" + } + + function canConnect(device) { + if (!device) { + return false + } + + return !device.paired && !device.pairing && !device.blocked + } + + function getSignalStrength(device) { + if (!device || device.signalStrength === undefined || device.signalStrength <= 0) { + return "Unknown" + } + + const signal = device.signalStrength + if (signal >= 80) { + return "Excellent" + } + if (signal >= 60) { + return "Good" + } + if (signal >= 40) { + return "Fair" + } + if (signal >= 20) { + return "Poor" + } + + return "Very Poor" + } + + function getSignalIcon(device) { + if (!device || device.signalStrength === undefined || device.signalStrength <= 0) { + return "signal_cellular_null" + } + + const signal = device.signalStrength + if (signal >= 80) { + return "signal_cellular_4_bar" + } + if (signal >= 60) { + return "signal_cellular_3_bar" + } + if (signal >= 40) { + return "signal_cellular_2_bar" + } + if (signal >= 20) { + return "signal_cellular_1_bar" + } + + return "signal_cellular_0_bar" + } + + function isDeviceBusy(device) { + if (!device) { + return false + } + return device.pairing || device.state === BluetoothDeviceState.Disconnecting || device.state === BluetoothDeviceState.Connecting + } + + function connectDeviceWithTrust(device) { + if (!device) { + return + } + + device.trusted = true + device.connect() + } + + function getCardName(device) { + if (!device) { + return "" + } + return `bluez_card.${device.address.replace(/:/g, "_")}` + } + + function isAudioDevice(device) { + if (!device) { + return false + } + const icon = getDeviceIcon(device) + return icon === "headset" || icon === "speaker" + } + + function getCodecInfo(codecName) { + const codec = codecName.replace(/-/g, "_").toUpperCase() + + const codecMap = { + "LDAC": { + "name": "LDAC", + "description": "Highest quality • Higher battery usage", + "qualityColor": "#4CAF50" + }, + "APTX_HD": { + "name": "aptX HD", + "description": "High quality • Balanced battery", + "qualityColor": "#FF9800" + }, + "APTX": { + "name": "aptX", + "description": "Good quality • Low latency", + "qualityColor": "#FF9800" + }, + "AAC": { + "name": "AAC", + "description": "Balanced quality and battery", + "qualityColor": "#2196F3" + }, + "SBC_XQ": { + "name": "SBC-XQ", + "description": "Enhanced SBC • Better compatibility", + "qualityColor": "#2196F3" + }, + "SBC": { + "name": "SBC", + "description": "Basic quality • Universal compatibility", + "qualityColor": "#9E9E9E" + }, + "MSBC": { + "name": "mSBC", + "description": "Modified SBC • Optimized for speech", + "qualityColor": "#9E9E9E" + }, + "CVSD": { + "name": "CVSD", + "description": "Basic speech codec • Legacy compatibility", + "qualityColor": "#9E9E9E" + } + } + + return codecMap[codec] || { + "name": codecName, + "description": "Unknown codec", + "qualityColor": "#9E9E9E" + } + } + + property var deviceCodecs: ({}) + + function updateDeviceCodec(deviceAddress, codec) { + deviceCodecs[deviceAddress] = codec + deviceCodecsChanged() + } + + function refreshDeviceCodec(device) { + if (!device || !device.connected || !isAudioDevice(device)) { + return + } + + const cardName = getCardName(device) + codecQueryProcess.cardName = cardName + codecQueryProcess.deviceAddress = device.address + codecQueryProcess.availableCodecs = [] + codecQueryProcess.parsingTargetCard = false + codecQueryProcess.detectedCodec = "" + codecQueryProcess.running = true + } + + function getCurrentCodec(device, callback) { + if (!device || !device.connected || !isAudioDevice(device)) { + callback("") + return + } + + const cardName = getCardName(device) + codecQueryProcess.cardName = cardName + codecQueryProcess.callback = callback + codecQueryProcess.availableCodecs = [] + codecQueryProcess.parsingTargetCard = false + codecQueryProcess.detectedCodec = "" + codecQueryProcess.running = true + } + + function getAvailableCodecs(device, callback) { + if (!device || !device.connected || !isAudioDevice(device)) { + callback([], "") + return + } + + const cardName = getCardName(device) + codecFullQueryProcess.cardName = cardName + codecFullQueryProcess.callback = callback + codecFullQueryProcess.availableCodecs = [] + codecFullQueryProcess.parsingTargetCard = false + codecFullQueryProcess.detectedCodec = "" + codecFullQueryProcess.running = true + } + + function switchCodec(device, profileName, callback) { + if (!device || !isAudioDevice(device)) { + callback(false, "Invalid device") + return + } + + const cardName = getCardName(device) + codecSwitchProcess.cardName = cardName + codecSwitchProcess.profile = profileName + codecSwitchProcess.callback = callback + codecSwitchProcess.running = true + } + + Process { + id: codecQueryProcess + + property string cardName: "" + property string deviceAddress: "" + property var callback: null + property bool parsingTargetCard: false + property string detectedCodec: "" + property var availableCodecs: [] + + command: ["pactl", "list", "cards"] + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0 && detectedCodec) { + if (deviceAddress) { + root.updateDeviceCodec(deviceAddress, detectedCodec) + } + if (callback) { + callback(detectedCodec) + } + } else if (callback) { + callback("") + } + + parsingTargetCard = false + detectedCodec = "" + availableCodecs = [] + deviceAddress = "" + callback = null + } + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => { + let line = data.trim() + + if (line.includes(`Name: ${codecQueryProcess.cardName}`)) { + codecQueryProcess.parsingTargetCard = true + return + } + + if (codecQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecQueryProcess.cardName)) { + codecQueryProcess.parsingTargetCard = false + return + } + + if (codecQueryProcess.parsingTargetCard) { + if (line.startsWith("Active Profile:")) { + let profile = line.split(": ")[1] || "" + let activeCodec = codecQueryProcess.availableCodecs.find(c => { + return c.profile === profile + }) + if (activeCodec) { + codecQueryProcess.detectedCodec = activeCodec.name + } + return + } + if (line.includes("codec") && line.includes("available: yes")) { + let parts = line.split(": ") + if (parts.length >= 2) { + let profile = parts[0].trim() + let description = parts[1] + let codecMatch = description.match(/codec ([^\)\s]+)/i) + let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN" + let codecInfo = root.getCodecInfo(codecName) + if (codecInfo && !codecQueryProcess.availableCodecs.some(c => { + return c.profile === profile + })) { + let newCodecs = codecQueryProcess.availableCodecs.slice() + newCodecs.push({ + "name": codecInfo.name, + "profile": profile, + "description": codecInfo.description, + "qualityColor": codecInfo.qualityColor + }) + codecQueryProcess.availableCodecs = newCodecs + } + } + } + } + } + } + } + + Process { + id: codecFullQueryProcess + + property string cardName: "" + property var callback: null + property bool parsingTargetCard: false + property string detectedCodec: "" + property var availableCodecs: [] + + command: ["pactl", "list", "cards"] + + onExited: function (exitCode, exitStatus) { + if (callback) { + callback(exitCode === 0 ? availableCodecs : [], exitCode === 0 ? detectedCodec : "") + } + parsingTargetCard = false + detectedCodec = "" + availableCodecs = [] + callback = null + } + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => { + let line = data.trim() + + if (line.includes(`Name: ${codecFullQueryProcess.cardName}`)) { + codecFullQueryProcess.parsingTargetCard = true + return + } + + if (codecFullQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecFullQueryProcess.cardName)) { + codecFullQueryProcess.parsingTargetCard = false + return + } + + if (codecFullQueryProcess.parsingTargetCard) { + if (line.startsWith("Active Profile:")) { + let profile = line.split(": ")[1] || "" + let activeCodec = codecFullQueryProcess.availableCodecs.find(c => { + return c.profile === profile + }) + if (activeCodec) { + codecFullQueryProcess.detectedCodec = activeCodec.name + } + return + } + if (line.includes("codec") && line.includes("available: yes")) { + let parts = line.split(": ") + if (parts.length >= 2) { + let profile = parts[0].trim() + let description = parts[1] + let codecMatch = description.match(/codec ([^\)\s]+)/i) + let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN" + let codecInfo = root.getCodecInfo(codecName) + if (codecInfo && !codecFullQueryProcess.availableCodecs.some(c => { + return c.profile === profile + })) { + let newCodecs = codecFullQueryProcess.availableCodecs.slice() + newCodecs.push({ + "name": codecInfo.name, + "profile": profile, + "description": codecInfo.description, + "qualityColor": codecInfo.qualityColor + }) + codecFullQueryProcess.availableCodecs = newCodecs + } + } + } + } + } + } + } + + Process { + id: codecSwitchProcess + + property string cardName: "" + property string profile: "" + property var callback: null + + command: ["pactl", "set-card-profile", cardName, profile] + + onExited: function (exitCode, exitStatus) { + if (callback) { + callback(exitCode === 0, exitCode === 0 ? "Codec switched successfully" : "Failed to switch codec") + } + + // If successful, refresh the codec for this device + if (exitCode === 0) { + if (root.adapter && root.adapter.devices) { + root.adapter.devices.values.forEach(device => { + if (device && root.getCardName(device) === cardName) { + Qt.callLater(() => root.refreshDeviceCodec(device)) + } + }) + } + } + + callback = null + } + } +} diff --git a/quickshell/.config/quickshell/Services/CalendarService.qml b/quickshell/.config/quickshell/Services/CalendarService.qml new file mode 100644 index 0000000..337fcfa --- /dev/null +++ b/quickshell/.config/quickshell/Services/CalendarService.qml @@ -0,0 +1,250 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property bool khalAvailable: false + property var eventsByDate: ({}) + property bool isLoading: false + property string lastError: "" + property date lastStartDate + property date lastEndDate + + function checkKhalAvailability() { + if (!khalCheckProcess.running) + khalCheckProcess.running = true + } + + function loadCurrentMonth() { + if (!root.khalAvailable) + return + + let today = new Date() + let firstDay = new Date(today.getFullYear(), today.getMonth(), 1) + let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0) + // Add padding + let startDate = new Date(firstDay) + startDate.setDate(startDate.getDate() - firstDay.getDay() - 7) + let endDate = new Date(lastDay) + endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7) + loadEvents(startDate, endDate) + } + + function loadEvents(startDate, endDate) { + if (!root.khalAvailable) { + return + } + if (eventsProcess.running) { + return + } + // Store last requested date range for refresh timer + root.lastStartDate = startDate + root.lastEndDate = endDate + root.isLoading = true + // Format dates for khal (MM/dd/yyyy based on printformats) + let startDateStr = Qt.formatDate(startDate, "MM/dd/yyyy") + let endDateStr = Qt.formatDate(endDate, "MM/dd/yyyy") + eventsProcess.requestStartDate = startDate + eventsProcess.requestEndDate = endDate + eventsProcess.command = ["khal", "list", "--json", "title", "--json", "description", "--json", "start-date", "--json", "start-time", "--json", "end-date", "--json", "end-time", "--json", "all-day", "--json", "location", "--json", "url", startDateStr, endDateStr] + eventsProcess.running = true + } + + function getEventsForDate(date) { + let dateKey = Qt.formatDate(date, "yyyy-MM-dd") + return root.eventsByDate[dateKey] || [] + } + + function hasEventsForDate(date) { + let events = getEventsForDate(date) + return events.length > 0 + } + + // Initialize on component completion + Component.onCompleted: { + checkKhalAvailability() + } + + // Process for checking khal configuration + Process { + id: khalCheckProcess + + command: ["khal", "list", "today"] + running: false + onExited: exitCode => { + root.khalAvailable = (exitCode === 0) + if (exitCode === 0) { + loadCurrentMonth() + } + } + } + + // Process for loading events + Process { + id: eventsProcess + + property date requestStartDate + property date requestEndDate + property string rawOutput: "" + + running: false + onExited: exitCode => { + root.isLoading = false + if (exitCode !== 0) { + root.lastError = "Failed to load events (exit code: " + exitCode + ")" + return + } + try { + let newEventsByDate = {} + let lines = eventsProcess.rawOutput.split('\n') + for (let line of lines) { + line = line.trim() + if (!line || line === "[]") + continue + + // Parse JSON line + let dayEvents = JSON.parse(line) + // Process each event in this day's array + for (let event of dayEvents) { + if (!event.title) + continue + + // Parse start and end dates + let startDate, endDate + if (event['start-date']) { + let startParts = event['start-date'].split('/') + startDate = new Date(parseInt(startParts[2]), + parseInt(startParts[0]) - 1, + parseInt(startParts[1])) + } else { + startDate = new Date() + } + if (event['end-date']) { + let endParts = event['end-date'].split('/') + endDate = new Date(parseInt(endParts[2]), + parseInt(endParts[0]) - 1, + parseInt(endParts[1])) + } else { + endDate = new Date(startDate) + } + // Create start/end times + let startTime = new Date(startDate) + let endTime = new Date(endDate) + if (event['start-time'] + && event['all-day'] !== "True") { + // Parse time if available and not all-day + let timeStr = event['start-time'] + if (timeStr) { + let timeParts = timeStr.match(/(\d+):(\d+)/) + if (timeParts) { + startTime.setHours(parseInt(timeParts[1]), + parseInt(timeParts[2])) + if (event['end-time']) { + let endTimeParts = event['end-time'].match( + /(\d+):(\d+)/) + if (endTimeParts) + endTime.setHours( + parseInt(endTimeParts[1]), + parseInt(endTimeParts[2])) + } else { + // Default to 1 hour duration on same day + endTime = new Date(startTime) + endTime.setHours( + startTime.getHours() + 1) + } + } + } + } + // Create unique ID for this event (to track multi-day events) + let eventId = event.title + "_" + event['start-date'] + + "_" + (event['start-time'] || 'allday') + // Create event object template + let eventTemplate = { + "id": eventId, + "title": event.title || "Untitled Event", + "start": startTime, + "end": endTime, + "location": event.location || "", + "description": event.description || "", + "url": event.url || "", + "calendar": "", + "color": "", + "allDay": event['all-day'] === "True", + "isMultiDay": startDate.toDateString( + ) !== endDate.toDateString() + } + // Add event to each day it spans + let currentDate = new Date(startDate) + while (currentDate <= endDate) { + let dateKey = Qt.formatDate(currentDate, + "yyyy-MM-dd") + if (!newEventsByDate[dateKey]) + newEventsByDate[dateKey] = [] + + // Check if this exact event is already added to this date (prevent duplicates) + let existingEvent = newEventsByDate[dateKey].find( + e => { + return e.id === eventId + }) + if (existingEvent) { + // Move to next day without adding duplicate + currentDate.setDate(currentDate.getDate() + 1) + continue + } + // Create a copy of the event for this date + let dayEvent = Object.assign({}, eventTemplate) + // For multi-day events, adjust the display time for this specific day + if (currentDate.getTime() === startDate.getTime()) { + // First day - use original start time + dayEvent.start = new Date(startTime) + } else { + // Subsequent days - start at beginning of day for all-day events + dayEvent.start = new Date(currentDate) + if (!dayEvent.allDay) + dayEvent.start.setHours(0, 0, 0, 0) + } + if (currentDate.getTime() === endDate.getTime()) { + // Last day - use original end time + dayEvent.end = new Date(endTime) + } else { + // Earlier days - end at end of day for all-day events + dayEvent.end = new Date(currentDate) + if (!dayEvent.allDay) + dayEvent.end.setHours(23, 59, 59, 999) + } + newEventsByDate[dateKey].push(dayEvent) + // Move to next day + currentDate.setDate(currentDate.getDate() + 1) + } + } + } + // Sort events by start time within each date + for (let dateKey in newEventsByDate) { + newEventsByDate[dateKey].sort((a, b) => { + return a.start.getTime( + ) - b.start.getTime() + }) + } + root.eventsByDate = newEventsByDate + root.lastError = "" + } catch (error) { + root.lastError = "Failed to parse events JSON: " + error.toString() + root.eventsByDate = {} + } + // Reset for next run + eventsProcess.rawOutput = "" + } + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => { + eventsProcess.rawOutput += data + "\n" + } + } + } +} diff --git a/quickshell/.config/quickshell/Services/CavaService.qml b/quickshell/.config/quickshell/Services/CavaService.qml new file mode 100644 index 0000000..e2cf161 --- /dev/null +++ b/quickshell/.config/quickshell/Services/CavaService.qml @@ -0,0 +1,58 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property list values: Array(6) + property int refCount: 0 + property bool cavaAvailable: false + + Process { + id: cavaCheck + + command: ["which", "cava"] + running: false + onExited: exitCode => { + root.cavaAvailable = exitCode === 0 + } + } + + Component.onCompleted: { + cavaCheck.running = true + } + + Process { + id: cavaProcess + + running: root.cavaAvailable && root.refCount > 0 + command: ["sh", "-c", `printf '[general]\\nmode=normal\\nframerate=25\\nautosens=0\\nsensitivity=30\\nbars=6\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin`] + + onRunningChanged: { + if (!running) { + root.values = Array(6).fill(0) + } + } + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => { + if (root.refCount > 0 && data.trim()) { + let points = data.split(";").map(p => { + return parseInt(p.trim(), 10) + }).filter(p => { + return !isNaN(p) + }) + if (points.length >= 6) { + root.values = points.slice(0, 6) + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Services/CompositorService.qml b/quickshell/.config/quickshell/Services/CompositorService.qml new file mode 100644 index 0000000..dbf742c --- /dev/null +++ b/quickshell/.config/quickshell/Services/CompositorService.qml @@ -0,0 +1,69 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland + +Singleton { + id: root + + property bool isNiri: false + property string compositor: "unknown" + + readonly property string niriSocket: Quickshell.env("NIRI_SOCKET") + + property bool useNiriSorting: isNiri && NiriService + + property var sortedToplevels: { + if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) { + return [] + } + + if (useNiriSorting) { + return NiriService.sortToplevels(ToplevelManager.toplevels.values) + + return ToplevelManager.toplevels.values + } + } + + Component.onCompleted: { + detectCompositor() + } + + function filterCurrentWorkspace(toplevels, screen) { + if (useNiriSorting) { + return NiriService.filterCurrentWorkspace(toplevels, screen) + } + return toplevels + } + + function detectCompositor() { + if (niriSocket && niriSocket.length > 0) { + niriSocketCheck.running = true + } else { + isNiri = false + compositor = "unknown" + console.warn("CompositorService: No compositor detected") + } + } + + Process { + id: niriSocketCheck + command: ["test", "-S", root.niriSocket] + + onExited: exitCode => { + if (exitCode === 0) { + root.isNiri = true + root.compositor = "niri" + console.log("CompositorService: Detected Niri with socket:", root.niriSocket) + } else { + root.isNiri = true + root.compositor = "niri" + console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway") + } + } + } +} diff --git a/quickshell/.config/quickshell/Services/DesktopService.qml b/quickshell/.config/quickshell/Services/DesktopService.qml new file mode 100644 index 0000000..2066e64 --- /dev/null +++ b/quickshell/.config/quickshell/Services/DesktopService.qml @@ -0,0 +1,73 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + function resolveIconPath(moddedAppId) { + const entry = DesktopEntries.heuristicLookup(moddedAppId) + const appIds = [moddedAppId, moddedAppId.toLowerCase()]; + + const lastPart = moddedAppId.split('.').pop(); + if (lastPart && lastPart !== moddedAppId) { + appIds.push(lastPart); + + const firstChar = lastPart.charAt(0); + const rest = lastPart.slice(1); + let toggled; + + if (firstChar === firstChar.toLowerCase()) { + toggled = firstChar.toUpperCase() + rest; + } else { + toggled = firstChar.toLowerCase() + rest; + } + + if (toggled !== lastPart) { + appIds.push(toggled); + } + } + for (const appId of appIds){ + let icon = Quickshell.iconPath(entry?.icon, true) + console.log(icon) + if (icon && icon !== "") return icon + + let iconPath = `/usr/share/pixmaps/${appId}` + icon = Quickshell.iconPath(iconPath, true) + console.log(icon) + if (icon && icon !== "") return icon + + iconPath = `/usr/share/pixmaps/${appId}-qt6` + icon = Quickshell.iconPath(iconPath, true) + console.log(icon) + if (icon && icon !== "") return icon + + let execPath = entry?.execString?.replace(/\/bin.*/, "") + console.log(execPath) + if (!execPath) continue + + //Check that the app is installed with nix/guix + if (execPath.startsWith("/nix/store/") || execPath.startsWith("/gnu/store/")) { + const basePath = execPath + const sizes = ["256x256", "128x128", "64x64", "48x48", "32x32", "24x24", "16x16"] + + iconPath = `${basePath}/share/icons/hicolor/scalable/apps/${appId}.svg` + icon = Quickshell.iconPath(iconPath, true) + console.log(icon) + if (icon && icon !== "") return icon + + for (const size of sizes) { + iconPath = `${basePath}/share/icons/hicolor/${size}/apps/${appId}.png` + icon = Quickshell.iconPath(iconPath, true) + console.log(icon) + if (icon && icon !== "") return icon + } + } + } + + return "" +} +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Services/DgopService.qml b/quickshell/.config/quickshell/Services/DgopService.qml new file mode 100644 index 0000000..7104a0e --- /dev/null +++ b/quickshell/.config/quickshell/Services/DgopService.qml @@ -0,0 +1,699 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common + +Singleton { + id: root + + property int refCount: 0 + property int updateInterval: refCount > 0 ? 3000 : 30000 + property bool isUpdating: false + property bool dgopAvailable: false + + property var moduleRefCounts: ({}) + property var enabledModules: [] + property var gpuPciIds: [] + property var gpuPciIdRefCounts: ({}) + property int processLimit: 20 + property string processSort: "cpu" + property bool noCpu: false + + // Cursor data for accurate CPU calculations + property string cpuCursor: "" + property string procCursor: "" + property int cpuSampleCount: 0 + property int processSampleCount: 0 + + property real cpuUsage: 0 + property real cpuFrequency: 0 + property real cpuTemperature: 0 + property int cpuCores: 1 + property string cpuModel: "" + property var perCoreCpuUsage: [] + + property real memoryUsage: 0 + property real totalMemoryMB: 0 + property real usedMemoryMB: 0 + property real freeMemoryMB: 0 + property real availableMemoryMB: 0 + property int totalMemoryKB: 0 + property int usedMemoryKB: 0 + property int totalSwapKB: 0 + property int usedSwapKB: 0 + + property real networkRxRate: 0 + property real networkTxRate: 0 + property var lastNetworkStats: null + property var networkInterfaces: [] + + property real diskReadRate: 0 + property real diskWriteRate: 0 + property var lastDiskStats: null + property var diskMounts: [] + property var diskDevices: [] + + property var processes: [] + property var allProcesses: [] + property string currentSort: "cpu" + property var availableGpus: [] + + property string kernelVersion: "" + property string distribution: "" + property string hostname: "" + property string architecture: "" + property string loadAverage: "" + property int processCount: 0 + property int threadCount: 0 + property string bootTime: "" + property string motherboard: "" + property string biosVersion: "" + + property int historySize: 60 + property var cpuHistory: [] + property var memoryHistory: [] + property var networkHistory: ({ + "rx": [], + "tx": [] + }) + property var diskHistory: ({ + "read": [], + "write": [] + }) + + function addRef(modules = null) { + refCount++ + let modulesChanged = false + + if (modules) { + const modulesToAdd = Array.isArray(modules) ? modules : [modules] + for (const module of modulesToAdd) { + // Increment reference count for this module + const currentCount = moduleRefCounts[module] || 0 + moduleRefCounts[module] = currentCount + 1 + console.log("Adding ref for module:", module, "count:", moduleRefCounts[module]) + + // Add to enabled modules if not already there + if (enabledModules.indexOf(module) === -1) { + enabledModules.push(module) + modulesChanged = true + } + } + } + + if (modulesChanged || refCount === 1) { + enabledModules = enabledModules.slice() // Force property change + moduleRefCounts = Object.assign({}, moduleRefCounts) // Force property change + updateAllStats() + } else if (gpuPciIds.length > 0 && refCount > 0) { + // If we have GPU PCI IDs and active modules, make sure to update + // This handles the case where PCI IDs were loaded after modules were added + updateAllStats() + } + } + + function removeRef(modules = null) { + refCount = Math.max(0, refCount - 1) + let modulesChanged = false + + if (modules) { + const modulesToRemove = Array.isArray(modules) ? modules : [modules] + for (const module of modulesToRemove) { + const currentCount = moduleRefCounts[module] || 0 + if (currentCount > 1) { + // Decrement reference count + moduleRefCounts[module] = currentCount - 1 + console.log("Removing ref for module:", module, "count:", moduleRefCounts[module]) + } else if (currentCount === 1) { + // Remove completely when count reaches 0 + delete moduleRefCounts[module] + const index = enabledModules.indexOf(module) + if (index > -1) { + enabledModules.splice(index, 1) + modulesChanged = true + console.log("Disabling module:", module, "(no more refs)") + } + } + } + } + + if (modulesChanged) { + enabledModules = enabledModules.slice() // Force property change + moduleRefCounts = Object.assign({}, moduleRefCounts) // Force property change + + // Clear cursor data when CPU or process modules are no longer active + if (!enabledModules.includes("cpu")) { + cpuCursor = "" + cpuSampleCount = 0 + } + if (!enabledModules.includes("processes")) { + procCursor = "" + processSampleCount = 0 + } + } + } + + function setGpuPciIds(pciIds) { + gpuPciIds = Array.isArray(pciIds) ? pciIds : [] + } + + function addGpuPciId(pciId) { + const currentCount = gpuPciIdRefCounts[pciId] || 0 + gpuPciIdRefCounts[pciId] = currentCount + 1 + + // Add to gpuPciIds array if not already there + if (!gpuPciIds.includes(pciId)) { + gpuPciIds = gpuPciIds.concat([pciId]) + } + + console.log("Adding GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId]) + // Force property change notification + gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts) + } + + function removeGpuPciId(pciId) { + const currentCount = gpuPciIdRefCounts[pciId] || 0 + if (currentCount > 1) { + // Decrement reference count + gpuPciIdRefCounts[pciId] = currentCount - 1 + console.log("Removing GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId]) + } else if (currentCount === 1) { + // Remove completely when count reaches 0 + delete gpuPciIdRefCounts[pciId] + const index = gpuPciIds.indexOf(pciId) + if (index > -1) { + gpuPciIds = gpuPciIds.slice() + gpuPciIds.splice(index, 1) + } + + // Clear temperature data for this GPU when no longer monitored + if (availableGpus && availableGpus.length > 0) { + const updatedGpus = availableGpus.slice() + for (var i = 0; i < updatedGpus.length; i++) { + if (updatedGpus[i].pciId === pciId) { + updatedGpus[i] = Object.assign({}, updatedGpus[i], { + "temperature": 0 + }) + } + } + availableGpus = updatedGpus + } + + console.log("Removing GPU PCI ID completely:", pciId) + } + + // Force property change notification + gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts) + } + + function setProcessOptions(limit = 20, sort = "cpu", disableCpu = false) { + processLimit = limit + processSort = sort + noCpu = disableCpu + } + + function updateAllStats() { + if (dgopAvailable && refCount > 0 && enabledModules.length > 0) { + isUpdating = true + dgopProcess.running = true + } else { + isUpdating = false + } + } + + function initializeGpuMetadata() { + if (!dgopAvailable) + return + // Load GPU metadata once at startup for basic info + gpuInitProcess.running = true + } + + function buildDgopCommand() { + const cmd = ["dgop", "meta", "--json"] + + if (enabledModules.length === 0) { + // Don't run if no modules are needed + return [] + } + + // Replace 'gpu' with 'gpu-temp' when we have PCI IDs to monitor + const finalModules = [] + for (const module of enabledModules) { + if (module === "gpu" && gpuPciIds.length > 0) { + finalModules.push("gpu-temp") + } else if (module !== "gpu") { + finalModules.push(module) + } + } + + // Add gpu-temp module automatically when we have PCI IDs to monitor + if (gpuPciIds.length > 0 && finalModules.indexOf("gpu-temp") === -1) { + finalModules.push("gpu-temp") + } + + if (enabledModules.indexOf("all") !== -1) { + cmd.push("--modules", "all") + } else if (finalModules.length > 0) { + const moduleList = finalModules.join(",") + cmd.push("--modules", moduleList) + } else { + return [] + } + + // Add cursor data if available for accurate CPU percentages + if ((enabledModules.includes("cpu") || enabledModules.includes("all")) && cpuCursor) { + cmd.push("--cpu-cursor", cpuCursor) + } + if ((enabledModules.includes("processes") || enabledModules.includes("all")) && procCursor) { + cmd.push("--proc-cursor", procCursor) + } + + if (gpuPciIds.length > 0) { + cmd.push("--gpu-pci-ids", gpuPciIds.join(",")) + } + + if (enabledModules.indexOf("processes") !== -1 || enabledModules.indexOf("all") !== -1) { + cmd.push("--limit", "100") // Get more data for client sorting + cmd.push("--sort", "cpu") // Always get CPU sorted data + if (noCpu) { + cmd.push("--no-cpu") + } + } + + return cmd + } + + function parseData(data) { + if (data.cpu) { + const cpu = data.cpu + cpuSampleCount++ + + cpuUsage = cpu.usage || 0 + cpuFrequency = cpu.frequency || 0 + cpuTemperature = cpu.temperature || 0 + cpuCores = cpu.count || 1 + cpuModel = cpu.model || "" + perCoreCpuUsage = cpu.coreUsage || [] + addToHistory(cpuHistory, cpuUsage) + + if (cpu.cursor) { + cpuCursor = cpu.cursor + } + } + + if (data.memory) { + const mem = data.memory + const totalKB = mem.total || 0 + const availableKB = mem.available || 0 + const freeKB = mem.free || 0 + + totalMemoryMB = totalKB / 1024 + availableMemoryMB = availableKB / 1024 + freeMemoryMB = freeKB / 1024 + usedMemoryMB = totalMemoryMB - availableMemoryMB + memoryUsage = totalKB > 0 ? ((totalKB - availableKB) / totalKB) * 100 : 0 + + totalMemoryKB = totalKB + usedMemoryKB = totalKB - availableKB + totalSwapKB = mem.swaptotal || 0 + usedSwapKB = (mem.swaptotal || 0) - (mem.swapfree || 0) + + addToHistory(memoryHistory, memoryUsage) + } + + if (data.network && Array.isArray(data.network)) { + networkInterfaces = data.network + + let totalRx = 0 + let totalTx = 0 + for (const iface of data.network) { + totalRx += iface.rx || 0 + totalTx += iface.tx || 0 + } + + if (lastNetworkStats) { + const timeDiff = updateInterval / 1000 + const rxDiff = totalRx - lastNetworkStats.rx + const txDiff = totalTx - lastNetworkStats.tx + networkRxRate = Math.max(0, rxDiff / timeDiff) + networkTxRate = Math.max(0, txDiff / timeDiff) + addToHistory(networkHistory.rx, networkRxRate / 1024) + addToHistory(networkHistory.tx, networkTxRate / 1024) + } + lastNetworkStats = { + "rx": totalRx, + "tx": totalTx + } + } + + if (data.disk && Array.isArray(data.disk)) { + diskDevices = data.disk + + let totalRead = 0 + let totalWrite = 0 + for (const disk of data.disk) { + totalRead += (disk.read || 0) * 512 + totalWrite += (disk.write || 0) * 512 + } + + if (lastDiskStats) { + const timeDiff = updateInterval / 1000 + const readDiff = totalRead - lastDiskStats.read + const writeDiff = totalWrite - lastDiskStats.write + diskReadRate = Math.max(0, readDiff / timeDiff) + diskWriteRate = Math.max(0, writeDiff / timeDiff) + addToHistory(diskHistory.read, diskReadRate / (1024 * 1024)) + addToHistory(diskHistory.write, diskWriteRate / (1024 * 1024)) + } + lastDiskStats = { + "read": totalRead, + "write": totalWrite + } + } + + if (data.diskmounts) { + diskMounts = data.diskmounts || [] + } + + if (data.processes && Array.isArray(data.processes)) { + const newProcesses = [] + processSampleCount++ + + for (const proc of data.processes) { + const cpuUsage = processSampleCount >= 2 ? (proc.cpu || 0) : 0 + + newProcesses.push({ + "pid": proc.pid || 0, + "ppid": proc.ppid || 0, + "cpu": cpuUsage, + "memoryPercent": proc.memoryPercent || proc.pssPercent || 0, + "memoryKB": proc.memoryKB || proc.pssKB || 0, + "command": proc.command || "", + "fullCommand": proc.fullCommand || "", + "displayName": (proc.command && proc.command.length > 15) ? proc.command.substring(0, 15) + "..." : (proc.command || "") + }) + } + allProcesses = newProcesses + applySorting() + + if (data.cursor) { + procCursor = data.cursor + } + } + + const gpuData = (data.gpu && data.gpu.gpus) || data.gpus + if (gpuData && Array.isArray(gpuData)) { + // Check if this is temperature update data (has PCI IDs being monitored) + if (gpuPciIds.length > 0 && availableGpus && availableGpus.length > 0) { + // This is temperature data - merge with existing GPU metadata + const updatedGpus = availableGpus.slice() + for (var i = 0; i < updatedGpus.length; i++) { + const existingGpu = updatedGpus[i] + const tempGpu = gpuData.find(g => g.pciId === existingGpu.pciId) + // Only update temperature if this GPU's PCI ID is being monitored + if (tempGpu && gpuPciIds.includes(existingGpu.pciId)) { + updatedGpus[i] = Object.assign({}, existingGpu, { + "temperature": tempGpu.temperature || 0 + }) + } + } + availableGpus = updatedGpus + } else { + // This is initial GPU metadata - set the full list + const gpuList = [] + for (const gpu of gpuData) { + let displayName = gpu.displayName || gpu.name || "Unknown GPU" + let fullName = gpu.fullName || gpu.name || "Unknown GPU" + + gpuList.push({ + "driver": gpu.driver || "", + "vendor": gpu.vendor || "", + "displayName": displayName, + "fullName": fullName, + "pciId": gpu.pciId || "", + "temperature": gpu.temperature || 0 + }) + } + availableGpus = gpuList + } + } + + if (data.system) { + const sys = data.system + loadAverage = sys.loadavg || "" + processCount = sys.processes || 0 + threadCount = sys.threads || 0 + bootTime = sys.boottime || "" + } + + if (data.hardware) { + const hw = data.hardware + hostname = hw.hostname || "" + kernelVersion = hw.kernel || "" + distribution = hw.distro || "" + architecture = hw.arch || "" + motherboard = (hw.bios && hw.bios.motherboard) || "" + biosVersion = (hw.bios && hw.bios.version) || "" + } + + isUpdating = false + } + + function addToHistory(array, value) { + array.push(value) + if (array.length > historySize) { + array.shift() + } + } + + function getProcessIcon(command) { + const cmd = command.toLowerCase() + if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser") || cmd.includes("chromium")) { + return "web" + } + if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) { + return "code" + } + if (cmd.includes("terminal") || cmd.includes("bash") || cmd.includes("zsh")) { + return "terminal" + } + if (cmd.includes("music") || cmd.includes("audio") || cmd.includes("spotify")) { + return "music_note" + } + if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv")) { + return "play_circle" + } + if (cmd.includes("systemd") || cmd.includes("elogind") || cmd.includes("kernel") || cmd.includes("kthread") || cmd.includes("kworker")) { + return "settings" + } + return "memory" + } + + function formatCpuUsage(cpu) { + return (cpu || 0).toFixed(1) + "%" + } + + function formatMemoryUsage(memoryKB) { + const mem = memoryKB || 0 + if (mem < 1024) { + return mem.toFixed(0) + " KB" + } else if (mem < 1024 * 1024) { + return (mem / 1024).toFixed(1) + " MB" + } else { + return (mem / (1024 * 1024)).toFixed(1) + " GB" + } + } + + function formatSystemMemory(memoryKB) { + const mem = memoryKB || 0 + if (mem === 0) { + return "--" + } + if (mem < 1024 * 1024) { + return (mem / 1024).toFixed(0) + " MB" + } else { + return (mem / (1024 * 1024)).toFixed(1) + " GB" + } + } + + function killProcess(pid) { + if (pid > 0) { + Quickshell.execDetached("kill", [pid.toString()]) + } + } + + function setSortBy(newSortBy) { + if (newSortBy !== currentSort) { + currentSort = newSortBy + applySorting() + } + } + + function applySorting() { + if (!allProcesses || allProcesses.length === 0) { + return + } + + const sorted = allProcesses.slice() + sorted.sort((a, b) => { + let valueA, valueB + + switch (currentSort) { + case "cpu": + valueA = a.cpu || 0 + valueB = b.cpu || 0 + return valueB - valueA + case "memory": + valueA = a.memoryKB || 0 + valueB = b.memoryKB || 0 + return valueB - valueA + case "name": + valueA = (a.command || "").toLowerCase() + valueB = (b.command || "").toLowerCase() + return valueA.localeCompare(valueB) + case "pid": + valueA = a.pid || 0 + valueB = b.pid || 0 + return valueA - valueB + default: + return 0 + } + }) + + processes = sorted.slice(0, processLimit) + } + + Timer { + id: updateTimer + interval: root.updateInterval + running: root.dgopAvailable && root.refCount > 0 && root.enabledModules.length > 0 + repeat: true + triggeredOnStart: true + onTriggered: root.updateAllStats() + } + + Process { + id: dgopProcess + command: root.buildDgopCommand() + running: false + onCommandChanged: { + + //console.log("DgopService command:", JSON.stringify(command)) + } + onExited: exitCode => { + if (exitCode !== 0) { + console.warn("Dgop process failed with exit code:", exitCode) + isUpdating = false + } + } + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + try { + const data = JSON.parse(text.trim()) + parseData(data) + } catch (e) { + console.warn("Failed to parse dgop JSON:", e) + console.warn("Raw text was:", text.substring(0, 200)) + isUpdating = false + } + } + } + } + } + + Process { + id: gpuInitProcess + command: ["dgop", "gpu", "--json"] + running: false + onExited: exitCode => { + if (exitCode !== 0) { + console.warn("GPU init process failed with exit code:", exitCode) + } + } + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + try { + const data = JSON.parse(text.trim()) + parseData(data) + } catch (e) { + console.warn("Failed to parse GPU init JSON:", e) + } + } + } + } + } + + Process { + id: dgopCheckProcess + command: ["which", "dgop"] + running: false + onExited: exitCode => { + dgopAvailable = (exitCode === 0) + if (dgopAvailable) { + initializeGpuMetadata() + // Load persisted GPU PCI IDs from session state + if (SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.length > 0) { + for (const pciId of SessionData.enabledGpuPciIds) { + addGpuPciId(pciId) + } + // Trigger update if we already have active modules + if (refCount > 0 && enabledModules.length > 0) { + updateAllStats() + } + } + } else { + console.warn("dgop is not installed or not in PATH") + } + } + } + + Process { + id: osReleaseProcess + command: ["cat", "/etc/os-release"] + running: false + onExited: exitCode => { + if (exitCode !== 0) { + console.warn("Failed to read /etc/os-release") + } + } + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + try { + const lines = text.trim().split('\n') + let prettyName = "" + let name = "" + + for (const line of lines) { + const trimmedLine = line.trim() + if (trimmedLine.startsWith('PRETTY_NAME=')) { + prettyName = trimmedLine.substring(12).replace(/^["']|["']$/g, '') + } else if (trimmedLine.startsWith('NAME=')) { + name = trimmedLine.substring(5).replace(/^["']|["']$/g, '') + } + } + + // Prefer PRETTY_NAME, fallback to NAME + const distroName = prettyName || name || "Linux" + distribution = distroName + console.log("Detected distribution:", distroName) + } catch (e) { + console.warn("Failed to parse /etc/os-release:", e) + distribution = "Linux" + } + } + } + } + } + + Component.onCompleted: { + dgopCheckProcess.running = true + osReleaseProcess.running = true + } +} diff --git a/quickshell/.config/quickshell/Services/DisplayService.qml b/quickshell/.config/quickshell/Services/DisplayService.qml new file mode 100644 index 0000000..e964785 --- /dev/null +++ b/quickshell/.config/quickshell/Services/DisplayService.qml @@ -0,0 +1,974 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common + +Singleton { + id: root + + property bool brightnessAvailable: devices.length > 0 + property var devices: [] + property var ddcDevices: [] + property var deviceBrightness: ({}) + property var ddcPendingInit: ({}) + property string currentDevice: "" + property string lastIpcDevice: "" + property bool ddcAvailable: false + property var ddcInitQueue: [] + property bool skipDdcRead: false + property int brightnessLevel: { + const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice) + if (!deviceToUse) { + return 50 + } + + return getDeviceBrightness(deviceToUse) + } + property int maxBrightness: 100 + property bool brightnessInitialized: false + + signal brightnessChanged + signal deviceSwitched + + property bool nightModeActive: nightModeEnabled + + property bool nightModeEnabled: false + property bool automationAvailable: false + property bool geoclueAvailable: false + property bool isAutomaticNightTime: false + + function buildGammastepCommand(gammastepArgs) { + const commandStr = "pkill gammastep; " + ["gammastep"].concat(gammastepArgs).join(" ") + return ["sh", "-c", commandStr] + } + + function setBrightnessInternal(percentage, device) { + const clampedValue = Math.max(1, Math.min(100, percentage)) + const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice()) + + if (actualDevice) { + const newBrightness = Object.assign({}, deviceBrightness) + newBrightness[actualDevice] = clampedValue + deviceBrightness = newBrightness + } + + const deviceInfo = getCurrentDeviceInfoByName(actualDevice) + + if (deviceInfo && deviceInfo.class === "ddc") { + ddcBrightnessSetProcess.command = ["ddcutil", "setvcp", "-d", String(deviceInfo.ddcDisplay), "10", String(clampedValue)] + ddcBrightnessSetProcess.running = true + } else { + if (device) { + brightnessSetProcess.command = ["brightnessctl", "-d", device, "set", `${clampedValue}%`] + } else { + brightnessSetProcess.command = ["brightnessctl", "set", `${clampedValue}%`] + } + brightnessSetProcess.running = true + } + } + + function setBrightness(percentage, device) { + setBrightnessInternal(percentage, device) + brightnessChanged() + } + + function setCurrentDevice(deviceName, saveToSession = false) { + if (currentDevice === deviceName) { + return + } + + currentDevice = deviceName + lastIpcDevice = deviceName + + if (saveToSession) { + SessionData.setLastBrightnessDevice(deviceName) + } + + deviceSwitched() + + const deviceInfo = getCurrentDeviceInfoByName(deviceName) + if (deviceInfo && deviceInfo.class === "ddc") { + return + } else { + brightnessGetProcess.command = ["brightnessctl", "-m", "-d", deviceName, "get"] + brightnessGetProcess.running = true + } + } + + function refreshDevices() { + deviceListProcess.running = true + } + + function refreshDevicesInternal() { + const allDevices = [...devices, ...ddcDevices] + + allDevices.sort((a, b) => { + if (a.class === "backlight" && b.class !== "backlight") { + return -1 + } + if (a.class !== "backlight" && b.class === "backlight") { + return 1 + } + + if (a.class === "ddc" && b.class !== "ddc" && b.class !== "backlight") { + return -1 + } + if (a.class !== "ddc" && b.class === "ddc" && a.class !== "backlight") { + return 1 + } + + return a.name.localeCompare(b.name) + }) + + devices = allDevices + + if (devices.length > 0 && !currentDevice) { + const lastDevice = SessionData.lastBrightnessDevice || "" + const deviceExists = devices.some(d => d.name === lastDevice) + if (deviceExists) { + setCurrentDevice(lastDevice, false) + } else { + const nonKbdDevice = devices.find(d => !d.name.includes("kbd")) || devices[0] + setCurrentDevice(nonKbdDevice.name, false) + } + } + } + + function getDeviceBrightness(deviceName) { + if (!deviceName) { + return + } 50 + + const deviceInfo = getCurrentDeviceInfoByName(deviceName) + if (!deviceInfo) { + return 50 + } + + if (deviceInfo.class === "ddc") { + return deviceBrightness[deviceName] || 50 + } + + return deviceBrightness[deviceName] || deviceInfo.percentage || 50 + } + + function getDefaultDevice() { + for (const device of devices) { + if (device.class === "backlight") { + return device.name + } + } + return devices.length > 0 ? devices[0].name : "" + } + + function getCurrentDeviceInfo() { + const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice) + if (!deviceToUse) { + return null + } + + for (const device of devices) { + if (device.name === deviceToUse) { + return device + } + } + return null + } + + function isCurrentDeviceReady() { + const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice) + if (!deviceToUse) { + return false + } + + if (ddcPendingInit[deviceToUse]) { + return false + } + + return true + } + + function getCurrentDeviceInfoByName(deviceName) { + if (!deviceName) { + return null + } + + for (const device of devices) { + if (device.name === deviceName) { + return device + } + } + return null + } + + function processNextDdcInit() { + if (ddcInitQueue.length === 0 || ddcInitialBrightnessProcess.running) { + return + } + + const displayId = ddcInitQueue.shift() + ddcInitialBrightnessProcess.command = ["ddcutil", "getvcp", "-d", String(displayId), "10", "--brief"] + ddcInitialBrightnessProcess.running = true + } + + // Night Mode Functions - Simplified + function enableNightMode() { + if (!automationAvailable) { + gammaStepTestProcess.running = true + return + } + + nightModeEnabled = true + SessionData.setNightModeEnabled(true) + + // Apply immediately or start automation + if (SessionData.nightModeAutoEnabled) { + startAutomation() + } else { + applyNightModeDirectly() + } + } + + function disableNightMode() { + nightModeEnabled = false + SessionData.setNightModeEnabled(false) + stopAutomation() + // Nuclear approach - kill ALL gammastep processes multiple times + Quickshell.execDetached(["pkill", "-f", "gammastep"]) + Quickshell.execDetached(["pkill", "-9", "gammastep"]) + Quickshell.execDetached(["killall", "gammastep"]) + // Also stop all related processes + gammaStepProcess.running = false + automationProcess.running = false + gammaStepTestProcess.running = false + } + + function toggleNightMode() { + if (nightModeEnabled) { + disableNightMode() + } else { + enableNightMode() + } + } + + function applyNightModeDirectly() { + const temperature = SessionData.nightModeTemperature || 4500 + gammaStepProcess.command = buildGammastepCommand(["-m", "wayland", "-O", String(temperature)]) + gammaStepProcess.running = true + } + + function resetToNormalMode() { + // Just kill gammastep to return to normal display temperature + Quickshell.execDetached(["pkill", "gammastep"]) + } + + function startAutomation() { + if (!automationAvailable) { + return + } + + const mode = SessionData.nightModeAutoMode || "time" + + switch (mode) { + case "time": + startTimeBasedMode() + break + case "location": + startLocationBasedMode() + break + } + } + + function stopAutomation() { + automationProcess.running = false + gammaStepProcess.running = false + isAutomaticNightTime = false + // Nuclear approach - kill ALL gammastep processes multiple times + Quickshell.execDetached(["pkill", "-f", "gammastep"]) + Quickshell.execDetached(["pkill", "-9", "gammastep"]) + Quickshell.execDetached(["killall", "gammastep"]) + } + + function startTimeBasedMode() { + checkTimeBasedMode() + } + + function startLocationBasedMode() { + const temperature = SessionData.nightModeTemperature || 4500 + const dayTemp = 6500 + + if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { + automationProcess.command = buildGammastepCommand(["-m", "wayland", "-l", `${SessionData.latitude.toFixed(6)}:${SessionData.longitude.toFixed(6)}`, "-t", `${dayTemp}:${temperature}`, "-v"]) + automationProcess.running = true + return + } + + if (SessionData.nightModeLocationProvider === "geoclue2") { + automationProcess.command = buildGammastepCommand(["-m", "wayland", "-l", "geoclue2", "-t", `${dayTemp}:${temperature}`, "-v"]) + automationProcess.running = true + return + } + + console.warn("DisplayService: Location mode selected but no coordinates or geoclue provider set") + } + + function checkTimeBasedMode() { + if (!nightModeEnabled || !SessionData.nightModeAutoEnabled || SessionData.nightModeAutoMode !== "time") { + return + } + + const currentTime = systemClock.hours * 60 + systemClock.minutes + + const startMinutes = SessionData.nightModeStartHour * 60 + SessionData.nightModeStartMinute + const endMinutes = SessionData.nightModeEndHour * 60 + SessionData.nightModeEndMinute + + let shouldBeNight = false + + if (startMinutes > endMinutes) { + shouldBeNight = (currentTime >= startMinutes) || (currentTime < endMinutes) + } else { + shouldBeNight = (currentTime >= startMinutes) && (currentTime < endMinutes) + } + + if (shouldBeNight !== isAutomaticNightTime) { + isAutomaticNightTime = shouldBeNight + + if (shouldBeNight) { + applyNightModeDirectly() + } else { + resetToNormalMode() + } + } + } + + function detectLocationProviders() { + geoclueDetectionProcess.running = true + } + + function setNightModeAutomationMode(mode) { + SessionData.setNightModeAutoMode(mode) + } + + function evaluateNightMode() { + // Always stop all processes first to clean slate + stopAutomation() + + if (!nightModeEnabled) { + return + } + + if (SessionData.nightModeAutoEnabled) { + restartTimer.nextAction = "automation" + restartTimer.start() + } else { + restartTimer.nextAction = "direct" + restartTimer.start() + } + } + + function checkNightModeAvailability() { + gammastepAvailabilityProcess.running = true + } + + Timer { + id: restartTimer + property string nextAction: "" + interval: 100 + repeat: false + + onTriggered: { + if (nextAction === "automation") { + startAutomation() + } else if (nextAction === "direct") { + applyNightModeDirectly() + } + nextAction = "" + } + } + + Component.onCompleted: { + ddcDetectionProcess.running = true + refreshDevices() + checkNightModeAvailability() + + // Initialize night mode state from session + nightModeEnabled = SessionData.nightModeEnabled + } + + SystemClock { + id: systemClock + precision: SystemClock.Minutes + onDateChanged: { + if (nightModeEnabled && SessionData.nightModeAutoEnabled && SessionData.nightModeAutoMode === "time") { + checkTimeBasedMode() + } + } + } + + Process { + id: ddcDetectionProcess + + command: ["which", "ddcutil"] + running: false + + onExited: function (exitCode) { + ddcAvailable = (exitCode === 0) + if (ddcAvailable) { + ddcDisplayDetectionProcess.running = true + } else { + console.log("DisplayService: ddcutil not available") + } + } + } + + Process { + id: ddcDisplayDetectionProcess + + command: ["bash", "-c", "ddcutil detect --brief 2>/dev/null | grep '^Display [0-9]' | awk '{print \"{\\\"display\\\":\" $2 \",\\\"name\\\":\\\"ddc-\" $2 \"\\\",\\\"class\\\":\\\"ddc\\\"}\"}' | tr '\\n' ',' | sed 's/,$//' | sed 's/^/[/' | sed 's/$/]/' || echo '[]'"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (!text.trim()) { + ddcDevices = [] + return + } + + try { + const parsedDevices = JSON.parse(text.trim()) + const newDdcDevices = [] + + for (const device of parsedDevices) { + if (device.display && device.class === "ddc") { + newDdcDevices.push({ + "name": device.name, + "class": "ddc", + "current": 50, + "percentage": 50, + "max": 100, + "ddcDisplay": device.display + }) + } + } + + ddcDevices = newDdcDevices + console.log("DisplayService: Found", ddcDevices.length, "DDC displays") + + // Queue initial brightness readings for DDC devices + ddcInitQueue = [] + for (const device of ddcDevices) { + ddcInitQueue.push(device.ddcDisplay) + // Mark DDC device as pending initialization + ddcPendingInit[device.name] = true + } + + // Start processing the queue + processNextDdcInit() + + // Refresh device list to include DDC devices + refreshDevicesInternal() + + // Retry setting last device now that DDC devices are available + const lastDevice = SessionData.lastBrightnessDevice || "" + if (lastDevice) { + const deviceExists = devices.some(d => d.name === lastDevice) + if (deviceExists && (!currentDevice || currentDevice !== lastDevice)) { + setCurrentDevice(lastDevice, false) + } + } + } catch (error) { + console.warn("DisplayService: Failed to parse DDC devices:", error) + ddcDevices = [] + } + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + console.warn("DisplayService: Failed to detect DDC displays:", exitCode) + ddcDevices = [] + } + } + } + + Process { + id: deviceListProcess + + command: ["brightnessctl", "-m", "-l"] + onExited: function (exitCode) { + if (exitCode !== 0) { + console.warn("DisplayService: Failed to list devices:", exitCode) + brightnessAvailable = false + } + } + + stdout: StdioCollector { + onStreamFinished: { + if (!text.trim()) { + console.warn("DisplayService: No devices found") + return + } + const lines = text.trim().split("\n") + const newDevices = [] + for (const line of lines) { + const parts = line.split(",") + if (parts.length >= 5) { + newDevices.push({ + "name": parts[0], + "class": parts[1], + "current": parseInt(parts[2]), + "percentage": parseInt(parts[3]), + "max": parseInt(parts[4]) + }) + } + } + // Store brightnessctl devices separately + devices = newDevices + + // Always refresh to combine with DDC devices and set up device selection + refreshDevicesInternal() + } + } + } + + Process { + id: brightnessSetProcess + + running: false + onExited: function (exitCode) { + if (exitCode !== 0) { + console.warn("DisplayService: Failed to set brightness:", exitCode) + } + } + } + + Process { + id: ddcBrightnessSetProcess + + running: false + onExited: function (exitCode) { + if (exitCode !== 0) { + console.warn("DisplayService: Failed to set DDC brightness:", exitCode) + } + } + } + + Process { + id: ddcInitialBrightnessProcess + + running: false + onExited: function (exitCode) { + if (exitCode !== 0) { + console.warn("DisplayService: Failed to get initial DDC brightness:", exitCode) + } + + processNextDdcInit() + } + + stdout: StdioCollector { + onStreamFinished: { + if (!text.trim()) + return + + const parts = text.trim().split(" ") + if (parts.length >= 5) { + const current = parseInt(parts[3]) || 50 + const max = parseInt(parts[4]) || 100 + const brightness = Math.round((current / max) * 100) + + const commandParts = ddcInitialBrightnessProcess.command + if (commandParts && commandParts.length >= 4) { + const displayId = commandParts[3] + const deviceName = "ddc-" + displayId + + var newBrightness = Object.assign({}, deviceBrightness) + newBrightness[deviceName] = brightness + deviceBrightness = newBrightness + + var newPending = Object.assign({}, ddcPendingInit) + delete newPending[deviceName] + ddcPendingInit = newPending + + console.log("DisplayService: Initial DDC Device", deviceName, "brightness:", brightness + "%") + } + } + } + } + } + + Process { + id: brightnessGetProcess + + running: false + onExited: function (exitCode) { + if (exitCode !== 0) { + console.warn("DisplayService: Failed to get brightness:", exitCode) + } + } + + stdout: StdioCollector { + onStreamFinished: { + if (!text.trim()) + return + + const parts = text.trim().split(",") + if (parts.length >= 5) { + const current = parseInt(parts[2]) + const max = parseInt(parts[4]) + maxBrightness = max + const brightness = Math.round((current / max) * 100) + + // Update the device brightness cache + if (currentDevice) { + var newBrightness = Object.assign({}, deviceBrightness) + newBrightness[currentDevice] = brightness + deviceBrightness = newBrightness + } + + brightnessInitialized = true + console.log("DisplayService: Device", currentDevice, "brightness:", brightness + "%") + brightnessChanged() + } + } + } + } + + Process { + id: ddcBrightnessGetProcess + + running: false + onExited: function (exitCode) { + if (exitCode !== 0) { + console.warn("DisplayService: Failed to get DDC brightness:", exitCode) + } + } + + stdout: StdioCollector { + onStreamFinished: { + if (!text.trim()) + return + + // Parse ddcutil getvcp output format: "VCP 10 C 50 100" + const parts = text.trim().split(" ") + if (parts.length >= 5) { + const current = parseInt(parts[3]) || 50 + const max = parseInt(parts[4]) || 100 + maxBrightness = max + const brightness = Math.round((current / max) * 100) + + // Update the device brightness cache + if (currentDevice) { + var newBrightness = Object.assign({}, deviceBrightness) + newBrightness[currentDevice] = brightness + deviceBrightness = newBrightness + } + + brightnessInitialized = true + console.log("DisplayService: DDC Device", currentDevice, "brightness:", brightness + "%") + brightnessChanged() + } + } + } + } + + Process { + id: gammastepAvailabilityProcess + command: ["which", "gammastep"] + running: false + + onExited: function (exitCode) { + automationAvailable = (exitCode === 0) + if (automationAvailable) { + detectLocationProviders() + + // If night mode should be enabled on startup + if (nightModeEnabled && SessionData.nightModeAutoEnabled) { + startAutomation() + } else if (nightModeEnabled) { + applyNightModeDirectly() + } + } else { + console.log("DisplayService: gammastep not available") + } + } + } + + Process { + id: geoclueDetectionProcess + command: ["sh", "-c", "busctl --system list | grep -qF org.freedesktop.GeoClue2"] + running: false + + onExited: function (exitCode) { + geoclueAvailable = (exitCode === 0) + console.log("DisplayService: geoclue available:", geoclueAvailable) + } + } + + Process { + id: gammaStepTestProcess + command: ["which", "gammastep"] + running: false + + onExited: function (exitCode) { + if (exitCode === 0) { + automationAvailable = true + nightModeEnabled = true + SessionData.setNightModeEnabled(true) + + if (SessionData.nightModeAutoEnabled) { + startAutomation() + } else { + applyNightModeDirectly() + } + } else { + console.warn("DisplayService: gammastep not found") + ToastService.showWarning("Night mode failed: gammastep not found") + } + } + } + + Process { + id: gammaStepProcess + running: false + + onExited: function (exitCode) { + if (nightModeEnabled && exitCode !== 0 && exitCode !== 15) { + console.warn("DisplayService: Night mode process failed:", exitCode) + } + } + } + + Process { + id: automationProcess + running: false + property string processType: "automation" + + onExited: function (exitCode) { + if (nightModeEnabled && SessionData.nightModeAutoEnabled && exitCode !== 0 && exitCode !== 15) { + console.warn("DisplayService: Night mode automation failed:", exitCode) + // Location mode failed + console.warn("DisplayService: Location-based night mode failed") + } + } + } + + // Session Data Connections + Connections { + target: SessionData + + function onNightModeEnabledChanged() { + nightModeEnabled = SessionData.nightModeEnabled + evaluateNightMode() + } + + function onNightModeAutoEnabledChanged() { + evaluateNightMode() + } + function onNightModeAutoModeChanged() { + evaluateNightMode() + } + function onNightModeStartHourChanged() { + evaluateNightMode() + } + function onNightModeStartMinuteChanged() { + evaluateNightMode() + } + function onNightModeEndHourChanged() { + evaluateNightMode() + } + function onNightModeEndMinuteChanged() { + evaluateNightMode() + } + function onNightModeTemperatureChanged() { + evaluateNightMode() + } + function onLatitudeChanged() { + evaluateNightMode() + } + function onLongitudeChanged() { + evaluateNightMode() + } + function onNightModeLocationProviderChanged() { + evaluateNightMode() + } + } + + // IPC Handler for external control + IpcHandler { + function set(percentage: string, device: string): string { + if (!root.brightnessAvailable) { + return "Brightness control not available" + } + + const value = parseInt(percentage) + if (isNaN(value)) { + return "Invalid brightness value: " + percentage + } + + const clampedValue = Math.max(1, Math.min(100, value)) + const targetDevice = device || "" + + // Ensure device exists if specified + if (targetDevice && !root.devices.some(d => d.name === targetDevice)) { + return "Device not found: " + targetDevice + } + + root.lastIpcDevice = targetDevice + if (targetDevice && targetDevice !== root.currentDevice) { + root.setCurrentDevice(targetDevice, false) + } + root.setBrightness(clampedValue, targetDevice) + + if (targetDevice) { + return "Brightness set to " + clampedValue + "% on " + targetDevice + } else { + return "Brightness set to " + clampedValue + "%" + } + } + + function increment(step: string, device: string): string { + if (!root.brightnessAvailable) { + return "Brightness control not available" + } + + const targetDevice = device || "" + const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice + + // Ensure device exists + if (actualDevice && !root.devices.some(d => d.name === actualDevice)) { + return "Device not found: " + actualDevice + } + + const currentLevel = actualDevice ? root.getDeviceBrightness(actualDevice) : root.brightnessLevel + const stepValue = parseInt(step || "10") + const newLevel = Math.max(1, Math.min(100, currentLevel + stepValue)) + + root.lastIpcDevice = targetDevice + if (targetDevice && targetDevice !== root.currentDevice) { + root.setCurrentDevice(targetDevice, false) + } + root.setBrightness(newLevel, targetDevice) + + if (targetDevice) { + return "Brightness increased to " + newLevel + "% on " + targetDevice + } else { + return "Brightness increased to " + newLevel + "%" + } + } + + function decrement(step: string, device: string): string { + if (!root.brightnessAvailable) { + return "Brightness control not available" + } + + const targetDevice = device || "" + const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice + + // Ensure device exists + if (actualDevice && !root.devices.some(d => d.name === actualDevice)) { + return "Device not found: " + actualDevice + } + + const currentLevel = actualDevice ? root.getDeviceBrightness(actualDevice) : root.brightnessLevel + const stepValue = parseInt(step || "10") + const newLevel = Math.max(1, Math.min(100, currentLevel - stepValue)) + + root.lastIpcDevice = targetDevice + if (targetDevice && targetDevice !== root.currentDevice) { + root.setCurrentDevice(targetDevice, false) + } + root.setBrightness(newLevel, targetDevice) + + if (targetDevice) { + return "Brightness decreased to " + newLevel + "% on " + targetDevice + } else { + return "Brightness decreased to " + newLevel + "%" + } + } + + function status(): string { + if (!root.brightnessAvailable) { + return "Brightness control not available" + } + + return "Device: " + root.currentDevice + " - Brightness: " + root.brightnessLevel + "%" + } + + function list(): string { + if (!root.brightnessAvailable) { + return "No brightness devices available" + } + + let result = "Available devices:\\n" + for (const device of root.devices) { + result += device.name + " (" + device.class + ")\\n" + } + return result + } + + target: "brightness" + } + + // IPC Handler for night mode control + IpcHandler { + function toggle(): string { + root.toggleNightMode() + return root.nightModeEnabled ? "Night mode enabled" : "Night mode disabled" + } + + function enable(): string { + root.enableNightMode() + return "Night mode enabled" + } + + function disable(): string { + root.disableNightMode() + return "Night mode disabled" + } + + function status(): string { + return root.nightModeEnabled ? "Night mode is enabled" : "Night mode is disabled" + } + + function temperature(value: string): string { + if (!value) { + return "Current temperature: " + SessionData.nightModeTemperature + "K" + } + + const temp = parseInt(value) + if (isNaN(temp)) { + return "Invalid temperature. Use a value between 2500 and 6000 (in steps of 500)" + } + + // Validate temperature is in valid range and steps + if (temp < 2500 || temp > 6000) { + return "Temperature must be between 2500K and 6000K" + } + + // Round to nearest 500 + const rounded = Math.round(temp / 500) * 500 + + SessionData.setNightModeTemperature(rounded) + + // Restart night mode with new temperature if active + if (root.nightModeEnabled) { + if (SessionData.nightModeAutoEnabled) { + root.startAutomation() + } else { + root.applyNightModeDirectly() + } + } + + if (rounded !== temp) { + return "Night mode temperature set to " + rounded + "K (rounded from " + temp + "K)" + } else { + return "Night mode temperature set to " + rounded + "K" + } + } + + target: "night" + } +} diff --git a/quickshell/.config/quickshell/Services/LockScreenService.qml b/quickshell/.config/quickshell/Services/LockScreenService.qml new file mode 100644 index 0000000..15fffb7 --- /dev/null +++ b/quickshell/.config/quickshell/Services/LockScreenService.qml @@ -0,0 +1,68 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell + +Singleton { + id: root + + property bool unlocking: false + property string randomFact: "" + property string pamState: "" + property bool powerDialogVisible: false + property bool rebootDialogVisible: false + property bool logoutDialogVisible: false + + property var facts: ["A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.", "A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.", "Right now, 100 trillion solar neutrinos are passing through your body every second.", "The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.", "The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.", "There's a nebula out there that's actually colder than empty space itself.", "We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.", "Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.", "Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.", "Distant galaxies can move away from us faster than light because space itself is stretching.", "The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.", "The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.", "A day on Venus lasts longer than its entire year around the Sun.", "On Mercury, the time between sunrises is 176 Earth days long.", "In about 4.5 billion years, our galaxy will smash into Andromeda.", "Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.", "PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.", "Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.", "Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.", "Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.", "Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.", "Counting to a billion at one number per second would take over 31 years.", "Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.", "Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.", "Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.", "Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.", "Even at light-speed, you'd never catch up to most galaxies—space expands faster.", "Only around 5% of galaxies are ever reachable—even at light-speed.", "If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.", "If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.", "Our oldest radio signals will reach the Milky Way's center in 26,000 years.", "Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.", "The Moon moves 3.8 centimeters farther from Earth every year.", "The universe creates 275 million new stars every single day.", "Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.", "If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.", "The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."] + + function pickRandomFact() { + randomFact = facts[Math.floor(Math.random() * facts.length)] + } + + function resetState() { + unlocking = false + pamState = "" + powerDialogVisible = false + rebootDialogVisible = false + logoutDialogVisible = false + pickRandomFact() + } + + function setPamState(state) { + pamState = state + } + + function setUnlocking(value) { + unlocking = value + } + + function showPowerDialog() { + powerDialogVisible = true + } + + function hidePowerDialog() { + powerDialogVisible = false + } + + function showRebootDialog() { + rebootDialogVisible = true + } + + function hideRebootDialog() { + rebootDialogVisible = false + } + + function showLogoutDialog() { + logoutDialogVisible = true + } + + function hideLogoutDialog() { + logoutDialogVisible = false + } + + Component.onCompleted: { + pickRandomFact() + } +} diff --git a/quickshell/.config/quickshell/Services/MprisController.qml b/quickshell/.config/quickshell/Services/MprisController.qml new file mode 100644 index 0000000..bfa8857 --- /dev/null +++ b/quickshell/.config/quickshell/Services/MprisController.qml @@ -0,0 +1,60 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris + +Singleton { + id: root + + readonly property list availablePlayers: Mpris.players.values + + property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null + + IpcHandler { + target: "mpris" + + function list(): string { + return root.availablePlayers.map(p => p.identity).join("\n") + } + + function play(): void { + if (root.activePlayer && root.activePlayer.canPlay) { + root.activePlayer.play() + } + } + + function pause(): void { + if (root.activePlayer && root.activePlayer.canPause) { + root.activePlayer.pause() + } + } + + function playPause(): void { + if (root.activePlayer && root.activePlayer.canTogglePlaying) { + root.activePlayer.togglePlaying() + } + } + + function previous(): void { + if (root.activePlayer && root.activePlayer.canGoPrevious) { + root.activePlayer.previous() + } + } + + function next(): void { + if (root.activePlayer && root.activePlayer.canGoNext) { + root.activePlayer.next() + } + } + + function stop(): void { + if (root.activePlayer) { + root.activePlayer.stop() + } + } + } +} diff --git a/quickshell/.config/quickshell/Services/NetworkService.qml b/quickshell/.config/quickshell/Services/NetworkService.qml new file mode 100644 index 0000000..a1ecd03 --- /dev/null +++ b/quickshell/.config/quickshell/Services/NetworkService.qml @@ -0,0 +1,1042 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common + +Singleton { + id: root + + property int refCount: 0 + property string networkStatus: "disconnected" + property string primaryConnection: "" + + property string ethernetIP: "" + property string ethernetInterface: "" + property bool ethernetConnected: false + property string ethernetConnectionUuid: "" + + property string wifiIP: "" + property string wifiInterface: "" + property bool wifiConnected: false + property bool wifiEnabled: true + property string wifiConnectionUuid: "" + + property string currentWifiSSID: "" + property int wifiSignalStrength: 0 + property var wifiNetworks: [] + property var savedConnections: [] + property var ssidToConnectionName: { + + } + property var wifiSignalIcon: { + if (!wifiConnected || networkStatus !== "wifi") { + return "signal_wifi_off" + } + if (wifiSignalStrength >= 70) { + return "signal_wifi_4_bar" + } + if (wifiSignalStrength >= 50) { + return "network_wifi_3_bar" + } + if (wifiSignalStrength >= 25) { + return "network_wifi_2_bar" + } + if (wifiSignalStrength >= 10) { + return "network_wifi_1_bar" + } + return "signal_wifi_bad" + } + + property string userPreference: "auto" // "auto", "wifi", "ethernet" + property bool isConnecting: false + property string connectingSSID: "" + property string connectionError: "" + + property bool isScanning: false + property bool autoScan: false + + property bool wifiAvailable: true + property bool wifiToggling: false + property bool changingPreference: false + property string targetPreference: "" + property var savedWifiNetworks: [] + property string connectionStatus: "" + property string lastConnectionError: "" + property bool passwordDialogShouldReopen: false + property bool autoRefreshEnabled: false + property string wifiPassword: "" + property string forgetSSID: "" + + property string networkInfoSSID: "" + property string networkInfoDetails: "" + property bool networkInfoLoading: false + + signal networksUpdated + signal connectionChanged + + function splitNmcliFields(line) { + const parts = [] + let cur = "" + let escape = false + for (var i = 0; i < line.length; i++) { + const ch = line[i] + if (escape) { + cur += ch + escape = false + } else if (ch === '\\') { + escape = true + } else if (ch === ':') { + parts.push(cur) + cur = "" + } else { + cur += ch + } + } + parts.push(cur) + return parts + } + + Component.onCompleted: { + root.userPreference = SettingsData.networkPreference + initializeDBusMonitors() + } + + function addRef() { + refCount++ + if (refCount === 1) { + startAutoScan() + } + } + + function removeRef() { + refCount = Math.max(0, refCount - 1) + if (refCount === 0) { + stopAutoScan() + } + } + + function initializeDBusMonitors() { + nmStateMonitor.running = true + doRefreshNetworkState() + } + + Process { + id: nmStateMonitor + command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.NetworkManager"] + running: false + + stdout: SplitParser { + splitMarker: "\n" + onRead: line => { + if (line.includes("StateChanged") || line.includes("PrimaryConnectionChanged") || line.includes("WirelessEnabled") || line.includes("ActiveConnection") || line.includes("PropertiesChanged")) { + refreshNetworkState() + } + } + } + + onExited: exitCode => { + if (exitCode !== 0 && !restartTimer.running) { + console.warn("NetworkManager monitor failed, restarting in 5s") + restartTimer.start() + } + } + } + + Timer { + id: restartTimer + interval: 5000 + running: false + onTriggered: nmStateMonitor.running = true + } + + Timer { + id: refreshDebounceTimer + interval: 100 + running: false + onTriggered: doRefreshNetworkState() + } + + function refreshNetworkState() { + refreshDebounceTimer.restart() + } + + function doRefreshNetworkState() { + updatePrimaryConnection() + updateDeviceStates() + updateActiveConnections() + updateWifiState() + if (root.refCount > 0 && root.wifiEnabled) { + scanWifiNetworks() + } + } + + function updatePrimaryConnection() { + primaryConnectionQuery.running = true + } + + Process { + id: primaryConnectionQuery + command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", "/org/freedesktop/NetworkManager", "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager", "PrimaryConnection"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const match = text.match(/objectpath '([^']+)'/) + if (match && match[1] !== '/') { + root.primaryConnection = match[1] + getPrimaryConnectionType.running = true + } else { + root.primaryConnection = "" + root.networkStatus = "disconnected" + } + } + } + } + + Process { + id: getPrimaryConnectionType + command: root.primaryConnection ? ["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", root.primaryConnection, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager.Connection.Active", "Type"] : [] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text.includes("802-3-ethernet")) { + root.networkStatus = "ethernet" + } else if (text.includes("802-11-wireless")) { + root.networkStatus = "wifi" + } + root.connectionChanged() + } + } + } + + function updateDeviceStates() { + getEthernetDevice.running = true + getWifiDevice.running = true + } + + Process { + id: getEthernetDevice + command: ["nmcli", "-t", "-f", "DEVICE,TYPE", "device"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const lines = text.trim().split('\n') + let ethernetInterface = "" + + for (const line of lines) { + const splitParts = line.split(':') + const device = splitParts[0] + const type = splitParts.length > 1 ? splitParts[1] : "" + if (type === "ethernet") { + ethernetInterface = device + break + } + } + + if (ethernetInterface) { + root.ethernetInterface = ethernetInterface + getEthernetDevicePath.command = ["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", "/org/freedesktop/NetworkManager", "--method", "org.freedesktop.NetworkManager.GetDeviceByIpIface", ethernetInterface] + getEthernetDevicePath.running = true + } else { + root.ethernetInterface = "" + root.ethernetConnected = false + } + } + } + } + + Process { + id: getEthernetDevicePath + running: false + + stdout: StdioCollector { + onStreamFinished: { + const match = text.match(/objectpath '([^']+)'/) + if (match && match[1] !== '/') { + checkEthernetState.command = ["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", match[1], "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager.Device", "State"] + checkEthernetState.running = true + } else { + root.ethernetInterface = "" + root.ethernetConnected = false + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + root.ethernetInterface = "" + root.ethernetConnected = false + } + } + } + + Process { + id: checkEthernetState + running: false + + stdout: StdioCollector { + onStreamFinished: { + const isConnected = text.includes("uint32 100") + root.ethernetConnected = isConnected + if (isConnected) { + getEthernetIP.running = true + } else { + root.ethernetIP = "" + if (root.networkStatus === "ethernet") { + updatePrimaryConnection() + } + } + } + } + } + + Process { + id: getEthernetIP + command: root.ethernetInterface ? ["ip", "-4", "addr", "show", root.ethernetInterface] : [] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/) + if (match) { + root.ethernetIP = match[1] + } + } + } + } + + Process { + id: getWifiDevice + command: ["nmcli", "-t", "-f", "DEVICE,TYPE", "device"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const lines = text.trim().split('\n') + let wifiInterface = "" + + for (const line of lines) { + const splitParts = line.split(':') + const device = splitParts[0] + const type = splitParts.length > 1 ? splitParts[1] : "" + if (type === "wifi") { + wifiInterface = device + break + } + } + + if (wifiInterface) { + root.wifiInterface = wifiInterface + getWifiDevicePath.command = ["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", "/org/freedesktop/NetworkManager", "--method", "org.freedesktop.NetworkManager.GetDeviceByIpIface", wifiInterface] + getWifiDevicePath.running = true + } else { + root.wifiInterface = "" + root.wifiConnected = false + } + } + } + } + + Process { + id: getWifiDevicePath + running: false + + stdout: StdioCollector { + onStreamFinished: { + const match = text.match(/objectpath '([^']+)'/) + if (match && match[1] !== '/') { + checkWifiState.command = ["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", match[1], "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager.Device", "State"] + checkWifiState.running = true + } else { + root.wifiInterface = "" + root.wifiConnected = false + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + root.wifiInterface = "" + root.wifiConnected = false + } + } + } + + Process { + id: checkWifiState + running: false + + stdout: StdioCollector { + onStreamFinished: { + root.wifiConnected = text.includes("uint32 100") + if (root.wifiConnected) { + getWifiIP.running = true + getCurrentWifiInfo.running = true + // Ensure SSID is resolved even if scan output lacks ACTIVE marker + if (root.currentWifiSSID === "") { + if (root.wifiConnectionUuid) { + resolveWifiSSID.running = true + } + if (root.wifiInterface) { + resolveWifiSSIDFromDevice.running = true + } + } + } else { + root.wifiIP = "" + root.currentWifiSSID = "" + root.wifiSignalStrength = 0 + } + } + } + } + + Process { + id: getWifiIP + command: root.wifiInterface ? ["ip", "-4", "addr", "show", root.wifiInterface] : [] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/) + if (match) { + root.wifiIP = match[1] + } + } + } + } + + Process { + id: getCurrentWifiInfo + command: root.wifiInterface ? ["nmcli", "-t", "-f", "IN-USE,SIGNAL,SSID", "device", "wifi", "list", "ifname", root.wifiInterface] : [] + running: false + + stdout: SplitParser { + splitMarker: "\n" + onRead: line => { + if (line.startsWith("*:")) { + const rest = line.substring(2) + const parts = root.splitNmcliFields(rest) + if (parts.length >= 2) { + const signal = parseInt(parts[0]) + root.wifiSignalStrength = isNaN(signal) ? 0 : signal + root.currentWifiSSID = parts.slice(1).join(":") + } + return + } + if (line.startsWith("yes:")) { + const rest = line.substring(4) + const parts = root.splitNmcliFields(rest) + if (parts.length >= 2) { + root.currentWifiSSID = parts[0] + const signal = parseInt(parts[1]) + root.wifiSignalStrength = isNaN(signal) ? 0 : signal + } + return + } + } + } + } + + function updateActiveConnections() { + getActiveConnections.running = true + } + + Process { + id: getActiveConnections + command: ["nmcli", "-t", "-f", "UUID,TYPE,DEVICE,STATE", "connection", "show", "--active"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const lines = text.trim().split('\n') + for (const line of lines) { + const parts = line.split(':') + if (parts.length >= 4) { + const uuid = parts[0] + const type = parts[1] + const device = parts[2] + const state = parts[3] + if (type === "802-3-ethernet" && state === "activated") { + root.ethernetConnectionUuid = uuid + } else if (type === "802-11-wireless" && state === "activated") { + root.wifiConnectionUuid = uuid + } + } + } + } + } + } + + // Resolve SSID from active WiFi connection UUID when scans don't mark any row as ACTIVE. + Process { + id: resolveWifiSSID + command: root.wifiConnectionUuid ? ["nmcli", "-g", "802-11-wireless.ssid", "connection", "show", "uuid", root.wifiConnectionUuid] : [] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const ssid = text.trim() + if (ssid) { + root.currentWifiSSID = ssid + } + } + } + } + + // Fallback 2: Resolve SSID from device info (GENERAL.CONNECTION usually matches SSID for WiFi) + Process { + id: resolveWifiSSIDFromDevice + command: root.wifiInterface ? ["nmcli", "-t", "-f", "GENERAL.CONNECTION", "device", "show", root.wifiInterface] : [] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (!root.currentWifiSSID) { + const name = text.trim() + if (name) { + root.currentWifiSSID = name + } + } + } + } + } + + function updateWifiState() { + checkWifiEnabled.running = true + } + + Process { + id: checkWifiEnabled + command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", "/org/freedesktop/NetworkManager", "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.NetworkManager", "WirelessEnabled"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + root.wifiEnabled = text.includes("true") + root.wifiAvailable = true // Always available if we can check it + } + } + } + + function scanWifi() { + if (root.isScanning || !root.wifiEnabled) { + return + } + + root.isScanning = true + requestWifiScan.running = true + } + + Process { + id: requestWifiScan + command: root.wifiInterface ? ["nmcli", "dev", "wifi", "rescan", "ifname", root.wifiInterface] : [] + running: false + + onExited: exitCode => { + if (exitCode === 0) { + scanWifiNetworks() + } else { + console.warn("WiFi scan request failed") + root.isScanning = false + } + } + } + + function scanWifiNetworks() { + if (!root.wifiInterface) { + root.isScanning = false + return + } + + getWifiNetworks.running = true + getSavedConnections.running = true + } + + Process { + id: getWifiNetworks + command: ["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,BSSID", "dev", "wifi", "list", "ifname", root.wifiInterface] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const networks = [] + const lines = text.trim().split('\n') + const seen = new Set() + + for (const line of lines) { + const parts = root.splitNmcliFields(line) + if (parts.length >= 4 && parts[0]) { + const ssid = parts[0] + if (!seen.has(ssid)) { + seen.add(ssid) + const signal = parseInt(parts[1]) || 0 + + networks.push({ + "ssid": ssid, + "signal": signal, + "secured": parts[2] !== "", + "bssid": parts[3], + "connected": ssid === root.currentWifiSSID, + "saved": false + }) + } + } + } + + networks.sort((a, b) => b.signal - a.signal) + root.wifiNetworks = networks + root.isScanning = false + root.networksUpdated() + } + } + } + + Process { + id: getSavedConnections + command: ["bash", "-c", "nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless$' | cut -d: -f1 | while read name; do ssid=$(nmcli -g 802-11-wireless.ssid connection show \"$name\"); echo \"$ssid:$name\"; done"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const saved = [] + const mapping = {} + const lines = text.trim().split('\n') + + for (const line of lines) { + const parts = line.trim().split(':') + if (parts.length >= 2) { + const ssid = parts[0] + const connectionName = parts[1] + if (ssid && ssid.length > 0 && connectionName && connectionName.length > 0) { + saved.push({ + "ssid": ssid, + "saved": true + }) + mapping[ssid] = connectionName + } + } + } + + root.savedConnections = saved + root.savedWifiNetworks = saved + root.ssidToConnectionName = mapping + + const updated = [...root.wifiNetworks] + for (const network of updated) { + network.saved = saved.some(s => s.ssid === network.ssid) + } + root.wifiNetworks = updated + } + } + } + + function connectToWifi(ssid, password = "") { + if (root.isConnecting) { + return + } + + root.isConnecting = true + root.connectingSSID = ssid + root.connectionError = "" + root.connectionStatus = "connecting" + + if (!password && root.ssidToConnectionName[ssid]) { + const connectionName = root.ssidToConnectionName[ssid] + wifiConnector.command = ["nmcli", "connection", "up", connectionName] + } else if (password) { + wifiConnector.command = ["nmcli", "dev", "wifi", "connect", ssid, "password", password] + } else { + wifiConnector.command = ["nmcli", "dev", "wifi", "connect", ssid] + } + wifiConnector.running = true + } + + Process { + id: wifiConnector + running: false + + property bool connectionSucceeded: false + + stdout: StdioCollector { + onStreamFinished: { + if (text.includes("successfully")) { + wifiConnector.connectionSucceeded = true + ToastService.showInfo(`Connected to ${root.connectingSSID}`) + root.connectionError = "" + root.connectionStatus = "connected" + + if (root.userPreference === "wifi" || root.userPreference === "auto") { + setConnectionPriority("wifi") + } + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.connectionError = text + root.lastConnectionError = text + if (!wifiConnector.connectionSucceeded && text.trim() !== "") { + if (text.includes("password") || text.includes("authentication")) { + root.connectionStatus = "invalid_password" + root.passwordDialogShouldReopen = true + } else { + root.connectionStatus = "failed" + } + } + } + } + + onExited: exitCode => { + if (exitCode === 0 || wifiConnector.connectionSucceeded) { + if (!wifiConnector.connectionSucceeded) { + ToastService.showInfo(`Connected to ${root.connectingSSID}`) + root.connectionStatus = "connected" + } + } else { + if (root.connectionStatus === "") { + root.connectionStatus = "failed" + } + if (root.connectionStatus === "invalid_password") { + ToastService.showError(`Invalid password for ${root.connectingSSID}`) + } else { + ToastService.showError(`Failed to connect to ${root.connectingSSID}`) + } + } + + wifiConnector.connectionSucceeded = false + root.isConnecting = false + root.connectingSSID = "" + refreshNetworkState() + } + } + + function disconnectWifi() { + if (!root.wifiInterface) { + return + } + + wifiDisconnector.command = ["nmcli", "dev", "disconnect", root.wifiInterface] + wifiDisconnector.running = true + } + + Process { + id: wifiDisconnector + running: false + + onExited: exitCode => { + if (exitCode === 0) { + ToastService.showInfo("Disconnected from WiFi") + root.currentWifiSSID = "" + root.connectionStatus = "" + } + refreshNetworkState() + } + } + + function forgetWifiNetwork(ssid) { + root.forgetSSID = ssid + const connectionName = root.ssidToConnectionName[ssid] || ssid + networkForgetter.command = ["nmcli", "connection", "delete", connectionName] + networkForgetter.running = true + } + + Process { + id: networkForgetter + running: false + + onExited: exitCode => { + if (exitCode === 0) { + ToastService.showInfo(`Forgot network ${root.forgetSSID}`) + + root.savedConnections = root.savedConnections.filter(s => s.ssid !== root.forgetSSID) + root.savedWifiNetworks = root.savedWifiNetworks.filter(s => s.ssid !== root.forgetSSID) + + const updated = [...root.wifiNetworks] + for (const network of updated) { + if (network.ssid === root.forgetSSID) { + network.saved = false + if (network.connected) { + network.connected = false + root.currentWifiSSID = "" + } + } + } + root.wifiNetworks = updated + root.networksUpdated() + refreshNetworkState() + } + root.forgetSSID = "" + } + } + + function toggleWifiRadio() { + if (root.wifiToggling) { + return + } + + root.wifiToggling = true + const targetState = root.wifiEnabled ? "off" : "on" + wifiRadioToggler.targetState = targetState + wifiRadioToggler.command = ["nmcli", "radio", "wifi", targetState] + wifiRadioToggler.running = true + } + + Process { + id: wifiRadioToggler + running: false + + property string targetState: "" + + onExited: exitCode => { + root.wifiToggling = false + if (exitCode === 0) { + ToastService.showInfo(targetState === "on" ? "WiFi enabled" : "WiFi disabled") + } + refreshNetworkState() + } + } + + function setNetworkPreference(preference) { + root.userPreference = preference + root.changingPreference = true + root.targetPreference = preference + SettingsData.setNetworkPreference(preference) + + if (preference === "wifi") { + setConnectionPriority("wifi") + } else if (preference === "ethernet") { + setConnectionPriority("ethernet") + } + } + + function setConnectionPriority(type) { + if (type === "wifi") { + setRouteMetrics.command = ["bash", "-c", "nmcli -t -f NAME,TYPE connection show | grep 802-11-wireless | cut -d: -f1 | " + "xargs -I {} bash -c 'nmcli connection modify \"{}\" ipv4.route-metric 50 ipv6.route-metric 50'; " + "nmcli -t -f NAME,TYPE connection show | grep 802-3-ethernet | cut -d: -f1 | " + "xargs -I {} bash -c 'nmcli connection modify \"{}\" ipv4.route-metric 100 ipv6.route-metric 100'"] + } else if (type === "ethernet") { + setRouteMetrics.command = ["bash", "-c", "nmcli -t -f NAME,TYPE connection show | grep 802-3-ethernet | cut -d: -f1 | " + "xargs -I {} bash -c 'nmcli connection modify \"{}\" ipv4.route-metric 50 ipv6.route-metric 50'; " + "nmcli -t -f NAME,TYPE connection show | grep 802-11-wireless | cut -d: -f1 | " + "xargs -I {} bash -c 'nmcli connection modify \"{}\" ipv4.route-metric 100 ipv6.route-metric 100'"] + } + setRouteMetrics.running = true + } + + Process { + id: setRouteMetrics + running: false + + onExited: exitCode => { + console.log("Set route metrics process exited with code:", exitCode) + if (exitCode === 0) { + restartConnections.running = true + } + } + } + + Process { + id: restartConnections + command: ["bash", "-c", "nmcli -t -f UUID,TYPE connection show --active | " + "grep -E '802-11-wireless|802-3-ethernet' | cut -d: -f1 | " + "xargs -I {} sh -c 'nmcli connection down {} && nmcli connection up {}'"] + running: false + + onExited: { + root.changingPreference = false + root.targetPreference = "" + refreshNetworkState() + } + } + + function startAutoScan() { + root.autoScan = true + root.autoRefreshEnabled = true + if (root.wifiEnabled) { + scanWifi() + } + } + + function stopAutoScan() { + root.autoScan = false + root.autoRefreshEnabled = false + } + + function fetchNetworkInfo(ssid) { + root.networkInfoSSID = ssid + root.networkInfoLoading = true + root.networkInfoDetails = "Loading network information..." + wifiInfoFetcher.running = true + } + + Process { + id: wifiInfoFetcher + command: ["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,FREQ,RATE,MODE,CHAN,WPA-FLAGS,RSN-FLAGS,ACTIVE,BSSID", "dev", "wifi", "list"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + let details = "" + if (text.trim()) { + const lines = text.trim().split('\n') + const bands = [] + + for (const line of lines) { + const parts = line.split(':') + if (parts.length >= 11 && parts[0] === root.networkInfoSSID) { + const signal = parts[1] || "0" + const security = parts[2] || "Open" + const freq = parts[3] || "Unknown" + const rate = parts[4] || "Unknown" + const channel = parts[6] || "Unknown" + const isActive = parts[9] === "yes" + let colonCount = 0 + let bssidStart = -1 + for (var i = 0; i < line.length; i++) { + if (line[i] === ':') { + colonCount++ + if (colonCount === 10) { + bssidStart = i + 1 + break + } + } + } + const bssid = bssidStart >= 0 ? line.substring(bssidStart).replace(/\\:/g, ":") : "" + + let band = "Unknown" + const freqNum = parseInt(freq) + if (freqNum >= 2400 && freqNum <= 2500) { + band = "2.4 GHz" + } else if (freqNum >= 5000 && freqNum <= 6000) { + band = "5 GHz" + } else if (freqNum >= 6000) { + band = "6 GHz" + } + + bands.push({ + "band": band, + "freq": freq, + "channel": channel, + "signal": signal, + "rate": rate, + "security": security, + "isActive": isActive, + "bssid": bssid + }) + } + } + + if (bands.length > 0) { + bands.sort((a, b) => { + if (a.isActive && !b.isActive) { + return -1 + } + if (!a.isActive && b.isActive) { + return 1 + } + return parseInt(b.signal) - parseInt(a.signal) + }) + + for (var i = 0; i < bands.length; i++) { + const b = bands[i] + if (b.isActive) { + details += "● " + b.band + " (Connected) - " + b.signal + "%\\n" + } else { + details += " " + b.band + " - " + b.signal + "%\\n" + } + details += " Channel " + b.channel + " (" + b.freq + " MHz) • " + b.rate + " Mbit/s\\n" + details += " " + b.bssid + if (i < bands.length - 1) { + details += "\\n\\n" + } + } + } + } + + if (details === "") { + details = "Network information not found or network not available." + } + + root.networkInfoDetails = details + root.networkInfoLoading = false + } + } + + onExited: exitCode => { + root.networkInfoLoading = false + if (exitCode !== 0) { + root.networkInfoDetails = "Failed to fetch network information" + } + } + } + + function enableWifiDevice() { + wifiDeviceEnabler.running = true + } + + Process { + id: wifiDeviceEnabler + command: ["sh", "-c", "WIFI_DEV=$(nmcli -t -f DEVICE,TYPE device | grep wifi | cut -d: -f1 | head -1); if [ -n \"$WIFI_DEV\" ]; then nmcli device connect \"$WIFI_DEV\"; else echo \"No WiFi device found\"; exit 1; fi"] + running: false + + onExited: exitCode => { + if (exitCode === 0) { + ToastService.showInfo("WiFi enabled") + } else { + ToastService.showError("Failed to enable WiFi") + } + refreshNetworkState() + } + } + + function connectToWifiAndSetPreference(ssid, password) { + connectToWifi(ssid, password) + setNetworkPreference("wifi") + } + + function toggleNetworkConnection(type) { + if (type === "ethernet") { + if (root.networkStatus === "ethernet") { + ethernetDisconnector.running = true + } else { + ethernetConnector.running = true + } + } + } + + Process { + id: ethernetDisconnector + command: ["sh", "-c", "nmcli device disconnect $(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1)"] + running: false + + onExited: function (exitCode) { + refreshNetworkState() + } + } + + Process { + id: ethernetConnector + command: ["sh", "-c", "ETH_DEV=$(nmcli -t -f DEVICE,TYPE device | grep ethernet | cut -d: -f1 | head -1); if [ -n \"$ETH_DEV\" ]; then nmcli device connect \"$ETH_DEV\"; else echo \"No ethernet device found\"; exit 1; fi"] + running: false + + onExited: function (exitCode) { + refreshNetworkState() + } + } + + function getNetworkInfo(ssid) { + const network = root.wifiNetworks.find(n => n.ssid === ssid) + if (!network) { + return null + } + + return { + "ssid": network.ssid, + "signal": network.signal, + "secured": network.secured, + "saved": network.saved, + "connected": network.connected, + "bssid": network.bssid + } + } +} diff --git a/quickshell/.config/quickshell/Services/NiriService.qml b/quickshell/.config/quickshell/Services/NiriService.qml new file mode 100644 index 0000000..88d29ef --- /dev/null +++ b/quickshell/.config/quickshell/Services/NiriService.qml @@ -0,0 +1,566 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland + +Singleton { + id: root + + property var workspaces: ({}) + property var allWorkspaces: [] + property int focusedWorkspaceIndex: 0 + property string focusedWorkspaceId: "" + property var currentOutputWorkspaces: [] + property string currentOutput: "" + + property var outputs: ({}) + + property var windows: [] + + property bool inOverview: false + + property int currentKeyboardLayoutIndex: 0 + property var keyboardLayoutNames: [] + + property string configValidationOutput: "" + property bool hasInitialConnection: false + property bool suppressConfigToast: true + + readonly property string socketPath: Quickshell.env("NIRI_SOCKET") + + Component.onCompleted: { + fetchOutputs() + } + + function fetchOutputs() { + if (CompositorService.isNiri) { + outputsProcess.running = true + } + } + + Process { + id: outputsProcess + command: ["niri", "msg", "-j", "outputs"] + + stdout: StdioCollector { + onStreamFinished: { + try { + const outputsData = JSON.parse(text) + outputs = outputsData + console.log("NiriService: Loaded", Object.keys(outputsData).length, "outputs") + if (windows.length > 0) { + windows = sortWindowsByLayout(windows) + } + } catch (e) { + console.warn("NiriService: Failed to parse outputs:", e) + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + console.warn("NiriService: Failed to fetch outputs, exit code:", exitCode) + } + } + } + + Socket { + id: eventStreamSocket + path: root.socketPath + connected: CompositorService.isNiri + + onConnectionStateChanged: { + if (connected) { + write('"EventStream"\n') + } + } + + parser: SplitParser { + onRead: line => { + try { + const event = JSON.parse(line) + handleNiriEvent(event) + } catch (e) { + console.warn("NiriService: Failed to parse event:", line, e) + } + } + } + } + + Socket { + id: requestSocket + path: root.socketPath + connected: CompositorService.isNiri + } + + function sortWindowsByLayout(windowList) { + return [...windowList].sort((a, b) => { + const aWorkspace = workspaces[a.workspace_id] + const bWorkspace = workspaces[b.workspace_id] + + if (aWorkspace && bWorkspace) { + const aOutput = aWorkspace.output + const bOutput = bWorkspace.output + + const aOutputInfo = outputs[aOutput] + const bOutputInfo = outputs[bOutput] + + if (aOutputInfo && bOutputInfo && aOutputInfo.logical && bOutputInfo.logical) { + if (aOutputInfo.logical.x !== bOutputInfo.logical.x) { + return aOutputInfo.logical.x - bOutputInfo.logical.x + } + if (aOutputInfo.logical.y !== bOutputInfo.logical.y) { + return aOutputInfo.logical.y - bOutputInfo.logical.y + } + } + + if (aOutput === bOutput && aWorkspace.idx !== bWorkspace.idx) { + return aWorkspace.idx - bWorkspace.idx + } + } + + if (a.workspace_id === b.workspace_id && a.layout && b.layout) { + + if (a.layout.pos_in_scrolling_layout && b.layout.pos_in_scrolling_layout) { + const aPos = a.layout.pos_in_scrolling_layout + const bPos = b.layout.pos_in_scrolling_layout + + if (aPos.length > 1 && bPos.length > 1) { + if (aPos[0] !== bPos[0]) { + return aPos[0] - bPos[0] + } + if (aPos[1] !== bPos[1]) { + return aPos[1] - bPos[1] + } + } + } + } + + return a.id - b.id + }) + } + + function handleNiriEvent(event) { + const eventType = Object.keys(event)[0]; + + switch (eventType) { + case 'WorkspacesChanged': + handleWorkspacesChanged(event.WorkspacesChanged); + break; + case 'WorkspaceActivated': + handleWorkspaceActivated(event.WorkspaceActivated); + break; + case 'WorkspaceActiveWindowChanged': + handleWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged); + break; + case 'WindowsChanged': + handleWindowsChanged(event.WindowsChanged); + break; + case 'WindowClosed': + handleWindowClosed(event.WindowClosed); + break; + case 'WindowOpenedOrChanged': + handleWindowOpenedOrChanged(event.WindowOpenedOrChanged); + break; + case 'WindowLayoutsChanged': + handleWindowLayoutsChanged(event.WindowLayoutsChanged); + break; + case 'OutputsChanged': + handleOutputsChanged(event.OutputsChanged); + break; + case 'OverviewOpenedOrClosed': + handleOverviewChanged(event.OverviewOpenedOrClosed); + break; + case 'ConfigLoaded': + handleConfigLoaded(event.ConfigLoaded); + break; + case 'KeyboardLayoutsChanged': + handleKeyboardLayoutsChanged(event.KeyboardLayoutsChanged); + break; + case 'KeyboardLayoutSwitched': + handleKeyboardLayoutSwitched(event.KeyboardLayoutSwitched); + break; + } + } + + function handleWorkspacesChanged(data) { + const workspaces = {} + + for (const ws of data.workspaces) { + workspaces[ws.id] = ws + } + + root.workspaces = workspaces + allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx) + + focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused) + if (focusedWorkspaceIndex >= 0) { + const focusedWs = allWorkspaces[focusedWorkspaceIndex] + focusedWorkspaceId = focusedWs.id + currentOutput = focusedWs.output || "" + } else { + focusedWorkspaceIndex = 0 + focusedWorkspaceId = "" + } + + updateCurrentOutputWorkspaces() + } + + function handleWorkspaceActivated(data) { + const ws = root.workspaces[data.id] + if (!ws) { + return + } + const output = ws.output + + for (const id in root.workspaces) { + const workspace = root.workspaces[id] + const got_activated = workspace.id === data.id + + if (workspace.output === output) { + workspace.is_active = got_activated + } + + if (data.focused) { + workspace.is_focused = got_activated + } + } + + focusedWorkspaceId = data.id + focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id) + + if (focusedWorkspaceIndex >= 0) { + currentOutput = allWorkspaces[focusedWorkspaceIndex].output || "" + } + + allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx) + + updateCurrentOutputWorkspaces() + workspacesChanged() + } + + function handleWorkspaceActiveWindowChanged(data) { + if (data.active_window_id !== null && data.active_window_id !== undefined) { + const updatedWindows = [] + for (var i = 0; i < windows.length; i++) { + const w = windows[i] + const updatedWindow = {} + for (let prop in w) { + updatedWindow[prop] = w[prop] + } + updatedWindow.is_focused = (w.id == data.active_window_id) + updatedWindows.push(updatedWindow) + } + windows = updatedWindows + } else { + const updatedWindows = [] + for (var i = 0; i < windows.length; i++) { + const w = windows[i] + const updatedWindow = {} + for (let prop in w) { + updatedWindow[prop] = w[prop] + } + updatedWindow.is_focused = w.workspace_id == data.workspace_id ? false : w.is_focused + updatedWindows.push(updatedWindow) + } + windows = updatedWindows + } + } + + function handleWindowsChanged(data) { + windows = sortWindowsByLayout(data.windows) + } + + function handleWindowClosed(data) { + windows = windows.filter(w => w.id !== data.id) + } + + function handleWindowOpenedOrChanged(data) { + if (!data.window) { + return + } + + const window = data.window + const existingIndex = windows.findIndex(w => w.id === window.id) + + if (existingIndex >= 0) { + const updatedWindows = [...windows] + updatedWindows[existingIndex] = window + windows = sortWindowsByLayout(updatedWindows) + } else { + windows = sortWindowsByLayout([...windows, window]) + } + } + + function handleWindowLayoutsChanged(data) { + if (!data.changes) { + return + } + + const updatedWindows = [...windows] + let hasChanges = false + + for (const change of data.changes) { + const windowId = change[0] + const layoutData = change[1] + + const windowIndex = updatedWindows.findIndex(w => w.id === windowId) + if (windowIndex >= 0) { + const updatedWindow = {} + for (var prop in updatedWindows[windowIndex]) { + updatedWindow[prop] = updatedWindows[windowIndex][prop] + } + updatedWindow.layout = layoutData + updatedWindows[windowIndex] = updatedWindow + hasChanges = true + } + } + + if (hasChanges) { + windows = sortWindowsByLayout(updatedWindows) + windowsChanged() + } + } + + function handleOutputsChanged(data) { + if (data.outputs) { + outputs = data.outputs + windows = sortWindowsByLayout(windows) + } + } + + function handleOverviewChanged(data) { + inOverview = data.is_open + } + + function handleConfigLoaded(data) { + if (data.failed) { + validateProcess.running = true + } else { + configValidationOutput = "" + if (ToastService.toastVisible && ToastService.currentLevel === ToastService.levelError) { + ToastService.hideToast() + } + if (hasInitialConnection && !suppressConfigToast) { + ToastService.showInfo("niri: config reloaded") + } + } + + if (!hasInitialConnection) { + hasInitialConnection = true + suppressToastTimer.start() + } + } + + function handleKeyboardLayoutsChanged(data) { + keyboardLayoutNames = data.keyboard_layouts.names + currentKeyboardLayoutIndex = data.keyboard_layouts.current_idx + } + + function handleKeyboardLayoutSwitched(data) { + currentKeyboardLayoutIndex = data.idx + } + + Process { + id: validateProcess + command: ["niri", "validate"] + running: false + + stderr: StdioCollector { + onStreamFinished: { + const lines = text.split('\n') + const trimmedLines = lines.map(line => line.replace(/\s+$/, '')).filter(line => line.length > 0) + configValidationOutput = trimmedLines.join('\n').trim() + if (hasInitialConnection) { + ToastService.showError("niri: failed to load config", configValidationOutput) + } + } + } + + onExited: exitCode => { + if (exitCode === 0) { + configValidationOutput = "" + } + } + } + + function updateCurrentOutputWorkspaces() { + if (!currentOutput) { + currentOutputWorkspaces = allWorkspaces + return + } + + const outputWs = allWorkspaces.filter(w => w.output === currentOutput) + currentOutputWorkspaces = outputWs + } + + function send(request) { + if (!CompositorService.isNiri || !requestSocket.connected) { + return false + } + requestSocket.write(JSON.stringify(request) + "\n") + return true + } + + function switchToWorkspace(workspaceIndex) { + return send({ + "Action": { + "FocusWorkspace": { + "reference": { + "Index": workspaceIndex + } + } + } + }) + } + + function focusWindow(windowId) { + return send({ + "Action": { + "FocusWindow": { + "id": windowId + } + } + }) + } + + function getCurrentOutputWorkspaceNumbers() { + return currentOutputWorkspaces.map(w => w.idx + 1) + } + + function getCurrentWorkspaceNumber() { + if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) { + return allWorkspaces[focusedWorkspaceIndex].idx + 1 + } + return 1 + } + + function getCurrentKeyboardLayoutName() { + if (currentKeyboardLayoutIndex >= 0 && currentKeyboardLayoutIndex < keyboardLayoutNames.length) { + return keyboardLayoutNames[currentKeyboardLayoutIndex] + } + + return "" + } + + function cycleKeyboardLayout() { + return send({ + "Action": { + "SwitchLayout": { + "layout": "Next" + } + } + }) + } + + function quit() { + return send({ + "Action": { + "Quit": { + "skip_confirmation": true + } + } + }) + } + + function findNiriWindow(toplevel) { + if (!toplevel.appId) { + return null + } + + for (var j = 0; j < windows.length; j++) { + const niriWindow = windows[j] + if (niriWindow.app_id === toplevel.appId) { + if (!niriWindow.title || niriWindow.title === toplevel.title) { + return { + "niriIndex": j, + "niriWindow": niriWindow + } + } + } + } + return null + } + + function sortToplevels(toplevels) { + if (!toplevels || toplevels.length === 0 || !CompositorService.isNiri || windows.length === 0) { + return [...toplevels] + } + + return [...toplevels].sort((a, b) => { + const aNiri = findNiriWindow(a) + const bNiri = findNiriWindow(b) + + if (!aNiri && !bNiri) { + return 0 + } + if (!aNiri) { + return 1 + } + if (!bNiri) { + return -1 + } + + const aWindow = aNiri.niriWindow + const bWindow = bNiri.niriWindow + const aWorkspace = allWorkspaces.find(ws => ws.id === aWindow.workspace_id) + const bWorkspace = allWorkspaces.find(ws => ws.id === bWindow.workspace_id) + + if (aWorkspace && bWorkspace) { + if (aWorkspace.output !== bWorkspace.output) { + return aWorkspace.output.localeCompare(bWorkspace.output) + } + + if (aWorkspace.output === bWorkspace.output && aWorkspace.idx !== bWorkspace.idx) { + return aWorkspace.idx - bWorkspace.idx + } + } + + if (aWindow.workspace_id === bWindow.workspace_id && aWindow.layout && bWindow.layout && aWindow.layout.pos_in_scrolling_layout && bWindow.layout.pos_in_scrolling_layout) { + const aPos = aWindow.layout.pos_in_scrolling_layout + const bPos = bWindow.layout.pos_in_scrolling_layout + + if (aPos.length > 1 && bPos.length > 1) { + if (aPos[0] !== bPos[0]) { + return aPos[0] - bPos[0] + } + if (aPos[1] !== bPos[1]) { + return aPos[1] - bPos[1] + } + } + } + + return aWindow.id - bWindow.id + }) + } + + function filterCurrentWorkspace(toplevels, screenName) { + let currentWorkspaceId = null + for (var i = 0; i < allWorkspaces.length; i++) { + const ws = allWorkspaces[i] + if (ws.output === screenName && ws.is_active) { + currentWorkspaceId = ws.id + break + } + } + + if (currentWorkspaceId === null) { + return toplevels + } + + return toplevels.filter(toplevel => { + const niriMatch = findNiriWindow(toplevel) + return niriMatch && niriMatch.niriWindow.workspace_id === currentWorkspaceId + }) + } + + Timer { + id: suppressToastTimer + interval: 3000 + onTriggered: root.suppressConfigToast = false + } +} diff --git a/quickshell/.config/quickshell/Services/NotificationService.qml b/quickshell/.config/quickshell/Services/NotificationService.qml new file mode 100644 index 0000000..b7271d7 --- /dev/null +++ b/quickshell/.config/quickshell/Services/NotificationService.qml @@ -0,0 +1,687 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Services.Notifications +import Quickshell.Widgets +import qs.Common +import "../Common/markdown2html.js" as Markdown2Html + +Singleton { + id: root + + readonly property list notifications: [] + readonly property list allWrappers: [] + readonly property list popups: allWrappers.filter(n => n && n.popup) + + property list notificationQueue: [] + property list visibleNotifications: [] + property int maxVisibleNotifications: 3 + property bool addGateBusy: false + property int enterAnimMs: 400 + property int seqCounter: 0 + property bool bulkDismissing: false + + property int maxQueueSize: 32 + property int maxIngressPerSecond: 20 + property double _lastIngressSec: 0 + property int _ingressCountThisSec: 0 + property int maxStoredNotifications: 300 + + property var _dismissQueue: [] + property int _dismissBatchSize: 8 + property int _dismissTickMs: 8 + property bool _suspendGrouping: false + property var _groupCache: ({ + "notifications": [], + "popups": [] + }) + property bool _groupsDirty: false + + Component.onCompleted: { + _recomputeGroups() + } + + function _nowSec() { + return Date.now() / 1000.0 + } + + function _ingressAllowed(notif) { + const t = _nowSec() + if (t - _lastIngressSec >= 1.0) { + _lastIngressSec = t + _ingressCountThisSec = 0 + } + _ingressCountThisSec += 1 + if (notif.urgency === NotificationUrgency.Critical) { + return true + } + return _ingressCountThisSec <= maxIngressPerSecond + } + + function _enqueuePopup(wrapper) { + if (notificationQueue.length >= maxQueueSize) { + const gk = getGroupKey(wrapper) + let idx = notificationQueue.findIndex(w => w && getGroupKey(w) === gk && w.urgency !== NotificationUrgency.Critical) + if (idx === -1) { + idx = notificationQueue.findIndex(w => w && w.urgency !== NotificationUrgency.Critical) + } + if (idx === -1) { + idx = 0 + } + const victim = notificationQueue[idx] + if (victim) { + victim.popup = false + } + notificationQueue.splice(idx, 1) + } + notificationQueue = [...notificationQueue, wrapper] + } + + function _initWrapperPersistence(wrapper) { + const timeoutMs = wrapper.timer ? wrapper.timer.interval : 5000 + const isCritical = wrapper.notification && wrapper.notification.urgency === NotificationUrgency.Critical + wrapper.isPersistent = isCritical || (timeoutMs === 0) + } + + function _trimStored() { + if (notifications.length > maxStoredNotifications) { + const overflow = notifications.length - maxStoredNotifications + const toDrop = [] + for (var i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) { + const w = notifications[i] + if (w && w.notification && w.urgency !== NotificationUrgency.Critical) { + toDrop.push(w) + } + } + for (var i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) { + const w = notifications[i] + if (w && w.notification && toDrop.indexOf(w) === -1) { + toDrop.push(w) + } + } + for (const w of toDrop) { + try { + w.notification.dismiss() + } catch (e) { + + } + } + } + } + + function onOverlayOpen() { + popupsDisabled = true + addGate.stop() + addGateBusy = false + + notificationQueue = [] + for (const w of visibleNotifications) { + if (w) { + w.popup = false + } + } + visibleNotifications = [] + _recomputeGroupsLater() + } + + function onOverlayClose() { + popupsDisabled = false + processQueue() + } + + Timer { + id: addGate + interval: enterAnimMs + 50 + running: false + repeat: false + onTriggered: { + addGateBusy = false + processQueue() + } + } + + Timer { + id: timeUpdateTimer + interval: 30000 + repeat: true + running: root.allWrappers.length > 0 || visibleNotifications.length > 0 + triggeredOnStart: false + onTriggered: { + root.timeUpdateTick = !root.timeUpdateTick + } + } + + Timer { + id: dismissPump + interval: _dismissTickMs + repeat: true + running: false + onTriggered: { + let n = Math.min(_dismissBatchSize, _dismissQueue.length) + for (var i = 0; i < n; ++i) { + const w = _dismissQueue.pop() + try { + if (w && w.notification) { + w.notification.dismiss() + } + } catch (e) { + + } + } + if (_dismissQueue.length === 0) { + dismissPump.stop() + _suspendGrouping = false + bulkDismissing = false + popupsDisabled = false + _recomputeGroupsLater() + } + } + } + + Timer { + id: groupsDebounce + interval: 16 + repeat: false + onTriggered: _recomputeGroups() + } + + property bool timeUpdateTick: false + property bool clockFormatChanged: false + + readonly property var groupedNotifications: _groupCache.notifications + readonly property var groupedPopups: _groupCache.popups + + property var expandedGroups: ({}) + property var expandedMessages: ({}) + property bool popupsDisabled: false + + NotificationServer { + id: server + + keepOnReload: false + actionsSupported: true + actionIconsSupported: true + bodyHyperlinksSupported: true + bodyImagesSupported: true + bodyMarkupSupported: true + imageSupported: true + inlineReplySupported: true + persistenceSupported: true + + onNotification: notif => { + notif.tracked = true + + if (!_ingressAllowed(notif)) { + if (notif.urgency !== NotificationUrgency.Critical) { + try { + notif.dismiss() + } catch (e) { + + } + return + } + } + + const shouldShowPopup = !root.popupsDisabled && !SessionData.doNotDisturb + const wrapper = notifComponent.createObject(root, { + "popup": shouldShowPopup, + "notification": notif + }) + + if (wrapper) { + root.allWrappers.push(wrapper) + root.notifications.push(wrapper) + _trimStored() + + Qt.callLater(() => { + _initWrapperPersistence(wrapper) + }) + + if (shouldShowPopup) { + _enqueuePopup(wrapper) + processQueue() + } + } + + _recomputeGroupsLater() + } + } + + component NotifWrapper: QtObject { + id: wrapper + + property bool popup: false + property bool removedByLimit: false + property bool isPersistent: true + property int seq: 0 + + onPopupChanged: { + if (!popup) { + removeFromVisibleNotifications(wrapper) + } + } + + readonly property Timer timer: Timer { + interval: { + if (!wrapper.notification) { + return 5000 + } + + switch (wrapper.notification.urgency) { + case NotificationUrgency.Low: + return SettingsData.notificationTimeoutLow + case NotificationUrgency.Critical: + return SettingsData.notificationTimeoutCritical + default: + return SettingsData.notificationTimeoutNormal + } + } + repeat: false + running: false + onTriggered: { + if (interval > 0) { + wrapper.popup = false + } + } + } + + readonly property date time: new Date() + readonly property string timeStr: { + root.timeUpdateTick + root.clockFormatChanged + + const now = new Date() + const diff = now.getTime() - time.getTime() + const minutes = Math.floor(diff / 60000) + const hours = Math.floor(minutes / 60) + + if (hours < 1) { + if (minutes < 1) { + return "now" + } + return `${minutes}m ago` + } + + const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate()) + const daysDiff = Math.floor((nowDate - timeDate) / (1000 * 60 * 60 * 24)) + + if (daysDiff === 0) { + return formatTime(time) + } + + if (daysDiff === 1) { + return `yesterday, ${formatTime(time)}` + } + + return `${daysDiff} days ago` + } + + function formatTime(date) { + let use24Hour = true + try { + if (typeof SettingsData !== "undefined" && SettingsData.use24HourClock !== undefined) { + use24Hour = SettingsData.use24HourClock + } + } catch (e) { + use24Hour = true + } + + if (use24Hour) { + return date.toLocaleTimeString(Qt.locale(), "HH:mm") + } else { + return date.toLocaleTimeString(Qt.locale(), "h:mm AP") + } + } + + required property Notification notification + readonly property string summary: notification.summary + readonly property string body: notification.body + readonly property string htmlBody: { + if (body && (body.includes('<') && body.includes('>'))) { + return body + } + return Markdown2Html.markdownToHtml(body) + } + readonly property string appIcon: notification.appIcon + readonly property string appName: { + if (notification.appName == "") { + const entry = DesktopEntries.heuristicLookup(notification.desktopEntry) + if (entry && entry.name) { + return entry.name.toLowerCase() + } + } + return notification.appName || "app" + } + readonly property string desktopEntry: notification.desktopEntry + readonly property string image: notification.image + readonly property string cleanImage: { + if (!image) { + return "" + } + if (image.startsWith("file://")) { + return image.substring(7) + } + return image + } + readonly property int urgency: notification.urgency + readonly property list actions: notification.actions + + readonly property Connections conn: Connections { + target: wrapper.notification.Retainable + + function onDropped(): void { + root.allWrappers = root.allWrappers.filter(w => w !== wrapper) + root.notifications = root.notifications.filter(w => w !== wrapper) + + if (root.bulkDismissing) { + return + } + + const groupKey = getGroupKey(wrapper) + const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey) + + if (remainingInGroup.length <= 1) { + clearGroupExpansionState(groupKey) + } + + cleanupExpansionStates() + root._recomputeGroupsLater() + } + + function onAboutToDestroy(): void { + wrapper.destroy() + } + } + } + + Component { + id: notifComponent + NotifWrapper {} + } + + function clearAllNotifications() { + bulkDismissing = true + popupsDisabled = true + addGate.stop() + addGateBusy = false + notificationQueue = [] + + for (const w of allWrappers) + w.popup = false + visibleNotifications = [] + + _dismissQueue = notifications.slice() + if (notifications.length) { + notifications = [] + } + expandedGroups = {} + expandedMessages = {} + + _suspendGrouping = true + + if (!dismissPump.running && _dismissQueue.length) { + dismissPump.start() + } + } + + function dismissNotification(wrapper) { + if (!wrapper || !wrapper.notification) { + return + } + wrapper.popup = false + wrapper.notification.dismiss() + } + + function disablePopups(disable) { + popupsDisabled = disable + if (disable) { + notificationQueue = [] + for (const notif of visibleNotifications) { + notif.popup = false + } + visibleNotifications = [] + } + } + + function processQueue() { + if (addGateBusy) { + return + } + if (popupsDisabled) { + return + } + if (SessionData.doNotDisturb) { + return + } + if (notificationQueue.length === 0) { + return + } + + const activePopupCount = visibleNotifications.filter(n => n && n.popup).length + if (activePopupCount >= 4) { + return + } + + const next = notificationQueue.shift() + + next.seq = ++seqCounter + visibleNotifications = [...visibleNotifications, next] + next.popup = true + + if (next.timer.interval > 0) { + next.timer.start() + } + + addGateBusy = true + addGate.restart() + } + + function removeFromVisibleNotifications(wrapper) { + visibleNotifications = visibleNotifications.filter(n => n !== wrapper) + processQueue() + } + + function releaseWrapper(w) { + visibleNotifications = visibleNotifications.filter(n => n !== w) + notificationQueue = notificationQueue.filter(n => n !== w) + + if (w && w.destroy && !w.isPersistent && notifications.indexOf(w) === -1) { + Qt.callLater(() => { + try { + w.destroy() + } catch (e) { + + } + }) + } + } + + function getGroupKey(wrapper) { + if (wrapper.desktopEntry && wrapper.desktopEntry !== "") { + return wrapper.desktopEntry.toLowerCase() + } + + return wrapper.appName.toLowerCase() + } + + function _recomputeGroups() { + if (_suspendGrouping) { + _groupsDirty = true + return + } + _groupCache = { + "notifications": _calcGroupedNotifications(), + "popups": _calcGroupedPopups() + } + _groupsDirty = false + } + + function _recomputeGroupsLater() { + _groupsDirty = true + if (!groupsDebounce.running) { + groupsDebounce.start() + } + } + + function _calcGroupedNotifications() { + const groups = {} + + for (const notif of notifications) { + const groupKey = getGroupKey(notif) + if (!groups[groupKey]) { + groups[groupKey] = { + "key": groupKey, + "appName": notif.appName, + "notifications": [], + "latestNotification": null, + "count": 0, + "hasInlineReply": false + } + } + + groups[groupKey].notifications.unshift(notif) + groups[groupKey].latestNotification = groups[groupKey].notifications[0] + groups[groupKey].count = groups[groupKey].notifications.length + + if (notif.notification.hasInlineReply) { + groups[groupKey].hasInlineReply = true + } + } + + return Object.values(groups).sort((a, b) => { + const aUrgency = a.latestNotification.urgency || NotificationUrgency.Low + const bUrgency = b.latestNotification.urgency || NotificationUrgency.Low + if (aUrgency !== bUrgency) { + return bUrgency - aUrgency + } + return b.latestNotification.time.getTime() - a.latestNotification.time.getTime() + }) + } + + function _calcGroupedPopups() { + const groups = {} + + for (const notif of popups) { + const groupKey = getGroupKey(notif) + if (!groups[groupKey]) { + groups[groupKey] = { + "key": groupKey, + "appName": notif.appName, + "notifications": [], + "latestNotification": null, + "count": 0, + "hasInlineReply": false + } + } + + groups[groupKey].notifications.unshift(notif) + groups[groupKey].latestNotification = groups[groupKey].notifications[0] + groups[groupKey].count = groups[groupKey].notifications.length + + if (notif.notification.hasInlineReply) { + groups[groupKey].hasInlineReply = true + } + } + + return Object.values(groups).sort((a, b) => { + return b.latestNotification.time.getTime() - a.latestNotification.time.getTime() + }) + } + + function toggleGroupExpansion(groupKey) { + let newExpandedGroups = {} + for (const key in expandedGroups) { + newExpandedGroups[key] = expandedGroups[key] + } + newExpandedGroups[groupKey] = !newExpandedGroups[groupKey] + expandedGroups = newExpandedGroups + } + + function dismissGroup(groupKey) { + const group = groupedNotifications.find(g => g.key === groupKey) + if (group) { + for (const notif of group.notifications) { + if (notif && notif.notification) { + notif.notification.dismiss() + } + } + } else { + for (const notif of allWrappers) { + if (notif && notif.notification && getGroupKey(notif) === groupKey) { + notif.notification.dismiss() + } + } + } + } + + function clearGroupExpansionState(groupKey) { + let newExpandedGroups = {} + for (const key in expandedGroups) { + if (key !== groupKey && expandedGroups[key]) { + newExpandedGroups[key] = true + } + } + expandedGroups = newExpandedGroups + } + + function cleanupExpansionStates() { + const currentGroupKeys = new Set(groupedNotifications.map(g => g.key)) + const currentMessageIds = new Set() + for (const group of groupedNotifications) { + for (const notif of group.notifications) { + currentMessageIds.add(notif.notification.id) + } + } + let newExpandedGroups = {} + for (const key in expandedGroups) { + if (currentGroupKeys.has(key) && expandedGroups[key]) { + newExpandedGroups[key] = true + } + } + expandedGroups = newExpandedGroups + let newExpandedMessages = {} + for (const messageId in expandedMessages) { + if (currentMessageIds.has(messageId) && expandedMessages[messageId]) { + newExpandedMessages[messageId] = true + } + } + expandedMessages = newExpandedMessages + } + + function toggleMessageExpansion(messageId) { + let newExpandedMessages = {} + for (const key in expandedMessages) { + newExpandedMessages[key] = expandedMessages[key] + } + newExpandedMessages[messageId] = !newExpandedMessages[messageId] + expandedMessages = newExpandedMessages + } + + Connections { + target: SessionData + function onDoNotDisturbChanged() { + if (SessionData.doNotDisturb) { + // Hide all current popups when DND is enabled + for (const notif of visibleNotifications) { + notif.popup = false + } + visibleNotifications = [] + notificationQueue = [] + } else { + // Re-enable popup processing when DND is disabled + processQueue() + } + } + } + + Connections { + target: typeof SettingsData !== "undefined" ? SettingsData : null + function onUse24HourClockChanged() { + root.clockFormatChanged = !root.clockFormatChanged + } + } +} diff --git a/quickshell/.config/quickshell/Services/PortalService.qml b/quickshell/.config/quickshell/Services/PortalService.qml new file mode 100644 index 0000000..8860d75 --- /dev/null +++ b/quickshell/.config/quickshell/Services/PortalService.qml @@ -0,0 +1,210 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property bool accountsServiceAvailable: false + property string systemProfileImage: "" + property string profileImage: "" + property bool settingsPortalAvailable: false + property int systemColorScheme: 0 // 0=default, 1=prefer-dark, 2=prefer-light + + function init() {} + + function getSystemProfileImage() { + systemProfileCheckProcess.running = true + } + + function setProfileImage(imagePath) { + profileImage = imagePath + if (accountsServiceAvailable && imagePath) { + setSystemProfileImage(imagePath) + } + } + + function getSystemColorScheme() { + systemColorSchemeCheckProcess.running = true + } + + function setLightMode(isLightMode) { + if (settingsPortalAvailable) { + setSystemColorScheme(isLightMode) + } + } + + function setSystemColorScheme(isLightMode) { + if (!settingsPortalAvailable) { + return + } + + const colorScheme = isLightMode ? "prefer-light" : "prefer-dark" + const script = `gsettings set org.gnome.desktop.interface color-scheme '${colorScheme}'` + + systemColorSchemeSetProcess.command = ["bash", "-c", script] + systemColorSchemeSetProcess.running = true + } + + function setSystemProfileImage(imagePath) { + if (!accountsServiceAvailable || !imagePath) { + return + } + + const script = `dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.Accounts.User.SetIconFile string:'${imagePath}'` + + systemProfileSetProcess.command = ["bash", "-c", script] + systemProfileSetProcess.running = true + } + + Component.onCompleted: { + checkAccountsService() + checkSettingsPortal() + } + + function checkAccountsService() { + accountsServiceCheckProcess.running = true + } + + function checkSettingsPortal() { + settingsPortalCheckProcess.running = true + } + + Process { + id: accountsServiceCheckProcess + command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts org.freedesktop.Accounts.FindUserByName string:\"$USER\""] + running: false + + onExited: exitCode => { + root.accountsServiceAvailable = (exitCode === 0) + if (root.accountsServiceAvailable) { + root.getSystemProfileImage() + } + } + } + + Process { + id: systemProfileCheckProcess + command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const match = text.match(/string\s+"([^"]+)"/) + if (match && match[1] && match[1] !== "" && match[1] !== "/var/lib/AccountsService/icons/") { + root.systemProfileImage = match[1] + + if (!root.profileImage || root.profileImage === "") { + root.profileImage = root.systemProfileImage + } + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + root.systemProfileImage = "" + } + } + } + + Process { + id: systemProfileSetProcess + running: false + + onExited: exitCode => { + if (exitCode === 0) { + root.getSystemProfileImage() + } + } + } + + Process { + id: settingsPortalCheckProcess + command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"] + running: false + + onExited: exitCode => { + root.settingsPortalAvailable = (exitCode === 0) + if (root.settingsPortalAvailable) { + root.getSystemColorScheme() + } + } + } + + Process { + id: systemColorSchemeCheckProcess + command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const match = text.match(/uint32 (\d+)/) + if (match && match[1]) { + root.systemColorScheme = parseInt(match[1]) + + if (typeof Theme !== "undefined") { + const shouldBeLightMode = (root.systemColorScheme === 2) + if (Theme.isLightMode !== shouldBeLightMode) { + Theme.isLightMode = shouldBeLightMode + if (typeof SessionData !== "undefined") { + SessionData.setLightMode(shouldBeLightMode) + } + } + } + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + root.systemColorScheme = 0 + } + } + } + + Process { + id: systemColorSchemeSetProcess + running: false + + onExited: exitCode => { + if (exitCode === 0) { + Qt.callLater(() => { + root.getSystemColorScheme() + }) + } + } + } + + IpcHandler { + target: "profile" + + function getImage(): string { + return root.profileImage + } + + function setImage(path: string): string { + if (!path) { + return "ERROR: No path provided" + } + + const absolutePath = path.startsWith("/") ? path : `${StandardPaths.writableLocation(StandardPaths.HomeLocation)}/${path}` + + try { + root.setProfileImage(absolutePath) + return "SUCCESS: Profile image set to " + absolutePath + } catch (e) { + return "ERROR: Failed to set profile image: " + e.toString() + } + } + + function clearImage(): string { + root.setProfileImage("") + return "SUCCESS: Profile image cleared" + } + } +} diff --git a/quickshell/.config/quickshell/Services/PrivacyService.qml b/quickshell/.config/quickshell/Services/PrivacyService.qml new file mode 100644 index 0000000..c092372 --- /dev/null +++ b/quickshell/.config/quickshell/Services/PrivacyService.qml @@ -0,0 +1,144 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire + +Singleton { + id: root + + readonly property bool microphoneActive: { + if (!Pipewire.ready || !Pipewire.nodes?.values) { + return false + } + + for (let i = 0; i < Pipewire.nodes.values.length; i++) { + const node = Pipewire.nodes.values[i] + if (!node) { + continue + } + + if ((node.type & PwNodeType.AudioInStream) === PwNodeType.AudioInStream) { + if (!looksLikeSystemVirtualMic(node)) { + console.log(node.audio) + if (node.audio && node.audio.muted) { + return false + } + return true + } + } + } + return false + } + + PwObjectTracker { + objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream) + } + + readonly property bool cameraActive: { + if (!Pipewire.ready || !Pipewire.nodes?.values) { + return false + } + + for (let i = 0; i < Pipewire.nodes.values.length; i++) { + const node = Pipewire.nodes.values[i] + if (!node || !node.ready) { + continue + } + + if (node.properties && node.properties["media.class"] === "Stream/Input/Video") { + if (node.properties["stream.is-live"] === "true") { + return true + } + } + } + return false + } + + readonly property bool screensharingActive: { + if (!Pipewire.ready || !Pipewire.nodes?.values) { + return false + } + + for (let i = 0; i < Pipewire.nodes.values.length; i++) { + const node = Pipewire.nodes.values[i] + if (!node || !node.ready) { + continue + } + + if ((node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource) { + if (looksLikeScreencast(node)) { + return true + } + } + + if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") { + const mediaName = (node.properties["media.name"] || "").toLowerCase() + const appName = (node.properties["application.name"] || "").toLowerCase() + + if (mediaName.includes("desktop") || appName.includes("screen") || appName === "obs") { + if (node.properties["stream.is-live"] === "true") { + if (node.audio && node.audio.muted) { + return false + } + return true + } + } + } + } + return false + } + + readonly property bool anyPrivacyActive: microphoneActive || cameraActive || screensharingActive + + function looksLikeSystemVirtualMic(node) { + if (!node) { + return false + } + const name = (node.name || "").toLowerCase() + const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase() + const appName = (node.properties && node.properties["application.name"] || "").toLowerCase() + const combined = name + " " + mediaName + " " + appName + return /cava|monitor|system/.test(combined) + } + + function looksLikeScreencast(node) { + if (!node) { + return false + } + const appName = (node.properties && node.properties["application.name"] || "").toLowerCase() + const nodeName = (node.name || "").toLowerCase() + const combined = appName + " " + nodeName + return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined) + } + + function getMicrophoneStatus() { + return microphoneActive ? "active" : "inactive" + } + + function getCameraStatus() { + return cameraActive ? "active" : "inactive" + } + + function getScreensharingStatus() { + return screensharingActive ? "active" : "inactive" + } + + function getPrivacySummary() { + const active = [] + if (microphoneActive) { + active.push("microphone") + } + if (cameraActive) { + active.push("camera") + } + if (screensharingActive) { + active.push("screensharing") + } + + return active.length > 0 ? `Privacy active: ${active.join(", ")}` : "No privacy concerns detected" + } +} diff --git a/quickshell/.config/quickshell/Services/SessionService.qml b/quickshell/.config/quickshell/Services/SessionService.qml new file mode 100644 index 0000000..3bdb59a --- /dev/null +++ b/quickshell/.config/quickshell/Services/SessionService.qml @@ -0,0 +1,187 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common + +Singleton { + id: root + + property bool hasUwsm: false + property bool isElogind: false + property bool inhibitorAvailable: true + property bool idleInhibited: false + property string inhibitReason: "Keep system awake" + + Component.onCompleted: { + detectElogindProcess.running = true + } + + Process { + id: detectUwsmProcess + running: false + command: ["which", "uwsm"] + + onExited: function (exitCode) { + hasUwsm = (exitCode === 0) + } + } + + Process { + id: detectElogindProcess + running: false + command: ["sh", "-c", "ps -eo comm= | grep -E '^(elogind|elogind-daemon)$'"] + + onExited: function (exitCode) { + console.log("SessionService: Elogind detection exited with code", exitCode) + isElogind = (exitCode === 0) + } + } + + Process { + id: uwsmLogout + command: ["uwsm", "stop"] + running: false + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => { + if (data.trim().toLowerCase().includes("not running")) { + _logout() + } + } + } + + onExited: function (exitCode) { + if (exitCode === 0) { + return + } + _logout() + } + } + + function logout() { + if (hasUwsm) { + uwsmLogout.running = true + } + _logout() + } + + function _logout() { + if (CompositorService.isNiri) { + NiriService.quit() + return + } + } + + function suspend() { + Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend"]) + } + + function reboot() { + Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "reboot"]) + } + + function poweroff() { + Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "poweroff"]) + } + + // * Idle Inhibitor + signal inhibitorChanged + + function enableIdleInhibit() { + if (idleInhibited) { + return + } + idleInhibited = true + inhibitorChanged() + } + + function disableIdleInhibit() { + if (!idleInhibited) { + return + } + idleInhibited = false + inhibitorChanged() + } + + function toggleIdleInhibit() { + if (idleInhibited) { + disableIdleInhibit() + } else { + enableIdleInhibit() + } + } + + function setInhibitReason(reason) { + inhibitReason = reason + + if (idleInhibited) { + const wasActive = idleInhibited + idleInhibited = false + + Qt.callLater(() => { + if (wasActive) { + idleInhibited = true + } + }) + } + } + + Process { + id: idleInhibitProcess + + command: { + if (!idleInhibited) { + return ["true"] + } + + return [isElogind ? "elogind-inhibit" : "systemd-inhibit", "--what=idle", "--who=quickshell", `--why=${inhibitReason}`, "--mode=block", "sleep", "infinity"] + } + + running: idleInhibited + + onExited: function (exitCode) { + if (idleInhibited && exitCode !== 0) { + console.warn("SessionService: Inhibitor process crashed with exit code:", exitCode) + idleInhibited = false + ToastService.showWarning("Idle inhibitor failed") + } + } + } + + IpcHandler { + function toggle(): string { + root.toggleIdleInhibit() + return root.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled" + } + + function enable(): string { + root.enableIdleInhibit() + return "Idle inhibit enabled" + } + + function disable(): string { + root.disableIdleInhibit() + return "Idle inhibit disabled" + } + + function status(): string { + return root.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled" + } + + function reason(newReason: string): string { + if (!newReason) { + return `Current reason: ${root.inhibitReason}` + } + + root.setInhibitReason(newReason) + return `Inhibit reason set to: ${newReason}` + } + + target: "inhibit" + } +} diff --git a/quickshell/.config/quickshell/Services/ToastService.qml b/quickshell/.config/quickshell/Services/ToastService.qml new file mode 100644 index 0000000..1c9c07e --- /dev/null +++ b/quickshell/.config/quickshell/Services/ToastService.qml @@ -0,0 +1,105 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell + +Singleton { + id: root + + readonly property int levelInfo: 0 + readonly property int levelWarn: 1 + readonly property int levelError: 2 + property string currentMessage: "" + property int currentLevel: levelInfo + property bool toastVisible: false + property var toastQueue: [] + property string currentDetails: "" + property bool hasDetails: false + property string wallpaperErrorStatus: "" + + function showToast(message, level = levelInfo, details = "") { + toastQueue.push({ + "message": message, + "level": level, + "details": details + }) + if (!toastVisible) { + processQueue() + } + } + + function showInfo(message, details = "") { + showToast(message, levelInfo, details) + } + + function showWarning(message, details = "") { + showToast(message, levelWarn, details) + } + + function showError(message, details = "") { + showToast(message, levelError, details) + } + + function hideToast() { + toastVisible = false + currentMessage = "" + currentDetails = "" + hasDetails = false + currentLevel = levelInfo + toastTimer.stop() + resetToastState() + if (toastQueue.length > 0) { + processQueue() + } + } + + function processQueue() { + if (toastQueue.length === 0) { + return + } + + const toast = toastQueue.shift() + currentMessage = toast.message + currentLevel = toast.level + currentDetails = toast.details || "" + hasDetails = currentDetails.length > 0 + toastVisible = true + resetToastState() + + if (toast.level === levelError && hasDetails) { + toastTimer.interval = 8000 + toastTimer.start() + } else { + toastTimer.interval = toast.level === levelError ? 5000 : toast.level === levelWarn ? 3000 : 1500 + toastTimer.start() + } + } + + signal resetToastState + + function stopTimer() { + toastTimer.stop() + } + + function restartTimer() { + if (hasDetails && currentLevel === levelError) { + toastTimer.interval = 8000 + toastTimer.restart() + } + } + + function clearWallpaperError() { + wallpaperErrorStatus = "" + } + + Timer { + id: toastTimer + + interval: 5000 + running: false + repeat: false + onTriggered: hideToast() + } +} diff --git a/quickshell/.config/quickshell/Services/UserInfoService.qml b/quickshell/.config/quickshell/Services/UserInfoService.qml new file mode 100644 index 0000000..92a5656 --- /dev/null +++ b/quickshell/.config/quickshell/Services/UserInfoService.qml @@ -0,0 +1,115 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property string username: "" + property string fullName: "" + property string profilePicture: "" + property string uptime: "" + property string shortUptime: "" + property string hostname: "" + property bool profileAvailable: false + + function getUserInfo() { + userInfoProcess.running = true + } + + function getUptime() { + uptimeProcess.running = true + } + + function refreshUserInfo() { + getUserInfo() + getUptime() + } + + Component.onCompleted: { + getUserInfo() + getUptime() + } + + Process { + id: userInfoProcess + + command: ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""] + running: false + onExited: exitCode => { + if (exitCode !== 0) { + + root.username = "User" + root.fullName = "User" + root.hostname = "System" + } + } + + stdout: StdioCollector { + onStreamFinished: { + const parts = text.trim().split("|") + if (parts.length >= 3) { + root.username = parts[0] || "" + root.fullName = parts[1] || parts[0] || "" + root.hostname = parts[2] || "" + } + } + } + } + + Process { + id: uptimeProcess + + command: ["cat", "/proc/uptime"] + running: false + + onExited: exitCode => { + if (exitCode !== 0) { + root.uptime = "Unknown" + } + } + + stdout: StdioCollector { + onStreamFinished: { + const seconds = parseInt(text.split(" ")[0]) + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + + const parts = [] + if (days > 0) { + parts.push(`${days} day${days === 1 ? "" : "s"}`) + } + if (hours > 0) { + parts.push(`${hours} hour${hours === 1 ? "" : "s"}`) + } + if (minutes > 0) { + parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`) + } + + if (parts.length > 0) { + root.uptime = `up ${parts.join(", ")}` + } else { + root.uptime = `up ${seconds} seconds` + } + + // Create short uptime format + let shortUptime = "up" + if (days > 0) { + shortUptime += ` ${days}d` + } + if (hours > 0) { + shortUptime += ` ${hours}h` + } + if (minutes > 0) { + shortUptime += ` ${minutes}m` + } + root.shortUptime = shortUptime + } + } + } +} diff --git a/quickshell/.config/quickshell/Services/VpnService.qml b/quickshell/.config/quickshell/Services/VpnService.qml new file mode 100644 index 0000000..8ac67e8 --- /dev/null +++ b/quickshell/.config/quickshell/Services/VpnService.qml @@ -0,0 +1,240 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +// Minimal VPN controller backed by NetworkManager (nmcli + D-Bus monitor) +Singleton { + id: root + + // State + property bool available: true + property bool isBusy: false + property string errorMessage: "" + + // Profiles discovered on the system + // [{ name, uuid, type }] + property var profiles: [] + + // Allow multiple active VPNs (set true to allow concurrent connections) + // Default: allow multiple, to align with NetworkManager capability + property bool singleActive: false + + // Active VPN connections (may be multiple) + // Full list and convenience projections + property var activeConnections: [] // [{ name, uuid, device, state }] + property var activeUuids: [] + property var activeNames: [] + // Back-compat single values (first active if present) + property string activeUuid: activeUuids.length > 0 ? activeUuids[0] : "" + property string activeName: activeNames.length > 0 ? activeNames[0] : "" + property string activeDevice: activeConnections.length > 0 ? (activeConnections[0].device || "") : "" + property string activeState: activeConnections.length > 0 ? (activeConnections[0].state || "") : "" + property bool connected: activeUuids.length > 0 + + // Use implicit property notify signals (profilesChanged, activeUuidChanged, etc.) + + Component.onCompleted: initialize() + + function initialize() { + // Start monitoring NetworkManager for changes + nmMonitor.running = true + refreshAll() + } + + function refreshAll() { + listProfiles() + refreshActive() + } + + // Monitor NetworkManager changes and refresh on activity + Process { + id: nmMonitor + command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.NetworkManager"] + running: false + + stdout: SplitParser { + splitMarker: "\n" + onRead: line => { + if (line.includes("ActiveConnection") || line.includes("PropertiesChanged") || line.includes("StateChanged")) { + refreshAll() + } + } + } + } + + // Query all VPN profiles + function listProfiles() { + getProfiles.running = true + } + + Process { + id: getProfiles + command: ["bash", "-lc", "nmcli -t -f NAME,UUID,TYPE connection show | while IFS=: read -r name uuid type; do case \"$type\" in vpn) svc=$(nmcli -g vpn.service-type connection show uuid \"$uuid\" 2>/dev/null); echo \"$name:$uuid:$type:$svc\" ;; wireguard) echo \"$name:$uuid:$type:\" ;; *) : ;; esac; done"] + running: false + stdout: StdioCollector { + onStreamFinished: { + const lines = text.trim().length ? text.trim().split('\n') : [] + const out = [] + for (const line of lines) { + const parts = line.split(':') + if (parts.length >= 3 && (parts[2] === "vpn" || parts[2] === "wireguard")) { + const svc = parts.length >= 4 ? parts[3] : "" + out.push({ name: parts[0], uuid: parts[1], type: parts[2], serviceType: svc }) + } + } + root.profiles = out + } + } + } + + // Query active VPN connection + function refreshActive() { + getActive.running = true + } + + Process { + id: getActive + command: ["nmcli", "-t", "-f", "NAME,UUID,TYPE,DEVICE,STATE", "connection", "show", "--active"] + running: false + stdout: StdioCollector { + onStreamFinished: { + const lines = text.trim().length ? text.trim().split('\n') : [] + let act = [] + for (const line of lines) { + const parts = line.split(':') + if (parts.length >= 5 && (parts[2] === "vpn" || parts[2] === "wireguard")) { + act.push({ name: parts[0], uuid: parts[1], device: parts[3], state: parts[4] }) + } + } + root.activeConnections = act + root.activeUuids = act.map(a => a.uuid).filter(u => !!u) + root.activeNames = act.map(a => a.name).filter(n => !!n) + } + } + } + + function isActiveUuid(uuid) { + return root.activeUuids && root.activeUuids.indexOf(uuid) !== -1 + } + + function _looksLikeUuid(s) { + // Very loose check for UUID pattern + return s && s.indexOf('-') !== -1 && s.length >= 8 + } + + function connect(uuidOrName) { + if (root.isBusy) return + root.isBusy = true + root.errorMessage = "" + if (root.singleActive) { + // Bring down all active VPNs, then bring up the requested one + const isUuid = _looksLikeUuid(uuidOrName) + const escaped = ('' + uuidOrName).replace(/'/g, "'\\''") + const upCmd = isUuid ? `nmcli connection up uuid '${escaped}'` : `nmcli connection up id '${escaped}'` + const script = `set -e\n` + + `nmcli -t -f UUID,TYPE connection show --active | awk -F: '$2 ~ /^(vpn|wireguard)$/ {print $1}' | while read u; do [ -n \"$u\" ] && nmcli connection down uuid \"$u\" || true; done\n` + + upCmd + `\n` + vpnSwitch.command = ["bash", "-lc", script] + vpnSwitch.running = true + } else { + if (_looksLikeUuid(uuidOrName)) { + vpnUp.command = ["nmcli", "connection", "up", "uuid", uuidOrName] + } else { + vpnUp.command = ["nmcli", "connection", "up", "id", uuidOrName] + } + vpnUp.running = true + } + } + + function disconnect(uuidOrName) { + if (root.isBusy) return + root.isBusy = true + root.errorMessage = "" + if (_looksLikeUuid(uuidOrName)) { + vpnDown.command = ["nmcli", "connection", "down", "uuid", uuidOrName] + } else { + vpnDown.command = ["nmcli", "connection", "down", "id", uuidOrName] + } + vpnDown.running = true + } + + function toggle(uuid) { + if (uuid) { + if (isActiveUuid(uuid)) disconnect(uuid) + else connect(uuid) + return + } + if (root.profiles.length > 0) { + connect(root.profiles[0].uuid) + } + } + + Process { + id: vpnUp + running: false + stdout: StdioCollector { + onStreamFinished: { + root.isBusy = false + if (!text.toLowerCase().includes("successfully")) { + root.errorMessage = text.trim() + } + refreshAll() + } + } + onExited: exitCode => { + root.isBusy = false + if (exitCode !== 0 && root.errorMessage === "") { + root.errorMessage = "Failed to connect VPN" + } + } + } + + Process { + id: vpnDown + running: false + stdout: StdioCollector { + onStreamFinished: { + root.isBusy = false + if (!text.toLowerCase().includes("deactivated") && !text.toLowerCase().includes("successfully")) { + root.errorMessage = text.trim() + } + refreshAll() + } + } + onExited: exitCode => { + root.isBusy = false + if (exitCode !== 0 && root.errorMessage === "") { + root.errorMessage = "Failed to disconnect VPN" + } + } + } + + function disconnectAllActive() { + if (root.isBusy) return + root.isBusy = true + const script = `nmcli -t -f UUID,TYPE connection show --active | awk -F: '$2 ~ /^(vpn|wireguard)$/ {print $1}' | while read u; do [ -n \"$u\" ] && nmcli connection down uuid \"$u\" || true; done` + vpnSwitch.command = ["bash", "-lc", script] + vpnSwitch.running = true + } + + // Sequenced down/up using a single shell for exclusive switch + Process { + id: vpnSwitch + running: false + stdout: StdioCollector { + onStreamFinished: { + root.isBusy = false + refreshAll() + } + } + onExited: exitCode => { + root.isBusy = false + if (exitCode !== 0 && root.errorMessage === "") { + root.errorMessage = "Failed to switch VPN" + } + } + } +} diff --git a/quickshell/.config/quickshell/Services/WallpaperCyclingService.qml b/quickshell/.config/quickshell/Services/WallpaperCyclingService.qml new file mode 100644 index 0000000..5d7b072 --- /dev/null +++ b/quickshell/.config/quickshell/Services/WallpaperCyclingService.qml @@ -0,0 +1,239 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common + +Singleton { + id: root + + property bool cyclingActive: false + property string cachedCyclingTime: SessionData.wallpaperCyclingTime + property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval + property string lastTimeCheck: "" + + Component.onCompleted: { + updateCyclingState() + } + + Connections { + target: SessionData + + function onWallpaperCyclingEnabledChanged() { + updateCyclingState() + } + + function onWallpaperCyclingModeChanged() { + updateCyclingState() + } + + function onWallpaperCyclingIntervalChanged() { + cachedCyclingInterval = SessionData.wallpaperCyclingInterval + if (SessionData.wallpaperCyclingMode === "interval") { + updateCyclingState() + } + } + + function onWallpaperCyclingTimeChanged() { + cachedCyclingTime = SessionData.wallpaperCyclingTime + if (SessionData.wallpaperCyclingMode === "time") { + updateCyclingState() + } + } + + function onPerMonitorWallpaperChanged() { + updateCyclingState() + } + } + + function updateCyclingState() { + if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath && !SessionData.perMonitorWallpaper) { + startCycling() + } else { + stopCycling() + } + } + + function startCycling() { + if (SessionData.wallpaperCyclingMode === "interval") { + intervalTimer.interval = cachedCyclingInterval * 1000 + intervalTimer.start() + cyclingActive = true + } else if (SessionData.wallpaperCyclingMode === "time") { + cyclingActive = true + checkTimeBasedCycling() + } + } + + function stopCycling() { + intervalTimer.stop() + cyclingActive = false + } + + function cycleToNextWallpaper(screenName, wallpaperPath) { + const currentWallpaper = wallpaperPath || SessionData.wallpaperPath + if (!currentWallpaper) return + + const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/')) + cyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] + cyclingProcess.targetScreenName = screenName || "" + cyclingProcess.currentWallpaper = currentWallpaper + cyclingProcess.running = true + } + + function cycleToPrevWallpaper(screenName, wallpaperPath) { + const currentWallpaper = wallpaperPath || SessionData.wallpaperPath + if (!currentWallpaper) return + + const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/')) + prevCyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] + prevCyclingProcess.targetScreenName = screenName || "" + prevCyclingProcess.currentWallpaper = currentWallpaper + prevCyclingProcess.running = true + } + + function cycleNextManually() { + if (SessionData.wallpaperPath) { + cycleToNextWallpaper() + // Restart timers if cycling is active + if (cyclingActive && SessionData.wallpaperCyclingEnabled) { + if (SessionData.wallpaperCyclingMode === "interval") { + intervalTimer.interval = cachedCyclingInterval * 1000 + intervalTimer.restart() + } + } + } + } + + function cyclePrevManually() { + if (SessionData.wallpaperPath) { + cycleToPrevWallpaper() + // Restart timers if cycling is active + if (cyclingActive && SessionData.wallpaperCyclingEnabled) { + if (SessionData.wallpaperCyclingMode === "interval") { + intervalTimer.interval = cachedCyclingInterval * 1000 + intervalTimer.restart() + } + } + } + } + + function cycleNextForMonitor(screenName) { + if (!screenName) return + + var currentWallpaper = SessionData.getMonitorWallpaper(screenName) + if (currentWallpaper) { + cycleToNextWallpaper(screenName, currentWallpaper) + } + } + + function cyclePrevForMonitor(screenName) { + if (!screenName) return + + var currentWallpaper = SessionData.getMonitorWallpaper(screenName) + if (currentWallpaper) { + cycleToPrevWallpaper(screenName, currentWallpaper) + } + } + + function checkTimeBasedCycling() { + const currentTime = Qt.formatTime(systemClock.date, "hh:mm") + + if (currentTime === cachedCyclingTime + && currentTime !== lastTimeCheck) { + lastTimeCheck = currentTime + cycleToNextWallpaper() + } else if (currentTime !== cachedCyclingTime) { + lastTimeCheck = "" + } + } + + Timer { + id: intervalTimer + interval: cachedCyclingInterval * 1000 + running: false + repeat: true + onTriggered: cycleToNextWallpaper() + } + + SystemClock { + id: systemClock + precision: SystemClock.Minutes + onDateChanged: { + if (SessionData.wallpaperCyclingMode === "time" && cyclingActive) { + checkTimeBasedCycling() + } + } + } + + Process { + id: cyclingProcess + + property string targetScreenName: "" + property string currentWallpaper: "" + + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + const files = text.trim().split('\n').filter(file => file.length > 0) + if (files.length <= 1) return + + const wallpaperList = files.sort() + const currentPath = cyclingProcess.currentWallpaper + let currentIndex = wallpaperList.findIndex(path => path === currentPath) + if (currentIndex === -1) currentIndex = 0 + + const nextIndex = (currentIndex + 1) % wallpaperList.length + const nextWallpaper = wallpaperList[nextIndex] + + if (nextWallpaper && nextWallpaper !== currentPath) { + if (cyclingProcess.targetScreenName) { + SessionData.setMonitorWallpaper(cyclingProcess.targetScreenName, nextWallpaper) + } else { + SessionData.setWallpaper(nextWallpaper) + } + } + } + } + } + } + + Process { + id: prevCyclingProcess + + property string targetScreenName: "" + property string currentWallpaper: "" + + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + const files = text.trim().split('\n').filter(file => file.length > 0) + if (files.length <= 1) return + + const wallpaperList = files.sort() + const currentPath = prevCyclingProcess.currentWallpaper + let currentIndex = wallpaperList.findIndex(path => path === currentPath) + if (currentIndex === -1) currentIndex = 0 + + const prevIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1 + const prevWallpaper = wallpaperList[prevIndex] + + if (prevWallpaper && prevWallpaper !== currentPath) { + if (prevCyclingProcess.targetScreenName) { + SessionData.setMonitorWallpaper(prevCyclingProcess.targetScreenName, prevWallpaper) + } else { + SessionData.setWallpaper(prevWallpaper) + } + } + } + } + } + } + +} diff --git a/quickshell/.config/quickshell/Services/WeatherService.qml b/quickshell/.config/quickshell/Services/WeatherService.qml new file mode 100644 index 0000000..0d3d7e6 --- /dev/null +++ b/quickshell/.config/quickshell/Services/WeatherService.qml @@ -0,0 +1,646 @@ +pragma Singleton + +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common + +Singleton { + id: root + + property int refCount: 0 + + property var weather: ({ + "available": false, + "loading": true, + "temp": 0, + "tempF": 0, + "feelsLike": 0, + "feelsLikeF": 0, + "city": "", + "country": "", + "wCode": 0, + "humidity": 0, + "wind": "", + "sunrise": "06:00", + "sunset": "18:00", + "uv": 0, + "pressure": 0, + "precipitationProbability": 0, + "isDay": true, + "forecast": [] + }) + + property var location: null + property int updateInterval: 300000 // 5 minutes + property int retryAttempts: 0 + property int maxRetryAttempts: 3 + property int retryDelay: 30000 + property int lastFetchTime: 0 + property int minFetchInterval: 30000 + property int persistentRetryCount: 0 + + property var weatherIcons: ({ + "0": "clear_day", + "1": "clear_day", + "2": "partly_cloudy_day", + "3": "cloud", + "45": "foggy", + "48": "foggy", + "51": "rainy", + "53": "rainy", + "55": "rainy", + "56": "rainy", + "57": "rainy", + "61": "rainy", + "63": "rainy", + "65": "rainy", + "66": "rainy", + "67": "rainy", + "71": "cloudy_snowing", + "73": "cloudy_snowing", + "75": "snowing_heavy", + "77": "cloudy_snowing", + "80": "rainy", + "81": "rainy", + "82": "rainy", + "85": "cloudy_snowing", + "86": "snowing_heavy", + "95": "thunderstorm", + "96": "thunderstorm", + "99": "thunderstorm" + }) + + property var nightWeatherIcons: ({ + "0": "clear_night", + "1": "clear_night", + "2": "partly_cloudy_night", + "3": "cloud", + "45": "foggy", + "48": "foggy", + "51": "rainy", + "53": "rainy", + "55": "rainy", + "56": "rainy", + "57": "rainy", + "61": "rainy", + "63": "rainy", + "65": "rainy", + "66": "rainy", + "67": "rainy", + "71": "cloudy_snowing", + "73": "cloudy_snowing", + "75": "snowing_heavy", + "77": "cloudy_snowing", + "80": "rainy", + "81": "rainy", + "82": "rainy", + "85": "cloudy_snowing", + "86": "snowing_heavy", + "95": "thunderstorm", + "96": "thunderstorm", + "99": "thunderstorm" + }) + + function getWeatherIcon(code, isDay) { + if (typeof isDay === "undefined") { + isDay = weather.isDay + } + const iconMap = isDay ? weatherIcons : nightWeatherIcons + return iconMap[String(code)] || "cloud" + } + + function getWeatherCondition(code) { + const conditions = { + "0": "Clear", + "1": "Clear", + "2": "Partly cloudy", + "3": "Overcast", + "45": "Fog", + "48": "Fog", + "51": "Drizzle", + "53": "Drizzle", + "55": "Drizzle", + "56": "Freezing drizzle", + "57": "Freezing drizzle", + "61": "Light rain", + "63": "Rain", + "65": "Heavy rain", + "66": "Light rain", + "67": "Heavy rain", + "71": "Light snow", + "73": "Snow", + "75": "Heavy snow", + "77": "Snow", + "80": "Light rain", + "81": "Rain", + "82": "Heavy rain", + "85": "Light snow showers", + "86": "Heavy snow showers", + "95": "Thunderstorm", + "96": "Thunderstorm with hail", + "99": "Thunderstorm with hail" + } + return conditions[String(code)] || "Unknown" + } + + function formatTime(isoString) { + if (!isoString) return "--" + + try { + const date = new Date(isoString) + const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP" + return date.toLocaleTimeString(Qt.locale(), format) + } catch (e) { + return "--" + } + } + + function formatForecastDay(isoString, index) { + if (!isoString) return "--" + + try { + const date = new Date(isoString) + if (index === 0) return qsTr("Today") + if (index === 1) return qsTr("Tomorrow") + + const locale = Qt.locale() + return locale.dayName(date.getDay(), Locale.ShortFormat) + } catch (e) { + return "--" + } + } + + function getWeatherApiUrl() { + if (!location) { + return null + } + + const params = [ + "latitude=" + location.latitude, + "longitude=" + location.longitude, + "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,surface_pressure,wind_speed_10m", + "daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max", + "timezone=auto", + "forecast_days=7" + ] + + if (SettingsData.useFahrenheit) { + params.push("temperature_unit=fahrenheit") + } + + return "https://api.open-meteo.com/v1/forecast?" + params.join('&') + } + + function getGeocodingUrl(query) { + return "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(query) + "&count=1&language=en&format=json" + } + + function addRef() { + refCount++ + + if (refCount === 1 && !weather.available && SettingsData.weatherEnabled) { + fetchWeather() + } + } + + function removeRef() { + refCount = Math.max(0, refCount - 1) + } + + function updateLocation() { + if (SettingsData.useAutoLocation) { + getLocationFromIP() + } else { + const coords = SettingsData.weatherCoordinates + if (coords) { + const parts = coords.split(",") + if (parts.length === 2) { + const lat = parseFloat(parts[0]) + const lon = parseFloat(parts[1]) + if (!isNaN(lat) && !isNaN(lon)) { + getLocationFromCoords(lat, lon) + return + } + } + } + + const cityName = SettingsData.weatherLocation + if (cityName) { + getLocationFromCity(cityName) + } + } + } + + function getLocationFromCoords(lat, lon) { + reverseGeocodeFetcher.command = ["bash", "-c", "curl -s --connect-timeout 10 --max-time 30 'https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en' -H 'User-Agent: DankMaterialShell Weather Widget'"] + reverseGeocodeFetcher.running = true + } + + function getLocationFromCity(city) { + cityGeocodeFetcher.command = ["bash", "-c", "curl -s --connect-timeout 10 --max-time 30 '" + getGeocodingUrl(city) + "'"] + cityGeocodeFetcher.running = true + } + + function getLocationFromIP() { + ipLocationFetcher.running = true + } + + function fetchWeather() { + if (root.refCount === 0 || !SettingsData.weatherEnabled) { + return + } + + if (!location) { + updateLocation() + return + } + + if (weatherFetcher.running) { + return + } + + const now = Date.now() + if (now - root.lastFetchTime < root.minFetchInterval) { + return + } + + const apiUrl = getWeatherApiUrl() + if (!apiUrl) { + return + } + + root.lastFetchTime = now + root.weather.loading = true + weatherFetcher.command = ["bash", "-c", "curl -s --connect-timeout 10 --max-time 30 '" + apiUrl + "'"] + weatherFetcher.running = true + } + + function forceRefresh() { + root.lastFetchTime = 0 // Reset throttle + fetchWeather() + } + + function handleWeatherSuccess() { + root.retryAttempts = 0 + root.persistentRetryCount = 0 + if (persistentRetryTimer.running) { + persistentRetryTimer.stop() + } + if (updateTimer.interval !== root.updateInterval) { + updateTimer.interval = root.updateInterval + } + } + + function handleWeatherFailure() { + root.retryAttempts++ + if (root.retryAttempts < root.maxRetryAttempts) { + retryTimer.start() + } else { + root.retryAttempts = 0 + if (!root.weather.available) { + root.weather.loading = false + } + const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount), 300000) + persistentRetryCount++ + persistentRetryTimer.interval = backoffDelay + persistentRetryTimer.start() + } + } + + Process { + id: ipLocationFetcher + command: ["curl", "-s", "--connect-timeout", "5", "--max-time", "10", "http://ipinfo.io/json"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const raw = text.trim() + if (!raw || raw[0] !== "{") { + root.handleWeatherFailure() + return + } + + try { + const data = JSON.parse(raw) + const coords = data.loc + const city = data.city + + if (!coords || !city) { + throw new Error("Missing location data") + } + + const coordsParts = coords.split(",") + if (coordsParts.length !== 2) { + throw new Error("Invalid coordinates format") + } + + const lat = parseFloat(coordsParts[0]) + const lon = parseFloat(coordsParts[1]) + + if (isNaN(lat) || isNaN(lon)) { + throw new Error("Invalid coordinate values") + } + + root.location = { + city: city, + latitude: lat, + longitude: lon + } + fetchWeather() + } catch (e) { + root.handleWeatherFailure() + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + root.handleWeatherFailure() + } + } + } + + Process { + id: reverseGeocodeFetcher + running: false + + stdout: StdioCollector { + onStreamFinished: { + const raw = text.trim() + if (!raw || raw[0] !== "{") { + root.handleWeatherFailure() + return + } + + try { + const data = JSON.parse(raw) + const address = data.address || {} + + root.location = { + city: address.hamlet || address.city || address.town || address.village || "Unknown", + country: address.country || "Unknown", + latitude: parseFloat(data.lat), + longitude: parseFloat(data.lon) + } + + fetchWeather() + } catch (e) { + root.handleWeatherFailure() + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + root.handleWeatherFailure() + } + } + } + + Process { + id: cityGeocodeFetcher + running: false + + stdout: StdioCollector { + onStreamFinished: { + const raw = text.trim() + if (!raw || raw[0] !== "{") { + root.handleWeatherFailure() + return + } + + try { + const data = JSON.parse(raw) + const results = data.results + + if (!results || results.length === 0) { + throw new Error("No results found") + } + + const result = results[0] + + root.location = { + city: result.name, + country: result.country, + latitude: result.latitude, + longitude: result.longitude + } + + fetchWeather() + } catch (e) { + root.handleWeatherFailure() + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + root.handleWeatherFailure() + } + } + } + + Process { + id: weatherFetcher + running: false + + stdout: StdioCollector { + onStreamFinished: { + const raw = text.trim() + if (!raw || raw[0] !== "{") { + root.handleWeatherFailure() + return + } + + try { + const data = JSON.parse(raw) + + if (!data.current || !data.daily) { + throw new Error("Required weather data fields missing") + } + + const current = data.current + const daily = data.daily + const currentUnits = data.current_units || {} + + const tempC = current.temperature_2m || 0 + const tempF = SettingsData.useFahrenheit ? tempC : (tempC * 9/5 + 32) + const feelsLikeC = current.apparent_temperature || tempC + const feelsLikeF = SettingsData.useFahrenheit ? feelsLikeC : (feelsLikeC * 9/5 + 32) + + const forecast = [] + if (daily.time && daily.time.length > 0) { + for (let i = 0; i < Math.min(daily.time.length, 7); i++) { + const tempMinC = daily.temperature_2m_min?.[i] || 0 + const tempMaxC = daily.temperature_2m_max?.[i] || 0 + const tempMinF = SettingsData.useFahrenheit ? tempMinC : (tempMinC * 9/5 + 32) + const tempMaxF = SettingsData.useFahrenheit ? tempMaxC : (tempMaxC * 9/5 + 32) + + forecast.push({ + "day": formatForecastDay(daily.time[i], i), + "wCode": daily.weather_code?.[i] || 0, + "tempMin": Math.round(tempMinC), + "tempMax": Math.round(tempMaxC), + "tempMinF": Math.round(tempMinF), + "tempMaxF": Math.round(tempMaxF), + "precipitationProbability": Math.round(daily.precipitation_probability_max?.[i] || 0), + "sunrise": daily.sunrise?.[i] ? formatTime(daily.sunrise[i]) : "", + "sunset": daily.sunset?.[i] ? formatTime(daily.sunset[i]) : "" + }) + } + } + + root.weather = { + "available": true, + "loading": false, + "temp": Math.round(tempC), + "tempF": Math.round(tempF), + "feelsLike": Math.round(feelsLikeC), + "feelsLikeF": Math.round(feelsLikeF), + "city": root.location?.city || "Unknown", + "country": root.location?.country || "Unknown", + "wCode": current.weather_code || 0, + "humidity": Math.round(current.relative_humidity_2m || 0), + "wind": Math.round(current.wind_speed_10m || 0) + " " + (currentUnits.wind_speed_10m || 'm/s'), + "sunrise": formatTime(daily.sunrise?.[0]) || "06:00", + "sunset": formatTime(daily.sunset?.[0]) || "18:00", + "uv": 0, + "pressure": Math.round(current.surface_pressure || 0), + "precipitationProbability": Math.round(current.precipitation || 0), + "isDay": Boolean(current.is_day), + "forecast": forecast + } + + const displayTemp = SettingsData.useFahrenheit ? root.weather.tempF : root.weather.temp + const unit = SettingsData.useFahrenheit ? "°F" : "°C" + + root.handleWeatherSuccess() + } catch (e) { + root.handleWeatherFailure() + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + root.handleWeatherFailure() + } + } + } + + Timer { + id: updateTimer + interval: root.updateInterval + running: root.refCount > 0 && SettingsData.weatherEnabled + repeat: true + triggeredOnStart: true + onTriggered: { + root.fetchWeather() + } + } + + Timer { + id: retryTimer + interval: root.retryDelay + running: false + repeat: false + onTriggered: { + root.fetchWeather() + } + } + + Timer { + id: persistentRetryTimer + interval: 60000 + running: false + repeat: false + onTriggered: { + if (!root.weather.available) { + root.weather.loading = true + } + root.fetchWeather() + } + } + + Component.onCompleted: { + + SettingsData.weatherCoordinatesChanged.connect(() => { + root.location = null + root.weather = { + "available": false, + "loading": true, + "temp": 0, + "tempF": 0, + "feelsLike": 0, + "feelsLikeF": 0, + "city": "", + "country": "", + "wCode": 0, + "humidity": 0, + "wind": "", + "sunrise": "06:00", + "sunset": "18:00", + "uv": 0, + "pressure": 0, + "precipitationProbability": 0, + "isDay": true, + "forecast": [] + } + root.lastFetchTime = 0 + root.forceRefresh() + }) + + SettingsData.weatherLocationChanged.connect(() => { + root.location = null + root.lastFetchTime = 0 + root.forceRefresh() + }) + + SettingsData.useAutoLocationChanged.connect(() => { + root.location = null + root.weather = { + "available": false, + "loading": true, + "temp": 0, + "tempF": 0, + "feelsLike": 0, + "feelsLikeF": 0, + "city": "", + "country": "", + "wCode": 0, + "humidity": 0, + "wind": "", + "sunrise": "06:00", + "sunset": "18:00", + "uv": 0, + "pressure": 0, + "precipitationProbability": 0, + "isDay": true, + "forecast": [] + } + root.lastFetchTime = 0 + root.forceRefresh() + }) + + SettingsData.useFahrenheitChanged.connect(() => { + root.lastFetchTime = 0 + root.forceRefresh() + }) + + SettingsData.weatherEnabledChanged.connect(() => { + if (SettingsData.weatherEnabled && root.refCount > 0 && !root.weather.available) { + root.forceRefresh() + } else if (!SettingsData.weatherEnabled) { + updateTimer.stop() + retryTimer.stop() + persistentRetryTimer.stop() + if (weatherFetcher.running) { + weatherFetcher.running = false + } + } + }) + } +} diff --git a/quickshell/.config/quickshell/Widgets/CachingImage.qml b/quickshell/.config/quickshell/Widgets/CachingImage.qml new file mode 100644 index 0000000..3c41fc4 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/CachingImage.qml @@ -0,0 +1,57 @@ +import QtQuick +import Quickshell.Io +import qs.Common + +Image { + id: root + + property string imagePath: "" + property string imageHash: "" + property int maxCacheSize: 512 + readonly property string cachePath: imageHash ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : "" + + asynchronous: true + fillMode: Image.PreserveAspectCrop + sourceSize.width: maxCacheSize + sourceSize.height: maxCacheSize + smooth: true + onImagePathChanged: { + if (!imagePath) { + source = "" + imageHash = "" + return + } + hashProcess.command = ["sha256sum", Paths.strip(imagePath)] + hashProcess.running = true + } + onCachePathChanged: { + if (!imageHash || !cachePath) + return + + Paths.mkdir(Paths.imagecache) + source = cachePath + } + onStatusChanged: { + if (source == cachePath && status === Image.Error) { + source = imagePath + return + } + if (source != imagePath || status !== Image.Ready || !imageHash || !cachePath) + return + + Paths.mkdir(Paths.imagecache) + const grabPath = cachePath + if (visible && width > 0 && height > 0 && Window.window && Window.window.visible) + grabToImage(res => { + return res.saveToFile(grabPath) + }) + } + + Process { + id: hashProcess + + stdout: StdioCollector { + onStreamFinished: root.imageHash = text.split(" ")[0] + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankActionButton.qml b/quickshell/.config/quickshell/Widgets/DankActionButton.qml new file mode 100644 index 0000000..472ebfe --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankActionButton.qml @@ -0,0 +1,34 @@ +import QtQuick +import qs.Common +import qs.Widgets + +StyledRect { + id: root + + property string iconName: "" + property int iconSize: Theme.iconSize - 4 + property color iconColor: Theme.surfaceText + property color backgroundColor: "transparent" + property bool circular: true + property int buttonSize: 32 + + signal clicked + + width: buttonSize + height: buttonSize + radius: circular ? buttonSize / 2 : Theme.cornerRadius + color: backgroundColor + + DankIcon { + anchors.centerIn: parent + name: root.iconName + size: root.iconSize + color: root.iconColor + } + + StateLayer { + stateColor: Theme.primary + cornerRadius: root.radius + onClicked: root.clicked() + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankBackdrop.qml b/quickshell/.config/quickshell/Widgets/DankBackdrop.qml new file mode 100644 index 0000000..f5ed62d --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankBackdrop.qml @@ -0,0 +1,63 @@ +import QtQuick +import qs.Common + +Item { + id: root + + anchors.fill: parent + + property string screenName: "" + property bool isColorWallpaper: { + var currentWallpaper = SessionData.getMonitorWallpaper(screenName) + return currentWallpaper && currentWallpaper.startsWith("#") + } + + Rectangle { + anchors.fill: parent + color: isColorWallpaper ? SessionData.getMonitorWallpaper(screenName) : Theme.background + } + + Rectangle { + x: parent.width * 0.7 + y: -parent.height * 0.3 + width: parent.width * 0.8 + height: parent.height * 1.5 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) + rotation: 35 + visible: !isColorWallpaper + } + + Rectangle { + x: parent.width * 0.85 + y: -parent.height * 0.2 + width: parent.width * 0.4 + height: parent.height * 1.2 + color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12) + rotation: 35 + visible: !isColorWallpaper + } + + Item { + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.leftMargin: Theme.spacingXL * 2 + anchors.bottomMargin: Theme.spacingXL * 2 + opacity: 0.25 + visible: !isColorWallpaper + + StyledText { + anchors.left: parent.left + anchors.bottom: parent.bottom + // ! TODO qmlfmt will brick this + text: `██████╗ █████╗ ███╗ ██╗██╗ ██╗ +██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝ +██║ ██║███████║██╔██╗ ██║█████╔╝ +██║ ██║██╔══██║██║╚██╗██║██╔═██╗ +██████╔╝██║ ██║██║ ╚████║██║ ██╗ +╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝` + isMonospace: true + font.pixelSize: Theme.fontSizeLarge * 1.2 + color: Theme.primary + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankColorPicker.qml b/quickshell/.config/quickshell/Widgets/DankColorPicker.qml new file mode 100644 index 0000000..709d2ce --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankColorPicker.qml @@ -0,0 +1,117 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string pickerTitle: "Choose Color" + property color selectedColor: Theme.primary + property bool isOpen: false + + signal colorSelected(color selectedColor) + + function open() { + customColorField.text = "" + isOpen = true + Qt.callLater(() => root.forceActiveFocus()) + } + + function close() { + isOpen = false + } + + anchors.centerIn: parent + width: 320 + height: 340 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outlineMedium + border.width: 1 + z: 1000 + visible: isOpen + focus: isOpen + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Escape) { + close() + event.accepted = true + } + } + + DankActionButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacingS + buttonSize: 28 + iconName: "close" + iconSize: 16 + iconColor: Theme.surfaceText + onClicked: root.close() + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + StyledText { + text: pickerTitle + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + Grid { + columns: 8 + spacing: 4 + anchors.horizontalCenter: parent.horizontalCenter + + property var colors: ["#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4", "#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722", "#795548", "#9e9e9e", "#607d8b", "#000000", "#ffffff", "#ff1744", "#f50057", "#d500f9", "#651fff", "#3d5afe", "#2979ff", "#00b0ff", "#00e5ff", "#1de9b6", "#00e676", "#76ff03", "#c6ff00", "#ffff00", "#ffc400", "#ff9100", "#ff3d00", "#bf360c", "#424242", "#37474f"] + + Repeater { + model: parent.colors + Rectangle { + width: 24 + height: 24 + color: modelData + radius: 4 + border.color: Theme.outline + border.width: root.selectedColor == modelData ? 2 : 1 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.selectedColor = modelData + root.colorSelected(modelData) + root.close() + } + } + } + } + } + + StyledText { + text: "Custom Color:" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + DankTextField { + id: customColorField + width: parent.width + height: 40 + placeholderText: "#ff0000" + text: "" + onAccepted: { + var hexColor = text.startsWith("#") ? text : "#" + text + if (/^#[0-9A-Fa-f]{6}$/.test(hexColor)) { + root.selectedColor = hexColor + root.colorSelected(hexColor) + root.close() + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankDropdown.qml b/quickshell/.config/quickshell/Widgets/DankDropdown.qml new file mode 100644 index 0000000..e4ceb6e --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankDropdown.qml @@ -0,0 +1,348 @@ +import "../Common/fzf.js" as Fzf +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string text: "" + property string description: "" + property string currentValue: "" + property var options: [] + property var optionIcons: [] + property bool forceRecreate: false + property bool enableFuzzySearch: false + property int popupWidthOffset: 0 + property int maxPopupHeight: 400 + + signal valueChanged(string value) + + width: parent.width + height: 60 + radius: Theme.cornerRadius + color: "transparent" + Component.onCompleted: forceRecreateTimer.start() + Component.onDestruction: { + const popup = popupLoader.item + if (popup && popup.visible) { + popup.close() + } + } + onVisibleChanged: { + const popup = popupLoader.item + if (!visible && popup && popup.visible) { + popup.close() + } else if (visible) { + forceRecreateTimer.start() + } + } + + Timer { + id: forceRecreateTimer + + interval: 50 + repeat: false + onTriggered: root.forceRecreate = !root.forceRecreate + } + + Column { + anchors.left: parent.left + anchors.right: dropdown.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingXS + + StyledText { + text: root.text + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: description.length > 0 + wrapMode: Text.WordWrap + width: parent.width + } + } + + Rectangle { + id: dropdown + + width: root.width <= 60 ? root.width : 180 + height: 36 + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + radius: Theme.cornerRadius + color: dropdownArea.containsMouse ? Theme.primaryHover : Theme.contentBackground() + border.color: Theme.surfaceVariantAlpha + border.width: 1 + + MouseArea { + id: dropdownArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + const popup = popupLoader.item + if (!popup) { + return + } + + if (popup.visible) { + popup.close() + return + } + + const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4) + popup.x = pos.x - (root.popupWidthOffset / 2) + popup.y = pos.y + popup.open() + } + } + + Row { + id: contentRow + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: { + const currentIndex = root.options.indexOf(root.currentValue) + return currentIndex >= 0 && root.optionIcons.length > currentIndex ? root.optionIcons[currentIndex] : "" + } + size: 18 + color: Theme.surfaceVariantText + visible: name !== "" && root.width > 60 + } + + StyledText { + text: root.currentValue + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: root.width <= 60 ? dropdown.width - expandIcon.width - Theme.spacingS * 2 : dropdown.width - contentRow.x - expandIcon.width - Theme.spacingM - Theme.spacingS + elide: root.width <= 60 ? Text.ElideNone : Text.ElideRight + horizontalAlignment: root.width <= 60 ? Text.AlignHCenter : Text.AlignLeft + } + } + + DankIcon { + id: expandIcon + + name: "expand_more" + size: 20 + color: Theme.surfaceVariantText + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: Theme.spacingS + } + } + + Loader { + id: popupLoader + + property bool recreateFlag: root.forceRecreate + + active: true + onRecreateFlagChanged: { + active = false + active = true + } + + sourceComponent: Component { + Popup { + id: dropdownMenu + + property string searchQuery: "" + property var filteredOptions: [] + property int selectedIndex: -1 + property var fzfFinder: new Fzf.Finder(root.options, { + "selector": option => option, + "limit": 50, + "casing": "case-insensitive" + }) + + function updateFilteredOptions() { + if (!root.enableFuzzySearch || searchQuery.length === 0) { + filteredOptions = root.options + selectedIndex = -1 + return + } + + const results = fzfFinder.find(searchQuery) + filteredOptions = results.map(result => result.item) + selectedIndex = -1 + } + + function selectNext() { + if (filteredOptions.length === 0) { + return + } + selectedIndex = (selectedIndex + 1) % filteredOptions.length + listView.positionViewAtIndex(selectedIndex, ListView.Contain) + } + + function selectPrevious() { + if (filteredOptions.length === 0) { + return + } + selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1 + listView.positionViewAtIndex(selectedIndex, ListView.Contain) + } + + function selectCurrent() { + if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) { + return + } + root.currentValue = filteredOptions[selectedIndex] + root.valueChanged(filteredOptions[selectedIndex]) + close() + } + + parent: Overlay.overlay + width: dropdown.width + root.popupWidthOffset + height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16) + padding: 0 + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + onOpened: { + searchQuery = "" + updateFilteredOptions() + if (root.enableFuzzySearch && searchField.visible) { + searchField.forceActiveFocus() + } + } + + background: Rectangle { + color: "transparent" + } + + contentItem: Rectangle { + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1) + border.color: Theme.primarySelected + border.width: 1 + radius: Theme.cornerRadius + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingS + + Rectangle { + id: searchContainer + + width: parent.width + height: 42 + visible: root.enableFuzzySearch + radius: Theme.cornerRadius + color: Theme.surfaceVariantAlpha + + DankTextField { + id: searchField + + anchors.fill: parent + anchors.margins: 1 + placeholderText: "Search..." + text: searchQuery + topPadding: Theme.spacingS + bottomPadding: Theme.spacingS + onTextChanged: { + searchQuery = text + updateFilteredOptions() + } + Keys.onDownPressed: selectNext() + Keys.onUpPressed: selectPrevious() + Keys.onReturnPressed: selectCurrent() + Keys.onEnterPressed: selectCurrent() + } + } + + Item { + width: 1 + height: Theme.spacingXS + visible: root.enableFuzzySearch + } + + DankListView { + id: listView + + property var popupRef: dropdownMenu + + width: parent.width + height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0) + clip: true + model: filteredOptions + spacing: 2 + + interactive: true + flickDeceleration: 1500 + maximumFlickVelocity: 2000 + boundsBehavior: Flickable.DragAndOvershootBounds + boundsMovement: Flickable.FollowBoundsBehavior + pressDelay: 0 + flickableDirection: Flickable.VerticalFlick + + delegate: Rectangle { + property bool isSelected: selectedIndex === index + property bool isCurrentValue: root.currentValue === modelData + property int optionIndex: root.options.indexOf(modelData) + + width: ListView.view.width + height: 32 + radius: Theme.cornerRadius + color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: optionIndex >= 0 && root.optionIcons.length > optionIndex ? root.optionIcons[optionIndex] : "" + size: 18 + color: isCurrentValue ? Theme.primary : Theme.surfaceVariantText + visible: name !== "" + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData + font.pixelSize: Theme.fontSizeMedium + color: isCurrentValue ? Theme.primary : Theme.surfaceText + font.weight: isCurrentValue ? Font.Medium : Font.Normal + width: parent.parent.width - parent.x - Theme.spacingS + elide: Text.ElideRight + } + } + + MouseArea { + id: optionArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.currentValue = modelData + root.valueChanged(modelData) + dropdownMenu.close() + } + } + } + } + } + } + } + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankFlickable.qml b/quickshell/.config/quickshell/Widgets/DankFlickable.qml new file mode 100644 index 0000000..1b4044e --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankFlickable.qml @@ -0,0 +1,169 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +Flickable { + id: flickable + + property real mouseWheelSpeed: 60 + property real momentumVelocity: 0 + property bool isMomentumActive: false + property real friction: 0.95 + property real minMomentumVelocity: 50 + property real maxMomentumVelocity: 2500 + property bool _scrollBarActive: false + + flickDeceleration: 1500 + maximumFlickVelocity: 2000 + boundsBehavior: Flickable.StopAtBounds + boundsMovement: Flickable.FollowBoundsBehavior + pressDelay: 0 + flickableDirection: Flickable.VerticalFlick + + WheelHandler { + id: wheelHandler + + property real touchpadSpeed: 1.8 + property real momentumRetention: 0.92 + property real lastWheelTime: 0 + property real momentum: 0 + property var velocitySamples: [] + + function startMomentum() { + flickable.isMomentumActive = true + momentumTimer.start() + } + + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + + onWheel: event => { + vbar._scrollBarActive = true + vbar.hideTimer.restart() + + const currentTime = Date.now() + const timeDelta = currentTime - lastWheelTime + lastWheelTime = currentTime + + const deltaY = event.angleDelta.y + const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0 + + if (isMouseWheel) { + momentumTimer.stop() + flickable.isMomentumActive = false + velocitySamples = [] + momentum = 0 + + const lines = Math.floor(Math.abs(deltaY) / 120) + const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed + let newY = flickable.contentY + scrollAmount + newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY)) + + if (flickable.flicking) { + flickable.cancelFlick() + } + + flickable.contentY = newY + } else { + momentumTimer.stop() + flickable.isMomentumActive = false + + let delta = 0 + if (event.pixelDelta.y !== 0) { + delta = event.pixelDelta.y * touchpadSpeed + } else { + delta = event.angleDelta.y / 8 * touchpadSpeed + } + + velocitySamples.push({ + "delta": delta, + "time": currentTime + }) + velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100) + + if (velocitySamples.length > 1) { + const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0) + const timeSpan = currentTime - velocitySamples[0].time + if (timeSpan > 0) { + flickable.momentumVelocity = Math.max(-flickable.maxMomentumVelocity, Math.min(flickable.maxMomentumVelocity, totalDelta / timeSpan * 1000)) + } + } + + if (event.pixelDelta.y !== 0 && timeDelta < 50) { + momentum = momentum * momentumRetention + delta * 0.15 + delta += momentum + } else { + momentum = 0 + } + + let newY = flickable.contentY - delta + newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY)) + + if (flickable.flicking) { + flickable.cancelFlick() + } + + flickable.contentY = newY + } + + event.accepted = true + } + + onActiveChanged: { + if (!active) { + if (Math.abs(flickable.momentumVelocity) >= flickable.minMomentumVelocity) { + startMomentum() + } else { + velocitySamples = [] + flickable.momentumVelocity = 0 + } + } + } + } + + onMovementStarted: { + vbar._scrollBarActive = true + vbar.hideTimer.stop() + } + onMovementEnded: vbar.hideTimer.restart() + + Timer { + id: momentumTimer + interval: 16 + repeat: true + + onTriggered: { + const newY = flickable.contentY - flickable.momentumVelocity * 0.016 + const maxY = Math.max(0, flickable.contentHeight - flickable.height) + + if (newY < 0 || newY > maxY) { + flickable.contentY = newY < 0 ? 0 : maxY + stop() + flickable.isMomentumActive = false + flickable.momentumVelocity = 0 + return + } + + flickable.contentY = newY + flickable.momentumVelocity *= flickable.friction + + if (Math.abs(flickable.momentumVelocity) < 5) { + stop() + flickable.isMomentumActive = false + flickable.momentumVelocity = 0 + } + } + } + + NumberAnimation { + id: returnToBoundsAnimation + target: flickable + property: "contentY" + duration: 300 + easing.type: Easing.OutQuad + } + + ScrollBar.vertical: DankScrollbar { + id: vbar + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankGridView.qml b/quickshell/.config/quickshell/Widgets/DankGridView.qml new file mode 100644 index 0000000..03e1f3d --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankGridView.qml @@ -0,0 +1,159 @@ +import QtQuick +import QtQuick.Controls +import qs.Widgets + +GridView { + id: gridView + + property real momentumVelocity: 0 + property bool isMomentumActive: false + property real friction: 0.95 + property real minMomentumVelocity: 50 + property real maxMomentumVelocity: 2500 + + flickDeceleration: 1500 + maximumFlickVelocity: 2000 + boundsBehavior: Flickable.StopAtBounds + boundsMovement: Flickable.FollowBoundsBehavior + pressDelay: 0 + flickableDirection: Flickable.VerticalFlick + + onMovementStarted: { + vbar._scrollBarActive = true + vbar.hideTimer.stop() + } + onMovementEnded: vbar.hideTimer.restart() + + WheelHandler { + id: wheelHandler + + property real mouseWheelSpeed: 60 + property real touchpadSpeed: 1.8 + property real momentumRetention: 0.92 + property real lastWheelTime: 0 + property real momentum: 0 + property var velocitySamples: [] + + function startMomentum() { + isMomentumActive = true + momentumTimer.start() + } + + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: event => { + vbar._scrollBarActive = true + vbar.hideTimer.restart() + + const currentTime = Date.now() + const timeDelta = currentTime - lastWheelTime + lastWheelTime = currentTime + + const deltaY = event.angleDelta.y + const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0 + + if (isMouseWheel) { + momentumTimer.stop() + isMomentumActive = false + velocitySamples = [] + momentum = 0 + + const lines = Math.floor(Math.abs(deltaY) / 120) + const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35 + let newY = contentY + scrollAmount + newY = Math.max(0, Math.min(contentHeight - height, newY)) + + if (flicking) { + cancelFlick() + } + + contentY = newY + } else { + momentumTimer.stop() + isMomentumActive = false + + let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * touchpadSpeed : event.angleDelta.y / 120 * cellHeight * 1.2 + + velocitySamples.push({ + "delta": delta, + "time": currentTime + }) + velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100) + + if (velocitySamples.length > 1) { + const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0) + const timeSpan = currentTime - velocitySamples[0].time + if (timeSpan > 0) { + momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000)) + } + } + + if (event.pixelDelta.y !== 0 && timeDelta < 50) { + momentum = momentum * momentumRetention + delta * 0.15 + delta += momentum + } else { + momentum = 0 + } + + let newY = contentY - delta + newY = Math.max(0, Math.min(contentHeight - height, newY)) + + if (flicking) { + cancelFlick() + } + + contentY = newY + } + + event.accepted = true + } + onActiveChanged: { + if (!active) { + if (Math.abs(momentumVelocity) >= minMomentumVelocity) { + startMomentum() + } else { + velocitySamples = [] + momentumVelocity = 0 + } + } + } + } + + Timer { + id: momentumTimer + interval: 16 + repeat: true + onTriggered: { + const newY = contentY - momentumVelocity * 0.016 + const maxY = Math.max(0, contentHeight - height) + + if (newY < 0 || newY > maxY) { + contentY = newY < 0 ? 0 : maxY + stop() + isMomentumActive = false + momentumVelocity = 0 + return + } + + contentY = newY + momentumVelocity *= friction + + if (Math.abs(momentumVelocity) < 5) { + stop() + isMomentumActive = false + momentumVelocity = 0 + } + } + } + + NumberAnimation { + id: returnToBoundsAnimation + target: gridView + property: "contentY" + duration: 300 + easing.type: Easing.OutQuad + } + + ScrollBar.vertical: DankScrollbar { + id: vbar + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankIcon.qml b/quickshell/.config/quickshell/Widgets/DankIcon.qml new file mode 100644 index 0000000..eb50924 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankIcon.qml @@ -0,0 +1,41 @@ +import QtQuick +import qs.Common + +StyledText { + id: icon + + property alias name: icon.text + property alias size: icon.font.pixelSize + property alias color: icon.color + property bool filled: false + property real fill: filled ? 1.0 : 0.0 + property int grade: Theme.isLightMode ? 0 : -25 + property int weight: filled ? 500 : 400 + + font.family: "Material Symbols Rounded" + font.pixelSize: Theme.fontSizeMedium + font.weight: weight + color: Theme.surfaceText + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + font.variableAxes: { + "FILL": fill.toFixed(1), + "GRAD": grade, + "opsz": 24, + "wght": weight + } + + Behavior on fill { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on weight { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankIconPicker.qml b/quickshell/.config/quickshell/Widgets/DankIconPicker.qml new file mode 100644 index 0000000..efd4803 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankIconPicker.qml @@ -0,0 +1,291 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string currentIcon: "" + property string iconType: "icon" // "icon" or "text" + + signal iconSelected(string iconName, string iconType) + + width: 240 + height: 32 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: dropdownLoader.active ? Theme.primary : Theme.outline + border.width: 1 + + property var iconCategories: [{ + "name": "Numbers", + "icons": ["looks_one", "looks_two", "looks_3", "looks_4", "looks_5", "looks_6", "filter_1", "filter_2", "filter_3", "filter_4", "filter_5", "filter_6", "filter_7", "filter_8", "filter_9", "filter_9_plus", "plus_one", "exposure_plus_1", "exposure_plus_2"] + }, { + "name": "Workspace", + "icons": ["work", "laptop", "desktop_windows", "folder", "view_module", "dashboard", "apps", "grid_view"] + }, { + "name": "Development", + "icons": ["code", "terminal", "bug_report", "build", "engineering", "integration_instructions", "data_object", "schema", "api", "webhook"] + }, { + "name": "Communication", + "icons": ["chat", "mail", "forum", "message", "video_call", "call", "contacts", "group", "notifications", "campaign"] + }, { + "name": "Media", + "icons": ["music_note", "headphones", "mic", "videocam", "photo", "movie", "library_music", "album", "radio", "volume_up"] + }, { + "name": "System", + "icons": ["memory", "storage", "developer_board", "monitor", "keyboard", "mouse", "battery_std", "wifi", "bluetooth", "security", "settings"] + }, { + "name": "Navigation", + "icons": ["home", "arrow_forward", "arrow_back", "expand_more", "expand_less", "menu", "close", "search", "filter_list", "sort"] + }, { + "name": "Actions", + "icons": ["add", "remove", "edit", "delete", "save", "download", "upload", "share", "content_copy", "content_paste", "content_cut", "undo", "redo"] + }, { + "name": "Status", + "icons": ["check", "error", "warning", "info", "done", "pending", "schedule", "update", "sync", "offline_bolt"] + }, { + "name": "Fun", + "icons": ["celebration", "cake", "star", "favorite", "pets", "sports_esports", "local_fire_department", "bolt", "auto_awesome", "diamond"] + }] + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: (root.iconType === "icon" && root.currentIcon) ? root.currentIcon : (root.iconType === "text" ? "text_fields" : "add") + size: 16 + color: root.currentIcon ? Theme.surfaceText : Theme.outline + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: root.currentIcon ? root.currentIcon : "Choose icon" + font.pixelSize: Theme.fontSizeSmall + color: root.currentIcon ? Theme.surfaceText : Theme.outline + anchors.verticalCenter: parent.verticalCenter + width: 160 + elide: Text.ElideRight + + MouseArea { + anchors.fill: parent + onClicked: { + dropdownLoader.active = !dropdownLoader.active + } + } + } + } + + DankIcon { + name: dropdownLoader.active ? "expand_less" : "expand_more" + size: 16 + color: Theme.outline + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + } + + Loader { + id: dropdownLoader + active: false + asynchronous: true + + sourceComponent: PanelWindow { + id: dropdownPopup + + visible: true + implicitWidth: 320 + implicitHeight: Math.min(500, dropdownContent.implicitHeight + 32) + color: "transparent" + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + + anchors { + top: true + left: true + right: true + bottom: true + } + + // Top area - above popup + MouseArea { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: popupContainer.y + onClicked: { + dropdownLoader.active = false + } + } + + // Bottom area - below popup + MouseArea { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: popupContainer.bottom + anchors.bottom: parent.bottom + onClicked: { + dropdownLoader.active = false + } + } + + // Left area - left of popup + MouseArea { + anchors.left: parent.left + anchors.top: popupContainer.top + anchors.bottom: popupContainer.bottom + width: popupContainer.x + onClicked: { + dropdownLoader.active = false + } + } + + // Right area - right of popup + MouseArea { + anchors.right: parent.right + anchors.top: popupContainer.top + anchors.bottom: popupContainer.bottom + anchors.left: popupContainer.right + onClicked: { + dropdownLoader.active = false + } + } + + Rectangle { + id: popupContainer + width: 320 + height: Math.min(500, dropdownContent.implicitHeight + 32) + x: Math.max(16, Math.min(root.mapToItem(null, 0, 0).x, parent.width - width - 16)) + y: Math.max(16, Math.min(root.mapToItem(null, 0, root.height + 4).y, parent.height - height - 16)) + radius: Theme.cornerRadius + color: Theme.surface + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowColor: Theme.shadowStrong + shadowBlur: 0.8 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 4 + } + + // Close button + Rectangle { + width: 24 + height: 24 + radius: 12 + color: closeMouseArea.containsMouse ? Theme.errorHover : "transparent" + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + z: 1 + + DankIcon { + name: "close" + size: 16 + color: closeMouseArea.containsMouse ? Theme.error : Theme.outline + anchors.centerIn: parent + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + dropdownLoader.active = false + } + } + } + + DankFlickable { + anchors.fill: parent + anchors.margins: Theme.spacingS + contentHeight: dropdownContent.height + clip: true + pressDelay: 0 + + Column { + id: dropdownContent + width: parent.width + spacing: Theme.spacingM + + // Icon categories + Repeater { + model: root.iconCategories + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + } + + Flow { + width: parent.width + spacing: 4 + + Repeater { + model: modelData.icons + + Rectangle { + width: 36 + height: 36 + radius: Theme.cornerRadius + color: iconMouseArea.containsMouse ? Theme.primaryHover : "transparent" + border.color: root.currentIcon === modelData ? Theme.primary : "transparent" + border.width: 2 + + DankIcon { + name: modelData + size: 20 + color: root.currentIcon === modelData ? Theme.primary : Theme.surfaceText + anchors.centerIn: parent + } + + MouseArea { + id: iconMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.iconSelected(modelData, "icon") + dropdownLoader.active = false + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + } + } + } + } + } + } + + function setIcon(iconName, type) { + root.iconType = type + root.iconType = "icon" + root.currentIcon = iconName + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankListView.qml b/quickshell/.config/quickshell/Widgets/DankListView.qml new file mode 100644 index 0000000..abb7dc1 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankListView.qml @@ -0,0 +1,182 @@ +import QtQuick +import QtQuick.Controls +import qs.Widgets + +ListView { + id: listView + + property real mouseWheelSpeed: 60 + property real savedY: 0 + property bool justChanged: false + property bool isUserScrolling: false + property real momentumVelocity: 0 + property bool isMomentumActive: false + property real friction: 0.95 + property real minMomentumVelocity: 50 + property real maxMomentumVelocity: 2500 + + flickDeceleration: 1500 + maximumFlickVelocity: 2000 + boundsBehavior: Flickable.StopAtBounds + boundsMovement: Flickable.FollowBoundsBehavior + pressDelay: 0 + flickableDirection: Flickable.VerticalFlick + + onMovementStarted: { + isUserScrolling = true + vbar._scrollBarActive = true + vbar.hideTimer.stop() + } + onMovementEnded: { + isUserScrolling = false + vbar.hideTimer.restart() + } + + onContentYChanged: { + if (!justChanged && isUserScrolling) { + savedY = contentY + } + justChanged = false + } + + onModelChanged: { + justChanged = true + contentY = savedY + } + + WheelHandler { + id: wheelHandler + property real touchpadSpeed: 1.8 + property real lastWheelTime: 0 + property real momentum: 0 + property var velocitySamples: [] + + function startMomentum() { + isMomentumActive = true + momentumTimer.start() + } + + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + + onWheel: event => { + isUserScrolling = true + vbar._scrollBarActive = true + vbar.hideTimer.restart() + + const currentTime = Date.now() + const timeDelta = currentTime - lastWheelTime + lastWheelTime = currentTime + + const deltaY = event.angleDelta.y + const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0 + + if (isMouseWheel) { + momentumTimer.stop() + isMomentumActive = false + velocitySamples = [] + momentum = 0 + + const lines = Math.floor(Math.abs(deltaY) / 120) + const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed + let newY = listView.contentY + scrollAmount + newY = Math.max(0, Math.min(listView.contentHeight - listView.height, newY)) + + if (listView.flicking) { + listView.cancelFlick() + } + + listView.contentY = newY + savedY = newY + } else { + momentumTimer.stop() + isMomentumActive = false + + let delta = 0 + if (event.pixelDelta.y !== 0) { + delta = event.pixelDelta.y * touchpadSpeed + } else { + delta = event.angleDelta.y / 8 * touchpadSpeed + } + + velocitySamples.push({ + "delta": delta, + "time": currentTime + }) + velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100) + + if (velocitySamples.length > 1) { + const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0) + const timeSpan = currentTime - velocitySamples[0].time + if (timeSpan > 0) { + momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000)) + } + } + + if (event.pixelDelta.y !== 0 && timeDelta < 50) { + momentum = momentum * 0.92 + delta * 0.15 + delta += momentum + } else { + momentum = 0 + } + + let newY = listView.contentY - delta + newY = Math.max(0, Math.min(listView.contentHeight - listView.height, newY)) + + if (listView.flicking) { + listView.cancelFlick() + } + + listView.contentY = newY + savedY = newY + } + + event.accepted = true + } + + onActiveChanged: { + if (!active) { + isUserScrolling = false + if (Math.abs(momentumVelocity) >= minMomentumVelocity) { + startMomentum() + } else { + velocitySamples = [] + momentumVelocity = 0 + } + } + } + } + + Timer { + id: momentumTimer + interval: 16 + repeat: true + + onTriggered: { + const newY = contentY - momentumVelocity * 0.016 + const maxY = Math.max(0, contentHeight - height) + + if (newY < 0 || newY > maxY) { + contentY = newY < 0 ? 0 : maxY + savedY = contentY + stop() + isMomentumActive = false + momentumVelocity = 0 + return + } + + contentY = newY + savedY = newY + momentumVelocity *= friction + + if (Math.abs(momentumVelocity) < 5) { + stop() + isMomentumActive = false + momentumVelocity = 0 + } + } + } + + ScrollBar.vertical: DankScrollbar { + id: vbar + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankLocationSearch.qml b/quickshell/.config/quickshell/Widgets/DankLocationSearch.qml new file mode 100644 index 0000000..265acbb --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankLocationSearch.qml @@ -0,0 +1,275 @@ +import QtQuick +import QtQuick.Controls +import Quickshell.Io +import qs.Common +import qs.Widgets + +Item { + id: root + + property string currentLocation: "" + property string placeholderText: "Search for a location..." + property bool _internalChange: false + property bool isLoading: false + property string currentSearchText: "" + + signal locationSelected(string displayName, string coordinates) + + function resetSearchState() { + locationSearchTimer.stop() + dropdownHideTimer.stop() + if (locationSearcher.running) + locationSearcher.running = false + isLoading = false + searchResultsModel.clear() + } + + width: parent.width + height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0) + + ListModel { + id: searchResultsModel + } + + Timer { + id: locationSearchTimer + + interval: 500 + running: false + repeat: false + onTriggered: { + if (locationInput.text.length > 2) { + if (locationSearcher.running) + locationSearcher.running = false + + searchResultsModel.clear() + root.isLoading = true + const searchLocation = locationInput.text + root.currentSearchText = searchLocation + const encodedLocation = encodeURIComponent(searchLocation) + const curlCommand = `curl -4 -s --connect-timeout 5 --max-time 10 'https://nominatim.openstreetmap.org/search?q=${encodedLocation}&format=json&limit=5&addressdetails=1'` + locationSearcher.command = ["bash", "-c", curlCommand] + locationSearcher.running = true + } + } + } + + Timer { + id: dropdownHideTimer + + interval: 200 + running: false + repeat: false + onTriggered: { + if (!locationInput.getActiveFocus() && !searchDropdown.hovered) + root.resetSearchState() + } + } + + Process { + id: locationSearcher + + command: ["bash", "-c", "echo"] + running: false + onExited: exitCode => { + root.isLoading = false + if (exitCode !== 0) { + searchResultsModel.clear() + } + } + + stdout: StdioCollector { + onStreamFinished: { + if (root.currentSearchText !== locationInput.text) + return + + const raw = text.trim() + root.isLoading = false + searchResultsModel.clear() + if (!raw || raw[0] !== "[") { + return + } + try { + const data = JSON.parse(raw) + if (data.length === 0) { + return + } + for (var i = 0; i < Math.min(data.length, 5); i++) { + const location = data[i] + if (location.display_name && location.lat && location.lon) { + const parts = location.display_name.split(', ') + let cleanName = parts[0] + if (parts.length > 1) { + const state = parts[parts.length - 2] + if (state && state !== cleanName) + cleanName += `, ${state}` + } + const query = `${location.lat},${location.lon}` + searchResultsModel.append({ + "name": cleanName, + "query": query + }) + } + } + } catch (e) { + + } + } + } + } + + Item { + id: searchInputField + + width: parent.width + height: 48 + + DankTextField { + id: locationInput + + width: parent.width + height: parent.height + leftIconName: "search" + placeholderText: root.placeholderText + text: root.currentLocation + backgroundColor: Theme.surfaceVariant + normalBorderColor: Theme.primarySelected + focusedBorderColor: Theme.primary + onTextEdited: { + if (root._internalChange) + return + if (getActiveFocus()) { + if (text.length > 2) { + root.isLoading = true + locationSearchTimer.restart() + } else { + root.resetSearchState() + } + } + } + onFocusStateChanged: hasFocus => { + if (hasFocus) { + dropdownHideTimer.stop() + } else { + dropdownHideTimer.start() + } + } + } + + DankIcon { + name: root.isLoading ? "hourglass_empty" : (searchResultsModel.count > 0 ? "check_circle" : "error") + size: Theme.iconSize - 4 + color: root.isLoading ? Theme.surfaceVariantText : (searchResultsModel.count > 0 ? Theme.primary : Theme.error) + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + opacity: (locationInput.getActiveFocus() && locationInput.text.length > 2) ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + StyledRect { + id: searchDropdown + + property bool hovered: false + + width: parent.width + height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200) + y: searchInputField.height + radius: Theme.cornerRadius + color: Theme.popupBackground() + border.color: Theme.primarySelected + border.width: 1 + visible: locationInput.getActiveFocus() && locationInput.text.length > 2 && (searchResultsModel.count > 0 || root.isLoading) + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + parent.hovered = true + dropdownHideTimer.stop() + } + onExited: { + parent.hovered = false + if (!locationInput.getActiveFocus()) + dropdownHideTimer.start() + } + acceptedButtons: Qt.NoButton + } + + Item { + anchors.fill: parent + anchors.margins: Theme.spacingS + + DankListView { + id: searchResultsList + + anchors.fill: parent + clip: true + model: searchResultsModel + spacing: 2 + + delegate: StyledRect { + width: searchResultsList.width + height: 36 + radius: Theme.cornerRadius + color: resultMouseArea.containsMouse ? Theme.surfaceLight : "transparent" + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: "place" + size: Theme.iconSize - 6 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: model.name || "Unknown" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + width: parent.width - 30 + } + } + + MouseArea { + id: resultMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root._internalChange = true + const selectedName = model.name + const selectedQuery = model.query + locationInput.text = selectedName + root.locationSelected(selectedName, selectedQuery) + root.resetSearchState() + locationInput.setFocus(false) + root._internalChange = false + } + } + } + } + + StyledText { + anchors.centerIn: parent + text: root.isLoading ? "Searching..." : "No locations found" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + visible: searchResultsList.count === 0 && locationInput.text.length > 2 + } + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankOSD.qml b/quickshell/.config/quickshell/Widgets/DankOSD.qml new file mode 100644 index 0000000..08f95f5 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankOSD.qml @@ -0,0 +1,155 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Common + +PanelWindow { + id: root + + property alias content: contentLoader.sourceComponent + property alias contentLoader: contentLoader + property var modelData + property bool shouldBeVisible: false + property int autoHideInterval: 2000 + property bool enableMouseInteraction: false + property real osdWidth: Theme.iconSize + Theme.spacingS * 2 + property real osdHeight: Theme.iconSize + Theme.spacingS * 2 + property int animationDuration: Theme.mediumDuration + property var animationEasing: Theme.emphasizedEasing + + signal osdShown + signal osdHidden + + function show() { + closeTimer.stop() + shouldBeVisible = true + visible = true + hideTimer.restart() + osdShown() + } + + function hide() { + shouldBeVisible = false + closeTimer.restart() + } + + function updateHoverState() { + let isHovered = (enableMouseInteraction && mouseArea.containsMouse) || osdContainer.childHovered + if (enableMouseInteraction) { + if (isHovered) { + hideTimer.stop() + } else if (shouldBeVisible) { + hideTimer.restart() + } + } + } + + function setChildHovered(hovered) { + osdContainer.childHovered = hovered + updateHoverState() + } + + screen: modelData + visible: shouldBeVisible + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + Timer { + id: hideTimer + + interval: autoHideInterval + repeat: false + onTriggered: { + if (!enableMouseInteraction || !mouseArea.containsMouse) { + hide() + } else { + hideTimer.restart() + } + } + } + + Timer { + id: closeTimer + interval: animationDuration + 50 + onTriggered: { + if (!shouldBeVisible) { + visible = false + osdHidden() + } + } + } + + Rectangle { + id: osdContainer + + property bool childHovered: false + + width: osdWidth + height: osdHeight + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.spacingM + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + opacity: shouldBeVisible ? 1 : 0 + scale: shouldBeVisible ? 1 : 0.9 + layer.enabled: true + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: enableMouseInteraction + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + z: -1 + onContainsMouseChanged: updateHoverState() + } + + onChildHoveredChanged: updateHoverState() + + Loader { + id: contentLoader + anchors.fill: parent + active: root.visible + asynchronous: false + } + + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 4 + shadowBlur: 0.8 + shadowColor: Qt.rgba(0, 0, 0, 0.3) + } + + Behavior on opacity { + NumberAnimation { + duration: animationDuration + easing.type: animationEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: animationDuration + easing.type: animationEasing + } + } + } + + mask: Region { + item: osdContainer + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankPopout.qml b/quickshell/.config/quickshell/Widgets/DankPopout.qml new file mode 100644 index 0000000..1fcf7cb --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankPopout.qml @@ -0,0 +1,150 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Common + +PanelWindow { + id: root + + WlrLayershell.namespace: "quickshell:popout" + + property alias content: contentLoader.sourceComponent + property alias contentLoader: contentLoader + property real popupWidth: 400 + property real popupHeight: 300 + property real triggerX: 0 + property real triggerY: 0 + property real triggerWidth: 40 + property string positioning: "center" + property int animationDuration: Theme.mediumDuration + property var animationEasing: Theme.emphasizedEasing + property bool shouldBeVisible: false + + signal opened + signal popoutClosed + signal backgroundClicked + + function open() { + closeTimer.stop() + shouldBeVisible = true + visible = true + opened() + } + + function close() { + shouldBeVisible = false + closeTimer.restart() + } + + function toggle() { + if (shouldBeVisible) + close() + else + open() + } + + Timer { + id: closeTimer + interval: animationDuration + 50 + onTriggered: { + if (!shouldBeVisible) { + visible = false + popoutClosed() + } + } + } + + color: "transparent" + WlrLayershell.layer: WlrLayershell.Top // if set to overlay -> virtual keyboards can be stuck under popup + WlrLayershell.exclusiveZone: -1 + + // WlrLayershell.keyboardFocus should be set to Exclusive, + // if popup contains input fields and does NOT create new popups/modals + // with input fields. + // With OnDemand virtual keyboards can't send input to popup + // If set to Exclusive AND this popup creates other popups, that also have + // input fields -> they can't get keyboard focus, because the parent popup + // already took the lock + WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + anchors { + top: true + left: true + right: true + bottom: true + } + + MouseArea { + anchors.fill: parent + enabled: shouldBeVisible + onClicked: mouse => { + var localPos = mapToItem(contentContainer, mouse.x, mouse.y) + if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) { + backgroundClicked() + close() + } + } + } + + Item { + id: contentContainer + + readonly property real screenWidth: root.screen ? root.screen.width : 1920 + readonly property real screenHeight: root.screen ? root.screen.height : 1080 + readonly property real calculatedX: { + if (positioning === "center") { + var centerX = triggerX + (triggerWidth / 2) - (popupWidth / 2) + return Math.max(Theme.spacingM, Math.min(screenWidth - popupWidth - Theme.spacingM, centerX)) + } else if (positioning === "left") { + return Math.max(Theme.spacingM, triggerX) + } else if (positioning === "right") { + return Math.min(screenWidth - popupWidth - Theme.spacingM, triggerX + triggerWidth - popupWidth) + } + return triggerX + } + readonly property real calculatedY: triggerY + + width: popupWidth + height: popupHeight + x: calculatedX + y: calculatedY + opacity: shouldBeVisible ? 1 : 0 + scale: shouldBeVisible ? 1 : 0.9 + + Behavior on opacity { + NumberAnimation { + duration: animationDuration + easing.type: animationEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: animationDuration + easing.type: animationEasing + } + } + + Loader { + id: contentLoader + anchors.fill: parent + active: root.visible + asynchronous: false + } + + Item { + anchors.fill: parent + focus: true + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + close() + event.accepted = true + } + } + Component.onCompleted: forceActiveFocus() + onVisibleChanged: if (visible) + forceActiveFocus() + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankScrollbar.qml b/quickshell/.config/quickshell/Widgets/DankScrollbar.qml new file mode 100644 index 0000000..2a19e4e --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankScrollbar.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Controls +import qs.Common + +ScrollBar { + id: scrollbar + + property bool _scrollBarActive: false + property alias hideTimer: hideScrollBarTimer + property bool _isParentMoving: parent && (parent.moving || parent.flicking || parent.isMomentumActive) + property bool _shouldShow: pressed || hovered || active || _isParentMoving || _scrollBarActive + + policy: (parent && parent.contentHeight > parent.height) ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff + minimumSize: 0.08 + implicitWidth: 10 + interactive: true + hoverEnabled: true + z: 1000 + opacity: (policy !== ScrollBar.AlwaysOff && _shouldShow) ? 1.0 : 0.0 + visible: policy !== ScrollBar.AlwaysOff + + Behavior on opacity { + NumberAnimation { + duration: 160 + easing.type: Easing.OutQuad + } + } + + contentItem: Rectangle { + implicitWidth: 6 + radius: width / 2 + color: scrollbar.pressed ? Theme.primary : scrollbar._shouldShow ? Theme.outline : Theme.outlineMedium + opacity: scrollbar.pressed ? 1.0 : scrollbar._shouldShow ? 1.0 : 0.6 + } + + background: Item {} + + Timer { + id: hideScrollBarTimer + interval: 1200 + onTriggered: scrollbar._scrollBarActive = false + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankSlider.qml b/quickshell/.config/quickshell/Widgets/DankSlider.qml new file mode 100644 index 0000000..529d6f6 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankSlider.qml @@ -0,0 +1,209 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Item { + id: slider + + property int value: 50 + property int minimum: 0 + property int maximum: 100 + property string leftIcon: "" + property string rightIcon: "" + property bool enabled: true + property string unit: "%" + property bool showValue: true + property bool isDragging: false + property bool wheelEnabled: true + readonly property bool containsMouse: sliderMouseArea.containsMouse + + signal sliderValueChanged(int newValue) + signal sliderDragFinished(int finalValue) + + height: 40 + + function updateValueFromPosition(x) { + let ratio = Math.max(0, Math.min(1, (x - sliderHandle.width / 2) / (sliderTrack.width - sliderHandle.width))) + let newValue = Math.round(minimum + ratio * (maximum - minimum)) + if (newValue !== value) { + value = newValue + sliderValueChanged(newValue) + } + } + + Row { + anchors.centerIn: parent + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: slider.leftIcon + size: Theme.iconSize + color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + visible: slider.leftIcon.length > 0 + } + + StyledRect { + id: sliderTrack + + property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0 + property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0 + + width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0)) + height: 6 + radius: 3 + color: slider.enabled ? Theme.surfaceVariantAlpha : Theme.surfaceLight + anchors.verticalCenter: parent.verticalCenter + + StyledRect { + id: sliderFill + + width: (parent.width - sliderHandle.width) * ((slider.value - slider.minimum) / (slider.maximum - slider.minimum)) + sliderHandle.width + height: parent.height + radius: parent.radius + color: slider.enabled ? Theme.primary : Theme.surfaceVariantText + + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + StyledRect { + id: sliderHandle + + width: 18 + height: 18 + radius: 9 + color: slider.enabled ? Theme.primary : Theme.surfaceVariantText + border.color: slider.enabled ? Qt.lighter(Theme.primary, 1.3) : Qt.lighter(Theme.surfaceVariantText, 1.3) + border.width: 2 + x: sliderFill.width - width + anchors.verticalCenter: parent.verticalCenter + scale: sliderMouseArea.containsMouse || sliderMouseArea.pressed ? 1.2 : 1 + + StyledRect { + anchors.centerIn: parent + width: parent.width + 4 + height: parent.height + 4 + radius: width / 2 + color: "transparent" + border.color: Theme.primarySelected + border.width: 2 + visible: sliderMouseArea.containsMouse && slider.enabled + } + + StyledRect { + id: valueTooltip + + width: tooltipText.contentWidth + Theme.spacingS * 2 + height: tooltipText.contentHeight + Theme.spacingXS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + visible: (sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue) + opacity: visible ? 1 : 0 + + StyledText { + id: tooltipText + + text: slider.value + slider.unit + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.centerIn: parent + font.hintingPreference: Font.PreferFullHinting + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + Item { + id: sliderContainer + + anchors.fill: parent + + MouseArea { + id: sliderMouseArea + + property bool isDragging: false + + anchors.fill: parent + anchors.topMargin: -10 + anchors.bottomMargin: -10 + hoverEnabled: true + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: slider.enabled + preventStealing: true + acceptedButtons: Qt.LeftButton + onWheel: wheelEvent => { + if (!slider.wheelEnabled) { + wheelEvent.accepted = false + return + } + let step = Math.max(1, (maximum - minimum) / 20) + let newValue = wheelEvent.angleDelta.y > 0 ? Math.min(maximum, value + step) : Math.max(minimum, value - step) + newValue = Math.round(newValue) + if (newValue !== value) { + value = newValue + sliderValueChanged(newValue) + } + wheelEvent.accepted = true + } + onPressed: mouse => { + if (slider.enabled) { + slider.isDragging = true + sliderMouseArea.isDragging = true + updateValueFromPosition(mouse.x) + } + } + onReleased: { + if (slider.enabled) { + slider.isDragging = false + sliderMouseArea.isDragging = false + slider.sliderDragFinished(slider.value) + } + } + onPositionChanged: mouse => { + if (pressed && slider.isDragging && slider.enabled) { + updateValueFromPosition(mouse.x) + } + } + onClicked: mouse => { + if (slider.enabled && !slider.isDragging) { + updateValueFromPosition(mouse.x) + } + } + } + } + } + + DankIcon { + name: slider.rightIcon + size: Theme.iconSize + color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + visible: slider.rightIcon.length > 0 + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankTabBar.qml b/quickshell/.config/quickshell/Widgets/DankTabBar.qml new file mode 100644 index 0000000..bfca6e1 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankTabBar.qml @@ -0,0 +1,253 @@ +import QtQuick +import qs.Common +import qs.Widgets + +FocusScope { + id: tabBar + + property alias model: tabRepeater.model + property int currentIndex: 0 + property int spacing: Theme.spacingL + property int tabHeight: 56 + property bool showIcons: true + property bool equalWidthTabs: true + property bool enableArrowNavigation: true + property Item nextFocusTarget: null + property Item previousFocusTarget: null + + signal tabClicked(int index) + signal actionTriggered(int index) + + focus: false + activeFocusOnTab: true + height: tabHeight + + KeyNavigation.tab: nextFocusTarget + KeyNavigation.down: nextFocusTarget + KeyNavigation.backtab: previousFocusTarget + KeyNavigation.up: previousFocusTarget + + Keys.onPressed: (event) => { + if (!tabBar.activeFocus || tabRepeater.count === 0) + return + + function findSelectableIndex(startIndex, step) { + let idx = startIndex + for (let i = 0; i < tabRepeater.count; i++) { + idx = (idx + step + tabRepeater.count) % tabRepeater.count + const item = tabRepeater.itemAt(idx) + if (item && !item.isAction) + return idx + } + return -1 + } + + const goToIndex = (nextIndex) => { + if (nextIndex >= 0 && nextIndex !== tabBar.currentIndex) { + tabBar.currentIndex = nextIndex + tabBar.tabClicked(nextIndex) + } + } + + const resolveTarget = (item) => { + if (!item) + return null + + if (item.focusTarget) + return resolveTarget(item.focusTarget) + + return item + } + + const focusItem = (item) => { + const target = resolveTarget(item) + if (!target) + return false + + if (target.requestFocus) { + Qt.callLater(() => target.requestFocus()) + return true + } + + if (target.forceActiveFocus) { + Qt.callLater(() => target.forceActiveFocus()) + return true + } + + return false + } + + if (event.key === Qt.Key_Right && tabBar.enableArrowNavigation) { + const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : -1 + const nextIndex = findSelectableIndex(baseIndex, 1) + if (nextIndex >= 0) { + goToIndex(nextIndex) + event.accepted = true + } + } else if (event.key === Qt.Key_Left && tabBar.enableArrowNavigation) { + const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : 0 + const nextIndex = findSelectableIndex(baseIndex, -1) + if (nextIndex >= 0) { + goToIndex(nextIndex) + event.accepted = true + } + } else if (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier)) { + if (focusItem(tabBar.previousFocusTarget)) { + event.accepted = true + } + } else if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down) { + if (focusItem(tabBar.nextFocusTarget)) { + event.accepted = true + } + } else if (event.key === Qt.Key_Up) { + if (focusItem(tabBar.previousFocusTarget)) { + event.accepted = true + } + } + } + + Row { + id: tabRow + anchors.fill: parent + spacing: tabBar.spacing + + Repeater { + id: tabRepeater + + Item { + id: tabItem + property bool isAction: modelData && modelData.isAction === true + property bool isActive: !isAction && tabBar.currentIndex === index + property bool hasIcon: tabBar.showIcons && modelData && modelData.icon && modelData.icon.length > 0 + property bool hasText: modelData && modelData.text && modelData.text.length > 0 + + width: tabBar.equalWidthTabs ? (tabBar.width - tabBar.spacing * Math.max(0, tabRepeater.count - 1)) / Math.max(1, tabRepeater.count) : Math.max(contentCol.implicitWidth + Theme.spacingXL, 64) + height: tabBar.tabHeight + + Column { + id: contentCol + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: modelData.icon || "" + anchors.horizontalCenter: parent.horizontalCenter + size: Theme.iconSize + color: tabItem.isActive ? Theme.primary : Theme.surfaceText + visible: hasIcon + } + + StyledText { + text: modelData.text || "" + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Theme.fontSizeMedium + color: tabItem.isActive ? Theme.primary : Theme.surfaceText + font.weight: tabItem.isActive ? Font.Medium : Font.Normal + visible: hasText + } + } + + Rectangle { + id: stateLayer + anchors.fill: parent + color: Theme.surfaceTint + opacity: tabArea.pressed ? 0.12 : (tabArea.containsMouse ? 0.08 : 0) + visible: opacity > 0 + radius: Theme.cornerRadius + Behavior on opacity { NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } } + } + + MouseArea { + id: tabArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (tabItem.isAction) { + tabBar.actionTriggered(index) + } else { + tabBar.tabClicked(index) + } + } + } + + } + } + } + + Rectangle { + id: indicator + y: parent.height + 7 + height: 3 + width: 60 + topLeftRadius: Theme.cornerRadius + topRightRadius: Theme.cornerRadius + bottomLeftRadius: 0 + bottomRightRadius: 0 + color: Theme.primary + visible: false + + property bool animationEnabled: false + property bool initialSetupComplete: false + + Behavior on x { + enabled: indicator.animationEnabled + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + + Behavior on width { + enabled: indicator.animationEnabled + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + } + + Rectangle { + width: parent.width + height: 1 + y: parent.height + 10 + color: Theme.outlineStrong + } + + function updateIndicator(enableAnimation = true) { + if (tabRepeater.count === 0 || currentIndex < 0 || currentIndex >= tabRepeater.count) { + return + } + + const item = tabRepeater.itemAt(currentIndex) + if (!item || item.isAction) { + return + } + + const tabPos = item.mapToItem(tabBar, 0, 0) + const tabCenterX = tabPos.x + item.width / 2 + const indicatorWidth = 60 + + if (tabPos.x < 10 && currentIndex > 0) { + Qt.callLater(() => updateIndicator(enableAnimation)) + return + } + + indicator.animationEnabled = enableAnimation + indicator.width = indicatorWidth + indicator.x = tabCenterX - indicatorWidth / 2 + indicator.visible = true + } + + onCurrentIndexChanged: { + if (indicator.initialSetupComplete) { + Qt.callLater(() => updateIndicator(true)) + } else { + Qt.callLater(() => { + updateIndicator(false) + indicator.initialSetupComplete = true + }) + } + } + onWidthChanged: Qt.callLater(() => updateIndicator(indicator.initialSetupComplete)) +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/Widgets/DankTextField.qml b/quickshell/.config/quickshell/Widgets/DankTextField.qml new file mode 100644 index 0000000..7216521 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankTextField.qml @@ -0,0 +1,178 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +StyledRect { + id: root + + property alias text: textInput.text + property string placeholderText: "" + property alias font: textInput.font + property alias textColor: textInput.color + property alias enabled: textInput.enabled + property alias echoMode: textInput.echoMode + property alias validator: textInput.validator + property alias maximumLength: textInput.maximumLength + property string leftIconName: "" + property int leftIconSize: Theme.iconSize + property color leftIconColor: Theme.surfaceVariantText + property color leftIconFocusedColor: Theme.primary + property bool showClearButton: false + property color backgroundColor: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9) + property color focusedBorderColor: Theme.primary + property color normalBorderColor: Theme.outlineStrong + property color placeholderColor: Theme.outlineButton + property int borderWidth: 1 + property int focusedBorderWidth: 2 + property real cornerRadius: Theme.cornerRadius + readonly property real leftPadding: Theme.spacingM + (leftIconName ? leftIconSize + Theme.spacingM : 0) + readonly property real rightPadding: Theme.spacingM + (showClearButton && text.length > 0 ? 24 + Theme.spacingM : 0) + property real topPadding: Theme.spacingM + property real bottomPadding: Theme.spacingM + property bool ignoreLeftRightKeys: false + property var keyForwardTargets: [] + + signal textEdited + signal editingFinished + signal accepted + signal focusStateChanged(bool hasFocus) + + function getActiveFocus() { + return textInput.activeFocus + } + function setFocus(value) { + textInput.focus = value + } + function forceActiveFocus() { + textInput.forceActiveFocus() + } + function selectAll() { + textInput.selectAll() + } + function clear() { + textInput.clear() + } + function insertText(str) { + textInput.insert(textInput.cursorPosition, str) + } + + width: 200 + height: 48 + radius: cornerRadius + color: backgroundColor + border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor + border.width: textInput.activeFocus ? focusedBorderWidth : borderWidth + + DankIcon { + id: leftIcon + + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + name: leftIconName + size: leftIconSize + color: textInput.activeFocus ? leftIconFocusedColor : leftIconColor + visible: leftIconName !== "" + } + + TextInput { + id: textInput + + anchors.fill: parent + anchors.leftMargin: root.leftPadding + anchors.rightMargin: root.rightPadding + anchors.topMargin: root.topPadding + anchors.bottomMargin: root.bottomPadding + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + verticalAlignment: TextInput.AlignVCenter + selectByMouse: !root.ignoreLeftRightKeys + clip: true + onTextChanged: root.textEdited() + onEditingFinished: root.editingFinished() + onAccepted: root.accepted() + onActiveFocusChanged: root.focusStateChanged(activeFocus) + Keys.forwardTo: root.ignoreLeftRightKeys ? root.keyForwardTargets : [] + Keys.onLeftPressed: event => { + if (root.ignoreLeftRightKeys) { + event.accepted = true + } else { + // Allow normal TextInput cursor movement + event.accepted = false + } + } + Keys.onRightPressed: event => { + if (root.ignoreLeftRightKeys) { + event.accepted = true + } else { + // Allow normal TextInput cursor movement + event.accepted = false + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + } + + StyledRect { + id: clearButton + + width: 24 + height: 24 + radius: 12 + color: clearArea.containsMouse ? Theme.outlineStrong : "transparent" + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + visible: showClearButton && text.length > 0 + + DankIcon { + anchors.centerIn: parent + name: "close" + size: 16 + color: clearArea.containsMouse ? Theme.outline : Theme.surfaceVariantText + } + + MouseArea { + id: clearArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + textInput.text = "" + } + } + } + + StyledText { + id: placeholderLabel + + anchors.fill: textInput + text: root.placeholderText + font: textInput.font + color: placeholderColor + verticalAlignment: textInput.verticalAlignment + visible: textInput.text.length === 0 && !textInput.activeFocus + elide: Text.ElideRight + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/DankToggle.qml b/quickshell/.config/quickshell/Widgets/DankToggle.qml new file mode 100644 index 0000000..4b1ccfb --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/DankToggle.qml @@ -0,0 +1,120 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Item { + id: toggle + + property bool checked: false + property bool enabled: true + property bool toggling: false + property string text: "" + property string description: "" + property bool hideText: false + + signal clicked + signal toggled(bool checked) + + readonly property bool showText: text && !hideText + + width: showText ? parent.width : 48 + height: showText ? 60 : 24 + + function handleClick() { + if (!enabled) { + return + } + checked = !checked + clicked() + toggled(checked) + } + + StyledRect { + id: background + + anchors.fill: parent + radius: showText ? Theme.cornerRadius : 0 + color: showText ? Theme.surfaceHover : "transparent" + visible: showText + + StateLayer { + visible: showText + disabled: !toggle.enabled + stateColor: Theme.primary + cornerRadius: parent.radius + onClicked: toggle.handleClick() + } + } + + Row { + anchors.left: parent.left + anchors.right: toggleTrack.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingXS + visible: showText + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + StyledText { + text: toggle.text + font.pixelSize: Appearance.fontSize.normal + font.weight: Font.Medium + opacity: toggle.enabled ? 1 : 0.4 + } + + StyledText { + text: toggle.description + font.pixelSize: Appearance.fontSize.small + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: Math.min(implicitWidth, toggle.width - 120) + visible: toggle.description.length > 0 + } + } + } + + StyledRect { + id: toggleTrack + + width: text ? 48 : parent.width + height: text ? 24 : parent.height + anchors.right: parent.right + anchors.rightMargin: text ? Theme.spacingM : 0 + anchors.verticalCenter: parent.verticalCenter + radius: height / 2 + color: (checked && enabled) ? Theme.primary : Theme.surfaceVariantAlpha + opacity: toggling ? 0.6 : (enabled ? 1 : 0.4) + + StyledRect { + id: toggleHandle + + width: 20 + height: 20 + radius: 10 + color: Theme.surface + anchors.verticalCenter: parent.verticalCenter + x: (checked && enabled) ? parent.width - width - 2 : 2 + border.color: Qt.rgba(0, 0, 0, 0.1) + border.width: 1 + + Behavior on x { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + } + } + } + + StateLayer { + disabled: !toggle.enabled + stateColor: Theme.primary + cornerRadius: parent.radius + onClicked: toggle.handleClick() + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/StateLayer.qml b/quickshell/.config/quickshell/Widgets/StateLayer.qml new file mode 100644 index 0000000..9e2e959 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/StateLayer.qml @@ -0,0 +1,22 @@ +import QtQuick +import qs.Common + +MouseArea { + id: root + + property bool disabled: false + property color stateColor: Theme.surfaceText + property real cornerRadius: parent && parent.radius !== undefined ? parent.radius : Theme.cornerRadius + + readonly property real stateOpacity: disabled ? 0 : pressed ? 0.12 : containsMouse ? 0.08 : 0 + + anchors.fill: parent + cursorShape: disabled ? undefined : Qt.PointingHandCursor + hoverEnabled: true + + Rectangle { + anchors.fill: parent + radius: root.cornerRadius + color: Qt.rgba(stateColor.r, stateColor.g, stateColor.b, stateOpacity) + } +} diff --git a/quickshell/.config/quickshell/Widgets/StyledRect.qml b/quickshell/.config/quickshell/Widgets/StyledRect.qml new file mode 100644 index 0000000..7a99d83 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/StyledRect.qml @@ -0,0 +1,37 @@ +import QtQuick +import qs.Common + +Rectangle { + color: "transparent" + radius: Appearance.rounding.normal + + readonly property var standardAnimation: { + "duration": Appearance.anim.durations.normal, + "easing.type": Easing.BezierSpline, + "easing.bezierCurve": Appearance.anim.curves.standard + } + + Behavior on color { + ColorAnimation { + duration: standardAnimation.duration + easing.type: standardAnimation["easing.type"] + easing.bezierCurve: standardAnimation["easing.bezierCurve"] + } + } + + Behavior on radius { + NumberAnimation { + duration: standardAnimation.duration + easing.type: standardAnimation["easing.type"] + easing.bezierCurve: standardAnimation["easing.bezierCurve"] + } + } + + Behavior on opacity { + NumberAnimation { + duration: standardAnimation.duration + easing.type: standardAnimation["easing.type"] + easing.bezierCurve: standardAnimation["easing.bezierCurve"] + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/StyledText.qml b/quickshell/.config/quickshell/Widgets/StyledText.qml new file mode 100644 index 0000000..9a96d38 --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/StyledText.qml @@ -0,0 +1,51 @@ +import QtQuick +import qs.Common +import qs.Services + +Text { + property bool isMonospace: false + + readonly property string resolvedFontFamily: { + const requestedFont = isMonospace ? SettingsData.monoFontFamily : SettingsData.fontFamily + const defaultFont = isMonospace ? SettingsData.defaultMonoFontFamily : SettingsData.defaultFontFamily + + if (requestedFont === defaultFont) { + const availableFonts = Qt.fontFamilies() + if (!availableFonts.includes(requestedFont)) { + return isMonospace ? "Monospace" : "DejaVu Sans" + } + } + return requestedFont + } + + readonly property var standardAnimation: { + "duration": Appearance.anim.durations.normal, + "easing.type": Easing.BezierSpline, + "easing.bezierCurve": Appearance.anim.curves.standard + } + + color: Theme.surfaceText + font.pixelSize: Appearance.fontSize.normal + font.family: resolvedFontFamily + font.weight: SettingsData.fontWeight + wrapMode: Text.WordWrap + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + antialiasing: true + + Behavior on color { + ColorAnimation { + duration: standardAnimation.duration + easing.type: standardAnimation["easing.type"] + easing.bezierCurve: standardAnimation["easing.bezierCurve"] + } + } + + Behavior on opacity { + NumberAnimation { + duration: standardAnimation.duration + easing.type: standardAnimation["easing.type"] + easing.bezierCurve: standardAnimation["easing.bezierCurve"] + } + } +} diff --git a/quickshell/.config/quickshell/Widgets/SystemLogo.qml b/quickshell/.config/quickshell/Widgets/SystemLogo.qml new file mode 100644 index 0000000..7519a6a --- /dev/null +++ b/quickshell/.config/quickshell/Widgets/SystemLogo.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import qs.Common + +IconImage { + property string colorOverride: "" + property real brightnessOverride: 0.5 + property real contrastOverride: 1 + + readonly property bool hasColorOverride: colorOverride !== "" + + smooth: true + asynchronous: true + layer.enabled: hasColorOverride + + Process { + running: true + command: ["sh", "-c", ". /etc/os-release && echo $LOGO"] + + stdout: StdioCollector { + onStreamFinished: () => { + source = Quickshell.iconPath(text.trim(), true) + } + } + } + + layer.effect: MultiEffect { + colorization: 1 + colorizationColor: colorOverride + brightness: brightnessOverride + contrast: contrastOverride + } +} diff --git a/quickshell/.config/quickshell/alejandra.toml b/quickshell/.config/quickshell/alejandra.toml new file mode 100644 index 0000000..636aa80 --- /dev/null +++ b/quickshell/.config/quickshell/alejandra.toml @@ -0,0 +1 @@ +indentation = "FourSpaces" diff --git a/quickshell/.config/quickshell/assets/dank.svg b/quickshell/.config/quickshell/assets/dank.svg new file mode 100644 index 0000000..a78609e --- /dev/null +++ b/quickshell/.config/quickshell/assets/dank.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickshell/.config/quickshell/assets/discord.svg b/quickshell/.config/quickshell/assets/discord.svg new file mode 100644 index 0000000..b636d15 --- /dev/null +++ b/quickshell/.config/quickshell/assets/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/quickshell/.config/quickshell/assets/hyprland.svg b/quickshell/.config/quickshell/assets/hyprland.svg new file mode 100644 index 0000000..ec049dd --- /dev/null +++ b/quickshell/.config/quickshell/assets/hyprland.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickshell/.config/quickshell/assets/matrix-logo-white.svg b/quickshell/.config/quickshell/assets/matrix-logo-white.svg new file mode 100644 index 0000000..900a5aa --- /dev/null +++ b/quickshell/.config/quickshell/assets/matrix-logo-white.svg @@ -0,0 +1,18 @@ + + + + matrix logo white + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickshell/.config/quickshell/assets/niri.svg b/quickshell/.config/quickshell/assets/niri.svg new file mode 100644 index 0000000..5bd2de0 --- /dev/null +++ b/quickshell/.config/quickshell/assets/niri.svg @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickshell/.config/quickshell/assets/reddit.svg b/quickshell/.config/quickshell/assets/reddit.svg new file mode 100644 index 0000000..114d425 --- /dev/null +++ b/quickshell/.config/quickshell/assets/reddit.svg @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/quickshell/.config/quickshell/docs/CUSTOM_THEMES.md b/quickshell/.config/quickshell/docs/CUSTOM_THEMES.md new file mode 100644 index 0000000..195b6a8 --- /dev/null +++ b/quickshell/.config/quickshell/docs/CUSTOM_THEMES.md @@ -0,0 +1,137 @@ +# Custom Themes + +This guide covers creating custom themes for DankMaterialShell. You can define your own color schemes by creating theme files that the shell can load. + +## Theme Structure + +Themes are defined using the same structure as the built-in themes. Each theme must specify a complete set of Material Design 3 colors that work together harmoniously. + +### Required Core Colors + +These are the essential colors that define your theme's appearance: + +```json +{ + "dark": { + "name": "Cyberpunk Electric Dark", + "primary": "#00FFCC", + "primaryText": "#000000", + "primaryContainer": "#00CC99", + "secondary": "#FF4DFF", + "surface": "#0F0F0F", + "surfaceText": "#E0FFE0", + "surfaceVariant": "#1F2F1F", + "surfaceVariantText": "#CCFFCC", + "surfaceTint": "#00FFCC", + "background": "#000000", + "backgroundText": "#F0FFF0", + "outline": "#80FF80", + "surfaceContainer": "#1A2B1A", + "surfaceContainerHigh": "#264026", + "error": "#FF0066", + "warning": "#CCFF00", + "info": "#00FFCC" + }, + "light": { + "name": "Cyberpunk Electric Light", + "primary": "#00B899", + "primaryText": "#FFFFFF", + "primaryContainer": "#66FFDD", + "secondary": "#CC00CC", + "surface": "#F0FFF0", + "surfaceText": "#1F2F1F", + "surfaceVariant": "#E6FFE6", + "surfaceVariantText": "#2D4D2D", + "surfaceTint": "#00B899", + "background": "#FFFFFF", + "backgroundText": "#000000", + "outline": "#4DCC4D", + "surfaceContainer": "#F5FFF5", + "surfaceContainerHigh": "#EBFFEB", + "error": "#B3004D", + "warning": "#99CC00", + "info": "#00B899" + } +} +``` + +You can define colors at the top level if you do not want "dark" and "light" variants. + +For example: + +```json +{ + "name": "Theme name", + "primary": "#eeeeee", + .... +} +``` + +## Example Themes + +There are example themes you can start from: + +- [Cyberpunk Electric](theme_cyberpunk_electric.json) - Neon green and magenta cyberpunk aesthetic +- [Hotline Miami](theme_hotline_miami.json) - Retro 80s inspired hot pink and blue +- [Miami Vice](theme_miami_vice.json) - Classic teal and pink vice aesthetic +- [Synthwave Electric](theme_synthwave_electric.json) - Electric purple and cyan synthwave vibes + +### Color Definitions + +**Primary Colors** +- `primary` - Main accent color used for buttons, highlights, and active states +- `primaryText` - Text color that contrasts well with primary background +- `primaryContainer` - Darker/lighter variant of primary for containers + +**Secondary Colors** +- `secondary` - Supporting accent color for variety and hierarchy +- `surfaceTint` - Tint color applied to surfaces, usually derived from primary + +**Surface Colors** +- `surface` - Default surface color for cards, panels, etc. +- `surfaceText` - Primary text color on surface backgrounds +- `surfaceVariant` - Alternate surface color for subtle differentiation +- `surfaceVariantText` - Text color for surfaceVariant backgrounds +- `surfaceContainer` - Container surface color, slightly different from surface +- `surfaceContainerHigh` - Elevated container color for layered interfaces + +**Background Colors** +- `background` - Main background color for the entire interface +- `backgroundText` - Text color for background areas + +**Outline Colors** +- `outline` - Border and divider color for subtle boundaries + +## Optional Properties + +While the core colors above are required, you can also customize these optional properties: + +### Semantic Colors +```json +{ + "error": "#f44336", + "warning": "#ff9800", + "info": "#2196f3" +} +``` + +- `error` - Used for error states, delete buttons, and critical warnings +- `warning` - Used for warning states and caution indicators +- `info` - Used for informational states and neutral indicators + +## Setting Custom Theme + +In settings -> Theme & Colors you can choose "Custom" to choose a path to your theme. + +You can also edit `~/.config/DankMaterialShell/settings.json` manually + +```json +{ + "currentThemeName": "custom", + "customThemeFile": "/path/to/mytheme.json" +} +``` + +### Reactivity + +Editing the custom theme file will auto-update the shell if it's the current theme. \ No newline at end of file diff --git a/quickshell/.config/quickshell/docs/IPC.md b/quickshell/.config/quickshell/docs/IPC.md new file mode 100644 index 0000000..301dc36 --- /dev/null +++ b/quickshell/.config/quickshell/docs/IPC.md @@ -0,0 +1,578 @@ +# IPC Commands Reference + +DankMaterialShell provides comprehensive IPC (Inter-Process Communication) functionality that allows external control of the shell through command-line commands. All IPC commands follow the format: + +```bash +dms ipc call [parameters...] +``` + +## Target: `audio` + +Audio system control and information. + +### Functions + +**`setvolume `** +- Set output volume to specific percentage (0-100) +- Returns: Confirmation message + +**`increment `** +- Increase output volume by step amount +- Parameters: `step` - Volume increase amount (default: 5) +- Returns: Confirmation message + +**`decrement `** +- Decrease output volume by step amount +- Parameters: `step` - Volume decrease amount (default: 5) +- Returns: Confirmation message + +**`mute`** +- Toggle output device mute state +- Returns: Current mute status + +**`setmic `** +- Set input (microphone) volume to specific percentage (0-100) +- Returns: Confirmation message + +**`micmute`** +- Toggle input device mute state +- Returns: Current mic mute status + +**`status`** +- Get current audio status for both input and output devices +- Returns: Volume levels and mute states + +### Examples +```bash +dms ipc call audio setvolume 50 +dms ipc call audio increment 10 +dms ipc call audio mute +``` + +## Target: `brightness` + +Display brightness control for internal and external displays. + +### Functions + +**`set [device]`** +- Set brightness to specific percentage (1-100) +- Parameters: + - `percentage` - Brightness level (1-100) + - `device` - Optional device name (empty string for default) +- Returns: Confirmation with device info + +**`increment [device]`** +- Increase brightness by step amount +- Parameters: + - `step` - Brightness increase amount + - `device` - Optional device name (empty string for default) +- Returns: Confirmation with new brightness level + +**`decrement [device]`** +- Decrease brightness by step amount +- Parameters: + - `step` - Brightness decrease amount + - `device` - Optional device name (empty string for default) +- Returns: Confirmation with new brightness level + +**`status`** +- Get current brightness status +- Returns: Current device and brightness level + +**`list`** +- List all available brightness devices +- Returns: Device names and classes + +### Examples +```bash +dms ipc call brightness set 80 +dms ipc call brightness increment 10 "" +dms ipc call brightness decrement 5 "intel_backlight" +``` + +## Target: `night` + +Night mode (gamma/color temperature) control. + +### Functions + +**`toggle`** +- Toggle night mode on/off +- Returns: Current night mode state + +**`enable`** +- Enable night mode +- Returns: Confirmation message + +**`disable`** +- Disable night mode +- Returns: Confirmation message + +**`status`** +- Get current night mode status +- Returns: Night mode enabled/disabled state + +**`temperature [value]`** +- Get or set night mode color temperature +- Parameters: + - `value` - Optional temperature in Kelvin (2500-6000, steps of 500) +- Returns: Current or newly set temperature + +**`automation [mode]`** +- Get or set night mode automation mode +- Parameters: + - `mode` - Optional automation mode: "manual", "time", or "location" +- Returns: Current or newly set automation mode + +**`schedule `** +- Set time-based automation schedule +- Parameters: + - `start` - Start time in HH:MM format (e.g., "20:00") + - `end` - End time in HH:MM format (e.g., "06:00") +- Returns: Confirmation of schedule update + +**`location `** +- Set manual coordinates for location-based automation +- Parameters: + - `latitude` - Latitude coordinate (e.g., 40.7128) + - `longitude` - Longitude coordinate (e.g., -74.0060) +- Returns: Confirmation of coordinates update + +### Examples +```bash +dms ipc call night toggle +dms ipc call night temperature 4000 +dms ipc call night automation time +dms ipc call night schedule 20:00 06:00 +dms ipc call night location 40.7128 -74.0060 +``` + +## Target: `mpris` + +Media player control via MPRIS interface. + +### Functions + +**`list`** +- List all available media players +- Returns: Player names + +**`play`** +- Start playback on active player +- Returns: Nothing + +**`pause`** +- Pause playback on active player +- Returns: Nothing + +**`playPause`** +- Toggle play/pause state on active player +- Returns: Nothing + +**`previous`** +- Skip to previous track +- Returns: Nothing + +**`next`** +- Skip to next track +- Returns: Nothing + +**`stop`** +- Stop playback on active player +- Returns: Nothing + +### Examples +```bash +dms ipc call mpris playPause +dms ipc call mpris next +``` + +## Target: `lock` + +Screen lock control and status. + +### Functions + +**`lock`** +- Lock the screen immediately +- Returns: Nothing + +**`demo`** +- Show lock screen in demo mode (doesn't actually lock) +- Returns: Nothing + +**`isLocked`** +- Check if screen is currently locked +- Returns: Boolean lock state + +### Examples +```bash +dms ipc call lock lock +dms ipc call lock isLocked +``` + +## Target: `inhibit` + +Idle inhibitor control to prevent automatic sleep/lock. + +### Functions + +**`toggle`** +- Toggle idle inhibit state +- Returns: Current inhibit state message + +**`enable`** +- Enable idle inhibit (prevent sleep/lock) +- Returns: Confirmation message + +**`disable`** +- Disable idle inhibit (allow sleep/lock) +- Returns: Confirmation message + +### Examples +```bash +dms ipc call inhibit toggle +dms ipc call inhibit enable +``` + +## Target: `wallpaper` + +Wallpaper management and retrieval with support for per-monitor configurations. + +### Legacy Functions (Global Wallpaper Mode) + +**`get`** +- Get current wallpaper path +- Returns: Full path to current wallpaper file, or error if per-monitor mode is enabled + +**`set `** +- Set wallpaper to specified path +- Parameters: `path` - Absolute or relative path to image file +- Returns: Confirmation message or error if per-monitor mode is enabled + +**`clear`** +- Clear all wallpapers and disable per-monitor mode +- Returns: Success confirmation + +**`next`** +- Cycle to next wallpaper in the same directory +- Returns: Success confirmation or error if per-monitor mode is enabled + +**`prev`** +- Cycle to previous wallpaper in the same directory +- Returns: Success confirmation or error if per-monitor mode is enabled + +### Per-Monitor Functions + +**`getFor `** +- Get wallpaper path for specific monitor +- Parameters: `screenName` - Monitor name (e.g., "DP-2", "eDP-1") +- Returns: Full path to wallpaper file for the specified monitor + +**`setFor `** +- Set wallpaper for specific monitor (automatically enables per-monitor mode) +- Parameters: + - `screenName` - Monitor name (e.g., "DP-2", "eDP-1") + - `path` - Absolute or relative path to image file +- Returns: Success confirmation with monitor and path info + +**`nextFor `** +- Cycle to next wallpaper for specific monitor +- Parameters: `screenName` - Monitor name (e.g., "DP-2", "eDP-1") +- Returns: Success confirmation + +**`prevFor `** +- Cycle to previous wallpaper for specific monitor +- Parameters: `screenName` - Monitor name (e.g., "DP-2", "eDP-1") +- Returns: Success confirmation + +### Examples + +**Global wallpaper mode:** +```bash +dms ipc call wallpaper get +dms ipc call wallpaper set /path/to/image.jpg +dms ipc call wallpaper next +dms ipc call wallpaper clear +``` + +**Per-monitor wallpaper mode:** +```bash +# Set different wallpapers for each monitor +dms ipc call wallpaper setFor DP-2 /path/to/image1.jpg +dms ipc call wallpaper setFor eDP-1 /path/to/image2.jpg + +# Get wallpaper for specific monitor +dms ipc call wallpaper getFor DP-2 + +# Cycle wallpapers for specific monitor +dms ipc call wallpaper nextFor eDP-1 +dms ipc call wallpaper prevFor DP-2 + +# Clear all wallpapers and return to global mode +dms ipc call wallpaper clear +``` + +**Error handling:** +When per-monitor mode is enabled, legacy functions will return helpful error messages: +```bash +dms ipc call wallpaper get +# Returns: "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead." + +dms ipc call wallpaper set /path/to/image.jpg +# Returns: "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead." +``` + +## Target: `profile` + +User profile image management. + +### Functions + +**`getImage`** +- Get current profile image path +- Returns: Full path to profile image or empty string if not set + +**`setImage `** +- Set profile image to specified path +- Parameters: `path` - Absolute or relative path to image file +- Returns: Success message with path or error message + +**`clearImage`** +- Clear the profile image +- Returns: Success confirmation message + +### Examples +```bash +dms ipc call profile getImage +dms ipc call profile setImage /path/to/avatar.png +dms ipc call profile clearImage +``` + +## Target: `theme` + +Theme mode control (light/dark mode switching). + +### Functions + +**`toggle`** +- Toggle between light and dark themes +- Returns: Current theme mode ("light" or "dark") + +**`light`** +- Switch to light theme mode +- Returns: "light" + +**`dark`** +- Switch to dark theme mode +- Returns: "dark" + +**`getMode`** +- Returns current mode +- Returns: "dark" or "light" + +### Examples +```bash +dms ipc call theme toggle +dms ipc call theme dark +``` + +## Target: `bar` + +Top bar visibility control. + +### Functions + +**`reveal`** +- Show the top bar +- Returns: Success confirmation + +**`hide`** +- Hide the top bar +- Returns: Success confirmation + +**`toggle`** +- Toggle top bar visibility +- Returns: Success confirmation with current state + +**`status`** +- Get current top bar visibility status +- Returns: "visible" or "hidden" + +### Examples +```bash +dms ipc call bar toggle +dms ipc call bar hide +dms ipc call bar status +``` + +## Modal Controls + +These targets control various modal windows and overlays. + +### Target: `spotlight` +Application launcher modal control. + +**Functions:** +- `open` - Show the spotlight launcher +- `close` - Hide the spotlight launcher +- `toggle` - Toggle spotlight launcher visibility + +### Target: `clipboard` +Clipboard history modal control. + +**Functions:** +- `open` - Show clipboard history +- `close` - Hide clipboard history +- `toggle` - Toggle clipboard history visibility + +### Target: `notifications` +Notification center modal control. + +**Functions:** +- `open` - Show notification center +- `close` - Hide notification center +- `toggle` - Toggle notification center visibility + +### Target: `settings` +Settings modal control. + +**Functions:** +- `open` - Show settings modal +- `close` - Hide settings modal +- `toggle` - Toggle settings modal visibility + +### Target: `processlist` +System process list and performance modal control. + +**Functions:** +- `open` - Show process list modal +- `close` - Hide process list modal +- `toggle` - Toggle process list modal visibility + +### Target: `powermenu` +Power menu modal control for system power actions. + +**Functions:** +- `open` - Show power menu modal +- `close` - Hide power menu modal +- `toggle` - Toggle power menu modal visibility + +### Target: `notepad` +Notepad/scratchpad modal control for quick note-taking. + +**Functions:** +- `open` - Show notepad modal +- `close` - Hide notepad modal +- `toggle` - Toggle notepad modal visibility + +### Target: `dash` +Dashboard popup control with tab selection for overview, media, and weather information. + +**Functions:** +- `open [tab]` - Show dashboard popup with optional tab selection + - Parameters: `tab` - Optional tab to open: "" (default), "overview", "media", or "weather" + - Returns: Success/failure message +- `close` - Hide dashboard popup + - Returns: Success/failure message +- `toggle [tab]` - Toggle dashboard popup visibility with optional tab selection + - Parameters: `tab` - Optional tab to open when showing: "" (default), "overview", "media", or "weather" + - Returns: Success/failure message + +### Target: `file` +File browser controls for selecting wallpapers and profile images. + +**Functions:** +- `browse ` - Open file browser for specific file type + - Parameters: `type` - Either "wallpaper" or "profile" + - `wallpaper` - Opens wallpaper file browser in Pictures directory + - `profile` - Opens profile image file browser in Pictures directory + - Both browsers support common image formats (jpg, jpeg, png, bmp, gif, webp) + +### Modal Examples +```bash +# Open application launcher +dms ipc call spotlight toggle + +# Show clipboard history +dms ipc call clipboard open + +# Toggle notification center +dms ipc call notifications toggle + +# Show settings +dms ipc call settings open + +# Show system monitor +dms ipc call processlist toggle + +# Show power menu +dms ipc call powermenu toggle + +# Open notepad +dms ipc call notepad toggle + +# Show dashboard with specific tabs +dms ipc call dash open overview +dms ipc call dash toggle media +dms ipc call dash open weather + +# Open file browsers +dms ipc call file browse wallpaper +dms ipc call file browse profile +``` + +## Common Usage Patterns + +### Keybinding Integration + +These IPC commands are designed to be used with window manager keybindings. Example niri configuration: + +```kdl +binds { + Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "toggle"; } + Mod+V { spawn "qs" "-c" "dms" "ipc" "call" "clipboard" "toggle"; } + Mod+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggle"; } + Mod+X { spawn "qs" "-c" "dms" "ipc" "call" "powermenu" "toggle"; } + XF86AudioRaiseVolume { spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3"; } + XF86MonBrightnessUp { spawn "qs" "-c" "dms" "ipc" "call" "brightness" "increment" "5" ""; } +} +``` + +### Scripting and Automation + +IPC commands can be used in scripts for automation: + +```bash +#!/bin/bash +# Toggle night mode based on time of day +hour=$(date +%H) +if [ $hour -ge 20 ] || [ $hour -le 6 ]; then + dms ipc call night enable +else + dms ipc call night disable +fi +``` + +### Status Checking + +Many commands provide status information useful for scripts: + +```bash +# Check if screen is locked before performing action +if dms ipc call lock isLocked | grep -q "false"; then + # Perform action only if unlocked + dms ipc call notifications open +fi +``` + +## Return Values + +Most IPC functions return string messages indicating: +- Success confirmation with current values +- Error messages if operation fails +- Status information for query functions +- Empty/void return for simple action functions + +Functions that return void (like media controls) execute the action but don't provide feedback. Check the application state through other means if needed. \ No newline at end of file diff --git a/quickshell/.config/quickshell/docs/theme_cyberpunk_electric.json b/quickshell/.config/quickshell/docs/theme_cyberpunk_electric.json new file mode 100644 index 0000000..c244872 --- /dev/null +++ b/quickshell/.config/quickshell/docs/theme_cyberpunk_electric.json @@ -0,0 +1,42 @@ +{ + "dark": { + "name": "Cyberpunk Electric Dark", + "primary": "#00FFCC", + "primaryText": "#000000", + "primaryContainer": "#00CC99", + "secondary": "#FF4DFF", + "surface": "#0F0F0F", + "surfaceText": "#E0FFE0", + "surfaceVariant": "#1F2F1F", + "surfaceVariantText": "#CCFFCC", + "surfaceTint": "#00FFCC", + "background": "#000000", + "backgroundText": "#F0FFF0", + "outline": "#80FF80", + "surfaceContainer": "#1A2B1A", + "surfaceContainerHigh": "#264026", + "error": "#FF0066", + "warning": "#CCFF00", + "info": "#00FFCC" + }, + "light": { + "name": "Cyberpunk Electric Light", + "primary": "#00B899", + "primaryText": "#FFFFFF", + "primaryContainer": "#66FFDD", + "secondary": "#CC00CC", + "surface": "#F0FFF0", + "surfaceText": "#1F2F1F", + "surfaceVariant": "#E6FFE6", + "surfaceVariantText": "#2D4D2D", + "surfaceTint": "#00B899", + "background": "#FFFFFF", + "backgroundText": "#000000", + "outline": "#4DCC4D", + "surfaceContainer": "#F5FFF5", + "surfaceContainerHigh": "#EBFFEB", + "error": "#B3004D", + "warning": "#99CC00", + "info": "#00B899" + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/docs/theme_hotline_miami.json b/quickshell/.config/quickshell/docs/theme_hotline_miami.json new file mode 100644 index 0000000..60bad5e --- /dev/null +++ b/quickshell/.config/quickshell/docs/theme_hotline_miami.json @@ -0,0 +1,42 @@ +{ + "dark": { + "name": "Hotline Miami Dark", + "primary": "#FF0080", + "primaryText": "#FFFFFF", + "primaryContainer": "#CC0066", + "secondary": "#00FF80", + "surface": "#0D0D0D", + "surfaceText": "#F0F0F0", + "surfaceVariant": "#1A0F1A", + "surfaceVariantText": "#E0E0E0", + "surfaceTint": "#FF0080", + "background": "#000000", + "backgroundText": "#FFFFFF", + "outline": "#8000FF", + "surfaceContainer": "#1A0D1A", + "surfaceContainerHigh": "#260F26", + "error": "#FF4080", + "warning": "#FFFF00", + "info": "#00FF80" + }, + "light": { + "name": "Hotline Miami Light", + "primary": "#CC0066", + "primaryText": "#FFFFFF", + "primaryContainer": "#FF80B3", + "secondary": "#00CC66", + "surface": "#FFF0FF", + "surfaceText": "#1A0F1A", + "surfaceVariant": "#F0E6F0", + "surfaceVariantText": "#2D1A2D", + "surfaceTint": "#CC0066", + "background": "#FFFFFF", + "backgroundText": "#0D0D0D", + "outline": "#6600CC", + "surfaceContainer": "#F5F0F5", + "surfaceContainerHigh": "#EBE0EB", + "error": "#B30040", + "warning": "#B3B300", + "info": "#00B359" + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/docs/theme_miami_vice.json b/quickshell/.config/quickshell/docs/theme_miami_vice.json new file mode 100644 index 0000000..e3c07aa --- /dev/null +++ b/quickshell/.config/quickshell/docs/theme_miami_vice.json @@ -0,0 +1,42 @@ +{ + "dark": { + "name": "Miami Vice Dark", + "primary": "#00FFFF", + "primaryText": "#000000", + "primaryContainer": "#00CCCC", + "secondary": "#FF1493", + "surface": "#0A0A0F", + "surfaceText": "#E0E0FF", + "surfaceVariant": "#1A1A2E", + "surfaceVariantText": "#C0C0FF", + "surfaceTint": "#00FFFF", + "background": "#000008", + "backgroundText": "#F0F0FF", + "outline": "#4040FF", + "surfaceContainer": "#131325", + "surfaceContainerHigh": "#1F1F40", + "error": "#FF0080", + "warning": "#FFFF00", + "info": "#00FFFF" + }, + "light": { + "name": "Miami Vice Light", + "primary": "#0099CC", + "primaryText": "#FFFFFF", + "primaryContainer": "#00CCFF", + "secondary": "#CC0066", + "surface": "#F8F8FF", + "surfaceText": "#1A1A2E", + "surfaceVariant": "#E8E8FF", + "surfaceVariantText": "#2A2A4E", + "surfaceTint": "#0099CC", + "background": "#FFFFFF", + "backgroundText": "#0A0A2E", + "outline": "#6666CC", + "surfaceContainer": "#F0F0FF", + "surfaceContainerHigh": "#E0E0FF", + "error": "#CC0055", + "warning": "#CC9900", + "info": "#0099CC" + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/docs/theme_synthwave_electric.json b/quickshell/.config/quickshell/docs/theme_synthwave_electric.json new file mode 100644 index 0000000..5981786 --- /dev/null +++ b/quickshell/.config/quickshell/docs/theme_synthwave_electric.json @@ -0,0 +1,42 @@ +{ + "dark": { + "name": "Synthwave Electric Dark", + "primary": "#FF6600", + "primaryText": "#000000", + "primaryContainer": "#CC5200", + "secondary": "#0080FF", + "surface": "#0A0A15", + "surfaceText": "#E6F0FF", + "surfaceVariant": "#1A1A33", + "surfaceVariantText": "#CCE0FF", + "surfaceTint": "#FF6600", + "background": "#000008", + "backgroundText": "#F0F8FF", + "outline": "#4D80FF", + "surfaceContainer": "#151529", + "surfaceContainerHigh": "#212147", + "error": "#FF3366", + "warning": "#FFCC00", + "info": "#0080FF" + }, + "light": { + "name": "Synthwave Electric Light", + "primary": "#CC5200", + "primaryText": "#FFFFFF", + "primaryContainer": "#FF9966", + "secondary": "#0066CC", + "surface": "#FFF8F0", + "surfaceText": "#1A1A33", + "surfaceVariant": "#F0F0FF", + "surfaceVariantText": "#333366", + "surfaceTint": "#CC5200", + "background": "#FFFFFF", + "backgroundText": "#000008", + "outline": "#3366CC", + "surfaceContainer": "#F5F5FF", + "surfaceContainerHigh": "#EBEBFF", + "error": "#CC1A40", + "warning": "#CC9900", + "info": "#0066CC" + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen-config.toml b/quickshell/.config/quickshell/matugen-config.toml new file mode 100644 index 0000000..db9004d --- /dev/null +++ b/quickshell/.config/quickshell/matugen-config.toml @@ -0,0 +1,33 @@ +[config] + +[templates.gtk3] +input_path = './templates/gtk-colors.css' +output_path = '~/.config/gtk-3.0/dank-colors.css' + +[templates.gtk4] +input_path = './templates/gtk-colors.css' +output_path = '~/.config/gtk-4.0/dank-colors.css' + +[templates.qt5ct] +input_path = './templates/qtct-colors.conf' +output_path = '~/.config/qt5ct/colors/matugen.conf' + +[templates.qt6ct] +input_path = './templates/qtct-colors.conf' +output_path = '~/.config/qt6ct/colors/matugen.conf' + +[templates.kcolorscheme] +input_path = './templates/matugen-kcolorscheme.colors' +output_path = '~/.local/share/color-schemes/DankMatugen.colors' + +[templates.dgop] +input_path = './templates/dgop.json' +output_path = '~/.config/dgop/colors.json' + +[templates.niri] +input_path = './templates/niri-colors.kdl' +output_path = '~/.config/niri/dankshell-colors.kdl' + +[templates.ghostty] +input_path = './templates/ghostty-colors.conf' +output_path = '~/.config/ghostty/config-dankcolors' \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/configs/base.toml b/quickshell/.config/quickshell/matugen/configs/base.toml new file mode 100644 index 0000000..620eaca --- /dev/null +++ b/quickshell/.config/quickshell/matugen/configs/base.toml @@ -0,0 +1,13 @@ +[config] + +[templates.gtk3] +input_path = './matugen/templates/gtk-colors.css' +output_path = '~/.config/gtk-3.0/dank-colors.css' + +[templates.gtk4] +input_path = './matugen/templates/gtk-colors.css' +output_path = '~/.config/gtk-4.0/dank-colors.css' + +[templates.kcolorscheme] +input_path = './matugen/templates/matugen-kcolorscheme.colors' +output_path = '~/.local/share/color-schemes/DankMatugen.colors' \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/configs/dgop.toml b/quickshell/.config/quickshell/matugen/configs/dgop.toml new file mode 100644 index 0000000..6a39142 --- /dev/null +++ b/quickshell/.config/quickshell/matugen/configs/dgop.toml @@ -0,0 +1,3 @@ +[templates.dgop] +input_path = './matugen/templates/dgop.json' +output_path = '~/.config/dgop/colors.json' \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/configs/ghostty.toml b/quickshell/.config/quickshell/matugen/configs/ghostty.toml new file mode 100644 index 0000000..db2a1ff --- /dev/null +++ b/quickshell/.config/quickshell/matugen/configs/ghostty.toml @@ -0,0 +1,3 @@ +[templates.ghostty] +input_path = './matugen/templates/ghostty.conf' +output_path = '~/.config/ghostty/config-dankcolors' \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/configs/kitty.toml b/quickshell/.config/quickshell/matugen/configs/kitty.toml new file mode 100644 index 0000000..30fc1bb --- /dev/null +++ b/quickshell/.config/quickshell/matugen/configs/kitty.toml @@ -0,0 +1,3 @@ +[templates.kitty] +input_path = './matugen/templates/kitty.conf' +output_path = '~/.config/kitty/dank-theme.conf' \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/configs/niri.toml b/quickshell/.config/quickshell/matugen/configs/niri.toml new file mode 100644 index 0000000..ce899fe --- /dev/null +++ b/quickshell/.config/quickshell/matugen/configs/niri.toml @@ -0,0 +1,3 @@ +[templates.niri] +input_path = './matugen/templates/niri-colors.kdl' +output_path = '~/.config/niri/dankshell-colors.kdl' \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/configs/qt5ct.toml b/quickshell/.config/quickshell/matugen/configs/qt5ct.toml new file mode 100644 index 0000000..a61fd6d --- /dev/null +++ b/quickshell/.config/quickshell/matugen/configs/qt5ct.toml @@ -0,0 +1,3 @@ +[templates.qt5ct] +input_path = './matugen/templates/qtct-colors.conf' +output_path = '~/.config/qt5ct/colors/matugen.conf' \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/configs/qt6ct.toml b/quickshell/.config/quickshell/matugen/configs/qt6ct.toml new file mode 100644 index 0000000..5955839 --- /dev/null +++ b/quickshell/.config/quickshell/matugen/configs/qt6ct.toml @@ -0,0 +1,3 @@ +[templates.qt6ct] +input_path = './matugen/templates/qtct-colors.conf' +output_path = '~/.config/qt6ct/colors/matugen.conf' \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/dank16.py b/quickshell/.config/quickshell/matugen/dank16.py new file mode 100755 index 0000000..60f36a3 --- /dev/null +++ b/quickshell/.config/quickshell/matugen/dank16.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +import colorsys +import sys + +def hex_to_rgb(hex_color): + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i+2], 16)/255.0 for i in (0, 2, 4)) + +def rgb_to_hex(r, g, b): + r = max(0, min(1, r)) + g = max(0, min(1, g)) + b = max(0, min(1, b)) + return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" + +def luminance(hex_color): + r, g, b = hex_to_rgb(hex_color) + def srgb_to_linear(c): + return c/12.92 if c <= 0.03928 else ((c + 0.055)/1.055) ** 2.4 + return 0.2126 * srgb_to_linear(r) + 0.7152 * srgb_to_linear(g) + 0.0722 * srgb_to_linear(b) + +def contrast_ratio(hex_fg, hex_bg): + lum_fg = luminance(hex_fg) + lum_bg = luminance(hex_bg) + lighter = max(lum_fg, lum_bg) + darker = min(lum_fg, lum_bg) + return (lighter + 0.05) / (darker + 0.05) + +def ensure_contrast(hex_color, hex_bg, min_ratio=4.5, is_light_mode=False): + current_ratio = contrast_ratio(hex_color, hex_bg) + if current_ratio >= min_ratio: + return hex_color + + r, g, b = hex_to_rgb(hex_color) + h, s, v = colorsys.rgb_to_hsv(r, g, b) + + for step in range(1, 30): + delta = step * 0.02 + + if is_light_mode: + new_v = max(0, v - delta) + candidate = rgb_to_hex(*colorsys.hsv_to_rgb(h, s, new_v)) + if contrast_ratio(candidate, hex_bg) >= min_ratio: + return candidate + + new_v = min(1, v + delta) + candidate = rgb_to_hex(*colorsys.hsv_to_rgb(h, s, new_v)) + if contrast_ratio(candidate, hex_bg) >= min_ratio: + return candidate + else: + new_v = min(1, v + delta) + candidate = rgb_to_hex(*colorsys.hsv_to_rgb(h, s, new_v)) + if contrast_ratio(candidate, hex_bg) >= min_ratio: + return candidate + + new_v = max(0, v - delta) + candidate = rgb_to_hex(*colorsys.hsv_to_rgb(h, s, new_v)) + if contrast_ratio(candidate, hex_bg) >= min_ratio: + return candidate + + return hex_color + +def generate_palette(base_color, is_light=False, honor_primary=None, background=None): + r, g, b = hex_to_rgb(base_color) + h, s, v = colorsys.rgb_to_hsv(r, g, b) + + palette = [] + + if background: + bg_color = background + palette.append(bg_color) + elif is_light: + bg_color = "#f8f8f8" + palette.append(bg_color) + else: + bg_color = "#1a1a1a" + palette.append(bg_color) + + red_h = 0.0 + if is_light: + red_color = rgb_to_hex(*colorsys.hsv_to_rgb(red_h, 0.75, 0.85)) + palette.append(ensure_contrast(red_color, bg_color, 4.5, is_light)) + else: + red_color = rgb_to_hex(*colorsys.hsv_to_rgb(red_h, 0.6, 0.8)) + palette.append(ensure_contrast(red_color, bg_color, 4.5, is_light)) + + green_h = 0.33 + if is_light: + green_color = rgb_to_hex(*colorsys.hsv_to_rgb(green_h, max(s * 0.9, 0.75), v * 0.6)) + palette.append(ensure_contrast(green_color, bg_color, 4.5, is_light)) + else: + green_color = rgb_to_hex(*colorsys.hsv_to_rgb(green_h, max(s * 0.65, 0.5), v * 0.9)) + palette.append(ensure_contrast(green_color, bg_color, 4.5, is_light)) + + yellow_h = 0.08 + if is_light: + yellow_color = rgb_to_hex(*colorsys.hsv_to_rgb(yellow_h, max(s * 0.85, 0.7), v * 0.7)) + palette.append(ensure_contrast(yellow_color, bg_color, 4.5, is_light)) + else: + yellow_color = rgb_to_hex(*colorsys.hsv_to_rgb(yellow_h, max(s * 0.5, 0.45), v * 1.4)) + palette.append(ensure_contrast(yellow_color, bg_color, 4.5, is_light)) + + if is_light: + blue_color = rgb_to_hex(*colorsys.hsv_to_rgb(h, max(s * 0.9, 0.7), v * 1.1)) + palette.append(ensure_contrast(blue_color, bg_color, 4.5, is_light)) + else: + blue_color = rgb_to_hex(*colorsys.hsv_to_rgb(h, max(s * 0.8, 0.6), min(v * 1.6, 1.0))) + palette.append(ensure_contrast(blue_color, bg_color, 4.5, is_light)) + + mag_h = h - 0.03 if h >= 0.03 else h + 0.97 + if honor_primary: + hr, hg, hb = hex_to_rgb(honor_primary) + hh, hs, hv = colorsys.rgb_to_hsv(hr, hg, hb) + if is_light: + mag_color = rgb_to_hex(*colorsys.hsv_to_rgb(hh, max(hs * 0.9, 0.7), hv * 0.85)) + palette.append(ensure_contrast(mag_color, bg_color, 4.5, is_light)) + else: + mag_color = rgb_to_hex(*colorsys.hsv_to_rgb(hh, hs * 0.8, hv * 0.75)) + palette.append(ensure_contrast(mag_color, bg_color, 4.5, is_light)) + elif is_light: + mag_color = rgb_to_hex(*colorsys.hsv_to_rgb(mag_h, max(s * 0.75, 0.6), v * 0.9)) + palette.append(ensure_contrast(mag_color, bg_color, 4.5, is_light)) + else: + mag_color = rgb_to_hex(*colorsys.hsv_to_rgb(mag_h, max(s * 0.7, 0.6), v * 0.85)) + palette.append(ensure_contrast(mag_color, bg_color, 4.5, is_light)) + + cyan_h = h + 0.08 + if honor_primary: + palette.append(ensure_contrast(honor_primary, bg_color, 4.5, is_light)) + elif is_light: + cyan_color = rgb_to_hex(*colorsys.hsv_to_rgb(cyan_h, max(s * 0.8, 0.65), v * 1.05)) + palette.append(ensure_contrast(cyan_color, bg_color, 4.5, is_light)) + else: + cyan_color = rgb_to_hex(*colorsys.hsv_to_rgb(cyan_h, max(s * 0.6, 0.5), min(v * 1.25, 0.85))) + palette.append(ensure_contrast(cyan_color, bg_color, 4.5, is_light)) + + if is_light: + palette.append("#2e2e2e") + palette.append("#4a4a4a") + else: + palette.append("#abb2bf") + palette.append("#5c6370") + + if is_light: + bright_red = rgb_to_hex(*colorsys.hsv_to_rgb(red_h, 0.6, 0.9)) + palette.append(ensure_contrast(bright_red, bg_color, 3.0, is_light)) + bright_green = rgb_to_hex(*colorsys.hsv_to_rgb(green_h, max(s * 0.8, 0.7), v * 0.65)) + palette.append(ensure_contrast(bright_green, bg_color, 3.0, is_light)) + bright_yellow = rgb_to_hex(*colorsys.hsv_to_rgb(yellow_h, max(s * 0.75, 0.65), v * 0.75)) + palette.append(ensure_contrast(bright_yellow, bg_color, 3.0, is_light)) + if honor_primary: + hr, hg, hb = hex_to_rgb(honor_primary) + hh, hs, hv = colorsys.rgb_to_hsv(hr, hg, hb) + bright_blue = rgb_to_hex(*colorsys.hsv_to_rgb(hh, min(hs * 1.1, 1.0), min(hv * 1.2, 1.0))) + palette.append(ensure_contrast(bright_blue, bg_color, 3.0, is_light)) + else: + bright_blue = rgb_to_hex(*colorsys.hsv_to_rgb(h, max(s * 0.8, 0.7), min(v * 1.3, 1.0))) + palette.append(ensure_contrast(bright_blue, bg_color, 3.0, is_light)) + bright_mag = rgb_to_hex(*colorsys.hsv_to_rgb(mag_h, max(s * 0.9, 0.75), min(v * 1.25, 1.0))) + palette.append(ensure_contrast(bright_mag, bg_color, 3.0, is_light)) + bright_cyan = rgb_to_hex(*colorsys.hsv_to_rgb(cyan_h, max(s * 0.75, 0.65), min(v * 1.25, 1.0))) + palette.append(ensure_contrast(bright_cyan, bg_color, 3.0, is_light)) + else: + bright_red = rgb_to_hex(*colorsys.hsv_to_rgb(red_h, 0.45, min(1.0, 0.9))) + palette.append(ensure_contrast(bright_red, bg_color, 3.0, is_light)) + bright_green = rgb_to_hex(*colorsys.hsv_to_rgb(green_h, max(s * 0.5, 0.4), min(v * 1.5, 0.9))) + palette.append(ensure_contrast(bright_green, bg_color, 3.0, is_light)) + bright_yellow = rgb_to_hex(*colorsys.hsv_to_rgb(yellow_h, max(s * 0.4, 0.35), min(v * 1.6, 0.95))) + palette.append(ensure_contrast(bright_yellow, bg_color, 3.0, is_light)) + if honor_primary: + hr, hg, hb = hex_to_rgb(honor_primary) + hh, hs, hv = colorsys.rgb_to_hsv(hr, hg, hb) + bright_blue = rgb_to_hex(*colorsys.hsv_to_rgb(hh, min(hs * 1.2, 1.0), min(hv * 1.1, 1.0))) + palette.append(ensure_contrast(bright_blue, bg_color, 3.0, is_light)) + else: + bright_blue = rgb_to_hex(*colorsys.hsv_to_rgb(h, max(s * 0.6, 0.5), min(v * 1.5, 0.9))) + palette.append(ensure_contrast(bright_blue, bg_color, 3.0, is_light)) + bright_mag = rgb_to_hex(*colorsys.hsv_to_rgb(mag_h, max(s * 0.7, 0.6), min(v * 1.3, 0.9))) + palette.append(ensure_contrast(bright_mag, bg_color, 3.0, is_light)) + bright_cyan = rgb_to_hex(*colorsys.hsv_to_rgb(h + 0.02 if h + 0.02 <= 1.0 else h + 0.02 - 1.0, max(s * 0.6, 0.5), min(v * 1.2, 0.85))) + palette.append(ensure_contrast(bright_cyan, bg_color, 3.0, is_light)) + + if is_light: + palette.append("#1a1a1a") + else: + palette.append("#ffffff") + + return palette + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: dank16.py [--light] [--kitty] [--honor-primary HEX] [--background HEX]", file=sys.stderr) + sys.exit(1) + + base = sys.argv[1] + if not base.startswith('#'): + base = '#' + base + + is_light = "--light" in sys.argv + is_kitty = "--kitty" in sys.argv + + honor_primary = None + if "--honor-primary" in sys.argv: + try: + honor_idx = sys.argv.index("--honor-primary") + if honor_idx + 1 < len(sys.argv): + honor_primary = sys.argv[honor_idx + 1] + if not honor_primary.startswith('#'): + honor_primary = '#' + honor_primary + except (ValueError, IndexError): + print("Error: --honor-primary requires a hex color", file=sys.stderr) + sys.exit(1) + + background = None + if "--background" in sys.argv: + try: + bg_idx = sys.argv.index("--background") + if bg_idx + 1 < len(sys.argv): + background = sys.argv[bg_idx + 1] + if not background.startswith('#'): + background = '#' + background + except (ValueError, IndexError): + print("Error: --background requires a hex color", file=sys.stderr) + sys.exit(1) + + colors = generate_palette(base, is_light, honor_primary, background) + + if is_kitty: + # Kitty color format mapping + kitty_colors = [ + ("color0", colors[0]), + ("color1", colors[1]), + ("color2", colors[2]), + ("color3", colors[3]), + ("color4", colors[4]), + ("color5", colors[5]), + ("color6", colors[6]), + ("color7", colors[7]), + ("color8", colors[8]), + ("color9", colors[9]), + ("color10", colors[10]), + ("color11", colors[11]), + ("color12", colors[12]), + ("color13", colors[13]), + ("color14", colors[14]), + ("color15", colors[15]) + ] + + for name, color in kitty_colors: + print(f"{name} {color}") + else: + for i, color in enumerate(colors): + print(f"palette = {i}={color}") \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/templates/dgop.json b/quickshell/.config/quickshell/matugen/templates/dgop.json new file mode 100644 index 0000000..e28899c --- /dev/null +++ b/quickshell/.config/quickshell/matugen/templates/dgop.json @@ -0,0 +1,49 @@ +{ + "ui": { + "border_primary": "{{colors.primary.default.hex}}", + "border_secondary": "{{colors.secondary.default.hex}}", + "header_background": "{{colors.primary.default.hex}}", + "header_text": "{{colors.on_primary.default.hex}}", + "footer_background": "{{colors.surface_container.default.hex}}", + "footer_text": "{{colors.on_surface_variant.default.hex}}", + "text_primary": "{{colors.on_surface.default.hex}}", + "text_secondary": "{{colors.on_surface_variant.default.hex}}", + "text_accent": "{{colors.primary.default.hex}}", + "selection_background": "{{colors.primary.default.hex}}", + "selection_text": "{{colors.on_primary.default.hex}}" + }, + "charts": { + "network_download": "{{colors.primary.default.hex}}", + "network_upload": "{{colors.primary_container.default.hex}}", + "network_line": "{{colors.secondary.default.hex}}", + "cpu_core_low": "{{colors.primary_container.default.hex}}", + "cpu_core_medium": "{{colors.primary.default.hex}}", + "cpu_core_high": "{{colors.tertiary.default.hex}}", + "disk_read": "{{colors.primary.default.hex}}", + "disk_write": "{{colors.primary_container.default.hex}}" + }, + "progress_bars": { + "memory_low": "{{colors.primary_container.default.hex}}", + "memory_medium": "{{colors.primary.default.hex}}", + "memory_high": "{{colors.tertiary.default.hex}}", + "disk_low": "{{colors.primary_container.default.hex}}", + "disk_medium": "{{colors.primary.default.hex}}", + "disk_high": "{{colors.tertiary.default.hex}}", + "cpu_low": "{{colors.primary_container.default.hex}}", + "cpu_medium": "{{colors.primary.default.hex}}", + "cpu_high": "{{colors.tertiary.default.hex}}", + "progress_background": "{{colors.surface_container_high.default.hex}}" + }, + "temperature": { + "cold": "{{colors.primary_container.default.hex}}", + "warm": "{{colors.primary.default.hex}}", + "hot": "{{colors.tertiary.default.hex}}", + "danger": "{{colors.error.default.hex}}" + }, + "status": { + "success": "#22C55E", + "warning": "#F59E0B", + "error": "{{colors.error.default.hex}}", + "info": "{{colors.primary.default.hex}}" + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/templates/ghostty.conf b/quickshell/.config/quickshell/matugen/templates/ghostty.conf new file mode 100644 index 0000000..f35391f --- /dev/null +++ b/quickshell/.config/quickshell/matugen/templates/ghostty.conf @@ -0,0 +1,5 @@ +background = {{colors.surface.default.hex}} +foreground = {{colors.on_surface.default.hex}} +cursor-color = {{colors.primary.default.hex}} +selection-background = {{colors.primary_container.default.hex}} +selection-foreground = {{colors.on_surface.default.hex}} \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/templates/gtk-colors.css b/quickshell/.config/quickshell/matugen/templates/gtk-colors.css new file mode 100644 index 0000000..7768071 --- /dev/null +++ b/quickshell/.config/quickshell/matugen/templates/gtk-colors.css @@ -0,0 +1,92 @@ +/* +* GTK Colors +* Generated with Matugen +*/ + +@define-color accent_color {{colors.primary_fixed_dim.default.hex}}; +@define-color accent_fg_color {{colors.on_primary_fixed.default.hex}}; +@define-color accent_bg_color {{colors.primary_fixed_dim.default.hex}}; +@define-color window_bg_color {{colors.surface_dim.default.hex}}; +@define-color window_fg_color {{colors.on_surface.default.hex}}; +@define-color headerbar_bg_color {{colors.surface_dim.default.hex}}; +@define-color headerbar_fg_color {{colors.on_surface.default.hex}}; +@define-color popover_bg_color {{colors.surface_dim.default.hex}}; +@define-color popover_fg_color {{colors.on_surface.default.hex}}; +@define-color view_bg_color {{colors.surface.default.hex}}; +@define-color view_fg_color {{colors.on_surface.default.hex}}; +@define-color card_bg_color {{colors.surface.default.hex}}; +@define-color card_fg_color {{colors.on_surface.default.hex}}; +@define-color sidebar_bg_color @window_bg_color; +@define-color sidebar_fg_color @window_fg_color; +@define-color sidebar_border_color @window_bg_color; +@define-color sidebar_backdrop_color @window_bg_color; + +/* Titlebar and headerbar specific colors for window decorations */ +@define-color theme_bg_color {{colors.surface.default.hex}}; +@define-color theme_fg_color {{colors.on_surface.default.hex}}; +@define-color theme_base_color {{colors.surface.default.hex}}; +@define-color theme_text_color {{colors.on_surface.default.hex}}; +@define-color theme_selected_bg_color {{colors.primary.default.hex}}; +@define-color theme_selected_fg_color {{colors.on_primary.default.hex}}; +@define-color theme_button_background_normal {{colors.surface.default.hex}}; +@define-color theme_button_foreground_normal {{colors.on_surface.default.hex}}; +@define-color theme_button_background_hover alpha({{colors.on_surface.default.hex}}, 0.08); +@define-color theme_button_foreground_hover {{colors.on_surface.default.hex}}; +@define-color theme_button_background_active alpha({{colors.on_surface.default.hex}}, 0.12); +@define-color theme_button_foreground_active {{colors.on_surface.default.hex}}; +@define-color theme_button_background_disabled alpha({{colors.on_surface.default.hex}}, 0.04); +@define-color theme_button_foreground_disabled alpha({{colors.on_surface.default.hex}}, 0.38); + +/* Additional headerbar-specific colors for GTK4 compatibility */ +@define-color headerbar_backdrop_color {{colors.surface_container_low.default.hex}}; +@define-color headerbar_border_color {{colors.outline.default.hex}}; +@define-color headerbar_darker_shade_color {{colors.outline_variant.default.hex}}; +@define-color headerbar_shade_color {{colors.surface_variant.default.hex}}; + +/* Window control and decoration colors */ +@define-color wm_bg_a {{colors.surface_dim.default.hex}}; +@define-color wm_bg_b {{colors.surface_container.default.hex}}; +@define-color wm_border_a {{colors.outline.default.hex}}; +@define-color wm_border_b {{colors.outline_variant.default.hex}}; +@define-color wm_shadow {{colors.shadow.default.hex}}; +@define-color wm_outline {{colors.outline.default.hex}}; + +/* Menu and popover colors */ +@define-color menu_bg_color {{colors.surface_container.default.hex}}; +@define-color menu_fg_color {{colors.on_surface.default.hex}}; +@define-color menu_selected_bg_color {{colors.primary.default.hex}}; +@define-color menu_selected_fg_color {{colors.on_primary.default.hex}}; + +/* Button and control colors */ +@define-color button_bg_color {{colors.surface.default.hex}}; +@define-color button_fg_color {{colors.on_surface.default.hex}}; +@define-color button_border_color {{colors.outline_variant.default.hex}}; +@define-color button_active_bg_color {{colors.primary.default.hex}}; +@define-color button_active_fg_color {{colors.on_primary.default.hex}}; + +/* Entry and input colors */ +@define-color entry_bg_color {{colors.surface_variant.default.hex}}; +@define-color entry_fg_color {{colors.on_surface_variant.default.hex}}; +@define-color entry_border_color {{colors.outline.default.hex}}; +@define-color entry_focus_border_color {{colors.primary.default.hex}}; + +/* Scrollbar colors */ +@define-color scrollbar_bg_color {{colors.surface_variant.default.hex}}; +@define-color scrollbar_fg_color {{colors.on_surface_variant.default.hex}}; +@define-color scrollbar_hover_color {{colors.primary.default.hex}}; + +/* Selection and highlight colors */ +@define-color selection_bg_color {{colors.primary_container.default.hex}}; +@define-color selection_fg_color {{colors.on_primary_container.default.hex}}; + +/* Tooltip colors */ +@define-color tooltip_bg_color {{colors.inverse_surface.default.hex}}; +@define-color tooltip_fg_color {{colors.inverse_on_surface.default.hex}}; + +/* Warning and error colors */ +@define-color warning_color {{colors.tertiary.default.hex}}; +@define-color warning_fg_color {{colors.on_tertiary.default.hex}}; +@define-color error_color {{colors.error.default.hex}}; +@define-color error_fg_color {{colors.on_error.default.hex}}; +@define-color success_color {{colors.tertiary_container.default.hex}}; +@define-color success_fg_color {{colors.on_tertiary_container.default.hex}}; \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/templates/gtk3-colloid-dark.css b/quickshell/.config/quickshell/matugen/templates/gtk3-colloid-dark.css new file mode 100644 index 0000000..1ddfa4d --- /dev/null +++ b/quickshell/.config/quickshell/matugen/templates/gtk3-colloid-dark.css @@ -0,0 +1,8372 @@ +@keyframes ripple { + to { + background-size: 1000% 1000%; + } +} + +@keyframes ripple-on-slider { + to { + background-size: auto, 1000% 1000%; + } +} + +@keyframes ripple-on-headerbar { + from { + background-image: radial-gradient(circle, {{colors.primary.default.hex}} 0%, transparent 0%); + } + to { + background-image: radial-gradient(circle, {{colors.primary.default.hex}} 100%, transparent 0%); + } +} + +* { + background-clip: padding-box; + -GtkToolButton-icon-spacing: 0; + -GtkTextView-error-underline-color: #F44336; + -GtkScrolledWindow-scrollbar-spacing: 0; + -GtkToolItemGroup-expander-size: 11; + -GtkWidget-text-handle-width: 24; + -GtkWidget-text-handle-height: 24; + -GtkDialog-button-spacing: 6; + -GtkDialog-action-area-border: 6; + outline-style: solid; + outline-width: 2px; + outline-color: transparent; + outline-offset: -4px; + -gtk-outline-radius: 6px; + -gtk-secondary-caret-color: {{colors.primary.default.hex}}; +} + +*:focus { + outline-color: alpha(currentColor, 0.1); +} + +XfdesktopIconView.view:active, calendar.raven-calendar:selected, box.vertical > widget > widget:selected, calendar:selected, popover.background modelbutton.flat:selected, +popover.background .menuitem.button.flat:selected, .csd treeview.view:selected, .background.csd .view:selected { + color: {{colors.on_surface.default.hex}}; + background-color: alpha(currentColor, 0.1); +} + +.nemo-window .view selection, .nemo-window .view:selected, .nautilus-window notebook .view:not(treeview) selection, .nautilus-window notebook .view:not(treeview):selected, .nautilus-window flowboxchild:selected .icon-item-background, label selection, flowbox flowboxchild:selected { + color: {{colors.primary.default.hex}}; + background-color: rgba(91, 155, 248, 0.2); +} + +.nemo-window .nemo-window-pane widget.entry:selected, window.background.csd evview.view.content-view:selected, window.background.csd evview.view.content-view:selected:backdrop, .nautilus-window.background.csd notebook widget.view:selected, entry selection, textview text selection:focus, textview text selection, widget.view:selected, .view:selected { + color: {{colors.on_surface.default.hex}}; + background-color: {{colors.primary.default.hex}}; +} + +.linked:not(.vertical) > button, .linked:not(.vertical) > entry { + border-radius: 0; +} + +.linked:not(.vertical) > button:first-child, .linked:not(.vertical) > entry:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} + +.linked:not(.vertical) > button:last-child, .linked:not(.vertical) > entry:last-child { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.linked:not(.vertical) > button:only-child, .linked:not(.vertical) > entry:only-child { + border-radius: 6px; +} + +.linked.vertical > button, .linked.vertical > entry { + border-radius: 0; +} + +.linked.vertical > button:first-child, .linked.vertical > entry:first-child { + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} + +.linked.vertical > button:last-child, .linked.vertical > entry:last-child { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; +} + +.linked.vertical > button:only-child, .linked.vertical > entry:only-child { + border-radius: 6px; +} + +/*************** + * Base States * + ***************/ +.background { + background-color: #1a1a1a; + color: {{colors.on_surface.default.hex}}; +} + +.background.csd { + border-radius: 0 0 12px 12px; +} + +.background.maximized, .background.solid-csd { + border-radius: 0; +} + +*:disabled { + -gtk-icon-effect: dim; +} + +.gtkstyle-fallback { + background-color: #141414; + color: {{colors.on_surface.default.hex}}; +} + +.gtkstyle-fallback:hover { + background-color: #0a0a0a; + color: {{colors.on_surface.default.hex}}; +} + +.gtkstyle-fallback:active { + background-color: #0a0a0a; + color: {{colors.on_surface.default.hex}}; +} + +.gtkstyle-fallback:disabled { + background-color: #1a1a1a; + color: rgba(255, 255, 255, 0.5); +} + +.gtkstyle-fallback:selected { + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; +} + +.view { + background-color: #1a1a1a; + color: {{colors.on_surface.default.hex}}; +} + +.view:hover { + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.08); +} + +.view:disabled { + color: rgba(255, 255, 255, 0.5); +} + +.view:selected:hover { + box-shadow: none; +} + +window.background.csd > stack.view { + border-radius: 0 0 12px 12px; +} + +textview text { + background-color: #1a1a1a; +} + +textview border { + background-color: #141414; + color: rgba(255, 255, 255, 0.7); +} + +iconview:hover, iconview:selected { + border-radius: 6px; +} + +.content-view rubberband, .content-view .rubberband, treeview.view rubberband, treeview.view XfdesktopIconView.view .rubberband, XfdesktopIconView.view treeview.view .rubberband, flowbox rubberband, +.rubberband, +rubberband, +XfdesktopIconView.view .rubberband { + border: 1px solid {{colors.primary.default.hex}}; + background-color: rgba(91, 155, 248, 0.3); +} + +flowbox flowboxchild { + padding: 3px; + border-radius: 6px; + color: {{colors.on_surface.default.hex}}; +} + +flowbox flowboxchild button.osd.remove-button { + min-height: 28px; + min-width: 28px; + padding: 0; + margin: 6px; +} + +.content-view .tile:selected { + background-color: transparent; +} + +label { + caret-color: currentColor; +} + +label.separator { + color: rgba(255, 255, 255, 0.7); +} + +label:disabled { + color: rgba(255, 255, 255, 0.5); +} + +headerbar label:disabled, tab label:disabled, button label:disabled { + color: inherit; +} + +label.osd { + border-radius: 6px; + background-color: rgba(25, 25, 25, 0.9); + color: {{colors.on_surface.default.hex}}; +} + +.dim-label { + color: rgba(255, 255, 255, 0.7); +} + +assistant .sidebar { + padding: 4px 0; +} + +assistant .sidebar label { + min-height: 34px; + padding: 0 12px; + color: rgba(255, 255, 255, 0.5); + font-weight: 500; +} + +assistant .sidebar label.highlight { + color: {{colors.on_surface.default.hex}}; +} + +/********************* + * Spinner Animation * + *********************/ +@keyframes spin { + to { + -gtk-icon-transform: rotate(1turn); + } +} + +spinner { + background: none; + opacity: 0; + -gtk-icon-source: -gtk-icontheme("process-working-symbolic"); +} + +spinner:checked { + opacity: 1; + animation: spin 1s linear infinite; +} + +spinner:checked:disabled { + opacity: 0.5; +} + +/**************** + * Text Entries * + ****************/ +spinbutton, entry { + min-height: 34px; + padding: 0 8px; + border-radius: 6px; + caret-color: currentColor; + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), box-shadow 300ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: inset 0 0 0 2px transparent; + background-color: rgba(255, 255, 255, 0.08); + color: {{colors.on_surface.default.hex}}; +} + +spinbutton:focus, entry:focus { + background-color: rgba(255, 255, 255, 0.08); + box-shadow: inset 0 0 0 2px {{colors.primary.default.hex}}; +} + +spinbutton:drop(active), entry:drop(active) { + background-color: alpha(currentColor, 0.08); + box-shadow: inset 0 0 0 2px alpha(currentColor, 0.08); +} + +spinbutton:disabled, entry:disabled { + box-shadow: inset 0 0 0 2px transparent; + background-color: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.5); +} + +entry.flat { + min-height: 0; + padding: 2px; + border-radius: 0; + background-color: transparent; +} + +entry image { + color: rgba(255, 255, 255, 0.7); +} + +entry image:hover, entry image:active { + color: {{colors.on_surface.default.hex}}; +} + +entry image:disabled { + color: rgba(255, 255, 255, 0.5); +} + +entry image.left { + margin-left: 1px; + margin-right: 6px; +} + +entry image.right { + margin-left: 6px; + margin-right: 1px; +} + +entry undershoot.left { + background-color: transparent; + background-image: linear-gradient(to top, transparent 50%, rgba(255, 255, 255, 0.3) 50%); + padding-left: 1px; + background-size: 1px 12px; + background-repeat: repeat-y; + background-origin: content-box; + background-position: left top; + margin: 0 4px; + margin: 4px 0; +} + +entry undershoot.right { + background-color: transparent; + background-image: linear-gradient(to top, transparent 50%, rgba(255, 255, 255, 0.3) 50%); + padding-right: 1px; + background-size: 1px 12px; + background-repeat: repeat-y; + background-origin: content-box; + background-position: right top; + margin: 0 4px; + margin: 4px 0; +} + +entry.error { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), box-shadow 300ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: inset 0 0 0 2px transparent; + background-color: rgba(255, 255, 255, 0.08); + color: {{colors.on_surface.default.hex}}; +} + +entry.error:focus { + background-color: rgba(255, 255, 255, 0.08); + box-shadow: inset 0 0 0 2px #F44336; +} + +entry.error:disabled { + box-shadow: inset 0 0 0 2px transparent; + background-color: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.5); +} + +entry.error image { + color: rgba(255, 255, 255, 0.7); +} + +entry.error image:hover, entry.error image:active { + color: {{colors.on_surface.default.hex}}; +} + +entry.error image:disabled { + color: rgba(255, 255, 255, 0.5); +} + +entry.warning { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), box-shadow 300ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: inset 0 0 0 2px transparent; + background-color: rgba(255, 255, 255, 0.08); + color: {{colors.on_surface.default.hex}}; +} + +entry.warning:focus { + background-color: rgba(255, 255, 255, 0.08); + box-shadow: inset 0 0 0 2px #FFD600; +} + +entry.warning:disabled { + box-shadow: inset 0 0 0 2px transparent; + background-color: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.5); +} + +entry.warning image { + color: rgba(0, 0, 0, 0.6); +} + +entry.warning image:hover, entry.warning image:active { + color: rgba(0, 0, 0, 0.87); +} + +entry.warning image:disabled { + color: rgba(0, 0, 0, 0.38); +} + +entry progress { + margin: 2px -8px; + border-bottom: 2px solid {{colors.primary.default.hex}}; + background-color: transparent; +} + +treeview entry.flat, treeview entry { + background-color: #1a1a1a; +} + +treeview entry.flat, treeview entry.flat:focus, treeview entry, treeview entry:focus { + border-image: none; + box-shadow: none; +} + +.entry-tag, .photos-entry-tag, .documents-entry-tag { + margin: 2px; + border-radius: 9999px; + box-shadow: none; + background-color: rgba(255, 255, 255, 0.12); + color: {{colors.on_surface.default.hex}}; +} + +.entry-tag:hover, .photos-entry-tag:hover, .documents-entry-tag:hover { + background-image: image(alpha(currentColor, 0.08)); +} + +:dir(ltr) .entry-tag, :dir(ltr) .photos-entry-tag, :dir(ltr) .documents-entry-tag { + margin-left: 4px; + margin-right: 0; + padding-left: 12px; + padding-right: 8px; +} + +:dir(rtl) .entry-tag, :dir(rtl) .photos-entry-tag, :dir(rtl) .documents-entry-tag { + margin-left: 0; + margin-right: 4px; + padding-left: 8px; + padding-right: 12px; +} + +.entry-tag.button, .button.photos-entry-tag, .button.documents-entry-tag { + box-shadow: none; + background-color: transparent; +} + +.entry-tag.button:not(:hover):not(:active), .button.photos-entry-tag:not(:hover):not(:active), .button.documents-entry-tag:not(:hover):not(:active) { + color: rgba(255, 255, 255, 0.7); +} + +/*********** + * Buttons * + ***********/ +@keyframes needs-attention { + from { + background-image: -gtk-gradient(radial, center center, 0, center center, 0.001, to({{colors.primary.default.hex}}), to(transparent)); + } + to { + background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to({{colors.primary.default.hex}}), to(transparent)); + } +} + +.raven-mpris button.image-button, .mate-panel-menu-bar button, .nautilus-window .floating-bar button, infobar.warning > revealer > box button, infobar.warning:backdrop > revealer > box button { + color: rgba(255, 255, 255, 0.7); +} + +.raven-mpris button.image-button:focus, .mate-panel-menu-bar button:focus, .nautilus-window .floating-bar button:focus, infobar.warning > revealer > box button:focus, .raven-mpris button.image-button:hover, .mate-panel-menu-bar button:hover, .nautilus-window .floating-bar button:hover, infobar.warning > revealer > box button:hover, .raven-mpris button.image-button:active, .mate-panel-menu-bar button:active, .nautilus-window .floating-bar button:active, infobar.warning > revealer > box button:active, .raven-mpris button.image-button:checked, .mate-panel-menu-bar button:checked, .nautilus-window .floating-bar button:checked, infobar.warning > revealer > box button:checked { + color: {{colors.on_surface.default.hex}}; +} + +.raven-mpris button.image-button:disabled, .mate-panel-menu-bar button:disabled, .nautilus-window .floating-bar button:disabled, infobar.warning > revealer > box button:disabled { + color: rgba(255, 255, 255, 0.32); +} + +.raven-mpris button.image-button:checked:disabled, .mate-panel-menu-bar button:checked:disabled, .nautilus-window .floating-bar button:checked:disabled, infobar.warning > revealer > box button:checked:disabled { + color: rgba(255, 255, 255, 0.5); +} + +actionbar > revealer > box .linked > button:not(.suggested-action):not(.destructive-action), button { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), background-size 300ms cubic-bezier(0, 0, 0.2, 1), background-image 1200ms cubic-bezier(0, 0, 0.2, 1); + outline: none; + box-shadow: inset 0 0 0 9999px transparent; + background-color: rgba(255, 255, 255, 0.08); + background-image: radial-gradient(circle, transparent 10%, transparent 0%); + background-repeat: no-repeat; + background-position: center; + background-size: 1000% 1000%; + color: {{colors.on_surface.default.hex}}; +} + +actionbar > revealer > box .linked > button:focus:not(.suggested-action):not(.destructive-action), button:focus { + box-shadow: 0 0 0 2px rgba(91, 155, 248, 0.35); +} + +actionbar > revealer > box .linked > button:hover:not(.suggested-action):not(.destructive-action), button:hover { + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.08); +} + +actionbar > revealer > box .linked > button:active:not(.suggested-action):not(.destructive-action), button:active { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), background-size 0ms, background-image 0ms, border 0ms; + animation: ripple 225ms cubic-bezier(0, 0, 0.2, 1) forwards; + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.08); + background-image: radial-gradient(circle, alpha(currentColor, 0.12) 10%, transparent 0%); + background-size: 0% 0%; +} + +actionbar > revealer > box .linked > button:disabled:not(.suggested-action):not(.destructive-action), button:disabled { + box-shadow: none; + background-color: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.5); +} + +actionbar > revealer > box .linked > button:checked:not(.suggested-action):not(.destructive-action), button:checked { + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; +} + +actionbar > revealer > box .linked > button:checked:hover:not(.suggested-action):not(.destructive-action), button:checked:hover { + box-shadow: inset 0 0 0 9999px transparent; +} + +actionbar > revealer > box .linked > button:checked:disabled:not(.suggested-action):not(.destructive-action), button:checked:disabled { + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.1); + background-color: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.5); +} + +.raven-notifications-group list > row button.flat, .raven .expander-button, .budgie-popover button.flat.switcher, window.background > box.vertical > toolbar.primary-toolbar > toolitem > box.horizontal:not(.linked) > button.toggle, +window.background > box.vertical > toolbar.primary-toolbar > toolitem > .linked > button:not(.toggle):not(.raised):not(.flat), window.csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > .linked > button, +window.csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > box.horizontal > button, +window.solid-csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > .linked > button, +window.solid-csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > box.horizontal > button, .nautilus-window headerbar revealer > button, button.titlebutton:not(.suggested-action):not(.destructive-action), filechooser #pathbarbox > stack > box > button, button.close, button.circular, .inline-toolbar button:not(.text-button) { + border-radius: 9999px; +} + +.raven-notifications-group list > row button.flat label, .raven .expander-button label, .budgie-popover button.flat.switcher label, window.background > box.vertical > toolbar.primary-toolbar > toolitem > box.horizontal:not(.linked) > button.toggle label, +window.background > box.vertical > toolbar.primary-toolbar > toolitem > .linked > button:not(.toggle):not(.raised):not(.flat) label, window.csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > .linked > button label, +window.csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > box.horizontal > button label, +window.solid-csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > .linked > button label, +window.solid-csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > box.horizontal > button label, .nautilus-window headerbar revealer > button label, button.titlebutton:not(.suggested-action):not(.destructive-action) label, filechooser #pathbarbox > stack > box > button label, button.close label, button.circular label, .inline-toolbar button:not(.text-button) label { + padding: 0; +} + +.pluma-window paned.horizontal box.vertical box.horizontal button.flat, .gedit-document-panel row button.flat, placessidebar.sidebar row button.sidebar-button, notebook > header tab button.flat, notebook > header tab button.close-button, spinbutton > button { + min-height: 24px; + min-width: 24px; + padding: 0; + border-radius: 9999px; +} + +button { + min-height: 24px; + min-width: 24px; + padding: 5px 5px; + border-radius: 6px; + font-weight: 500; +} + +button:drop(active) { + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.08); +} + +.budgie-session-dialog buttonbox.linked > button, +.budgie-polkit-dialog buttonbox.linked > button, +.budgie-run-dialog buttonbox.linked > button, .budgie-panel button, .budgie-popover row button, .budgie-settings-window buttonbox.inline-toolbar button, #mate-menu button, #MatePanelPopupWindow button, popover.messagepopover .popover-action-area button, tabbox > tab button, placessidebar.sidebar row button.sidebar-button, calendar.button, .budgie-popover scrolledwindow.sidebar:not(.categories) list > row.activatable button.circular, treeview.view header button button.circular, row.activatable button.circular, scrollbar button, notebook > header > tabs > arrow, popover.background modelbutton.flat, +popover.background .menuitem.button.flat, spinbutton > button, .nemo-window .toolbar button, #buttonbox_frame button, .xfce4-panel.background button, .raven stackswitcher.linked > button, .budgie-popover.budgie-menu scrolledwindow.sidebar.categories button.flat.radio.category-button, .lock-dialog button, .mate-panel-menu-bar button, window.background.csd.geary-main-window stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button, layouttabbar button, .floating-bar button, filechooser #pathbarbox > stack > box > button, messagedialog .dialog-action-box button, messagedialog .dialog-action-box .linked:not(.vertical) > button, .app-notification button, actionbar > revealer > box button:not(.suggested-action):not(.destructive-action), popover.background.menu button, +popover.background button.model, .nemo-window .primary-toolbar button:not(.text-button), headerbar button:not(.suggested-action):not(.destructive-action), combobox > .linked:not(.vertical) > button:not(:only-child), button.flat { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), border-image 225ms cubic-bezier(0, 0, 0.2, 1), background-size 300ms cubic-bezier(0, 0, 0.2, 1), background-image 1200ms cubic-bezier(0, 0, 0.2, 1); + outline: none; + box-shadow: inset 0 0 0 9999px transparent; + background-color: transparent; + background-image: radial-gradient(circle, transparent 10%, transparent 0%); + background-repeat: no-repeat; + background-position: center; + background-size: 1000% 1000%; + color: rgba(255, 255, 255, 0.7); +} + +.budgie-session-dialog buttonbox.linked > button:focus, +.budgie-polkit-dialog buttonbox.linked > button:focus, +.budgie-run-dialog buttonbox.linked > button:focus, .budgie-panel button:focus, .budgie-popover row button:focus, .budgie-settings-window buttonbox.inline-toolbar button:focus, #mate-menu button:focus, #MatePanelPopupWindow button:focus, popover.messagepopover .popover-action-area button:focus, tabbox > tab button:focus, placessidebar.sidebar row button.sidebar-button:focus, calendar.button:focus, .budgie-popover scrolledwindow.sidebar:not(.categories) list > row.activatable button.circular:focus, treeview.view header button button.circular:focus, row.activatable button.circular:focus, scrollbar button:focus, notebook > header > tabs > arrow:focus, popover.background modelbutton.flat:focus, +popover.background .menuitem.button.flat:focus, spinbutton > button:focus, .nemo-window .toolbar button:focus, #buttonbox_frame button:focus, .xfce4-panel.background button:focus, .raven stackswitcher.linked > button:focus, .budgie-popover.budgie-menu scrolledwindow.sidebar.categories button.flat.radio.category-button:focus, .lock-dialog button:focus, .mate-panel-menu-bar button:focus, window.background.csd.geary-main-window stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:focus, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:focus, layouttabbar button:focus, .floating-bar button:focus, filechooser #pathbarbox > stack > box > button:focus, messagedialog .dialog-action-box button:focus, messagedialog .dialog-action-box .linked:not(.vertical) > button:focus, .app-notification button:focus, actionbar > revealer > box button:focus:not(.suggested-action):not(.destructive-action), popover.background.menu button:focus, +popover.background button.model:focus, .nemo-window .primary-toolbar button:focus:not(.text-button), headerbar button:focus:not(.suggested-action):not(.destructive-action), combobox > .linked:not(.vertical) > button:focus:not(:only-child), button.flat:focus { + box-shadow: inset 0 0 0 2px alpha(currentColor, 0.08); + color: {{colors.on_surface.default.hex}}; +} + +.budgie-session-dialog buttonbox.linked > button:hover, +.budgie-polkit-dialog buttonbox.linked > button:hover, +.budgie-run-dialog buttonbox.linked > button:hover, .budgie-panel button:hover, .budgie-popover row button:hover, .budgie-settings-window buttonbox.inline-toolbar button:hover, #mate-menu button:hover, #MatePanelPopupWindow button:hover, popover.messagepopover .popover-action-area button:hover, tabbox > tab button:hover, placessidebar.sidebar row button.sidebar-button:hover, calendar.button:hover, .budgie-popover scrolledwindow.sidebar:not(.categories) list > row.activatable button.circular:hover, treeview.view header button button.circular:hover, row.activatable button.circular:hover, scrollbar button:hover, notebook > header > tabs > arrow:hover, popover.background modelbutton.flat:hover, +popover.background .menuitem.button.flat:hover, spinbutton > button:hover, .nemo-window .toolbar button:hover, #buttonbox_frame button:hover, .xfce4-panel.background button:hover, .raven stackswitcher.linked > button:hover, .budgie-popover.budgie-menu scrolledwindow.sidebar.categories button.flat.radio.category-button:hover, .lock-dialog button:hover, .mate-panel-menu-bar button:hover, window.background.csd.geary-main-window stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:hover, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:hover, layouttabbar button:hover, .floating-bar button:hover, filechooser #pathbarbox > stack > box > button:hover, messagedialog .dialog-action-box button:hover, messagedialog .dialog-action-box .linked:not(.vertical) > button:hover, .app-notification button:hover, actionbar > revealer > box button:hover:not(.suggested-action):not(.destructive-action), popover.background.menu button:hover, +popover.background button.model:hover, .nemo-window .primary-toolbar button:hover:not(.text-button), headerbar button:hover:not(.suggested-action):not(.destructive-action), combobox > .linked:not(.vertical) > button:hover:not(:only-child), button.flat:hover { + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.08); + color: {{colors.on_surface.default.hex}}; +} + +.budgie-session-dialog buttonbox.linked > button:active, +.budgie-polkit-dialog buttonbox.linked > button:active, +.budgie-run-dialog buttonbox.linked > button:active, .budgie-panel button:active, .budgie-popover row button:active, .budgie-settings-window buttonbox.inline-toolbar button:active, #mate-menu button:active, #MatePanelPopupWindow button:active, popover.messagepopover .popover-action-area button:active, tabbox > tab button:active, placessidebar.sidebar row button.sidebar-button:active, calendar.button:active, .budgie-popover scrolledwindow.sidebar:not(.categories) list > row.activatable button.circular:active, treeview.view header button button.circular:active, row.activatable button.circular:active, scrollbar button:active, notebook > header > tabs > arrow:active, popover.background modelbutton.flat:active, +popover.background .menuitem.button.flat:active, spinbutton > button:active, .nemo-window .toolbar button:active, #buttonbox_frame button:active, .xfce4-panel.background button:active, .raven stackswitcher.linked > button:active, .budgie-popover.budgie-menu scrolledwindow.sidebar.categories button.flat.radio.category-button:active, .lock-dialog button:active, .mate-panel-menu-bar button:active, window.background.csd.geary-main-window stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:active, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:active, layouttabbar button:active, .floating-bar button:active, filechooser #pathbarbox > stack > box > button:active, messagedialog .dialog-action-box button:active, messagedialog .dialog-action-box .linked:not(.vertical) > button:active, .app-notification button:active, actionbar > revealer > box button:active:not(.suggested-action):not(.destructive-action), popover.background.menu button:active, +popover.background button.model:active, .nemo-window .primary-toolbar button:active:not(.text-button), headerbar button:active:not(.suggested-action):not(.destructive-action), combobox > .linked:not(.vertical) > button:active:not(:only-child), button.flat:active { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), border-image 225ms cubic-bezier(0, 0, 0.2, 1), background-size 0ms, background-image 0ms; + animation: ripple 225ms cubic-bezier(0, 0, 0.2, 1) forwards; + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.08); + background-image: radial-gradient(circle, alpha(currentColor, 0.12) 10%, transparent 0%); + background-size: 0% 0%; + color: {{colors.on_surface.default.hex}}; +} + +.budgie-session-dialog buttonbox.linked > button:disabled, +.budgie-polkit-dialog buttonbox.linked > button:disabled, +.budgie-run-dialog buttonbox.linked > button:disabled, .budgie-panel button:disabled, .budgie-popover row button:disabled, .budgie-settings-window buttonbox.inline-toolbar button:disabled, #mate-menu button:disabled, #MatePanelPopupWindow button:disabled, popover.messagepopover .popover-action-area button:disabled, tabbox > tab button:disabled, placessidebar.sidebar row button.sidebar-button:disabled, calendar.button:disabled, .budgie-popover scrolledwindow.sidebar:not(.categories) list > row.activatable button.circular:disabled, treeview.view header button button.circular:disabled, row.activatable button.circular:disabled, scrollbar button:disabled, notebook > header > tabs > arrow:disabled, popover.background modelbutton.flat:disabled, +popover.background .menuitem.button.flat:disabled, spinbutton > button:disabled, .nemo-window .toolbar button:disabled, #buttonbox_frame button:disabled, .xfce4-panel.background button:disabled, .raven stackswitcher.linked > button:disabled, .budgie-popover.budgie-menu scrolledwindow.sidebar.categories button.flat.radio.category-button:disabled, .lock-dialog button:disabled, .mate-panel-menu-bar button:disabled, window.background.csd.geary-main-window stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:disabled, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:disabled, layouttabbar button:disabled, .floating-bar button:disabled, filechooser #pathbarbox > stack > box > button:disabled, messagedialog .dialog-action-box button:disabled, messagedialog .dialog-action-box .linked:not(.vertical) > button:disabled, .app-notification button:disabled, actionbar > revealer > box button:disabled:not(.suggested-action):not(.destructive-action), popover.background.menu button:disabled, +popover.background button.model:disabled, .nemo-window .primary-toolbar button:disabled:not(.text-button), headerbar button:disabled:not(.suggested-action):not(.destructive-action), combobox > .linked:not(.vertical) > button:disabled:not(:only-child), button.flat:disabled { + box-shadow: none; + background-color: transparent; + color: rgba(255, 255, 255, 0.32); +} + +.nemo-window .toolbar button:checked, #buttonbox_frame button:checked, .xfce4-panel.background button:checked, .raven stackswitcher.linked > button:checked, .budgie-popover.budgie-menu scrolledwindow.sidebar.categories button.flat.radio.category-button:checked, .lock-dialog button:checked, .mate-panel-menu-bar button:checked, window.background.csd.geary-main-window stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:checked, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:checked, layouttabbar button:checked, .floating-bar button:checked, filechooser #pathbarbox > stack > box > button:checked, messagedialog .dialog-action-box button:checked, messagedialog .dialog-action-box .linked:not(.vertical) > button:checked, .app-notification button:checked, actionbar > revealer > box button:checked:not(.suggested-action):not(.destructive-action), popover.background.menu button:checked, +popover.background button.model:checked, .nemo-window .primary-toolbar button:checked:not(.text-button), headerbar button:checked:not(.suggested-action):not(.destructive-action), combobox > .linked:not(.vertical) > button:checked:not(:only-child), button.flat:checked, button.flat:checked:hover { + background-color: alpha(currentColor, 0.1); + color: {{colors.on_surface.default.hex}}; +} + +.nemo-window .toolbar button:checked:disabled, #buttonbox_frame button:checked:disabled, .xfce4-panel.background button:checked:disabled, .raven stackswitcher.linked > button:checked:disabled, .budgie-popover.budgie-menu scrolledwindow.sidebar.categories button.flat.radio.category-button:checked:disabled, .lock-dialog button:checked:disabled, .mate-panel-menu-bar button:checked:disabled, window.background.csd.geary-main-window stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:checked:disabled, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar button:checked:disabled, layouttabbar button:checked:disabled, .floating-bar button:checked:disabled, filechooser #pathbarbox > stack > box > button:checked:disabled, messagedialog .dialog-action-box button:checked:disabled, .app-notification button:checked:disabled, actionbar > revealer > box button:checked:disabled:not(.suggested-action):not(.destructive-action), popover.background.menu button:checked:disabled, +popover.background button.model:checked:disabled, .nemo-window .primary-toolbar button:checked:disabled:not(.text-button), headerbar button:checked:disabled:not(.suggested-action):not(.destructive-action), combobox > .linked:not(.vertical) > button:checked:disabled:not(:only-child), button.flat:checked:disabled { + background-color: alpha(currentColor, 0.1); + color: rgba(255, 255, 255, 0.5); +} + +button.text-button { + min-width: 32px; + padding-left: 16px; + padding-right: 16px; +} + +button.image-button { + min-width: 24px; + padding: 5px; +} + +button.text-button.image-button { + min-width: 24px; + padding: 5px; + border-radius: 6px; +} + +button.text-button.image-button label:first-child { + margin-left: 11px; +} + +button.text-button.image-button label:last-child { + margin-right: 11px; +} + +button.text-button.image-button.flat label:first-child { + margin-left: 7px; +} + +button.text-button.image-button.flat label:last-child { + margin-right: 7px; +} + +button.text-button.image-button image:not(:only-child) { + margin: 0 4px; +} + +.linked:not(.vertical) > button.flat:not(:only-child), .linked.vertical > button.flat:not(:only-child) { + border-radius: 6px; +} + +button.osd { + min-width: 24px; + min-width: 24px; + padding: 5px; + background-color: #0f0f0f; + color: {{colors.on_surface.default.hex}}; +} + +button.osd:focus { + box-shadow: none; +} + +button.osd:hover { + background-color: #424242; + color: {{colors.on_surface.default.hex}}; +} + +button.osd:active { + background-color: #595959; + color: {{colors.on_surface.default.hex}}; +} + +button.osd:disabled { + opacity: 0; +} + +button.osd.image-button, button.osd.circular { + padding: 11px; +} + +button.osd.image-button > image, button.osd.circular > image { + padding: 0; +} + +button.suggested-action { + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; + box-shadow: none; +} + +button.suggested-action:disabled { + box-shadow: none; + background-color: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.5); +} + +button.suggested-action:hover { + box-shadow: inset 0 0 0 9999px transparent, 0 2px 2.4px -1px rgba(91, 155, 248, 0.2), 0 4px 3px 0 rgba(91, 155, 248, 0.14), 0 1px 6px 0 rgba(91, 155, 248, 0.12); +} + +button.suggested-action:checked { + background-color: #8cb9fa; +} + +button.suggested-action:checked:hover { + box-shadow: inset 0 0 0 9999px transparent, 0 3px 3px -3px rgba(91, 155, 248, 0.3), 0 2px 3px -1px rgba(91, 155, 248, 0.24), 0 2px 5px 0 rgba(91, 155, 248, 0.12); +} + +button.suggested-action:focus { + box-shadow: 0 0 0 2px rgba(91, 155, 248, 0.35); +} + +button.suggested-action.flat { + background-color: transparent; + color: {{colors.primary.default.hex}}; +} + +button.suggested-action.flat:disabled { + box-shadow: none; + background-color: transparent; + color: rgba(255, 255, 255, 0.32); +} + +button.suggested-action.flat:checked { + background-color: rgba(91, 155, 248, 0.3); +} + +button.destructive-action { + background-color: #F44336; + color: {{colors.on_surface.default.hex}}; + box-shadow: none; +} + +button.destructive-action:disabled { + box-shadow: none; + background-color: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.5); +} + +button.destructive-action:hover { + box-shadow: inset 0 0 0 9999px transparent, 0 2px 2.4px -1px rgba(244, 67, 54, 0.2), 0 4px 3px 0 rgba(244, 67, 54, 0.14), 0 1px 6px 0 rgba(244, 67, 54, 0.12); +} + +button.destructive-action:checked { + background-color: #f77b72; +} + +button.destructive-action:checked:hover { + box-shadow: inset 0 0 0 9999px transparent, 0 3px 3px -3px rgba(244, 67, 54, 0.3), 0 2px 3px -1px rgba(244, 67, 54, 0.24), 0 2px 5px 0 rgba(244, 67, 54, 0.12); +} + +button.destructive-action:focus { + box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.35); +} + +button.destructive-action.flat { + background-color: transparent; + color: #F44336; +} + +button.destructive-action.flat:disabled { + box-shadow: none; + background-color: transparent; + color: rgba(255, 255, 255, 0.32); +} + +button.destructive-action.flat:checked { + background-color: rgba(244, 67, 54, 0.3); +} + +.stack-switcher > button > label { + margin: 0 -6px; + padding: 0 6px; +} + +.stack-switcher > button > image { + margin: -3px -6px; + padding: 3px 6px; +} + +.stack-switcher > button.needs-attention:checked > label, +.stack-switcher > button.needs-attention:checked > image { + animation: none; + background-image: none; +} + +.primary-toolbar button { + -gtk-icon-shadow: none; +} + +button.close, button.circular { + min-width: 24px; + padding: 5px; +} + +stacksidebar.sidebar row.needs-attention > label, .stack-switcher > button.needs-attention > label, +.stack-switcher > button.needs-attention > image { + animation: needs-attention 225ms cubic-bezier(0, 0, 0.2, 1) forwards; + background-repeat: no-repeat; + background-position: right 3px; + background-size: 6px 6px; +} + +stacksidebar.sidebar row.needs-attention > label:dir(rtl), .stack-switcher > button.needs-attention > label:dir(rtl), +.stack-switcher > button.needs-attention > image:dir(rtl) { + background-position: left 3px; +} + +button.color { + min-height: 24px; + min-width: 24px; + padding: 6px; +} + +/********* + * Links * + *********/ +*:link, link { + color: #5bd3f8; +} + +*:visited { + color: #BA68C8; +} + +button.link:link, button.link:link:focus, button.link:link:hover, button.link:link:active { + color: #5bd3f8; +} + +button.link:visited, button.link:visited:focus, button.link:visited:hover, button.link:visited:active { + color: #BA68C8; +} + +button.link > label { + text-decoration-line: underline; +} + +/***************** + * GtkSpinButton * + *****************/ +spinbutton { + padding: 0; +} + +spinbutton > entry, .background:not(.csd) spinbutton > entry { + min-width: 30px; + margin: 0; + border-radius: 0; + border-image: none; +} + +spinbutton > entry, spinbutton > entry:focus, spinbutton > entry:disabled, .background:not(.csd) spinbutton > entry, .background:not(.csd) spinbutton > entry:focus, .background:not(.csd) spinbutton > entry:disabled { + border: none; + box-shadow: none; + background-color: transparent; +} + +spinbutton > button { + border: solid 6px transparent; +} + +spinbutton > button:focus:not(:hover):not(:active):not(:disabled) { + box-shadow: inset 0 0 0 9999px transparent; + color: rgba(255, 255, 255, 0.7); +} + +spinbutton > button.up:dir(ltr), spinbutton > button.down:dir(rtl) { + margin-left: -3px; +} + +spinbutton > button.up:dir(rtl), spinbutton > button.down:dir(ltr) { + margin-right: -3px; +} + +spinbutton.vertical { + padding: 3px; +} + +spinbutton.vertical > entry { + margin: 0; + padding: 0; + min-height: 34px; + min-width: 0; +} + +spinbutton.vertical > button.up { + margin: 0; +} + +spinbutton.vertical > button.down { + margin: 0; +} + +treeview spinbutton:not(.vertical) { + min-height: 0; + border-style: none; + border-radius: 0; +} + +treeview spinbutton:not(.vertical) entry { + min-height: 0; + padding: 1px 2px; +} + +/************** + * ComboBoxes * + **************/ +combobox arrow { + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); + min-height: 16px; + min-width: 16px; +} + +combobox decoration { + transition: none; +} + +combobox button.combo cellview:dir(ltr) { + margin-left: -1px; +} + +combobox button.combo cellview:dir(rtl) { + margin-right: -1px; +} + +combobox.linked button:nth-child(2):dir(ltr) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +combobox.linked button:nth-child(2):dir(rtl) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +combobox > .linked:not(.vertical) > entry:not(:only-child) { + border-radius: 6px; +} + +combobox > .linked:not(.vertical) > entry:not(:only-child):first-child { + margin-right: -34px; + padding-right: 34px; +} + +combobox > .linked:not(.vertical) > entry:not(:only-child):last-child { + margin-left: -34px; + padding-left: 34px; +} + +combobox > .linked:not(.vertical) > button:not(:only-child) { + min-height: 16px; + min-width: 16px; + margin: 5px; + padding: 4px; + border-radius: 6px; +} + +combobox > .linked > button.combo { + padding: 5px 9px; +} + +.linked:not(.vertical) > combobox:not(:first-child) > box > button.combo { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.linked:not(.vertical) > combobox:not(:last-child) > box > button.combo { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.linked.vertical > combobox:not(:first-child) > box > button.combo { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.linked.vertical > combobox:not(:last-child) > box > button.combo { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +button.combo:only-child { + border-radius: 6px; + font-weight: normal; + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), box-shadow 300ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: inset 0 0 0 2px transparent; + background-color: rgba(255, 255, 255, 0.08); + color: {{colors.on_surface.default.hex}}; +} + +button.combo:only-child:focus { + background-color: alpha(currentColor, 0.08); + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.3); +} + +button.combo:only-child:hover { + background-color: alpha(currentColor, 0.08); + box-shadow: inset 0 0 0 2px alpha(currentColor, 0.08); +} + +button.combo:only-child:checked { + background-color: rgba(255, 255, 255, 0.08); + box-shadow: inset 0 0 0 2px {{colors.primary.default.hex}}; +} + +button.combo:only-child:disabled { + box-shadow: inset 0 0 0 2px transparent; + background-color: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.5); +} + +/************ + * Toolbars * + ************/ +toolbar { + -GtkWidget-window-dragging: true; + padding: 2px 3px; + background-color: #1a1a1a; +} + +.osd toolbar { + background-color: transparent; +} + +frame.documents-dropdown, .app-notification, toolbar.osd { + transition: box-shadow 200ms ease-out; + padding: 6px; + border-radius: 12px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 3px 3px 0 rgba(0, 0, 0, 0.18), 0 3px 6px 0 rgba(0, 0, 0, 0.12), inset 0 1px rgba(255, 255, 255, 0.1); + background-color: #2a2a2a; +} + +frame.documents-dropdown:backdrop, .app-notification:backdrop, toolbar.osd:backdrop { + box-shadow: 0 4px 3px -3px rgba(0, 0, 0, 0.2), 0 2px 2px -1px rgba(0, 0, 0, 0.24), 0 1px 3px 0 rgba(0, 0, 0, 0.12), inset 0 1px rgba(255, 255, 255, 0.1); +} + +frame.left.documents-dropdown, .left.app-notification, frame.right.documents-dropdown, .right.app-notification, frame.top.documents-dropdown, .top.app-notification, frame.bottom.documents-dropdown, .bottom.app-notification, toolbar.osd.left, toolbar.osd.right, toolbar.osd.top, toolbar.osd.bottom { + border-radius: 0; +} + +frame.bottom.documents-dropdown, .bottom.app-notification, toolbar.osd.bottom { + box-shadow: none; + background-color: transparent; + background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1) 30%, rgba(0, 0, 0, 0.2) 50%, rgba(0, 0, 0, 0.4)); +} + +toolbar.horizontal > separator { + margin: 2px; +} + +toolbar.vertical > separator { + margin: 2px; +} + +toolbar:not(.inline-toolbar):not(.osd) scale, +toolbar:not(.inline-toolbar):not(.osd) entry, +toolbar:not(.inline-toolbar):not(.osd) spinbutton, +toolbar:not(.inline-toolbar):not(.osd) button { + margin: 2px 1px; +} + +toolbar:not(.inline-toolbar):not(.osd) .linked entry:not(:first-child), +toolbar:not(.inline-toolbar):not(.osd) .linked spinbutton:not(:first-child), +toolbar:not(.inline-toolbar):not(.osd) .linked button:not(:first-child) { + margin-left: 0; +} + +toolbar:not(.inline-toolbar):not(.osd) .linked entry:not(:last-child), +toolbar:not(.inline-toolbar):not(.osd) .linked spinbutton:not(:last-child), +toolbar:not(.inline-toolbar):not(.osd) .linked button:not(:last-child) { + margin-right: 0; +} + +toolbar:not(.inline-toolbar):not(.osd) spinbutton entry, +toolbar:not(.inline-toolbar):not(.osd) spinbutton button { + margin: 0; +} + +toolbar:not(.inline-toolbar):not(.osd) switch { + margin: 8px 2px; +} + +.toolbar { + background-color: #141414; +} + +frame .toolbar { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.inline-toolbar { + padding: 6px; + border-style: solid; + border-width: 0 1px 1px; + border-color: rgba(255, 255, 255, 0.12); + background-color: #141414; +} + +.frame .inline-toolbar { + border-width: 1px 0 0; + background-color: transparent; +} + +searchbar > revealer > box, +.location-bar { + padding: 6px; + border-style: solid; + border-width: 0 0 1px; + border-color: rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; + background-clip: border-box; +} + +searchbar > revealer > box { + margin: -6px; +} + +/*************** + * Header bars * + ***************/ +.nemo-window .primary-toolbar button:not(.text-button), headerbar button:not(.suggested-action):not(.destructive-action) { + color: rgba(255, 255, 255, 0.7); +} + +.nemo-window .primary-toolbar .linked:not(.vertical) > button:not(.text-button), headerbar .linked:not(.vertical) > button:not(.suggested-action):not(.destructive-action) { + border-radius: 6px; +} + +.nemo-window .primary-toolbar button:focus:not(.text-button), headerbar button:focus:not(.suggested-action):not(.destructive-action), .nemo-window .primary-toolbar button:hover:not(.text-button), headerbar button:hover:not(.suggested-action):not(.destructive-action), .nemo-window .primary-toolbar button:active:not(.text-button), headerbar button:active:not(.suggested-action):not(.destructive-action), .nemo-window .primary-toolbar button:checked:not(.text-button), headerbar button:checked:not(.suggested-action):not(.destructive-action) { + color: {{colors.on_surface.default.hex}}; +} + +.nemo-window .primary-toolbar button:disabled:not(.text-button), headerbar button:disabled:not(.suggested-action):not(.destructive-action) { + color: rgba(255, 255, 255, 0.32); +} + +.nemo-window .primary-toolbar button:checked:disabled:not(.text-button), headerbar button:checked:disabled:not(.suggested-action):not(.destructive-action) { + background-color: transparent; + color: rgba(255, 255, 255, 0.5); +} + +.nemo-window .primary-toolbar button:backdrop:not(.text-button), headerbar button:backdrop:not(.suggested-action):not(.destructive-action) { + color: rgba(255, 255, 255, 0.5); +} + +.nemo-window .primary-toolbar button:backdrop:focus:not(.text-button), headerbar button:backdrop:focus:not(.suggested-action):not(.destructive-action), .nemo-window .primary-toolbar button:backdrop:hover:not(.text-button), headerbar button:backdrop:hover:not(.suggested-action):not(.destructive-action), .nemo-window .primary-toolbar button:backdrop:active:not(.text-button), headerbar button:backdrop:active:not(.suggested-action):not(.destructive-action) { + color: rgba(255, 255, 255, 0.7); +} + +.nemo-window .primary-toolbar button:backdrop:disabled:not(.text-button), headerbar button:backdrop:disabled:not(.suggested-action):not(.destructive-action) { + color: rgba(255, 255, 255, 0.32); +} + +.nemo-window .primary-toolbar button:backdrop:checked:not(.text-button), headerbar button:backdrop:checked:not(.suggested-action):not(.destructive-action) { + color: rgba(255, 255, 255, 0.7); +} + +.nemo-window .primary-toolbar button:backdrop:checked:disabled:not(.text-button), headerbar button:backdrop:checked:disabled:not(.suggested-action):not(.destructive-action) { + color: rgba(255, 255, 255, 0.32); +} + +.nemo-window .primary-toolbar entry, .titlebar entry { + background-color: rgba(255, 255, 255, 0.04); + color: {{colors.on_surface.default.hex}}; +} + +.nemo-window .primary-toolbar entry:disabled, .titlebar entry:disabled { + background-color: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.5); +} + +.nemo-window .primary-toolbar entry image, .titlebar entry image { + color: rgba(255, 255, 255, 0.7); +} + +.nemo-window .primary-toolbar entry image:hover, .titlebar entry image:hover, .nemo-window .primary-toolbar entry image:active, .titlebar entry image:active { + color: {{colors.on_surface.default.hex}}; +} + +.nemo-window .primary-toolbar entry image:disabled, .titlebar entry image:disabled { + color: rgba(255, 255, 255, 0.5); +} + +.titlebar { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + background-color: #141414; + color: {{colors.on_surface.default.hex}}; + border-radius: 12px 12px 0 0; + box-shadow: inset 0 -1px rgba(255, 255, 255, 0.12); +} + +.titlebar:disabled { + color: rgba(255, 255, 255, 0.5); +} + +.titlebar:backdrop { + color: rgba(255, 255, 255, 0.7); +} + +.titlebar:backdrop:disabled { + color: rgba(255, 255, 255, 0.32); +} + +.csd .titlebar:backdrop { + background-color: #1a1a1a; +} + +.titlebar .title { + padding: 0 12px; + font-weight: bold; +} + +.titlebar .subtitle { + padding: 0 12px; + font-size: smaller; +} + +.titlebar .subtitle, +.titlebar .dim-label { + transition: color 75ms cubic-bezier(0, 0, 0.2, 1); + color: rgba(255, 255, 255, 0.7); +} + +.titlebar .subtitle:backdrop, +.titlebar .dim-label:backdrop { + color: rgba(255, 255, 255, 0.5); +} + +.titlebar .titlebar { + background-color: transparent; + box-shadow: none; +} + +.titlebar + separator, .titlebar + separator.sidebar { + background-color: #141414; + background-image: none; + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: inset 0 -1px rgba(255, 255, 255, 0.12); +} + +.titlebar + separator:backdrop, .titlebar + separator.sidebar:backdrop { + background-color: #1a1a1a; +} + +.titlebar.selection-mode + separator, .titlebar.selection-mode + separator.sidebar, .selection-mode .titlebar + separator, .selection-mode .titlebar + separator.sidebar { + background-color: {{colors.primary.default.hex}}; +} + +.titlebar.selection-mode + separator:backdrop, .titlebar.selection-mode + separator.sidebar:backdrop, .selection-mode .titlebar + separator:backdrop, .selection-mode .titlebar + separator.sidebar:backdrop { + background-color: {{colors.primary.default.hex}}; +} + +.background.csd.unified .titlebar + separator, .background.csd.unified .titlebar + separator.sidebar { + box-shadow: inset 0 -1px rgba(255, 255, 255, 0.12); +} + +.titlebar .linked:not(.vertical) > entry { + border-radius: 6px; + margin-left: 3px; + margin-right: 3px; +} + +.titlebar button.suggested-action:disabled, .titlebar button.destructive-action:disabled { + background-color: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.5); +} + +.titlebar .path-bar button:not(.suggested-action):not(.destructive-action).text-button { + min-width: 0; + padding-left: 5px; + padding-right: 5px; +} + +.titlebar.selection-mode { + transition: background-color 0.1ms 225ms, color 75ms cubic-bezier(0, 0, 0.2, 1); + animation: ripple-on-headerbar 225ms cubic-bezier(0, 0, 0.2, 1); + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; + box-shadow: inset 0 -1px rgba(255, 255, 255, 0.12); +} + +.titlebar.selection-mode:backdrop { + color: rgba(255, 255, 255, 0.7); + background-color: #78adf9; +} + +.titlebar.selection-mode .subtitle:link { + color: {{colors.on_surface.default.hex}}; +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action) { + color: {{colors.on_surface.default.hex}}; +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):disabled { + color: rgba(255, 255, 255, 0.5); +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):checked { + color: {{colors.on_surface.default.hex}}; +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):checked:disabled { + color: rgba(255, 255, 255, 0.5); +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):backdrop:not(.titlebutton) { + color: rgba(255, 255, 255, 0.7); +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):backdrop:disabled { + color: rgba(255, 255, 255, 0.32); +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):backdrop:checked { + color: rgba(255, 255, 255, 0.7); +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):backdrop:checked:disabled { + color: rgba(255, 255, 255, 0.32); +} + +.titlebar.selection-mode .selection-menu { + padding-left: 16px; + padding-right: 16px; +} + +.titlebar.selection-mode .selection-menu arrow { + -GtkArrow-arrow-scaling: 1; +} + +.titlebar.selection-mode .selection-menu .arrow { + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); +} + +.tiled .titlebar, .tiled-top .titlebar, .tiled-right .titlebar, .tiled-bottom .titlebar, .tiled-left .titlebar, .maximized .titlebar, .fullscreen .titlebar { + border-radius: 0; +} + +.titlebar.default-decoration { + min-height: 24px; + padding: 6px 12px; + border-radius: 12px 12px 0 0; + border: none; + background-color: #141414; + background-image: none; + box-shadow: inset 0 1px rgba(255, 255, 255, 0.1); +} + +.titlebar.default-decoration:backdrop { + background-color: #1a1a1a; +} + +.tiled .titlebar.default-decoration, .maximized .titlebar.default-decoration, .fullscreen .titlebar.default-decoration { + box-shadow: none; + border-radius: 0; +} + +.titlebar.default-decoration button.titlebutton { + min-height: 24px; + min-width: 24px; + margin: 0; + padding: 0; +} + +.background.csd .titlebar.default-decoration { + padding: 6px; + box-shadow: none; +} + +.background:not(.csd) .titlebar.default-decoration button.titlebutton:active { + background-size: 1000% 1000%; +} + +.solid-csd .titlebar:dir(rtl), .solid-csd .titlebar:dir(ltr) { + border-radius: 0; + box-shadow: none; +} + +headerbar { + min-height: 46px; + padding: 0 6px; +} + +box.vertical headerbar { + background-color: #141414; +} + +headerbar entry, +headerbar spinbutton, +headerbar button, +headerbar stackswitcher { + margin-top: 6px; + margin-bottom: 6px; +} + +headerbar button, headerbar button.image-button { + border-radius: 6px; +} + +headerbar > box.left, +headerbar > box.right { + padding: 0 4px; +} + +headerbar separator.titlebutton, headerbar separator.sidebar { + margin-top: 11.5px; + margin-bottom: 11.5px; + background-color: transparent; +} + +headerbar switch { + margin-top: 11px; + margin-bottom: 11px; +} + +headerbar spinbutton button { + margin-top: 0; + margin-bottom: 0; +} + +headerbar .entry-tag, headerbar .photos-entry-tag, headerbar .documents-entry-tag { + margin-top: 5px; + margin-bottom: 5px; +} + +headerbar.windowhandle viewswitcher button:not(.titlebutton):not(.suggested-action):not(.destructive-action) { + border-radius: 0; + margin: 0; + min-width: 120px; + padding: 0; +} + +headerbar.windowhandle viewswitcher button:not(.titlebutton):not(.suggested-action):not(.destructive-action) > stack > box { + padding: 0 12px; +} + +headerbar.windowhandle viewswitcher button:not(.titlebutton):not(.suggested-action):not(.destructive-action):focus { + box-shadow: none; +} + +headerbar.windowhandle > button.popup label, headerbar.windowhandle > button.popup image { + min-height: 0; +} + +headerbar.windowhandle viewswitchertitle > squeezer { + margin-top: 0; + margin-bottom: 0; + background: none; +} + +headerbar.windowhandle viewswitchertitle > squeezer > viewswitcher { + margin: 0 0; + background: none; +} + +headerbar.windowhandle viewswitchertitle > squeezer > viewswitcher > box.horizontal > button.radio { + margin: 0; + padding: 0; + border-radius: 0; +} + +/************ + * Pathbars * + ************/ +.caja-pathbar button, +.path-bar.linked:not(.vertical) > button { + padding-left: 5px; + padding-right: 5px; + border-radius: 3px; + margin-left: 1px; + margin-right: 1px; + background-color: alpha(currentColor, 0.08); +} + +.caja-pathbar button:disabled, +.path-bar.linked:not(.vertical) > button:disabled { + background-color: alpha(currentColor, 0.05); +} + +.caja-pathbar button:first-child, +.path-bar.linked:not(.vertical) > button:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} + +.caja-pathbar button:last-child, +.path-bar.linked:not(.vertical) > button:last-child { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.caja-pathbar button:checked, +.path-bar.linked:not(.vertical) > button:checked { + background-color: alpha(currentColor, 0.16); + color: {{colors.on_surface.default.hex}}; +} + +.caja-pathbar button label, .caja-pathbar button image, +.path-bar.linked:not(.vertical) > button label, +.path-bar.linked:not(.vertical) > button image { + margin-left: 3px; + margin-right: 3px; +} + +.caja-pathbar button.slider-button, +.path-bar.linked:not(.vertical) > button.slider-button { + padding-left: 4px; + padding-right: 4px; +} + +/************** + * Tree Views * + **************/ +treeview.view { + border-left-color: rgba(255, 255, 255, 0.3); + border-top-color: rgba(255, 255, 255, 0.12); +} + +* { + -GtkTreeView-horizontal-separator: 4; + -GtkTreeView-grid-line-width: 1; + -GtkTreeView-grid-line-pattern: ''; + -GtkTreeView-tree-line-width: 1; + -GtkTreeView-tree-line-pattern: ''; + -GtkTreeView-expander-size: 16; +} + +.csd treeview.view:not(:selected):not(:hover):not(.progressbar):not(.expander):not(.trough):not(.separator) { + background-color: transparent; +} + +treeview.view:selected { + background-color: #4c4c4c; + color: {{colors.on_surface.default.hex}}; +} + +treeview.view.separator { + min-height: 6px; + color: rgba(255, 255, 255, 0.12); +} + +treeview.view:drop(active) { + border-style: solid none; + border-width: 9999px; + border-color: alpha(currentColor, 0.08); +} + +treeview.view:drop(active).after { + border-top-style: none; +} + +treeview.view:drop(active).before { + border-bottom-style: none; +} + +treeview.view.expander { + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); + -gtk-icon-transform: rotate(-90deg); + color: rgba(255, 255, 255, 0.7); +} + +treeview.view.expander:dir(rtl) { + -gtk-icon-transform: rotate(90deg); +} + +treeview.view.expander:checked { + -gtk-icon-transform: unset; +} + +treeview.view.expander:hover, treeview.view.expander:active { + color: {{colors.on_surface.default.hex}}; +} + +treeview.view.expander:disabled { + color: rgba(255, 255, 255, 0.32); +} + +treeview.view.progressbar { + border: none; + box-shadow: none; + background-color: {{colors.primary.default.hex}}; + background-image: none; + border-radius: 9999px; + color: {{colors.on_surface.default.hex}}; +} + +treeview.view.progressbar:selected, treeview.view.progressbar:selected:hover, treeview.view.progressbar:selected:focus { + box-shadow: none; + background-color: #74aaf9; + color: {{colors.on_surface.default.hex}}; +} + +treeview.view.progressbar:selected:backdrop, treeview.view.progressbar:selected:hover:backdrop, treeview.view.progressbar:selected:focus:backdrop { + color: {{colors.on_surface.default.hex}}; +} + +treeview.view.progressbar:backdrop, treeview.view.progressbar:selected:backdrop { + background-color: rgba(255, 255, 255, 0.3); +} + +treeview.view.trough { + border: none; + box-shadow: none; + background-color: rgba(255, 255, 255, 0.12); + background-image: none; + border-radius: 9999px; + padding: 0; + margin: 0; +} + +treeview.view.trough:selected, treeview.view.trough:selected:hover, treeview.view.trough:selected:focus { + box-shadow: none; + background-color: rgba(255, 255, 255, 0.12); +} + +treeview.view.trough:backdrop, treeview.view.trough:selected:backdrop { + background-color: rgba(255, 255, 255, 0.12); +} + +treeview.view header button { + padding: 2px 6px; + border-style: none solid solid none; + border-width: 1px; + border-color: rgba(255, 255, 255, 0.12); + border-radius: 0; + background-clip: border-box; +} + +treeview.view header button:not(:focus):not(:hover):not(:active) { + color: rgba(255, 255, 255, 0.7); +} + +treeview.view header button, treeview.view header button:disabled { + background-color: #1a1a1a; +} + +treeview.view header button:last-child { + border-right-style: none; +} + +treeview.view button.dnd, +treeview.view header.button.dnd { + padding: 2px 6px; + border-style: none solid solid; + border-width: 1px; + border-color: rgba(255, 255, 255, 0.12); + border-radius: 0; + box-shadow: none; + background-color: #1a1a1a; + background-clip: border-box; + color: {{colors.primary.default.hex}}; +} + +treeview.view acceleditor > label { + background-color: {{colors.primary.default.hex}}; +} + +/********* + * Menus * + *********/ +menubar, +.menubar { + -GtkWidget-window-dragging: true; + padding: 0; + background-color: #141414; + color: {{colors.on_surface.default.hex}}; + box-shadow: inset 0 -1px rgba(255, 255, 255, 0.12); +} + +menubar:backdrop, +.menubar:backdrop { + color: rgba(255, 255, 255, 0.7); + background-color: #1a1a1a; +} + +.csd menubar, .csd .menubar { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); +} + +menubar > menuitem, +.menubar > menuitem { + transition: none; + min-height: 20px; + padding: 4px 8px; + color: rgba(255, 255, 255, 0.7); + border-radius: 6px; +} + +menubar > menuitem:hover, +.menubar > menuitem:hover { + transition: none; + background-color: alpha(currentColor, 0.1); + color: {{colors.on_surface.default.hex}}; +} + +menubar > menuitem:backdrop, +.menubar > menuitem:backdrop { + color: rgba(255, 255, 255, 0.5); +} + +menubar > menuitem:disabled, +.menubar > menuitem:disabled { + color: rgba(255, 255, 255, 0.32); +} + +menubar > menuitem label:disabled, +.menubar > menuitem label:disabled { + color: inherit; +} + +menubar > menuitem > window.popup.background > menu menuitem, +.menubar > menuitem > window.popup.background > menu menuitem { + transition: none; +} + +.background.popup { + background-color: transparent; +} + +menu { + margin: 6px; + padding: 6px; + background-color: #2a2a2a; + background-clip: border-box; + border-radius: 12px; + border: 1px solid #333333; +} + +.csd menu { + border: none; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +menu menuitem { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1); + min-height: 20px; + min-width: 40px; + padding: 4px 8px; + color: {{colors.on_surface.default.hex}}; + font: initial; + text-shadow: none; + border-radius: 6px; +} + +menu menuitem:hover { + background-color: alpha(currentColor, 0.08); +} + +menu menuitem:active { + background-color: alpha(currentColor, 0.12); +} + +menu menuitem:disabled { + color: rgba(255, 255, 255, 0.5); +} + +menu menuitem accelerator { + color: rgba(255, 255, 255, 0.7); +} + +menu menuitem:disabled accelerator { + color: rgba(255, 255, 255, 0.32); +} + +menu menuitem arrow { + min-height: 16px; + min-width: 16px; +} + +menu menuitem arrow:dir(ltr) { + -gtk-icon-source: -gtk-icontheme("pan-end-symbolic"); + margin-left: 8px; +} + +menu menuitem arrow:dir(rtl) { + -gtk-icon-source: -gtk-icontheme("pan-end-symbolic-rtl"); + margin-right: 8px; +} + +menu menuitem label:dir(rtl), menu menuitem label:dir(ltr) { + color: inherit; +} + +menu .view:selected { + background-color: #505050; +} + +menu > arrow { + min-height: 16px; + min-width: 16px; + padding: 4px; + background-color: #2a2a2a; + color: rgba(255, 255, 255, 0.7); +} + +menu > arrow.top { + margin-top: 0; + border-radius: 6px; + -gtk-icon-source: -gtk-icontheme("pan-up-symbolic"); +} + +menu > arrow.bottom { + margin-top: 8px; + margin-bottom: -12px; + border-radius: 6px; + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); +} + +menu > arrow:hover { + background-image: image(alpha(currentColor, 0.08)); + color: {{colors.on_surface.default.hex}}; +} + +menu > arrow:disabled { + border-color: transparent; + background-color: transparent; + color: transparent; +} + +menu separator { + margin: 4px 0; +} + +/************ + * Popovers * + ************/ +popover.background { + transition: box-shadow 200ms ease-out; + padding: 0; + background-color: #2a2a2a; + border-radius: 12px; +} + +popover.background, .csd popover.background { + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 2px 3px -1px rgba(0, 0, 0, 0.05), 0 4px 6px 0 rgba(0, 0, 0, 0.06), 0 1px 10px 0 rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.75); + background-clip: border-box; +} + +popover.background:backdrop, .csd popover.background:backdrop { + box-shadow: 0 3px 3px -2px rgba(0, 0, 0, 0.05), 0 2px 3px -1px rgba(0, 0, 0, 0.06), 0 1px 4px 0 rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.75); +} + +popover.background > stack { + margin: -4px; +} + +popover.background > toolbar { + margin: 0; +} + +popover.background > list, +popover.background > .view, +popover.background > toolbar { + border-style: none; + box-shadow: none; + background-color: transparent; +} + +popover.background > scrolledwindow > viewport.frame > list { + background-color: transparent; + padding: 6px; +} + +popover.background > scrolledwindow > viewport.frame > list > row { + border-radius: 6px; + padding: 6px; +} + +popover.background .view:not(:selected), +popover.background toolbar { + background-color: #2a2a2a; +} + +popover.background .linked > button:not(.radio) { + border-radius: 6px; +} + +popover.background .linked > button:not(.radio):first-child { + border-radius: 6px; +} + +popover.background .linked > button:not(.radio):last-child { + border-radius: 6px; +} + +popover.background .linked > button:not(.radio):only-child { + border-radius: 6px; +} + +popover.background.menu button, +popover.background button.model { + min-height: 32px; + padding: 0 8px; + border-radius: 6px; +} + +popover.background modelbutton.flat, +popover.background .menuitem.button.flat { + min-height: 28px; + padding: 0 8px; + border-radius: 6px; + color: {{colors.on_surface.default.hex}}; +} + +popover.background modelbutton.flat + modelbutton.flat, +popover.background modelbutton.flat + box.vertical, +popover.background .menuitem.button.flat + .menuitem.button.flat { + margin-top: 2px; +} + +popover.background modelbutton.flat arrow.left { + -gtk-icon-source: -gtk-icontheme("pan-start-symbolic"); +} + +popover.background modelbutton.flat arrow.right { + -gtk-icon-source: -gtk-icontheme("pan-end-symbolic"); +} + +popover.background separator { + margin: 3px 0; +} + +popover.background list separator { + margin: 0; +} + +/************* + * Notebooks * + *************/ +tabbox > tab, notebook > header tab { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), background-size 0ms, background-image 0ms; + min-height: 24px; + min-width: 24px; + padding: 3px 12px; + border: none; + outline: none; + background-clip: padding-box; + color: rgba(255, 255, 255, 0.7); + font-weight: 500; + border-radius: 6px; +} + +tabbox > tab:hover, notebook > header tab:hover { + background-color: rgba(255, 255, 255, 0.04); + color: {{colors.on_surface.default.hex}}; +} + +tabbox > tab:disabled, notebook > header tab:disabled { + color: rgba(255, 255, 255, 0.32); +} + +tabbox > tab:checked, notebook > header tab:checked { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + background-color: rgba(255, 255, 255, 0.15); + color: {{colors.on_surface.default.hex}}; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +tabbox > tab:checked:disabled, notebook > header tab:checked:disabled { + color: rgba(255, 255, 255, 0.5); +} + +notebook { + background-color: #1a1a1a; +} + +frame > paned > notebook > header, notebook.frame > header { + background-color: rgba(255, 255, 255, 0.04); +} + +notebook.frame { + border-radius: 12px; +} + +notebook.frame scrolledwindow.frame { + border: none; +} + +notebook.frame frame > border { + border: none; + border-radius: 6px; +} + +notebook.frame frame > list row.activatable { + border-radius: 6px; +} + +notebook > header { + border: none; + background-color: rgba(255, 255, 255, 0.04); + padding: 3px; + margin: 3px; + border-radius: 9px; +} + +notebook > header.top > tabs > arrow { + border-top-style: none; +} + +notebook > header.bottom > tabs > arrow { + border-bottom-style: none; +} + +notebook > header.top > tabs > arrow, notebook > header.bottom > tabs > arrow { + padding-left: 4px; + padding-right: 4px; +} + +notebook > header.top > tabs > arrow.down, notebook > header.bottom > tabs > arrow.down { + margin-left: 0; + -gtk-icon-source: -gtk-icontheme("pan-start-symbolic"); +} + +notebook > header.top > tabs > arrow.up, notebook > header.bottom > tabs > arrow.up { + margin-right: 0; + -gtk-icon-source: -gtk-icontheme("pan-end-symbolic"); +} + +notebook > header.left > tabs > arrow { + border-left-style: none; +} + +notebook > header.right > tabs > arrow { + border-right-style: none; +} + +notebook > header.left > tabs > arrow, notebook > header.right > tabs > arrow { + padding-top: 4px; + padding-bottom: 4px; +} + +notebook > header.left > tabs > arrow.down, notebook > header.right > tabs > arrow.down { + margin-top: 0; + -gtk-icon-source: -gtk-icontheme("pan-up-symbolic"); +} + +notebook > header.left > tabs > arrow.up, notebook > header.right > tabs > arrow.up { + margin-bottom: 0; + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); +} + +notebook > header > tabs > arrow { + min-height: 16px; + min-width: 16px; + border-radius: 6px; +} + +notebook > header tab > box { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1); + margin: -6px -12px; + padding: 6px 12px; +} + +notebook > header tab > box:drop(active) { + background-color: rgba(255, 255, 255, 0.12); + color: {{colors.on_surface.default.hex}}; +} + +notebook > header tab button.flat:last-child, notebook > header tab button.close-button:last-child { + margin-left: 6px; + margin-right: -6px; +} + +notebook > header tab button.flat:first-child, notebook > header tab button.close-button:first-child { + margin-left: -6px; + margin-right: 6px; +} + +notebook > header.top tabs > tab + tab, notebook > header.bottom tabs > tab + tab { + margin-left: 3px; +} + +notebook > header.top tabs:not(:only-child):first-child, notebook > header.bottom tabs:not(:only-child):first-child { + margin-left: 0; +} + +notebook > header.top tabs:not(:only-child):last-child, notebook > header.bottom tabs:not(:only-child):last-child { + margin-right: 0; +} + +notebook > header.top tabs tab.reorderable-page, notebook > header.bottom tabs tab.reorderable-page { + border-style: solid; +} + +notebook > header.left tabs > tab + tab, notebook > header.right tabs > tab + tab { + margin-top: 3px; +} + +notebook > header.left tabs:not(:only-child):first-child, notebook > header.right tabs:not(:only-child):first-child { + margin-top: 0; +} + +notebook > header.left tabs:not(:only-child):last-child, notebook > header.right tabs:not(:only-child):last-child { + margin-bottom: 0; +} + +notebook > header.left tabs tab.reorderable-page, notebook > header.right tabs tab.reorderable-page { + border-style: solid; +} + +notebook > header > button.image-button { + min-height: 24px; + min-width: 24px; + padding: 3px; +} + +notebook > stack:not(:only-child) { + background-color: transparent; + border-radius: 6px; +} + +/************** + * Scrollbars * + **************/ +scrollbar { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + background-color: #1a1a1a; +} + +* { + -GtkScrollbar-has-backward-stepper: false; + -GtkScrollbar-has-forward-stepper: false; +} + +scrollbar.top { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +scrollbar.bottom { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +scrollbar.left { + border-right: 1px solid rgba(255, 255, 255, 0.12); +} + +scrollbar.right { + border-left: 1px solid rgba(255, 255, 255, 0.12); +} + +scrollbar slider { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1); + min-width: 8px; + min-height: 8px; + border: 4px solid transparent; + border-radius: 9999px; + background-clip: padding-box; + background-color: rgba(255, 255, 255, 0.5); +} + +scrollbar slider:hover { + background-color: rgba(255, 255, 255, 0.7); +} + +scrollbar slider:active { + background-color: {{colors.on_surface.default.hex}}; +} + +scrollbar slider:disabled { + background-color: rgba(255, 255, 255, 0.32); +} + +scrollbar.fine-tune slider { + min-width: 4px; + min-height: 4px; +} + +scrollbar.fine-tune.horizontal slider { + margin: 2px 0; +} + +scrollbar.fine-tune.vertical slider { + margin: 0 2px; +} + +scrollbar.overlay-indicator:not(.fine-tune) slider { + transition-property: background-color, min-height, min-width; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering) { + border-color: transparent; + background-color: transparent; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering) slider { + min-width: 4px; + min-height: 4px; + margin: 3px; + border: 1px solid rgba(44, 44, 44, 0.3); +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering) button { + min-width: 4px; + min-height: 4px; + margin: 3px; + border: 1px solid rgba(44, 44, 44, 0.3); + border-radius: 9999px; + background-color: rgba(255, 255, 255, 0.5); + background-clip: padding-box; + -gtk-icon-source: none; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering) button:disabled { + background-color: rgba(255, 255, 255, 0.32); +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering).horizontal slider { + min-width: 24px; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering).horizontal button { + min-width: 8px; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering).vertical slider { + min-height: 24px; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering).vertical button { + min-height: 8px; +} + +scrollbar.overlay-indicator.dragging, scrollbar.overlay-indicator.hovering { + background-color: rgba(60, 60, 60, 0.9); +} + +scrollbar.horizontal slider { + min-width: 24px; +} + +scrollbar.vertical slider { + min-height: 24px; +} + +scrollbar button { + min-width: 16px; + min-height: 16px; + padding: 0; + border-radius: 0; +} + +scrollbar.vertical button.down { + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); +} + +scrollbar.vertical button.up { + -gtk-icon-source: -gtk-icontheme("pan-up-symbolic"); +} + +scrollbar.horizontal button.down { + -gtk-icon-source: -gtk-icontheme("pan-end-symbolic"); +} + +scrollbar.horizontal button.up { + -gtk-icon-source: -gtk-icontheme("pan-start-symbolic"); +} + +/********** + * Switch * + **********/ +switch { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + margin: 6px 0; + border: none; + border-radius: 9999px; + background-color: rgba(255, 255, 255, 0.5); + background-clip: padding-box; + font-size: 0; + color: transparent; +} + +switch:checked { + background-color: {{colors.primary.default.hex}}; +} + +switch:disabled { + opacity: 0.5; +} + +switch slider { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + min-width: 18px; + min-height: 18px; + margin: 3px; + border-radius: 9999px; + outline: none; + box-shadow: 0 3px 3px -2px rgba(0, 0, 0, 0.05), 0 2px 3px -1px rgba(0, 0, 0, 0.06), 0 1px 4px 0 rgba(0, 0, 0, 0.05); + background-color: white; + border: none; + color: transparent; +} + +switch:focus slider, switch:hover slider, switch:focus:hover slider { + box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.12); +} + +/************************* + * Check and Radio items * + *************************/ +.view.content-view.check:not(list), +.content-view .tile check:not(list) { + min-height: 40px; + min-width: 40px; + margin: 0; + padding: 0; + box-shadow: none; + background-color: transparent; + background-image: none; +} + +.view.content-view.check:not(list), +.content-view .tile check:not(list) { + -gtk-icon-source: -gtk-scaled(url("assets/selectionmode-checkbox-unchecked-dark.svg"), url("assets/selectionmode-checkbox-unchecked-dark@2.svg")); +} + +.view.content-view.check:not(list):checked, +.content-view .tile check:not(list):checked { + -gtk-icon-source: -gtk-scaled(url("assets/selectionmode-checkbox-checked-dark.svg"), url("assets/selectionmode-checkbox-checked-dark@2.svg")); +} + +checkbutton, +radiobutton { + outline: none; +} + +checkbutton.text-button, +radiobutton.text-button { + padding: 2px; +} + +checkbutton.text-button label:not(:only-child), +radiobutton.text-button label:not(:only-child) { + margin: 0 4px; +} + +check, +radio { + min-height: 20px; + min-width: 20px; + margin: 3px; + padding: 0; + border-radius: 9999px; + color: transparent; + background-color: rgba(255, 255, 255, 0.12); + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), box-shadow 150ms cubic-bezier(0, 0, 0.2, 1); +} + +check:hover, +radio:hover { + box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.04); + background-color: rgba(255, 255, 255, 0.15); +} + +check:active, +radio:active { + box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.12); + background-color: rgba(255, 255, 255, 0.2); +} + +check:focus, +radio:focus { + outline: none; +} + +check:disabled, +radio:disabled { + background-color: rgba(255, 255, 255, 0.04); +} + +check:checked, check:indeterminate, +radio:checked, +radio:indeterminate { + color: {{colors.on_surface.default.hex}}; + background-color: {{colors.primary.default.hex}}; +} + +check:checked:hover, check:indeterminate:hover, +radio:checked:hover, +radio:indeterminate:hover { + box-shadow: 0 0 0 6px rgba(91, 155, 248, 0.15); + background-color: #8cb9fa; +} + +check:checked:active, check:indeterminate:active, +radio:checked:active, +radio:indeterminate:active { + box-shadow: 0 0 0 6px rgba(91, 155, 248, 0.2); + background-color: {{colors.primary.default.hex}}; +} + +check:checked:disabled, check:indeterminate:disabled, +radio:checked:disabled, +radio:indeterminate:disabled { + color: rgba(255, 255, 255, 0.5); + background-color: rgba(91, 155, 248, 0.35); +} + +popover modelbutton.flat check, popover modelbutton.flat check:focus, popover modelbutton.flat check:hover, popover modelbutton.flat check:focus:hover, popover modelbutton.flat check:active, popover modelbutton.flat check:disabled, popover modelbutton.flat radio, popover modelbutton.flat radio:focus, popover modelbutton.flat radio:hover, popover modelbutton.flat radio:focus:hover, popover modelbutton.flat radio:active, popover modelbutton.flat radio:disabled { + transition: none; + box-shadow: none; + background-image: none; +} + +popover modelbutton.flat check.left:dir(rtl), popover modelbutton.flat radio.left:dir(rtl) { + margin-left: -3px; + margin-right: 6px; +} + +popover modelbutton.flat check.right:dir(ltr), popover modelbutton.flat radio.right:dir(ltr) { + margin-left: 6px; + margin-right: -3px; +} + +menu menuitem check, menu menuitem radio { + transition: none; + margin: 0; + padding: 0; +} + +menu menuitem check:dir(ltr), menu menuitem radio:dir(ltr) { + margin-right: 6px; + margin-left: -3px; +} + +menu menuitem check:dir(rtl), menu menuitem radio:dir(rtl) { + margin-left: 6px; + margin-right: -3px; +} + +menu menuitem check, menu menuitem check:hover, menu menuitem check:disabled, menu menuitem check:checked:hover, menu menuitem check:indeterminate:hover, menu menuitem radio, menu menuitem radio:hover, menu menuitem radio:disabled, menu menuitem radio:checked:hover, menu menuitem radio:indeterminate:hover { + box-shadow: none; +} + + +check:checked { + -gtk-icon-source: -gtk-recolor(url("assets/checkbox-checked-symbolic.svg")); +} + + +check:indeterminate { + -gtk-icon-source: -gtk-recolor(url("assets/checkbox-mixed-symbolic.svg")); +} + + +radio:checked { + -gtk-icon-source: -gtk-recolor(url("assets/radio-checked-symbolic.svg")); +} + + +radio:indeterminate { + -gtk-icon-source: -gtk-recolor(url("assets/radio-mixed-symbolic.svg")); +} + +#MozillaGtkWidget > widget > checkbutton > check, +menu menuitem check { + min-height: 16px; + min-width: 16px; +} + +#MozillaGtkWidget > widget > checkbutton > check:checked, +menu menuitem check:checked { + -gtk-icon-source: -gtk-recolor(url("assets/small-checkbox-checked-symbolic.svg")); +} + +#MozillaGtkWidget > widget > checkbutton > check:indeterminate, +menu menuitem check:indeterminate { + -gtk-icon-source: -gtk-recolor(url("assets/small-checkbox-mixed-symbolic.svg")); +} + +#MozillaGtkWidget > widget > radiobutton > radio, +menu menuitem radio { + min-height: 16px; + min-width: 16px; +} + +#MozillaGtkWidget > widget > radiobutton > radio:checked, +menu menuitem radio:checked { + -gtk-icon-source: -gtk-recolor(url("assets/small-radio-checked-symbolic.svg")); +} + +#MozillaGtkWidget > widget > radiobutton > radio:indeterminate, +menu menuitem radio:indeterminate { + -gtk-icon-source: -gtk-recolor(url("assets/small-radio-mixed-symbolic.svg")); +} + +check:not(:checked):active { + -gtk-icon-transform: rotate(90deg); +} + +check:not(:checked):indeterminate:active, +radio:not(:checked):indeterminate:active { + -gtk-icon-transform: scaleX(-1); +} + +treeview.view radio, treeview.view check { + padding: 0; + margin: 0; +} + +treeview.view radio:not(:disabled):not(:checked):not(:indeterminate), treeview.view check:not(:disabled):not(:checked):not(:indeterminate) { + background-color: rgba(255, 255, 255, 0.12); +} + +treeview.view radio, treeview.view radio:hover, treeview.view radio:disabled, treeview.view radio:checked:hover, treeview.view radio:indeterminate:hover, treeview.view check, treeview.view check:hover, treeview.view check:disabled, treeview.view check:checked:hover, treeview.view check:indeterminate:hover { + box-shadow: none; +} + +treeview.view radio:checked, treeview.view radio:indeterminate, treeview.view check:checked, treeview.view check:indeterminate, treeview.view:hover radio:checked, treeview.view:hover radio:indeterminate, treeview.view:hover check:checked, treeview.view:hover check:indeterminate, treeview.view:selected radio:checked, treeview.view:selected radio:indeterminate, treeview.view:selected check:checked, treeview.view:selected check:indeterminate { + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; + background-image: none; +} + +/************ + * GtkScale * + ************/ +scale { + min-height: 2px; + min-width: 2px; +} + +scale.horizontal { + padding: 16px 12px; +} + +scale.vertical { + padding: 12px 16px; +} + +scale slider { + min-height: 18px; + min-width: 18px; + margin: -8px; +} + +scale.fine-tune.horizontal { + min-height: 4px; + padding-top: 15px; + padding-bottom: 15px; +} + +scale.fine-tune.vertical { + min-width: 4px; + padding-left: 15px; + padding-right: 15px; +} + +scale.fine-tune slider { + margin: -7px; +} + +scale trough { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1); + outline: none; + background-color: rgba(255, 255, 255, 0.3); +} + +scale trough:disabled { + background-color: rgba(255, 255, 255, 0.12); +} + +scale highlight { + transition: background-image 75ms cubic-bezier(0, 0, 0.2, 1); + background-image: image({{colors.primary.default.hex}}); +} + +scale highlight:disabled { + background-color: #1a1a1a; + background-image: image(rgba(255, 255, 255, 0.32)); +} + +scale fill { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1); + background-color: rgba(255, 255, 255, 0.3); +} + +scale fill:disabled { + background-color: transparent; +} + +scale slider { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + border-radius: 9999px; + color: {{colors.primary.default.hex}}; + background-color: #1a1a1a; + box-shadow: inset 0 0 0 2px {{colors.primary.default.hex}}; +} + +scale slider:hover { + box-shadow: inset 0 0 0 2px {{colors.primary.default.hex}}, 0 0 0 8px rgba(255, 255, 255, 0.12); +} + +scale slider:active { + box-shadow: inset 0 0 0 4px {{colors.primary.default.hex}}, 0 0 0 8px rgba(255, 255, 255, 0.12); +} + +scale slider:disabled { + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.32); +} + +scale marks, +scale value { + color: rgba(255, 255, 255, 0.7); +} + +scale indicator { + background-color: rgba(255, 255, 255, 0.3); + color: transparent; +} + +scale.horizontal marks.top { + margin-bottom: 7px; + margin-top: -15px; +} + +scale.horizontal.fine-tune marks.top { + margin-bottom: 6px; + margin-top: -14px; +} + +scale.horizontal marks.bottom { + margin-top: 7px; + margin-bottom: -15px; +} + +scale.horizontal.fine-tune marks.bottom { + margin-top: 6px; + margin-bottom: -14px; +} + +scale.vertical marks.top { + margin-right: 7px; + margin-left: -15px; +} + +scale.vertical.fine-tune marks.top { + margin-right: 6px; + margin-left: -14px; +} + +scale.vertical marks.bottom { + margin-left: 7px; + margin-right: -15px; +} + +scale.vertical.fine-tune marks.bottom { + margin-left: 6px; + margin-right: -14px; +} + +scale.horizontal indicator { + min-height: 8px; + min-width: 1px; +} + +scale.vertical indicator { + min-height: 1px; + min-width: 8px; +} + +scale.horizontal.marks-before:not(.marks-after) slider { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1), background-size 300ms cubic-bezier(0, 0, 0.2, 1), background-image 1200ms cubic-bezier(0, 0, 0.2, 1); + min-height: 32px; + min-width: 32px; + margin: -15px; + border-radius: 50%; + background-size: auto, 1000% 1000%; + background-repeat: no-repeat; + background-position: center center; + background-color: transparent; +} + +scale.horizontal.marks-before:not(.marks-after) slider, scale.horizontal.marks-before:not(.marks-after) slider:hover, scale.horizontal.marks-before:not(.marks-after) slider:active, scale.horizontal.marks-before:not(.marks-after) slider:disabled { + box-shadow: none; +} + +scale.horizontal.marks-before:not(.marks-after) slider:focus { + background-color: alpha(currentColor, 0.08); +} + +scale.horizontal.marks-before:not(.marks-after) slider:hover { + background-color: alpha(currentColor, 0.08); +} + +scale.horizontal.marks-before:not(.marks-after) slider:focus:hover { + background-color: alpha(currentColor, 0.16); +} + +scale.horizontal.marks-before:not(.marks-after) slider:active { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1), background-size 0ms, background-image 0ms; + animation: ripple-on-slider 225ms cubic-bezier(0, 0, 0.2, 1) forwards; + background-size: auto, 0% 0%; + background-color: alpha(currentColor, 0.08); +} + +scale.horizontal.marks-before:not(.marks-after) slider { + background-image: -gtk-scaled(url("assets/scale-horz-marks-before-slider-dark.svg"), url("assets/scale-horz-marks-before-slider-dark@2.svg")), radial-gradient(circle, transparent 10%, transparent 0%); +} + +scale.horizontal.marks-before:not(.marks-after) slider:disabled { + background-image: -gtk-scaled(url("assets/scale-horz-marks-before-slider-disabled-dark.svg"), url("assets/scale-horz-marks-before-slider-disabled-dark@2.svg")), radial-gradient(circle, transparent 10%, transparent 0%); +} + +scale.horizontal.marks-before:not(.marks-after) slider:active { + background-image: -gtk-scaled(url("assets/scale-horz-marks-before-slider-dark.svg"), url("assets/scale-horz-marks-before-slider-dark@2.svg")), radial-gradient(circle, alpha(currentColor, 0.12) 10%, transparent 0%); +} + +scale.horizontal.marks-after:not(.marks-before) slider { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1), background-size 300ms cubic-bezier(0, 0, 0.2, 1), background-image 1200ms cubic-bezier(0, 0, 0.2, 1); + min-height: 32px; + min-width: 32px; + margin: -15px; + border-radius: 50%; + background-size: auto, 1000% 1000%; + background-repeat: no-repeat; + background-position: center center; + background-color: transparent; +} + +scale.horizontal.marks-after:not(.marks-before) slider, scale.horizontal.marks-after:not(.marks-before) slider:hover, scale.horizontal.marks-after:not(.marks-before) slider:active, scale.horizontal.marks-after:not(.marks-before) slider:disabled { + box-shadow: none; +} + +scale.horizontal.marks-after:not(.marks-before) slider:focus { + background-color: alpha(currentColor, 0.08); +} + +scale.horizontal.marks-after:not(.marks-before) slider:hover { + background-color: alpha(currentColor, 0.08); +} + +scale.horizontal.marks-after:not(.marks-before) slider:focus:hover { + background-color: alpha(currentColor, 0.16); +} + +scale.horizontal.marks-after:not(.marks-before) slider:active { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1), background-size 0ms, background-image 0ms; + animation: ripple-on-slider 225ms cubic-bezier(0, 0, 0.2, 1) forwards; + background-size: auto, 0% 0%; + background-color: alpha(currentColor, 0.08); +} + +scale.horizontal.marks-after:not(.marks-before) slider { + background-image: -gtk-scaled(url("assets/scale-horz-marks-after-slider-dark.svg"), url("assets/scale-horz-marks-after-slider-dark@2.svg")), radial-gradient(circle, transparent 10%, transparent 0%); +} + +scale.horizontal.marks-after:not(.marks-before) slider:disabled { + background-image: -gtk-scaled(url("assets/scale-horz-marks-after-slider-disabled-dark.svg"), url("assets/scale-horz-marks-after-slider-disabled-dark@2.svg")), radial-gradient(circle, transparent 10%, transparent 0%); +} + +scale.horizontal.marks-after:not(.marks-before) slider:active { + background-image: -gtk-scaled(url("assets/scale-horz-marks-after-slider-dark.svg"), url("assets/scale-horz-marks-after-slider-dark@2.svg")), radial-gradient(circle, alpha(currentColor, 0.12) 10%, transparent 0%); +} + +scale.vertical.marks-before:not(.marks-after) slider { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1), background-size 300ms cubic-bezier(0, 0, 0.2, 1), background-image 1200ms cubic-bezier(0, 0, 0.2, 1); + min-height: 32px; + min-width: 32px; + margin: -15px; + border-radius: 50%; + background-size: auto, 1000% 1000%; + background-repeat: no-repeat; + background-position: center center; + background-color: transparent; +} + +scale.vertical.marks-before:not(.marks-after) slider, scale.vertical.marks-before:not(.marks-after) slider:hover, scale.vertical.marks-before:not(.marks-after) slider:active, scale.vertical.marks-before:not(.marks-after) slider:disabled { + box-shadow: none; +} + +scale.vertical.marks-before:not(.marks-after) slider:focus { + background-color: alpha(currentColor, 0.08); +} + +scale.vertical.marks-before:not(.marks-after) slider:hover { + background-color: alpha(currentColor, 0.08); +} + +scale.vertical.marks-before:not(.marks-after) slider:focus:hover { + background-color: alpha(currentColor, 0.16); +} + +scale.vertical.marks-before:not(.marks-after) slider:active { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1), background-size 0ms, background-image 0ms; + animation: ripple-on-slider 225ms cubic-bezier(0, 0, 0.2, 1) forwards; + background-size: auto, 0% 0%; + background-color: alpha(currentColor, 0.08); +} + +scale.vertical.marks-before:not(.marks-after) slider { + background-image: -gtk-scaled(url("assets/scale-vert-marks-before-slider-dark.svg"), url("assets/scale-vert-marks-before-slider-dark@2.svg")), radial-gradient(circle, transparent 10%, transparent 0%); +} + +scale.vertical.marks-before:not(.marks-after) slider:disabled { + background-image: -gtk-scaled(url("assets/scale-vert-marks-before-slider-disabled-dark.svg"), url("assets/scale-vert-marks-before-slider-disabled-dark@2.svg")), radial-gradient(circle, transparent 10%, transparent 0%); +} + +scale.vertical.marks-before:not(.marks-after) slider:active { + background-image: -gtk-scaled(url("assets/scale-vert-marks-before-slider-dark.svg"), url("assets/scale-vert-marks-before-slider-dark@2.svg")), radial-gradient(circle, alpha(currentColor, 0.12) 10%, transparent 0%); +} + +scale.vertical.marks-after:not(.marks-before) slider { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1), background-size 300ms cubic-bezier(0, 0, 0.2, 1), background-image 1200ms cubic-bezier(0, 0, 0.2, 1); + min-height: 32px; + min-width: 32px; + margin: -15px; + border-radius: 50%; + background-size: auto, 1000% 1000%; + background-repeat: no-repeat; + background-position: center center; + background-color: transparent; +} + +scale.vertical.marks-after:not(.marks-before) slider, scale.vertical.marks-after:not(.marks-before) slider:hover, scale.vertical.marks-after:not(.marks-before) slider:active, scale.vertical.marks-after:not(.marks-before) slider:disabled { + box-shadow: none; +} + +scale.vertical.marks-after:not(.marks-before) slider:focus { + background-color: alpha(currentColor, 0.08); +} + +scale.vertical.marks-after:not(.marks-before) slider:hover { + background-color: alpha(currentColor, 0.08); +} + +scale.vertical.marks-after:not(.marks-before) slider:focus:hover { + background-color: alpha(currentColor, 0.16); +} + +scale.vertical.marks-after:not(.marks-before) slider:active { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1), background-size 0ms, background-image 0ms; + animation: ripple-on-slider 225ms cubic-bezier(0, 0, 0.2, 1) forwards; + background-size: auto, 0% 0%; + background-color: alpha(currentColor, 0.08); +} + +scale.vertical.marks-after:not(.marks-before) slider { + background-image: -gtk-scaled(url("assets/scale-vert-marks-after-slider-dark.svg"), url("assets/scale-vert-marks-after-slider-dark@2.svg")), radial-gradient(circle, transparent 10%, transparent 0%); +} + +scale.vertical.marks-after:not(.marks-before) slider:disabled { + background-image: -gtk-scaled(url("assets/scale-vert-marks-after-slider-disabled-dark.svg"), url("assets/scale-vert-marks-after-slider-disabled-dark@2.svg")), radial-gradient(circle, transparent 10%, transparent 0%); +} + +scale.vertical.marks-after:not(.marks-before) slider:active { + background-image: -gtk-scaled(url("assets/scale-vert-marks-after-slider-dark.svg"), url("assets/scale-vert-marks-after-slider-dark@2.svg")), radial-gradient(circle, alpha(currentColor, 0.12) 10%, transparent 0%); +} + +scale.color { + min-height: 0; + min-width: 0; +} + +scale.color.horizontal { + padding: 0 0 12px 0; +} + +scale.color.horizontal slider:dir(ltr), scale.color.horizontal slider:dir(rtl) { + margin-bottom: -13.5px; + margin-top: 11.5px; +} + +scale.color.vertical:dir(ltr) { + padding: 0 0 0 12px; +} + +scale.color.vertical:dir(ltr) slider { + margin-left: -13.5px; + margin-right: 11.5px; +} + +scale.color.vertical:dir(rtl) { + padding: 0 12px 0 0; +} + +scale.color.vertical:dir(rtl) slider { + margin-right: -13.5px; + margin-left: 11.5px; +} + +/***************** + * Progress bars * + *****************/ +progressbar { + color: rgba(255, 255, 255, 0.7); + font-size: smaller; +} + +progressbar.horizontal trough, +progressbar.horizontal progress { + min-height: 6px; +} + +progressbar.vertical trough, +progressbar.vertical progress { + min-width: 6px; +} + +progressbar trough { + border-radius: 6px; + background-color: rgba(255, 255, 255, 0.12); +} + +progressbar progress { + border-radius: 6px; + background-color: {{colors.primary.default.hex}}; +} + +progressbar.osd { + min-width: 6px; + min-height: 6px; + background-color: transparent; +} + +progressbar.osd trough { + background-color: transparent; +} + +progressbar.osd progress { + background-color: {{colors.primary.default.hex}}; +} + +progressbar trough.empty progress { + all: unset; +} + +/************* + * Level Bar * + *************/ +levelbar.horizontal block { + min-height: 6px; +} + +levelbar.horizontal.discrete block { + min-width: 34px; +} + +levelbar.horizontal.discrete block:not(:last-child) { + margin-right: 2px; +} + +levelbar.vertical block { + min-width: 6px; +} + +levelbar.vertical.discrete block { + min-height: 34px; +} + +levelbar.vertical.discrete block:not(:last-child) { + margin-bottom: 2px; +} + +levelbar trough { + border-radius: 6px; +} + +levelbar block.low { + background-color: #FFD600; +} + +levelbar block.high, levelbar block:not(.empty) { + background-color: {{colors.primary.default.hex}}; +} + +levelbar block.full { + background-color: #66BB6A; +} + +levelbar block.empty { + background-color: rgba(255, 255, 255, 0.12); +} + +/**************** + * Print dialog * +*****************/ +printdialog paper { + padding: 0; + border: 1px solid rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; + color: {{colors.on_surface.default.hex}}; +} + +printdialog .dialog-action-box { + margin: 12px; +} + +/********** + * Frames * + **********/ +frame > border, .frame { + margin: 0; + padding: 0; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 0; + box-shadow: none; +} + +.frame.view { + border-radius: 6px; +} + +.frame.flat { + border-style: none; +} + +frame.flat > border, frame > border.flat, statusbar frame > border { + border: none; +} + +actionbar > revealer > box { + padding: 6px; + border-top: 1px solid rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; + background-clip: border-box; +} + +.background.csd box.vertical > actionbar > revealer > box .background.csd box.vertical > revealer > actionbar > revealer > box { + border-radius: 0 0 12px 12px; +} + +scrolledwindow viewport.frame { + border: none; +} + +stack scrolledwindow.frame viewport.frame list { + border: none; +} + +overshoot.top { + background-image: -gtk-gradient(radial, center top, 0, center top, 0.5, to(rgba(255, 255, 255, 0.12)), to(rgba(255, 255, 255, 0))), -gtk-gradient(radial, center top, 0, center top, 0.6, from(rgba(255, 255, 255, 0.07)), to(rgba(255, 255, 255, 0))); + background-size: 100% 5%, 100% 100%; + background-repeat: no-repeat; + background-position: center top; + background-color: transparent; + border: none; + box-shadow: none; +} + +overshoot.bottom { + background-image: -gtk-gradient(radial, center bottom, 0, center bottom, 0.5, to(rgba(255, 255, 255, 0.12)), to(rgba(255, 255, 255, 0))), -gtk-gradient(radial, center bottom, 0, center bottom, 0.6, from(rgba(255, 255, 255, 0.07)), to(rgba(255, 255, 255, 0))); + background-size: 100% 5%, 100% 100%; + background-repeat: no-repeat; + background-position: center bottom; + background-color: transparent; + border: none; + box-shadow: none; +} + +overshoot.left { + background-image: -gtk-gradient(radial, left center, 0, left center, 0.5, to(rgba(255, 255, 255, 0.12)), to(rgba(255, 255, 255, 0))), -gtk-gradient(radial, left center, 0, left center, 0.6, from(rgba(255, 255, 255, 0.07)), to(rgba(255, 255, 255, 0))); + background-size: 5% 100%, 100% 100%; + background-repeat: no-repeat; + background-position: left center; + background-color: transparent; + border: none; + box-shadow: none; +} + +overshoot.right { + background-image: -gtk-gradient(radial, right center, 0, right center, 0.5, to(rgba(255, 255, 255, 0.12)), to(rgba(255, 255, 255, 0))), -gtk-gradient(radial, right center, 0, right center, 0.6, from(rgba(255, 255, 255, 0.07)), to(rgba(255, 255, 255, 0))); + background-size: 5% 100%, 100% 100%; + background-repeat: no-repeat; + background-position: right center; + background-color: transparent; + border: none; + box-shadow: none; +} + +junction { + border-style: solid none none solid; + border-width: 1px; + border-color: rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; +} + +junction:dir(rtl) { + border-style: solid solid none none; +} + +separator { + min-width: 1px; + min-height: 1px; + background-color: rgba(255, 255, 255, 0.12); +} + +preferences stacksidebar.sidebar list separator, hdyleaflet > box.vertical > scrolledwindow > viewport.frame list:not(.view):not(.tweak-group):not(.tweak-group-startup) separator, leaflet > box.vertical > scrolledwindow > viewport.frame list:not(.view):not(.tweak-group):not(.tweak-group-startup) separator, box.horizontal > stack.background > box.vertical > scrolledwindow > viewport.frame list:not(.view):not(.tweak-group):not(.tweak-group-startup) separator, window.background.csd > leaflet > box.vertical > scrolledwindow.view > viewport.frame > stack list separator, +window.background.csd > hdyleaflet > box.vertical > scrolledwindow.view > viewport.frame > stack list separator, +window.background.csd > box.horizontal > box.vertical > scrolledwindow.view > viewport.frame > stack list separator, .tweak-categories separator, placessidebar.sidebar list > separator, stacksidebar.sidebar + separator.vertical, +stacksidebar.sidebar separator.horizontal, button.font separator, button.file separator { + min-width: 0; + min-height: 0; + background-color: transparent; +} + +/********* + * Lists * + *********/ +window.background.csd stack stack stack frame > list, +window.background.csd > stack > stack > box > frame > list, +window.background.csd > stack > stack > box > box > frame > list, +window.background.csd > stack > box > stack > box > frame > list, +window.background.csd > stack > box > stack > scrolledwindow > viewport frame > list, +window.background.csd > stack > box > stack > box > scrolledwindow > viewport > frame > list, +window.background.csd > stack > grid > scrolledwindow > viewport > box > frame > list, window.background.csd > stack > list, +window.background.csd > stack > scrolledwindow > viewport > box > list, +window.background.csd > box > stack > scrolledwindow > viewport > box > list, preferencesgroup list, .geary-accounts-editor-pane list, window.background.csd.unified > deck > deck > deck list, hdyleaflet list.view, hdyleaflet list.frame, leaflet list.view, leaflet list.frame, box.horizontal > stack.background list.view, box.horizontal > stack.background list.frame, hdyleaflet stack.background scrolledwindow > viewport list, hdyleaflet overlay scrolledwindow > viewport list, leaflet stack.background scrolledwindow > viewport list, leaflet overlay scrolledwindow > viewport list, box.horizontal > stack.background stack.background scrolledwindow > viewport list, box.horizontal > stack.background overlay scrolledwindow > viewport list, hdyleaflet frame:not(.view) list:not(.contacts-contact-list), leaflet frame:not(.view) list:not(.contacts-contact-list), box.horizontal > stack.background frame:not(.view) list:not(.contacts-contact-list), list.tweak-group list, list#ListBoxTweakGroup list, .tweak-group-startup, list.content:not(.conversation-listbox) { + border-radius: 7px; + box-shadow: none; + border: 1px solid rgba(255, 255, 255, 0.12); + background-color: #2a2a2a; +} + +window.background.csd stack stack stack frame > list > separator, +window.background.csd > stack > stack > box > frame > list > separator, +window.background.csd > stack > box > stack > scrolledwindow > viewport frame > list > separator, +window.background.csd > stack > grid > scrolledwindow > viewport > box > frame > list > separator, window.background.csd > stack > list > separator, +window.background.csd > stack > scrolledwindow > viewport > box > list > separator, preferencesgroup list > separator, .geary-accounts-editor-pane list > separator, window.background.csd.unified > deck > deck > deck list > separator, hdyleaflet list.view > separator, hdyleaflet list.frame > separator, leaflet list.view > separator, leaflet list.frame > separator, box.horizontal > stack.background list.view > separator, box.horizontal > stack.background list.frame > separator, hdyleaflet stack.background scrolledwindow > viewport list > separator, hdyleaflet overlay scrolledwindow > viewport list > separator, leaflet stack.background scrolledwindow > viewport list > separator, leaflet overlay scrolledwindow > viewport list > separator, box.horizontal > stack.background stack.background scrolledwindow > viewport list > separator, box.horizontal > stack.background overlay scrolledwindow > viewport list > separator, hdyleaflet frame:not(.view) list:not(.contacts-contact-list) > separator, leaflet frame:not(.view) list:not(.contacts-contact-list) > separator, box.horizontal > stack.background frame:not(.view) list:not(.contacts-contact-list) > separator, list.tweak-group list > separator, list#ListBoxTweakGroup list > separator, .tweak-group-startup > separator, list.content:not(.conversation-listbox) > separator { + background: none; + min-height: 0; +} + +window.background.csd stack stack stack frame > list row, +window.background.csd > stack > stack > box > frame > list row, +window.background.csd > stack > box > stack > scrolledwindow > viewport frame > list row, +window.background.csd > stack > grid > scrolledwindow > viewport > box > frame > list row, window.background.csd > stack > list row, +window.background.csd > stack > scrolledwindow > viewport > box > list row, preferencesgroup list row, .geary-accounts-editor-pane list row, window.background.csd.unified > deck > deck > deck list row, hdyleaflet list.view row, hdyleaflet list.frame row, leaflet list.view row, leaflet list.frame row, box.horizontal > stack.background list.view row, box.horizontal > stack.background list.frame row, hdyleaflet stack.background scrolledwindow > viewport list row, hdyleaflet overlay scrolledwindow > viewport list row, leaflet stack.background scrolledwindow > viewport list row, leaflet overlay scrolledwindow > viewport list row, box.horizontal > stack.background stack.background scrolledwindow > viewport list row, box.horizontal > stack.background overlay scrolledwindow > viewport list row, hdyleaflet frame:not(.view) list:not(.contacts-contact-list) row, leaflet frame:not(.view) list:not(.contacts-contact-list) row, box.horizontal > stack.background frame:not(.view) list:not(.contacts-contact-list) row, list.tweak-group list row, list#ListBoxTweakGroup list row, .tweak-group-startup > row, list.content:not(.conversation-listbox) > row { + border-radius: 0; +} + +window.background.csd stack stack stack frame > list row:not(:first-child), window.background.csd > stack > list row:not(:first-child), preferencesgroup list row:not(:first-child), .geary-accounts-editor-pane list row:not(:first-child), window.background.csd.unified > deck > deck > deck list row:not(:first-child), hdyleaflet list.view row:not(:first-child), hdyleaflet list.frame row:not(:first-child), leaflet list.view row:not(:first-child), leaflet list.frame row:not(:first-child), box.horizontal > stack.background list.view row:not(:first-child), box.horizontal > stack.background list.frame row:not(:first-child), hdyleaflet stack.background scrolledwindow > viewport list row:not(:first-child), hdyleaflet overlay scrolledwindow > viewport list row:not(:first-child), leaflet stack.background scrolledwindow > viewport list row:not(:first-child), leaflet overlay scrolledwindow > viewport list row:not(:first-child), box.horizontal > stack.background stack.background scrolledwindow > viewport list row:not(:first-child), box.horizontal > stack.background overlay scrolledwindow > viewport list row:not(:first-child), hdyleaflet frame:not(.view) list:not(.contacts-contact-list) row:not(:first-child), leaflet frame:not(.view) list:not(.contacts-contact-list) row:not(:first-child), box.horizontal > stack.background frame:not(.view) list:not(.contacts-contact-list) row:not(:first-child), list.tweak-group list row:not(:first-child), list#ListBoxTweakGroup list row:not(:first-child), .tweak-group-startup > row:not(:first-child), list.content:not(.conversation-listbox) > row:not(:first-child) { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +window.background.csd stack stack stack frame > list row:first-child, window.background.csd > stack > list row:first-child, preferencesgroup list row:first-child, .geary-accounts-editor-pane list row:first-child, window.background.csd.unified > deck > deck > deck list row:first-child, hdyleaflet list.view row:first-child, hdyleaflet list.frame row:first-child, leaflet list.view row:first-child, leaflet list.frame row:first-child, box.horizontal > stack.background list.view row:first-child, box.horizontal > stack.background list.frame row:first-child, hdyleaflet stack.background scrolledwindow > viewport list row:first-child, hdyleaflet overlay scrolledwindow > viewport list row:first-child, leaflet stack.background scrolledwindow > viewport list row:first-child, leaflet overlay scrolledwindow > viewport list row:first-child, box.horizontal > stack.background stack.background scrolledwindow > viewport list row:first-child, box.horizontal > stack.background overlay scrolledwindow > viewport list row:first-child, hdyleaflet frame:not(.view) list:not(.contacts-contact-list) row:first-child, leaflet frame:not(.view) list:not(.contacts-contact-list) row:first-child, box.horizontal > stack.background frame:not(.view) list:not(.contacts-contact-list) row:first-child, list.tweak-group list row:first-child, list#ListBoxTweakGroup list row:first-child, .tweak-group-startup > row:first-child, list.content:not(.conversation-listbox) > row:first-child { + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} + +window.background.csd stack stack stack frame > list row:last-child, window.background.csd > stack > list row:last-child, preferencesgroup list row:last-child, .geary-accounts-editor-pane list row:last-child, window.background.csd.unified > deck > deck > deck list row:last-child, hdyleaflet list.view row:last-child, hdyleaflet list.frame row:last-child, leaflet list.view row:last-child, leaflet list.frame row:last-child, box.horizontal > stack.background list.view row:last-child, box.horizontal > stack.background list.frame row:last-child, hdyleaflet stack.background scrolledwindow > viewport list row:last-child, hdyleaflet overlay scrolledwindow > viewport list row:last-child, leaflet stack.background scrolledwindow > viewport list row:last-child, leaflet overlay scrolledwindow > viewport list row:last-child, box.horizontal > stack.background stack.background scrolledwindow > viewport list row:last-child, box.horizontal > stack.background overlay scrolledwindow > viewport list row:last-child, hdyleaflet frame:not(.view) list:not(.contacts-contact-list) row:last-child, leaflet frame:not(.view) list:not(.contacts-contact-list) row:last-child, box.horizontal > stack.background frame:not(.view) list:not(.contacts-contact-list) row:last-child, list.tweak-group list row:last-child, list#ListBoxTweakGroup list row:last-child, .tweak-group-startup > row:last-child, list.content:not(.conversation-listbox) > row:last-child { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; +} + +window.background.csd stack stack stack frame > list row:only-child, window.background.csd > stack > list row:only-child, preferencesgroup list row:only-child, .geary-accounts-editor-pane list row:only-child, window.background.csd.unified > deck > deck > deck list row:only-child, hdyleaflet list.view row:only-child, hdyleaflet list.frame row:only-child, leaflet list.view row:only-child, leaflet list.frame row:only-child, box.horizontal > stack.background list.view row:only-child, box.horizontal > stack.background list.frame row:only-child, hdyleaflet stack.background scrolledwindow > viewport list row:only-child, hdyleaflet overlay scrolledwindow > viewport list row:only-child, leaflet stack.background scrolledwindow > viewport list row:only-child, leaflet overlay scrolledwindow > viewport list row:only-child, box.horizontal > stack.background stack.background scrolledwindow > viewport list row:only-child, box.horizontal > stack.background overlay scrolledwindow > viewport list row:only-child, hdyleaflet frame:not(.view) list:not(.contacts-contact-list) row:only-child, leaflet frame:not(.view) list:not(.contacts-contact-list) row:only-child, box.horizontal > stack.background frame:not(.view) list:not(.contacts-contact-list) row:only-child, list.tweak-group list row:only-child, list#ListBoxTweakGroup list row:only-child, .tweak-group-startup > row:only-child, list.content:not(.conversation-listbox) > row:only-child { + border-radius: 6px; +} + +window.background.csd stack stack stack frame > list row:focus, window.background.csd > stack > list row:focus, preferencesgroup list row:focus, .geary-accounts-editor-pane list row:focus, window.background.csd.unified > deck > deck > deck list row:focus, hdyleaflet list.view row:focus, hdyleaflet list.frame row:focus, leaflet list.view row:focus, leaflet list.frame row:focus, box.horizontal > stack.background list.view row:focus, box.horizontal > stack.background list.frame row:focus, hdyleaflet stack.background scrolledwindow > viewport list row:focus, hdyleaflet overlay scrolledwindow > viewport list row:focus, leaflet stack.background scrolledwindow > viewport list row:focus, leaflet overlay scrolledwindow > viewport list row:focus, box.horizontal > stack.background stack.background scrolledwindow > viewport list row:focus, box.horizontal > stack.background overlay scrolledwindow > viewport list row:focus, hdyleaflet frame:not(.view) list:not(.contacts-contact-list) row:focus, leaflet frame:not(.view) list:not(.contacts-contact-list) row:focus, box.horizontal > stack.background frame:not(.view) list:not(.contacts-contact-list) row:focus, list.tweak-group list row:focus, list#ListBoxTweakGroup list row:focus, .tweak-group-startup > row:focus, list.content:not(.conversation-listbox) > row:focus { + box-shadow: inset 0 0 0 1000px alpha(currentColor, 0.05); +} + +list { + border-color: rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; +} + +list row { + padding: 3px; +} + +list.navigation-sidebar { + padding: 3px; +} + +list.navigation-sidebar > row { + border-radius: 6px; +} + +paned scrolledwindow > viewport.frame > list { + background-color: transparent; +} + +.budgie-popover scrolledwindow.sidebar:not(.categories) list > row.activatable, treeview.view header button, row.activatable { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), background-size 300ms cubic-bezier(0, 0, 0.2, 1), background-image 1200ms cubic-bezier(0, 0, 0.2, 1); + outline: none; + box-shadow: inset 0 0 0 9999px transparent; + background-image: radial-gradient(circle, transparent 10%, transparent 0%); + background-repeat: no-repeat; + background-position: center; + background-size: 1000% 1000%; +} + +.budgie-popover scrolledwindow.sidebar:not(.categories) list > row.activatable:focus, treeview.view header button:focus, row.activatable:focus { + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.05); +} + +.budgie-popover scrolledwindow.sidebar:not(.categories) list > row.activatable:hover, treeview.view header button:hover, row.activatable:hover { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), box-shadow 0ms, background-size 300ms cubic-bezier(0, 0, 0.2, 1), background-image 1200ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.05); +} + +.budgie-popover scrolledwindow.sidebar:not(.categories) list > row.has-open-popup.activatable, treeview.view header button.has-open-popup, .budgie-popover scrolledwindow.sidebar:not(.categories) list > row.activatable:active, treeview.view header button:active, row.activatable.has-open-popup, row.activatable:active { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), background-size 0ms, background-image 0ms; + animation: ripple 225ms cubic-bezier(0, 0, 0.2, 1) forwards; + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.05); + background-image: radial-gradient(circle, alpha(currentColor, 0.05) 10%, transparent 0%); + background-size: 0% 0%; +} + +row:selected { + color: inherit; + background-color: alpha(currentColor, 0.06); +} + +row:selected image, +row:selected label { + color: {{colors.on_surface.default.hex}}; +} + +row:selected button image, +row:selected button label { + color: inherit; +} + +row:selected:disabled image, +row:selected:disabled label { + color: rgba(255, 255, 255, 0.5); +} + +/********************* + * App Notifications * + *********************/ +.app-notification { + margin: 8px; + padding: 6px 15px; +} + +.app-notification button.flat:last-child { + margin-right: -9px; +} + +.app-notification button.text-button:not(:disabled) { + color: {{colors.primary.default.hex}}; +} + +.app-notification.frame, +.app-notification border { + border-style: none; +} + +/************* + * Expanders * + *************/ +expander title > arrow { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + min-width: 16px; + min-height: 16px; + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); + -gtk-icon-transform: rotate(-90deg); + color: rgba(255, 255, 255, 0.7); +} + +expander title > arrow:dir(rtl) { + -gtk-icon-transform: rotate(90deg); +} + +expander title > arrow:checked { + -gtk-icon-transform: unset; +} + +expander title > arrow:hover, expander title > arrow:active { + color: {{colors.on_surface.default.hex}}; +} + +expander title > arrow:disabled { + color: rgba(255, 255, 255, 0.32); +} + +/************ + * Calendar * + ************/ +calendar { + padding: 1px; + border: 1px solid rgba(255, 255, 255, 0.12); + color: {{colors.on_surface.default.hex}}; +} + +calendar:disabled { + color: rgba(255, 255, 255, 0.5); +} + +calendar:selected { + border-radius: 7px; +} + +calendar.header { + border-style: none none solid; + border-color: rgba(255, 255, 255, 0.12); + border-radius: 0; +} + +calendar.highlight { + color: rgba(255, 255, 255, 0.7); + font-weight: 500; +} + +calendar:indeterminate { + color: rgba(255, 255, 255, 0.32); +} + +/*********** + * Dialogs * + ***********/ +messagedialog.background { + background-color: #2a2a2a; +} + +messagedialog.background .titlebar, messagedialog.background .titlebar:backdrop { + background-color: #2a2a2a; +} + +messagedialog.background.csd { + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + background-color: #2a2a2a; +} + +messagedialog.background.csd .titlebar, messagedialog.background.csd .titlebar:backdrop { + background-color: #2a2a2a; +} + +messagedialog .titlebar, messagedialog.background.csd .titlebar { + border: none; + box-shadow: none; +} + +messagedialog .dialog-action-box { + margin-top: -6px; +} + +messagedialog .dialog-action-box button, messagedialog .dialog-action-box button:first-child, messagedialog .dialog-action-box button:last-child, messagedialog .dialog-action-box .linked:not(.vertical) > button, messagedialog .dialog-action-box .linked:not(.vertical) > button:first-child, messagedialog .dialog-action-box .linked:not(.vertical) > button:last-child { + border-radius: 6px; +} + +messagedialog .dialog-action-box button:not(:last-child), messagedialog .dialog-action-box .linked:not(.vertical) > button:not(:last-child) { + margin-right: 6px; +} + +messagedialog .dialog-action-box button.suggested-action:not(:disabled), messagedialog .dialog-action-box .linked:not(.vertical) > button.suggested-action:not(:disabled) { + color: {{colors.primary.default.hex}}; +} + +messagedialog .dialog-action-box button.destructive-action:not(:disabled), messagedialog .dialog-action-box .linked:not(.vertical) > button.destructive-action:not(:disabled) { + color: #F44336; +} + +.csd filechooser { + background-color: #1a1a1a; + border-radius: 0 0 12px 12px; +} + +filechooser .dialog-action-box { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +filechooser #pathbarbox { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; +} + +filechooser stack.view { + background-color: transparent; + padding: 0; +} + +filechooser stack.view scrolledwindow { + background-color: transparent; + border-radius: 0 0 12px 0; +} + +filechooser stack.view scrolledwindow list { + background-color: transparent; +} + +filechooser stack.view > placesview { + background-color: transparent; +} + +filechooser stack.view > placesview > actionbar, filechooser stack.view > placesview > actionbar > revealer > box { + background-color: transparent; +} + +filechooser stack.view frame > border { + border: none; +} + +.csd filechooser placessidebar { + background: none; + border-bottom-left-radius: 12px; +} + +filechooser actionbar, filechooser actionbar > revealer > box { + background-color: transparent; +} + +/*********** + * Sidebar * + ***********/ +.sidebar { + border-style: none; +} + +stacksidebar.sidebar:dir(ltr) list, stacksidebar.sidebar.left list, stacksidebar.sidebar.left:dir(rtl) list, .sidebar:not(separator):dir(ltr), .sidebar:not(separator).left { + border-right: 1px solid rgba(255, 255, 255, 0.12); + border-left-style: none; +} + +stacksidebar.sidebar:dir(rtl) list, stacksidebar.sidebar.right list, .sidebar:not(separator):dir(rtl), .sidebar:not(separator).right { + border-left: 1px solid rgba(255, 255, 255, 0.12); + border-right-style: none; +} + +.sidebar list { + background-color: transparent; +} + +paned .sidebar.left, paned .sidebar.right, paned .sidebar.left:dir(rtl), paned .sidebar:dir(rtl), paned .sidebar:dir(ltr), paned .sidebar { + border-style: none; +} + +stacksidebar.sidebar list { + padding: 3px; + background-color: #1a1a1a; +} + +stacksidebar.sidebar row { + min-height: 34px; + padding: 0 3px; + border-radius: 6px; +} + +stacksidebar.sidebar row + row { + margin-top: 3px; +} + +stacksidebar.sidebar row:selected { + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; + font-weight: 500; +} + +stacksidebar.sidebar row:selected label, stacksidebar.sidebar row:selected image { + color: {{colors.on_surface.default.hex}}; +} + +stacksidebar.sidebar row > label { + padding-left: 6px; + padding-right: 6px; + color: inherit; +} + +/**************** + * File chooser * + ****************/ +row image.sidebar-icon { + transition: color 75ms cubic-bezier(0, 0, 0.2, 1); + color: rgba(255, 255, 255, 0.7); +} + +row image.sidebar-icon:disabled { + color: rgba(255, 255, 255, 0.32); +} + +placessidebar.sidebar > viewport.frame { + border-style: none; +} + +placessidebar.sidebar list { + padding: 6px; +} + +placessidebar.sidebar row { + min-height: 34px; + margin: 0; + padding: 0; + border-radius: 6px; +} + +placessidebar.sidebar row + row { + margin-top: 3px; +} + +placessidebar.sidebar row > revealer { + padding: 0 8px 0 16px; +} + +placessidebar.sidebar row:selected { + background-color: rgba(255, 255, 255, 0.12); + font-weight: 500; +} + +placessidebar.sidebar row:disabled { + color: rgba(255, 255, 255, 0.5); +} + +placessidebar.sidebar row image.sidebar-icon:dir(ltr) { + padding-right: 8px; +} + +placessidebar.sidebar row image.sidebar-icon:dir(rtl) { + padding-left: 8px; +} + +placessidebar.sidebar row label.sidebar-label { + color: inherit; +} + +placessidebar.sidebar row label.sidebar-label:dir(ltr) { + padding-right: 2px; +} + +placessidebar.sidebar row label.sidebar-label:dir(rtl) { + padding-left: 2px; +} + +placessidebar.sidebar row.sidebar-placeholder-row { + background-color: alpha(currentColor, 0.08); +} + +placessidebar.sidebar row.sidebar-new-bookmark-row { + color: {{colors.primary.default.hex}}; +} + +placessidebar.sidebar row.sidebar-new-bookmark-row image.sidebar-icon { + color: {{colors.primary.default.hex}}; +} + +placessidebar.sidebar row:drop(active) { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), box-shadow 0ms, background-size 300ms cubic-bezier(0, 0, 0.2, 1), background-image 1200ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.08); +} + +placesview .server-list-button > image { + -gtk-icon-transform: rotate(0turn); +} + +placesview .server-list-button:checked > image { + -gtk-icon-transform: rotate(-0.5turn); +} + +placesview > actionbar > revealer > box > label { + padding-left: 8px; + padding-right: 8px; +} + +/********* + * Paned * + *********/ +paned > separator { + min-width: 1px; + min-height: 1px; + -gtk-icon-source: none; + border-style: none; + background-color: transparent; + background-image: image(rgba(255, 255, 255, 0.12)); + background-size: 1px 1px; + background-clip: content-box; +} + +paned > separator.wide { + min-width: 6px; + min-height: 6px; + background-color: #1a1a1a; + background-image: image(rgba(255, 255, 255, 0.12)), image(rgba(255, 255, 255, 0.12)); + background-size: 1px 1px, 1px 1px; +} + +paned.horizontal > separator { + background-repeat: repeat-y; +} + +paned.horizontal > separator:dir(ltr) { + margin: 0 -8px 0 0; + padding: 0 8px 0 0; + background-position: left; +} + +paned.horizontal > separator:dir(rtl) { + margin: 0 0 0 -8px; + padding: 0 0 0 8px; + background-position: right; +} + +paned.horizontal > separator.wide { + margin: 0; + padding: 0; + background-repeat: repeat-y, repeat-y; + background-position: left, right; +} + +paned.vertical > separator { + margin: 0 0 -8px 0; + padding: 0 0 8px 0; + background-repeat: repeat-x; + background-position: top; +} + +paned.vertical > separator.wide { + margin: 0; + padding: 0; + background-repeat: repeat-x, repeat-x; + background-position: bottom, top; +} + +/************** + * GtkInfoBar * + **************/ +infobar { + border: none; + margin-bottom: 0; +} + +infobar.info > revealer > box, infobar.info:hover > revealer > box, infobar.info:backdrop > revealer > box { + background-color: #1a1a1a; +} + +infobar.info > revealer > box, infobar.info > revealer > box link:link, infobar.info > revealer > box flowboxchild, infobar.info:hover > revealer > box, infobar.info:hover > revealer > box link:link, infobar.info:hover > revealer > box flowboxchild, infobar.info:backdrop > revealer > box, infobar.info:backdrop > revealer > box link:link, infobar.info:backdrop > revealer > box flowboxchild { + color: {{colors.primary.default.hex}}; +} + +infobar.info > revealer > box button.text-button:not(:disabled):not(.suggested-action):not(.destructive-action), infobar.info:hover > revealer > box button.text-button:not(:disabled):not(.suggested-action):not(.destructive-action), infobar.info:backdrop > revealer > box button.text-button:not(:disabled):not(.suggested-action):not(.destructive-action) { + color: {{colors.primary.default.hex}}; +} + +infobar.action > revealer > box, infobar.action:backdrop > revealer > box, infobar.question > revealer > box, infobar.question:backdrop > revealer > box { + background-color: {{colors.primary.default.hex}}; +} + +infobar.action > revealer > box, infobar.action > revealer > box link:link, infobar.action > revealer > box flowboxchild, infobar.action:backdrop > revealer > box, infobar.action:backdrop > revealer > box link:link, infobar.action:backdrop > revealer > box flowboxchild, infobar.question > revealer > box, infobar.question > revealer > box link:link, infobar.question > revealer > box flowboxchild, infobar.question:backdrop > revealer > box, infobar.question:backdrop > revealer > box link:link, infobar.question:backdrop > revealer > box flowboxchild { + color: {{colors.on_surface.default.hex}}; +} + +infobar.action > revealer > box button, infobar.action > revealer > box button:hover, infobar.action > revealer > box button:focus, infobar.action > revealer > box button:active, infobar.action > revealer > box button:checked, infobar.action > revealer > box button.text-button:not(:disabled), infobar.action:backdrop > revealer > box button, infobar.action:backdrop > revealer > box button:hover, infobar.action:backdrop > revealer > box button:focus, infobar.action:backdrop > revealer > box button:active, infobar.action:backdrop > revealer > box button:checked, infobar.action:backdrop > revealer > box button.text-button:not(:disabled), infobar.question > revealer > box button, infobar.question > revealer > box button:hover, infobar.question > revealer > box button:focus, infobar.question > revealer > box button:active, infobar.question > revealer > box button:checked, infobar.question > revealer > box button.text-button:not(:disabled), infobar.question:backdrop > revealer > box button, infobar.question:backdrop > revealer > box button:hover, infobar.question:backdrop > revealer > box button:focus, infobar.question:backdrop > revealer > box button:active, infobar.question:backdrop > revealer > box button:checked, infobar.question:backdrop > revealer > box button.text-button:not(:disabled) { + color: {{colors.on_surface.default.hex}}; +} + +infobar.action:hover > revealer > box, infobar.question:hover > revealer > box { + background-color: #438cf7; +} + +infobar.warning > revealer > box, infobar.warning:backdrop > revealer > box { + background-color: #FFD600; +} + +infobar.warning > revealer > box, infobar.warning > revealer > box link:link, infobar.warning > revealer > box flowboxchild, infobar.warning:backdrop > revealer > box, infobar.warning:backdrop > revealer > box link:link, infobar.warning:backdrop > revealer > box flowboxchild { + color: rgba(0, 0, 0, 0.87); +} + +infobar.warning > revealer > box button, infobar.warning > revealer > box button:hover, infobar.warning > revealer > box button:focus, infobar.warning > revealer > box button:active, infobar.warning > revealer > box button:checked, infobar.warning > revealer > box button.text-button:not(:disabled), infobar.warning:backdrop > revealer > box button, infobar.warning:backdrop > revealer > box button:hover, infobar.warning:backdrop > revealer > box button:focus, infobar.warning:backdrop > revealer > box button:active, infobar.warning:backdrop > revealer > box button:checked, infobar.warning:backdrop > revealer > box button.text-button:not(:disabled) { + color: rgba(0, 0, 0, 0.87); +} + +infobar.warning:hover > revealer > box { + background-color: #e6c100; +} + +infobar.error > revealer > box, infobar.error:backdrop > revealer > box { + background-color: #F44336; + color: {{colors.on_surface.default.hex}}; +} + +infobar.error > revealer > box, infobar.error > revealer > box link:link, infobar.error > revealer > box flowboxchild, infobar.error:backdrop > revealer > box, infobar.error:backdrop > revealer > box link:link, infobar.error:backdrop > revealer > box flowboxchild { + color: {{colors.on_surface.default.hex}}; +} + +infobar.error > revealer > box button, infobar.error > revealer > box button:hover, infobar.error > revealer > box button:focus, infobar.error > revealer > box button:active, infobar.error > revealer > box button:checked, infobar.error > revealer > box button.text-button:not(:disabled), infobar.error:backdrop > revealer > box button, infobar.error:backdrop > revealer > box button:hover, infobar.error:backdrop > revealer > box button:focus, infobar.error:backdrop > revealer > box button:active, infobar.error:backdrop > revealer > box button:checked, infobar.error:backdrop > revealer > box button.text-button:not(:disabled) { + color: {{colors.on_surface.default.hex}}; +} + +infobar.error:hover > revealer > box { + background-color: #f32c1e; +} + +/************ + * Tooltips * + ************/ +tooltip { + box-shadow: none; +} + +tooltip.background { + background-color: rgba(25, 25, 25, 0.9); + color: {{colors.on_surface.default.hex}}; + border-radius: 6px; +} + +tooltip.background.csd { + border-radius: 6px; + box-shadow: 0 3px 3px -2px rgba(0, 0, 0, 0.05), 0 2px 3px -1px rgba(0, 0, 0, 0.06), 0 1px 4px 0 rgba(0, 0, 0, 0.05); +} + +tooltip decoration { + background-color: transparent; +} + +tooltip > box { + margin: -6px; + min-height: 24px; + padding: 4px 8px; +} + +/***************** + * Color Chooser * + *****************/ +colorswatch.top { + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} + +colorswatch.top overlay { + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} + +colorswatch.bottom { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; +} + +colorswatch.bottom overlay { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; +} + +colorswatch.left, colorswatch:first-child:not(.top) { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} + +colorswatch.left overlay, colorswatch:first-child:not(.top) overlay { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} + +colorswatch.right, colorswatch:last-child:not(.bottom) { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +colorswatch.right overlay, colorswatch:last-child:not(.bottom) overlay { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +colorswatch.dark { + color: {{colors.on_surface.default.hex}}; +} + +colorswatch.light { + color: rgba(0, 0, 0, 0.87); +} + +colorswatch overlay { + transition: box-shadow 200ms ease-out; +} + +colorswatch overlay:hover { + box-shadow: 0 0 0 2px {{colors.primary.default.hex}}; +} + +colorswatch#add-color-button { + border-radius: 6px 0 0 6px; +} + +colorswatch#add-color-button:only-child { + border-radius: 6px; +} + +colorswatch#add-color-button overlay { + background-color: rgba(255, 255, 255, 0.04); +} + +colorswatch#add-color-button overlay:hover { + background-color: rgba(255, 255, 255, 0.12); + box-shadow: none; +} + +colorswatch#add-color-button overlay:active { + background-color: rgba(255, 255, 255, 0.3); +} + +colorswatch:disabled { + opacity: 0.5; +} + +colorswatch:disabled overlay { + box-shadow: none; +} + +colorswatch#editor-color-sample { + border-radius: 12px; +} + +colorswatch#editor-color-sample overlay { + border-radius: 12px; +} + +colorswatch#editor-color-sample overlay:hover { + box-shadow: 0 2px 3px -2px rgba(0, 0, 0, 0.3), 0 1px 2px -1px rgba(0, 0, 0, 0.24), 0 1px 2px -1px rgba(0, 0, 0, 0.17); +} + +colorchooser .popover.osd { + transition: box-shadow 200ms ease-out; + border-radius: 6px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 3px 3px 0 rgba(0, 0, 0, 0.18), 0 3px 6px 0 rgba(0, 0, 0, 0.12), inset 0 1px rgba(255, 255, 255, 0.1); + background-color: #2a2a2a; +} + +colorchooser .popover.osd:backdrop { + box-shadow: 0 4px 3px -3px rgba(0, 0, 0, 0.2), 0 2px 2px -1px rgba(0, 0, 0, 0.24), 0 1px 3px 0 rgba(0, 0, 0, 0.12), inset 0 1px rgba(255, 255, 255, 0.1); +} + +/******** + * Misc * + ********/ +.content-view { + background-color: #1a1a1a; +} + +/********************** + * Window Decorations * + **********************/ +decoration { + transition: none; + border-radius: 12px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 0 15px 16px 2px rgba(0, 0, 0, 0.14), 0 6px 18px 5px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.75), 0 0 36px transparent; + margin: 10px; +} + +.background.csd decoration { + border: 1px solid rgba(255, 255, 255, 0.1); + background-clip: border-box; + background-color: #1a1a1a; +} + +decoration:backdrop { + transition: box-shadow 200ms ease-out; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 3px 3px 0 rgba(0, 0, 0, 0.18), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.75), 0 0 36px transparent; +} + +.tiled decoration, .tiled-top decoration, .tiled-right decoration, .tiled-bottom decoration, .tiled-left decoration { + border-radius: 0; +} + +.maximized decoration, .fullscreen decoration { + border-radius: 0; + box-shadow: none; +} + +.popup decoration { + box-shadow: none; + border: none; +} + +.ssd decoration { + border: none; + border-radius: 12px 12px 0 0; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.75); +} + +.metacity decoration { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border: none; +} + +.csd.popup decoration { + border-radius: 12px; + border: none; + box-shadow: 0 2px 3px -1px rgba(0, 0, 0, 0.05), 0 4px 6px 0 rgba(0, 0, 0, 0.06), 0 1px 10px 0 rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.75); +} + +tooltip.csd decoration { + border-radius: 6px; + box-shadow: none; + border: none; +} + +messagedialog.background.csd decoration { + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); + background-color: #2a2a2a; + background-clip: border-box; +} + +.solid-csd decoration { + margin: 0; + padding: 2px; + border-radius: 0; + box-shadow: none; + background-color: #141414; + border: 1px solid #333333; +} + +.solid-csd decoration:backdrop { + background-color: #1a1a1a; +} + +button.titlebutton:not(.suggested-action):not(.destructive-action) { + min-height: 22px; + min-width: 22px; + padding: 0; + margin: 0 4px; +} + +button.minimize.titlebutton:not(.suggested-action):not(.destructive-action), button.maximize.titlebutton:not(.suggested-action):not(.destructive-action), button.close.titlebutton:not(.suggested-action):not(.destructive-action) { + color: rgba(255, 255, 255, 0.7); + background-color: alpha(currentColor, 0.1); +} + +button.minimize.titlebutton:hover:not(.suggested-action):not(.destructive-action), button.maximize.titlebutton:hover:not(.suggested-action):not(.destructive-action), button.close.titlebutton:hover:not(.suggested-action):not(.destructive-action) { + color: {{colors.on_surface.default.hex}}; + background-color: alpha(currentColor, 0.15); +} + +button.minimize.titlebutton:active:not(.suggested-action):not(.destructive-action), button.maximize.titlebutton:active:not(.suggested-action):not(.destructive-action), button.close.titlebutton:active:not(.suggested-action):not(.destructive-action) { + color: {{colors.on_surface.default.hex}}; + background-color: alpha(currentColor, 0.2); +} + +button.minimize.titlebutton:backdrop:not(.suggested-action):not(.destructive-action), button.maximize.titlebutton:backdrop:not(.suggested-action):not(.destructive-action), button.close.titlebutton:backdrop:not(.suggested-action):not(.destructive-action) { + opacity: 0.65; +} + +#MozillaGtkWidget.background headerbar.titlebar.default-decoration button.minimize.titlebutton, #MozillaGtkWidget.background headerbar.titlebar.default-decoration button.maximize.titlebutton, #MozillaGtkWidget.background headerbar.titlebar.default-decoration button.close.titlebutton { + background-repeat: no-repeat; + background-size: 16px 16px; + background-position: center; +} + +#MozillaGtkWidget.background headerbar.titlebar.default-decoration button.minimize.titlebutton, #MozillaGtkWidget.background headerbar.titlebar.default-decoration button.maximize.titlebutton, #MozillaGtkWidget.background headerbar.titlebar.default-decoration button.close.titlebutton { + box-shadow: none; + color: transparent; + animation: none; + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); +} + +#MozillaGtkWidget.background headerbar.titlebar.default-decoration button.minimize.titlebutton:hover, #MozillaGtkWidget.background headerbar.titlebar.default-decoration button.minimize.titlebutton:active { + background-image: -gtk-scaled(url("assets/minimize-symbolic.svg"), url("assets/minimize-symbolic@2.svg")); +} + +#MozillaGtkWidget.background headerbar.titlebar.default-decoration button.maximize.titlebutton:hover, #MozillaGtkWidget.background headerbar.titlebar.default-decoration button.maximize.titlebutton:active { + background-image: -gtk-scaled(url("assets/maximize-symbolic.svg"), url("assets/maximize-symbolic@2.svg")); +} + +#MozillaGtkWidget.background headerbar.titlebar.default-decoration button.close.titlebutton:hover, #MozillaGtkWidget.background headerbar.titlebar.default-decoration button.close.titlebutton:active { + background-image: -gtk-scaled(url("assets/close-symbolic.svg"), url("assets/close-symbolic@2.svg")); +} + +.background.csd.maximized headerbar.titlebar.default-decoration button.titlebutton:not(.suggested-action):not(.destructive-action).maximize:hover, .background.csd.maximized headerbar.titlebar.default-decoration button.titlebutton:not(.suggested-action):not(.destructive-action).maximize:active { + background-image: -gtk-scaled(url("assets/unmaximize-symbolic.svg"), url("assets/unmaximize-symbolic@2.svg")); +} + +.monospace { + font-family: monospace; +} + +/********************** + * Touch Copy & Paste * + **********************/ +cursor-handle { + color: {{colors.primary.default.hex}}; + -gtk-icon-source: -gtk-recolor(url("assets/cursor-handle-symbolic.svg")); +} + +cursor-handle.top:dir(ltr), cursor-handle.bottom:dir(rtl) { + -gtk-icon-transform: rotate(90deg); +} + +cursor-handle.bottom:dir(ltr), cursor-handle.top:dir(rtl) { + -gtk-icon-transform: unset; +} + +cursor-handle.insertion-cursor:dir(ltr), cursor-handle.insertion-cursor:dir(rtl) { + padding-top: 6px; + -gtk-icon-transform: rotate(45deg); +} + +.context-menu { + font: initial; +} + +.keycap { + min-width: 12px; + min-height: 26px; + margin-top: 2px; + padding-bottom: 2px; + padding-left: 8px; + padding-right: 8px; + border: solid 1px rgba(255, 255, 255, 0.12); + border-radius: 7px; + box-shadow: inset 0 -2px rgba(255, 255, 255, 0.12); + background-color: #2a2a2a; + color: {{colors.on_surface.default.hex}}; + font-size: smaller; +} + +:not(decoration):not(window):drop(active) { + caret-color: {{colors.primary.default.hex}}; +} + +stackswitcher { + min-height: 0; + padding: 3px; + border-radius: 9px; + background-color: rgba(255, 255, 255, 0.04); + border: none; +} + +stackswitcher.linked:not(.vertical) > button:not(.suggested-action):not(.destructive-action) { + margin: 0; + background-color: transparent; + border-radius: 6px; + padding: 2px 9px; +} + +stackswitcher.linked:not(.vertical) > button:not(.suggested-action):not(.destructive-action) + button { + margin-left: 3px; +} + +stackswitcher.linked:not(.vertical) > button:not(.suggested-action):not(.destructive-action).text-button { + min-width: 100px; +} + +stackswitcher.linked:not(.vertical) > button:not(.suggested-action):not(.destructive-action):focus:not(:hover):not(:checked) { + box-shadow: none; +} + +stackswitcher.linked:not(.vertical) > button:not(.suggested-action):not(.destructive-action):checked { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + background-color: rgba(255, 255, 255, 0.15); + color: {{colors.on_surface.default.hex}}; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); +} + +stackswitcher button.text-button { + min-width: 100px; +} + +stackswitcher button.circular, +stackswitcher button.text-button.circular { + min-width: 34px; + min-height: 34px; + padding: 0; +} + +/********* + * Emoji * + *********/ +popover.emoji-picker { + padding: 0; +} + +popover.emoji-picker.background entry { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + border-image: none; + border-radius: 0; + box-shadow: none; + background-color: transparent; +} + +popover.emoji-picker.background entry:focus { + border-bottom: 1px solid {{colors.primary.default.hex}}; + box-shadow: inset 0 -1px {{colors.primary.default.hex}}; + background-color: transparent; +} + +popover.emoji-picker scrolledwindow { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +button.emoji-section { + margin: 4px; +} + +button.emoji-section:checked { + color: {{colors.primary.default.hex}}; +} + +button.emoji-section:not(:last-child) { + margin-right: 0; +} + +popover.emoji-picker .emoji { + min-width: 3em; + min-height: 3em; + padding: 0 8px; +} + +popover.emoji-picker .emoji widget { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + border-radius: 6px; +} + +popover.emoji-picker .emoji widget:hover { + background-color: alpha(currentColor, 0.08); +} + +popover.emoji-completion { + padding: 8px 0; +} + +popover.emoji-completion arrow { + border: none; + background: none; +} + +popover.emoji-completion .emoji-completion-row { + min-height: 28px; + padding: 0 12px; +} + +popover.emoji-completion .emoji:hover { + background-color: alpha(currentColor, 0.08); +} + +/************ + * Nautilus * + ************/ +.nautilus-window.background.csd { + border-radius: 0 0 12px 12px; + background-color: #141414; +} + +.nautilus-window.background.csd:backdrop { + background-color: #1a1a1a; +} + +.nautilus-window.background.csd > grid.horizontal > paned.horizontal > separator, +.nautilus-window.background.csd > deck > box.vertical > paned.horizontal > separator { + margin-left: 0; +} + +.nautilus-window.background.csd placessidebar > viewport.frame > list > separator { + background: none; +} + +.nautilus-window.background.csd.unified notebook { + border-radius: 0; +} + +.nautilus-window.background.csd:not(.unified) .nautilus-list-view { + background-color: transparent; + border-bottom-right-radius: 12px; +} + +.nautilus-window.background.csd:not(.unified) notebook { + background-color: #1a1a1a; + border-radius: 0 0 12px 12px; +} + +.nautilus-window.background.csd:not(.unified) notebook > stack { + background-color: transparent; +} + +.nautilus-window.background.csd:not(.unified) notebook scrolledwindow > .view:not(:selected):not(:hover):not(:checked) { + background-color: transparent; +} + +.nautilus-window.background.csd:not(.unified) notebook placesview > stack > frame > scrolledwindow > viewport > list { + background-color: transparent; +} + +.nautilus-window.background.csd:not(.unified) notebook placesview > actionbar { + background-color: transparent; + border-radius: 0 0 12px 12px; +} + +.nautilus-window.background.csd:not(.unified) notebook placesview > actionbar > revealer > box { + background-color: transparent; +} + +.nautilus-window.background.csd:not(.unified) paned > separator.wide { + min-width: 12px; + box-shadow: 12px 0 #1a1a1a; + background-color: #1a1a1a; + background-image: image(#3e3e3e); +} + +.nautilus-window.maximized, .nautilus-window.maximized placessidebar { + border-radius: 0; +} + +.nautilus-window .floating-bar { + margin: 6px; + padding: 2px; + box-shadow: 0 3px 3px -2px rgba(0, 0, 0, 0.05), 0 2px 3px -1px rgba(0, 0, 0, 0.06), 0 1px 4px 0 rgba(0, 0, 0, 0.05); + border-radius: 6px; + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; + border: none; +} + +.nautilus-window .floating-bar button { + min-height: 16px; + min-width: 16px; + padding: 2px; + margin: 2px; +} + +.floating-bar { + min-height: 28px; + padding: 0; + border: 1px solid #333333; + border-radius: 0; + background-color: #1a1a1a; + color: {{colors.on_surface.default.hex}}; +} + +.floating-bar.bottom.right { + border-top-left-radius: 6px; + border-right: none; + border-bottom: none; +} + +.floating-bar.bottom.left { + border-top-right-radius: 6px; + border-left: none; + border-bottom: none; +} + +.floating-bar button { + min-height: 16px; + min-width: 16px; + padding: 0; + margin: 6px; + border-radius: 9999px; +} + +.nautilus-canvas-item.dim-label, +.nautilus-list-dim-label { + color: rgba(255, 255, 255, 0.7); +} + +.nemo-desktop.nemo-canvas-item, .caja-desktop.caja-canvas-item, +.nautilus-desktop.nautilus-canvas-item { + color: {{colors.on_surface.default.hex}}; +} + +@keyframes nautilus-operations-button-needs-attention { + to { + background-color: alpha(currentColor, 0.08); + } +} + +.nautilus-operations-button-needs-attention { + animation: nautilus-operations-button-needs-attention 300ms cubic-bezier(0.4, 0, 0.2, 1) 2 alternate; +} + +.nautilus-operations-button-needs-attention-multiple { + animation: nautilus-operations-button-needs-attention 300ms cubic-bezier(0.4, 0, 0.2, 1) 6 alternate; +} + +.path-bar-box { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1); + margin: 6px 0; + border-radius: 6px; +} + +.path-bar-box button { + margin: 0; +} + +.path-bar-box.width-maximized { + background-color: rgba(255, 255, 255, 0.04); +} + +.path-bar-box.background.frame { + border-style: none; + background-color: rgba(255, 255, 255, 0.04); +} + +.path-bar-box .path-bar button label:not(:only-child):first-child { + margin-left: 0; +} + +.path-bar-box .path-bar button label:not(:only-child):last-child { + margin-right: 0; +} + +.path-bar-box .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action) { + padding-left: 11px; + padding-right: 11px; + margin-left: 1px; + margin-right: 1px; +} + +.path-bar-box .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action) label:not(:only-child):first-child { + margin-left: 0; +} + +.path-bar-box .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action) label:not(:only-child):last-child { + margin-right: 0; +} + +.path-bar-box .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action).text-button { + min-width: 0; +} + +.path-bar-box .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action).text-button.image-button image:not(:only-child) { + margin: 0; +} + +.path-bar-box .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action):last-child:dir(ltr), .path-bar-box .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action):first-child:dir(rtl) { + background-color: rgba(255, 255, 255, 0.08); +} + +.path-bar-box .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action):last-child:dir(ltr):disabled, .path-bar-box .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action):first-child:dir(rtl):disabled { + background-color: transparent; + color: rgba(255, 255, 255, 0.5); +} + +.windowhandle .linked.nautilus-path-bar { + background-color: rgba(255, 255, 255, 0.04); + border-radius: 6px; + margin: 6px 0; +} + +.windowhandle .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action) { + margin-top: 0; + margin-bottom: 0; +} + +.windowhandle .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action).current-dir { + color: {{colors.on_surface.default.hex}}; +} + +.windowhandle .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action).current-dir:hover, .windowhandle .linked.nautilus-path-bar button:not(.suggested-action):not(.destructive-action).current-dir:active { + background: none; + box-shadow: none; +} + +.disk-space-display.unknown { + background-color: rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.3); +} + +.disk-space-display.used { + background-color: {{colors.primary.default.hex}}; + color: {{colors.primary.default.hex}}; +} + +.disk-space-display.free { + background-color: rgba(255, 255, 255, 0.12); + color: rgba(255, 255, 255, 0.12); +} + +.search-information { + padding: 2px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; + color: {{colors.on_surface.default.hex}}; +} + +.conflict-row:not(:selected) { + background-color: #6b5f1f; +} + +.nautilus-window flowboxchild .icon-item-background { + padding: 4px; + border-radius: 6px; +} + +.nautilus-window flowboxchild:selected { + background-color: transparent; +} + +.nautilus-window notebook :not(treeview).view { + border-radius: 6px; +} + +dialog.background > box.dialog-vbox.vertical > grid.horizontal > scrolledwindow.frame { + border-style: none; +} + +dialog.background > box.dialog-vbox.vertical > grid.horizontal > box.horizontal:last-child { + margin: -6px 0 0 -6px; + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +dialog.background > box.dialog-vbox.vertical > grid.horizontal > box.horizontal:last-child > label { + margin: 0 8px; +} + +dialog.background > box.dialog-vbox.vertical > grid.horizontal > box.horizontal:last-child > box > button { + border-radius: 0; +} + +.nautilus-window > popover.menu:not(:last-child) { + padding: 3px; +} + +.nautilus-window > popover.menu:not(:last-child) > stack > box > box > box { + margin-top: -6px; +} + +.nautilus-window > popover.menu:not(:last-child) > stack > box > box > box > box { + margin-bottom: -6px; +} + +.nautilus-window > popover.menu:not(:last-child) > stack > box > box > box > box.linked { + margin-top: 1px; +} + +.nautilus-window > popover.menu:not(:last-child) separator { + margin-bottom: -2px; +} + +.nautilus-menu-sort-heading { + margin: 1px 3px; + font-weight: 500; +} + +.nautilus-menu-sort-heading:disabled { + color: rgba(255, 255, 255, 0.7); +} + +.nautilus-window paned > separator { + background-color: #141414; +} + +/********* + * gedit * + *********/ +window.org-gnome-gedit > paned.titlebar > separator { + background-color: transparent; +} + +window.org-gnome-gedit > overlay > box.vertical > paned.gedit-side-panel-paned > box.vertical > stack > grid.horizontal > box.horizontal { + margin: 4px 0; +} + +window.org-gnome-gedit > overlay > box.vertical > paned.gedit-side-panel-paned > box.vertical > stack > grid.horizontal > scrolledwindow { + border-bottom-left-radius: 12px; +} + +window.org-gnome-gedit > overlay > box.vertical > paned.gedit-side-panel-paned stack scrolledwindow viewport.frame list.gedit-document-panel { + background: none; +} + +.open-document-selector-path-label { + color: rgba(255, 255, 255, 0.7); + font-size: smaller; +} + +.open-document-selector-match { + background-color: #FFD600; + color: rgba(0, 0, 0, 0.87); +} + +.gedit-document-panel { + background-color: #141414; +} + +.gedit-document-panel row button.flat { + margin-top: 8px; + margin-bottom: 8px; +} + +.gedit-document-panel-group-row:not(:first-child) { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.gedit-side-panel-paned statusbar { + border-top: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 0 0 12px 12px; +} + +.gedit-search-slider { + margin: 0 6px 10px; + padding: 6px; + background-color: #2a2a2a; + border-radius: 0 0 12px 12px; + box-shadow: 0 2px 3px -1px rgba(0, 0, 0, 0.05), 0 4px 6px 0 rgba(0, 0, 0, 0.06), 0 1px 10px 0 rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.75); +} + +.gedit-search-slider .linked:not(.vertical) > entry .gedit-search-entry-occurrences-tag { + all: unset; + color: rgba(255, 255, 255, 0.7); +} + +.gedit-search-slider .linked:not(.vertical) > entry:dir(ltr) .gedit-search-entry-occurrences-tag { + margin-left: 6px; +} + +.gedit-search-slider .linked:not(.vertical) > entry:dir(ltr) image.right { + margin-right: 0; +} + +.gedit-search-slider .linked:not(.vertical) > entry:dir(rtl) .gedit-search-entry-occurrences-tag { + margin-right: 6px; +} + +.gedit-search-slider .linked:not(.vertical) > entry:dir(rtl) image.left { + margin-left: 0; +} + +.gedit-search-slider .linked:not(.vertical) > entry:not(.error) { + background-color: #2a2a2a; +} + +.gedit-search-slider .linked:not(.vertical) > entry.error ~ button { + color: rgba(255, 255, 255, 0.7); +} + +.gedit-search-slider .linked:not(.vertical) > entry.error ~ button:hover, .gedit-search-slider .linked:not(.vertical) > entry.error ~ button:active { + color: {{colors.on_surface.default.hex}}; +} + +.gedit-search-slider .linked:not(.vertical) > entry.error ~ button:disabled { + color: rgba(255, 255, 255, 0.32); +} + +frame.gedit-map-frame > border:dir(ltr) { + border-style: none none none solid; +} + +frame.gedit-map-frame > border:dir(rtl) { + border-style: none solid none none; +} + +/********** + * Tweaks * + **********/ +.tweak-categories > row + row { + margin-top: 3px; +} + +.csd .tweak-categories { + border-bottom-left-radius: 12px; +} + +leaflet list.navigation-sidebar { + background-color: #141414; +} + +leaflet list.navigation-sidebar > row + row { + margin-top: 3px; +} + +window.background.csd:not(.maximized) leaflet list.navigation-sidebar { + border-bottom-left-radius: 12px; +} + +.tweak-group-white, +.tweak-white, +.tweak-white:hover { + background-image: image(#1a1a1a); +} + +.tweak-group-startup { + padding: 0; +} + +.tweak-group-startup > row.tweak-startup { + background-color: transparent; + background-image: none; +} + +list.tweak-group list, list#ListBoxTweakGroup list { + padding: 0; +} + +row#Focus, +row#ClickMethod, +row#PrimaryWorkspaceTweak, +row#workspaces-only-on-primary { + margin-top: 4px; +} + +leaflet.titlebar > .titlebar.tweak-titlebar-left, +leaflet.titlebar > .titlebar.tweak-titlebar-right, +hdyleaflet.titlebar > .titlebar.tweak-titlebar-left, +hdyleaflet.titlebar > .titlebar.tweak-titlebar-right { + background-color: inherit; + box-shadow: inherit; + transition: color 75ms cubic-bezier(0, 0, 0.2, 1); +} + +/************************ + * Gnome Control Center * + ************************/ +window.background.csd > headerbar.titlebar > leaflet > headerbar:first-child:not(:only-child), +window.background.csd > headerbar.titlebar > hdyleaflet > headerbar:first-child:not(:only-child) { + border-top-left-radius: 12px; +} + +window.background.csd > headerbar.titlebar > leaflet > headerbar:last-child:not(:only-child), +window.background.csd > headerbar.titlebar > hdyleaflet > headerbar:last-child:not(:only-child) { + border-top-right-radius: 12px; +} + +window.background.csd > headerbar.titlebar > leaflet > headerbar:first-child:only-child, window.background.csd > headerbar.titlebar > leaflet > headerbar:last-child:only-child, +window.background.csd > headerbar.titlebar > hdyleaflet > headerbar:first-child:only-child, +window.background.csd > headerbar.titlebar > hdyleaflet > headerbar:last-child:only-child { + border-top-right-radius: 12px; + border-top-left-radius: 12px; +} + +window.background.csd > stack:not(.titlebar) > stack.background { + border-radius: 0 0 12px 12px; +} + +window.background.csd > leaflet > stack.background, +window.background.csd > hdyleaflet > stack.background, +window.background.csd > box.horizontal > stack.background { + background: none; +} + +window.background.csd > leaflet > stack.background frame > border, +window.background.csd > hdyleaflet > stack.background frame > border, +window.background.csd > box.horizontal > stack.background frame > border { + border: none; +} + +window.background.csd > leaflet > stack.background > widget > box.vertical > box.vertical > scrolledwindow > viewport.frame, +window.background.csd > hdyleaflet > stack.background > widget > box.vertical > box.vertical > scrolledwindow > viewport.frame, +window.background.csd > box.horizontal > stack.background > widget > box.vertical > box.vertical > scrolledwindow > viewport.frame { + background-color: #1a1a1a; + border-bottom-right-radius: 12px; +} + +window.background.csd > leaflet > stack.background > widget > box.vertical > box.vertical > scrolledwindow > viewport.frame > box.vertical.view, +window.background.csd > hdyleaflet > stack.background > widget > box.vertical > box.vertical > scrolledwindow > viewport.frame > box.vertical.view, +window.background.csd > box.horizontal > stack.background > widget > box.vertical > box.vertical > scrolledwindow > viewport.frame > box.vertical.view { + background: none; +} + +window.background.csd > leaflet frame.view, +window.background.csd > hdyleaflet frame.view, +window.background.csd > box.horizontal frame.view { + border-radius: 6px; + background: none; +} + +window.background.csd > leaflet > box.vertical > scrolledwindow.view, +window.background.csd > hdyleaflet > box.vertical > scrolledwindow.view, +window.background.csd > box.horizontal > box.vertical > scrolledwindow.view { + background-color: #1a1a1a; + border-bottom-left-radius: 12px; +} + +window.background.csd > leaflet > box.vertical > scrolledwindow.view > viewport.frame > stack, +window.background.csd > hdyleaflet > box.vertical > scrolledwindow.view > viewport.frame > stack, +window.background.csd > box.horizontal > box.vertical > scrolledwindow.view > viewport.frame > stack { + background-color: transparent; +} + +window.background.csd > leaflet > box.vertical > scrolledwindow.view > viewport.frame > stack list, +window.background.csd > hdyleaflet > box.vertical > scrolledwindow.view > viewport.frame > stack list, +window.background.csd > box.horizontal > box.vertical > scrolledwindow.view > viewport.frame > stack list { + background-color: transparent; + padding: 6px; +} + +window.background.csd > leaflet > box.vertical > scrolledwindow.view > viewport.frame > stack list > row.activatable, +window.background.csd > hdyleaflet > box.vertical > scrolledwindow.view > viewport.frame > stack list > row.activatable, +window.background.csd > box.horizontal > box.vertical > scrolledwindow.view > viewport.frame > stack list > row.activatable { + border-radius: 6px; +} + +window.background.csd > leaflet > box.vertical > scrolledwindow.view > viewport.frame > stack list > row.activatable + row, +window.background.csd > hdyleaflet > box.vertical > scrolledwindow.view > viewport.frame > stack list > row.activatable + row, +window.background.csd > box.horizontal > box.vertical > scrolledwindow.view > viewport.frame > stack list > row.activatable + row { + margin-top: 3px; +} + +window.background.csd > leaflet > box.vertical > scrolledwindow.view > viewport.frame > stack list > row.activatable:not(:hover):not(:active):not(:selected), +window.background.csd > hdyleaflet > box.vertical > scrolledwindow.view > viewport.frame > stack list > row.activatable:not(:hover):not(:active):not(:selected), +window.background.csd > box.horizontal > box.vertical > scrolledwindow.view > viewport.frame > stack list > row.activatable:not(:hover):not(:active):not(:selected) { + background-color: transparent; +} + +window.background.csd stack.background clamp.medium frame > box.vertical > box.vertical > list { + border-top-width: 0; + border-bottom-width: 0; +} + +window.background.csd stack.background clamp.medium frame > box.vertical > box.vertical > list, window.background.csd stack.background clamp.medium frame > box.vertical > box.vertical > list > row { + border-radius: 0; +} + +window.background.csd stack.background clamp.medium frame > box.vertical > box.vertical:first-child > list { + border-top-width: 1px; +} + +window.background.csd stack.background clamp.medium frame > box.vertical > box.vertical:first-child > list, window.background.csd stack.background clamp.medium frame > box.vertical > box.vertical:first-child > list > row { + border-radius: 6px 6px 0 0; +} + +window.background.csd stack.background clamp.medium frame > box.vertical > box.vertical:last-child > list { + border-bottom-width: 1px; +} + +window.background.csd stack.background clamp.medium frame > box.vertical > box.vertical:last-child > list, window.background.csd stack.background clamp.medium frame > box.vertical > box.vertical:last-child > list > row { + border-radius: 0 0 6px 6px; +} + +window.background.csd hdycolumn stack frame.view > stack > stack list > separator { + background-color: rgba(255, 255, 255, 0.12); +} + +dialog.background.csd > box.vertical.dialog-vbox > stack > notebook, +dialog.background.csd > box.vertical.dialog-vbox > notebook > stack > box.horizontal > notebook > stack { + border-radius: 0 0 12px 12px; +} + +dialog.background.csd > box.vertical.dialog-vbox > scrolledwindow > viewport.frame > list { + background: none; +} + +dialog.background.csd > box.vertical.dialog-vbox > scrolledwindow > viewport.frame > list > row:not(:hover):not(:active):not(:selected) { + background-color: transparent; +} + +hdyleaflet > box.vertical > scrolledwindow > viewport.frame list:not(.view):not(.tweak-group):not(.tweak-group-startup), leaflet > box.vertical > scrolledwindow > viewport.frame list:not(.view):not(.tweak-group):not(.tweak-group-startup), box.horizontal > stack.background > box.vertical > scrolledwindow > viewport.frame list:not(.view):not(.tweak-group):not(.tweak-group-startup) { + padding: 3px; +} + +hdyleaflet > box.vertical > scrolledwindow > viewport.frame list:not(.view):not(.tweak-group):not(.tweak-group-startup) row.activatable, leaflet > box.vertical > scrolledwindow > viewport.frame list:not(.view):not(.tweak-group):not(.tweak-group-startup) row.activatable, box.horizontal > stack.background > box.vertical > scrolledwindow > viewport.frame list:not(.view):not(.tweak-group):not(.tweak-group-startup) row.activatable { + border-radius: 6px; +} + +/************************ + * Gnome system monitor * + ************************/ +window#gnome-system-monitor.background.csd > box.vertical > stack { + background-color: #1a1a1a; + border-radius: 0 0 12px 12px; +} + +window#gnome-system-monitor.background.csd > box.vertical > stack > box.vertical > revealer > actionbar > revealer > box { + border-radius: 0 0 12px 12px; +} + +window#gnome-system-monitor.background:not(.csd) > box.vertical > headerbar { + box-shadow: none; +} + +/************************ + * Gnome Sound Recorder * + ************************/ +stack > grid.vertical > scrolledwindow { + border: none; + border-radius: 0 0 12px 12px; +} + +stack > grid.vertical > scrolledwindow > viewport.frame list { + border-radius: 0 0 12px 12px; +} + +stack > grid.vertical scrolledwindow.frame.emptyGrid { + border: none; +} + +/****************** + * Gnome Contacts * + ******************/ +window.background.csd scrolledwindow.contacts-contact-form { + border-bottom-right-radius: 12px; +} + +/************************ + * Epiphany (Gnome Web) * + ************************/ +tabbox { + border: none; + background-color: rgba(255, 255, 255, 0.04); + padding: 3px; + margin: 3px; + border-radius: 9px; +} + +tabbox > tab button { + min-height: 24px; + min-width: 24px; + border-radius: 9999px; + border: none; + padding: 0; + margin-right: -6px; +} + +/***************** + * Gnome Weather * + *****************/ +#weather-page, +#weekly-forecast-frame { + border-bottom-right-radius: 12px; +} + +#weather-page-content-view { + border-bottom-right-radius: 12px; + border-bottom-left-radius: 12px; +} + +/*************** + * Gnome Music * + ***************/ +window.background.csd box.vertical > overlay > stack.background { + border-radius: 0 0 12px 12px; +} + +/**************** + * Gnome Clocks * + ****************/ +/************* + * Rhythmbox * + *************/ +window.background > box.vertical > toolbar.primary-toolbar > toolitem > box.horizontal:not(.linked) > button.toggle, +window.background > box.vertical > toolbar.primary-toolbar > toolitem > .linked > button:not(.toggle):not(.raised):not(.flat), window.background > box.vertical > toolbar.primary-toolbar > toolitem button.flat.scale, window.csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > .linked > button, +window.csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > box.horizontal > button, +window.solid-csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > .linked > button, +window.solid-csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > box.horizontal > button { + min-height: 24px; + min-width: 24px; + padding: 5px; + margin: 0; +} + +.sidebar-paned .inline-toolbar.horizontal.sidebar-toolbar { + box-shadow: inset 0 1px rgba(255, 255, 255, 0.12); +} + +.sidebar-paned .inline-toolbar.horizontal.sidebar-toolbar button.image-button { + border-radius: 9999px; +} + +.sidebar-paned .inline-toolbar.horizontal.sidebar-toolbar button.image-button:not(:first-child), .sidebar-paned .inline-toolbar.horizontal.sidebar-toolbar button.image-button:not(:last-child) { + border-top-left-radius: 9999px; + border-bottom-left-radius: 9999px; + border-top-right-radius: 9999px; + border-bottom-right-radius: 9999px; +} + +.sidebar-paned .inline-toolbar.horizontal.sidebar-toolbar button.image-button.image-button:not(.text-button):first-child { + border-top-left-radius: 9999px; + border-bottom-left-radius: 9999px; +} + +.sidebar-paned .inline-toolbar.horizontal.sidebar-toolbar button.image-button.image-button:not(.text-button):last-child { + border-top-right-radius: 9999px; + border-bottom-right-radius: 9999px; +} + +.sidebar-paned .inline-toolbar.horizontal.sidebar-toolbar button.image-button > widget > box > image { + padding: 0; +} + +window.csd > box.vertical > box.vertical > toolbar.horizontal, +window.solid-csd > box.vertical > box.vertical > toolbar.horizontal { + padding: 6px; + margin: -1px 0; + border-bottom: none; + border-top: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: none; + background-color: transparent; +} + +window.csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > .linked > button, +window.csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > box.horizontal > button, +window.solid-csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > .linked > button, +window.solid-csd > box.vertical > box.vertical > toolbar.horizontal > toolitem > box.horizontal > button { + margin: 6px 0; +} + +window.csd > box.vertical > box.vertical > frame, +window.solid-csd > box.vertical > box.vertical > frame { + margin: -1px 0; + padding: 0; +} + +window.csd > box.vertical > box.vertical > frame > border, +window.solid-csd > box.vertical > box.vertical > frame > border { + border: none; +} + +window.background > box.vertical > toolbar.primary-toolbar { + padding: 0 12px 0 6px; +} + +window.background > box.vertical > toolbar.primary-toolbar > toolitem > .linked > button.image-button.raised { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), background-size 300ms cubic-bezier(0, 0, 0.2, 1), background-image 1200ms cubic-bezier(0, 0, 0.2, 1); + outline: none; + box-shadow: inset 0 0 0 9999px transparent; + background-color: rgba(255, 255, 255, 0.08); + background-image: radial-gradient(circle, transparent 10%, transparent 0%); + background-repeat: no-repeat; + background-position: center; + background-size: 1000% 1000%; + color: {{colors.on_surface.default.hex}}; +} + +window.background > box.vertical > toolbar.primary-toolbar > toolitem > .linked > button.image-button.raised:hover { + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.08); +} + +window.background > box.vertical > toolbar.primary-toolbar > toolitem > .linked > button.image-button.raised:active { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), background-size 0ms, background-image 0ms, border 0ms; + animation: ripple 225ms cubic-bezier(0, 0, 0.2, 1) forwards; + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.08); + background-image: radial-gradient(circle, alpha(currentColor, 0.12) 10%, transparent 0%); + background-size: 0% 0%; +} + +window.background > box.vertical > toolbar.primary-toolbar > toolitem > .linked > button.image-button.raised:disabled { + box-shadow: none; + background-color: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.5); +} + +window.background > box.vertical > toolbar.primary-toolbar > toolitem > .linked > button.image-button.raised:checked { + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; +} + +window.background > box.vertical > toolbar.primary-toolbar > toolitem > .linked > button.image-button.raised > widget > box > image { + padding: 0 3px; +} + +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > grid > grid > grid, +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > box > grid > grid > grid, +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > notebook > stack > grid > grid > grid { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + padding: 6px; + margin: -6px; +} + +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > grid > grid > box, +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > box > grid > grid > box, +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > notebook > stack > grid > grid > box { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + padding: 0 6px 6px; + margin: 0 -6px -6px 0; +} + +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > grid > paned > box > scrolledwindow:not(:last-child), +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > notebook > stack > grid > paned > box > scrolledwindow:not(:last-child) { + border-right: 1px solid rgba(255, 255, 255, 0.12); + margin-right: -1px; +} + +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > box > grid > grid > grid, +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > box > box > grid > grid > grid, +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > box > notebook > stack > grid > grid > grid { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + padding: 3px; + margin: -1px -6px -6px; +} + +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > box > grid > paned > box > scrolledwindow:not(:last-child), +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > box > box > grid > paned > box > scrolledwindow:not(:last-child), +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > box > notebook > stack > grid > paned > box > scrolledwindow:not(:last-child) { + border-right: 1px solid rgba(255, 255, 255, 0.12); + margin-right: -1px; +} + +window.background > box > .sidebar-paned > paned > box > notebook > stack > box > box > paned > box:first-child > box { + padding: 0 6px 6px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +/********** + * Polari * + **********/ +.polari-room-list .sidebar { + background: none; +} + +.polari-room-list .sidebar > row.activatable:selected { + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; +} + +stack.view.polari-entry-area { + background-color: #1a1a1a; + border-top: 1px solid rgba(255, 255, 255, 0.12); + border-bottom-right-radius: 12px; +} + +stack:disabled.view.polari-entry-area { + background-image: image(#1a1a1a); +} + +/*********** + * Builder * + ***********/ +layouttabbar { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; +} + +layouttabbar > box > button { + margin: 3px 0; +} + +layouttab { + margin: 0 8px; + border-style: none solid; + border-width: 1px; + border-color: rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -2px {{colors.primary.default.hex}}; + background-color: #1a1a1a; +} + +layouttab separator.vertical { + margin: 8px 4px; +} + +layouttab button.text-button, layouttab button.image-button, layouttab button { + margin-top: 8px; + margin-bottom: 8px; + padding: 0 4px; +} + +layout { + border: 1px solid rgba(0, 0, 0, 0.25); + -PnlDockBin-handle-size: 1; +} + +entry.search-missing { + background-color: #F44336; + color: {{colors.on_surface.default.hex}}; +} + +window.workbench treeview.image { + color: rgba(255, 255, 255, 0.7); +} + +popover.popover-selector list { + padding: 6px; +} + +popover.popover-selector list row { + border-radius: 6px; +} + +popover.popover-selector list row image:dir(ltr) { + margin-right: 6px; +} + +popover.popover-selector list row image:dir(rtl) { + margin-left: 6px; +} + +popover.popover-selector list row .accel:dir(ltr) { + margin-left: 6px; +} + +popover.popover-selector list row .accel:dir(rtl) { + margin-right: 6px; +} + +omnibar.linked:not(.vertical) entry { + border-radius: 6px; +} + +omnibar:not(:hover):not(:active) entry { + color: rgba(255, 255, 255, 0.7); +} + +popover.omnibar list row:not(:last-child) { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +entry.preferences-search { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: none; + background-color: #1a1a1a; +} + +preferences stacksidebar.sidebar list { + background-color: #141414; +} + +preferences stacksidebar.sidebar:dir(ltr) list, preferences stacksidebar.sidebar:dir(rtl) list { + border-style: none; +} + +preferences > box > box:dir(ltr) { + border-right: 1px solid rgba(255, 255, 255, 0.12); +} + +preferences > box > box:dir(rtl) { + border-left: 1px solid rgba(255, 255, 255, 0.12); +} + +popover.messagepopover.background { + padding: 0; +} + +popover.messagepopover .popover-action-area button { + padding: 8px 16px; + border-top: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 0; +} + +popover.messagepopover .popover-action-area button:first-child { + border-bottom-left-radius: 6px; +} + +popover.messagepopover .popover-action-area button:last-child { + border-bottom-right-radius: 6px; +} + +popover.messagepopover .popover-content-area { + margin: 16px; +} + +popover.transfers list { + background-color: transparent; +} + +popover.transfers list row:not(:first-child) { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +popover.transfers list row > box { + padding: 10px; +} + +dockbin { + border: 1px solid rgba(0, 0, 0, 0.25); + -PnlDockBin-handle-size: 1; +} + +dockpaned { + border: 1px solid rgba(0, 0, 0, 0.25); +} + +eggsearchbar box.search-bar { + padding: 0 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; +} + +docktabstrip { + padding: 0 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; +} + +docktab { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), background-size 0ms, background-image 0ms; + min-height: 24px; + min-width: 24px; + margin-bottom: -1px; + padding: 6px 6px; + border-width: 1px; + border-color: transparent; + box-shadow: inset 0 -2px transparent; + background-image: radial-gradient(circle, {{colors.primary.default.hex}} 10%, transparent 0%); + background-repeat: no-repeat; + background-position: center; + background-size: 0% 0%; + color: rgba(255, 255, 255, 0.7); + font-weight: 500; +} + +docktab:hover { + background-color: alpha(currentColor, 0.08); + color: {{colors.on_surface.default.hex}}; +} + +docktab:checked { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1), background-size 225ms cubic-bezier(0, 0, 0.2, 1), background-image 525ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: inset 0 -2px {{colors.primary.default.hex}}; + background-color: transparent; + background-image: radial-gradient(circle, transparent 10%, transparent 0%); + background-size: 1000% 1000%; + color: {{colors.on_surface.default.hex}}; +} + +dockoverlayedge { + background-color: #1a1a1a; +} + +dockoverlayedge docktabstrip { + padding: 0; + border: none; +} + +dockoverlayedge.left-edge docktab:checked { + box-shadow: inset -2px 0 {{colors.primary.default.hex}}; +} + +dockoverlayedge.right-edge docktab:checked { + box-shadow: inset 2px 0 {{colors.primary.default.hex}}; +} + +pillbox { + background-color: #1a1a1a; + border-radius: 6px; +} + +layoutpane entry.search { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: none; + background-color: #1a1a1a; +} + +editortweak entry.search { + margin-bottom: -1px; + box-shadow: none; + background-color: transparent; +} + +.gb-search-entry-occurrences-tag { + box-shadow: none; + background-color: transparent; +} + +docktabstrip { + min-height: 39px; +} + +window.workbench preferences preferencesgroup list entry { + padding-top: 8px; + padding-bottom: 8px; +} + +button.run-arrow-button { + padding-left: 9px; + padding-right: 9px; +} + +button.dzlmenubutton image { + min-width: 28px; +} + +button.dzlmenubutton image.arrow { + min-width: 25px; +} + +button.dzlmenubuttonitem { + color: {{colors.on_surface.default.hex}}; + font-weight: normal; +} + +button.dzlmenubuttonitem:disabled { + color: rgba(255, 255, 255, 0.5); +} + +idelayoutstackheader { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +idelayoutstackheader button:checked { + color: {{colors.on_surface.default.hex}}; +} + +ideeditorutilities > dzldockpaned > box > stackswitcher { + padding: 8px 0; + background-color: #1a1a1a; +} + +ideeditorutilities > dzldockpaned > box > stackswitcher:dir(ltr) { + border-right: 1px solid rgba(255, 255, 255, 0.12); +} + +ideeditorutilities > dzldockpaned > box > stackswitcher:dir(rtl) { + border-left: 1px solid rgba(255, 255, 255, 0.12); +} + +ideeditorutilities > dzldockpaned > box > stackswitcher button { + border-radius: 0; + box-shadow: none; + background-color: transparent; +} + +ideeditorutilities > dzldockpaned > box > stackswitcher button:active { + background-image: radial-gradient(circle, rgba(91, 155, 248, 0.7) 10%, transparent 0%); +} + +ideeditorutilities > dzldockpaned > box > stackswitcher button:checked { + background-color: transparent; + color: {{colors.on_surface.default.hex}}; +} + +ideeditorutilities > dzldockpaned > box > stackswitcher button:dir(ltr) { + margin-right: -1px; +} + +ideeditorutilities > dzldockpaned > box > stackswitcher button:dir(ltr):checked { + box-shadow: inset -2px 0 {{colors.primary.default.hex}}; +} + +ideeditorutilities > dzldockpaned > box > stackswitcher button:dir(rtl) { + margin-left: -1px; +} + +ideeditorutilities > dzldockpaned > box > stackswitcher button:dir(rtl):checked { + box-shadow: inset 2px 0 {{colors.primary.default.hex}}; +} + +ideeditorsidebar notebook header { + background: transparent; +} + +popover.messagepopover list { + border: 1px solid rgba(255, 255, 255, 0.12); +} + +popover.messagepopover list row:not(:last-child) { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +/********** + * Photos * + **********/ +GdMainIconView.content-view { + -GdMainIconView-icon-size: 48; +} + +.documents-counter { + margin: 8px; + border-radius: 9999px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 3px 3px 0 rgba(0, 0, 0, 0.18), 0 3px 6px 0 rgba(0, 0, 0, 0.12); + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; + font-weight: bold; +} + +.documents-scrolledwin.frame { + border-style: none; +} + +.documents-scrolledwin.frame frame.content-view > border { + border-style: none; +} + +.photos-fade-in { + opacity: 1; + transition: opacity 75ms cubic-bezier(0, 0, 0.2, 1); +} + +.photos-fade-out { + opacity: 0; + transition: opacity 75ms cubic-bezier(0, 0, 0.2, 1); +} + +button.photos-filter-preview { + color: {{colors.on_surface.default.hex}}; + font-weight: normal; +} + +button.photos-filter-preview:checked { + background-color: alpha(currentColor, 0.06); + color: {{colors.on_surface.default.hex}}; +} + +button.photos-filter-preview:checked image { + color: {{colors.on_surface.default.hex}}; +} + +overlay grid.horizontal > revealer > scrolledwindow.frame:dir(ltr) { + border-style: none none none solid; +} + +overlay grid.horizontal > revealer > scrolledwindow.frame:dir(rtl) { + border-style: none solid none none; +} + +/********* + * Music * + *********/ +.side-panel:dir(ltr) { + border-style: solid; + border-color: rgba(255, 255, 255, 0.12); +} + +.side-panel:dir(rtl) { + border-style: solid; + border-color: rgba(255, 255, 255, 0.12); +} + +.side-panel .view { + background-image: image(#141414); +} + +.side-panel .view:hover { + background-image: image(#363636); +} + +.side-panel .view:selected { + background-image: image({{colors.primary.default.hex}}); +} + +.side-panel .view:selected:hover { + background-image: image(#68a3f9); +} + +.songs-list:hover { + background-image: image(alpha(currentColor, 0.08)); +} + +frame.documents-dropdown { + margin: 8px; +} + +frame.documents-dropdown > border { + border: none; +} + +box.vertical > revealer > toolbar.search-bar { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + background-clip: border-box; +} + +box.vertical > revealer > toolbar.search-bar button > widget { + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); +} + +/************* + * Documents * + *************/ +.documents-scrolledwin { + background-color: transparent; +} + +.documents-scrolledwin .content-view:not(:selected):not(:hover) { + background-color: transparent; +} + +.documents-scrolledwin viewport.frame { + background-color: transparent; +} + +.documents-scrolledwin viewport.frame widget > frame.content-view:not(:selected):not(:hover) { + background-color: transparent; +} + +.documents-scrolledwin viewport.frame widget > frame.content-view:not(:selected):not(:hover) border { + border: none; +} + +window.background.csd > stack > box > revealer > actionbar > revealer > box { + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; +} + +/******************* + * Document Viewer * + *******************/ +window.background.csd evview.view.content-view { + border-radius: 0 0 12px 12px; +} + +/********************************* + * Archive Manager (File roller) * + *********************************/ +.background.csd > grid.horizontal > paned.horizontal > scrolledwindow { + border-radius: 0 0 12px 12px; + background-color: #1a1a1a; +} + +.background.csd > grid.horizontal > paned.horizontal > scrolledwindow > treeview.view:not(:hover):not(:selected):not(:selected):not(:hover):not(.progressbar):not(.expander):not(.trough):not(.separator) { + background-color: #1a1a1a; +} + +.background.csd > grid.horizontal > paned.horizontal > box.vertical > scrolledwindow { + border-radius: 0 0 0 12px; + background-color: #1a1a1a; +} + +/************ + * Terminal * + ************/ +terminal-window decoration { + border-radius: 12px 12px 0 0; +} + +terminal-window.background.csd, terminal-window.background.csd.maximized { + border-radius: 0 0 0 0; +} + +terminal-window notebook > header > box { + margin: -2px -2px -2px 1px; +} + +terminal-window notebook > header > box button { + min-height: 24px; + min-width: 24px; + padding: 3px; +} + +window.background > box.vertical > box.horizontal > frame > border { + border-width: 0 1px 0 0; +} + +window.background > box.vertical > box.horizontal > frame > scrolledwindow > viewport.frame list { + border-bottom-left-radius: 12px; +} + +window.background > box.vertical > box.horizontal > stack > widget > notebook.frame { + border-width: 0; + border-radius: 0 0 12px 0; +} + +window.background > box.vertical > box.horizontal > stack > widget > notebook.frame > stack { + border-bottom-right-radius: 12px; +} + +.terminal-window { + background-color: #1a1a1a; +} + +.terminal-window tabbar tabbox { + background-color: #141414; + margin: 0; + border-radius: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + background-clip: border-box; +} + +/********* + * To Do * + *********/ +task-list-view taskrow { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + margin: 0 -8px; +} + +task-list-view taskrow:hover { + transition: none; +} + +task-list-view taskrow label { + margin: 0 8px; +} + +task-list-view taskrow image.dim-label { + min-width: 16px; +} + +task-list-view > box > revealer > box > button { + margin: -5px; +} + +task-list-view > box > revealer > box > button .dim-label { + color: inherit; +} + +tasklistview taskrow { + outline: none; +} + +tasklistview taskrow entry, tasklistview taskrow entry:focus, tasklistview taskrow entry:disabled { + box-shadow: none; +} + +tasklistview taskrow image.dim-label { + min-width: 16px; +} + +tasklistview > box > revealer > box > button { + margin: -5px; +} + +tasklistview > box > revealer > box > button .dim-label { + color: inherit; +} + +/******* + * Eog * + *******/ +#eog-thumb-nav scrolledwindow { + border-top: none; +} + +/************* + * Evolution * + *************/ +frame.taskbar > border { + border-style: solid none none; +} + +box.vertical > paned.horizontal notebook widget .frame { + border-style: none; +} + +/*********** + * Fractal * + ***********/ +.background.csd.main-window .sidebar.rooms-sidebar { + border-bottom-left-radius: 12px; +} + +/******** + * Gitg * + ********/ +frame.commit-frame > border { + border-style: solid none none; +} + +/************** + * Characters * + **************/ +box.dialog-vbox scrolledwindow.related { + border: 1px solid rgba(0, 0, 0, 0.25); +} + +list.categories { + background-image: image(#141414); +} + +/********* + * Boxes * + *********/ +.transparent-bg + stack overlay > label { + min-height: 24px; + padding: 0 4px; + border-radius: 6px; + background-color: #141414; + color: {{colors.on_surface.default.hex}}; +} + +/************** + * Calculator * + **************/ +button.title label { + min-height: 34px; +} + +/********* + * Geary * + *********/ +window.background.csd.geary-main-window > deck > overlay > box.vertical > paned.horizontal > box.sidebar.vertical, +window#GearyMainWindow.background.csd > deck > overlay > box.vertical > paned.horizontal > box.sidebar.vertical { + border-bottom-left-radius: 12px; +} + +window.background.csd.geary-main-window > deck > overlay > box.vertical > paned.horizontal > box.sidebar.vertical statusbar, +window#GearyMainWindow.background.csd > deck > overlay > box.vertical > paned.horizontal > box.sidebar.vertical statusbar { + border-bottom-left-radius: 12px; +} + +window.background.csd.geary-main-window stack#conversation_viewer, +window#GearyMainWindow.background.csd stack#conversation_viewer { + border-bottom-right-radius: 12px; +} + +window.background.csd.geary-main-window stack#conversation_viewer scrolledwindow.geary-conversation-scroller viewport.frame list.conversation-listbox, +window#GearyMainWindow.background.csd stack#conversation_viewer scrolledwindow.geary-conversation-scroller viewport.frame list.conversation-listbox { + background: none; + border-bottom-right-radius: 12px; +} + +window.background.csd.geary-main-window stack#conversation_viewer .geary-expanded, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-expanded { + animation: none; + background-image: none; +} + +window.background.csd.geary-main-window stack#conversation_viewer .geary-expanded > .geary-composer-embed actionbar > revealer > box, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-expanded > .geary-composer-embed actionbar > revealer > box { + border-radius: 0; +} + +window.background.csd.geary-main-window stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-expanded > .geary-composer-embed headerbar { + color: {{colors.on_surface.default.hex}}; + background-color: #1a1a1a; + box-shadow: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.25); +} + +window.background.csd.geary-main-window stack#conversation_viewer .geary-composer-box actionbar > revealer > box, +window#GearyMainWindow.background.csd stack#conversation_viewer .geary-composer-box actionbar > revealer > box { + border-bottom-left-radius: 0; +} + +.geary-accounts-editor-pane frame:not(.geary-signature) > border, +.geary-accounts-editor-pane scrolledwindow.frame { + border: none; +} + +.geary-main-window.unified > deck > overlay > .geary-main-layout { + background-color: #1a1a1a; +} + +.geary-main-window.unified > deck > overlay > .geary-main-layout > leaflet > headerbar, +.geary-main-window.unified > deck > overlay > .geary-main-layout > leaflet > leaflet > headerbar { + box-shadow: inset 0 -1px rgba(255, 255, 255, 0.12); +} + +.geary-main-window.unified > deck > overlay > .geary-main-layout > leaflet > separator.sidebar, +.geary-main-window.unified > deck > overlay > .geary-main-layout > leaflet > leaflet > separator.sidebar { + background-color: #141414; + box-shadow: inset 0 -1px rgba(255, 255, 255, 0.12); + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1), color 75ms cubic-bezier(0, 0, 0.2, 1), box-shadow 75ms cubic-bezier(0, 0, 0.2, 1); +} + +.geary-main-window.unified > deck > overlay > .geary-main-layout > leaflet > separator.sidebar:backdrop, +.geary-main-window.unified > deck > overlay > .geary-main-layout > leaflet > leaflet > separator.sidebar:backdrop { + background-color: #1a1a1a; +} + +.geary-main-window.unified > deck > overlay > .geary-main-layout > leaflet > leaflet > box.vertical + separator.sidebar { + min-width: 1px; + background-color: rgba(255, 255, 255, 0.12); +} + +.geary-main-window.unified frame.geary-conversation-frame scrolledwindow { + padding: 3px; +} + +.geary-main-window.unified frame.geary-conversation-frame scrolledwindow treeview.view { + border: 1px solid transparent; + border-radius: 6px; + padding: 6px; +} + +.geary-main-window.unified frame.geary-conversation-frame scrolledwindow treeview.view:selected, .geary-main-window.unified frame.geary-conversation-frame scrolledwindow treeview.view:active { + border-radius: 6px; + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; +} + +.geary-main-window.unified separator.geary-sidebar-pane-separator { + min-width: 1px; + background-color: rgba(255, 255, 255, 0.12); +} + +.geary-main-window.unified geary-conversation-viewer#conversation_viewer list.background.conversation-listbox.content > row.activatable { + border: 1px solid rgba(255, 255, 255, 0.12); + border-bottom-width: 0; + background-color: #1a1a1a; +} + +.geary-main-window.unified geary-conversation-viewer#conversation_viewer list.background.conversation-listbox.content > row.activatable:first-child { + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +.geary-main-window.unified geary-conversation-viewer#conversation_viewer list.background.conversation-listbox.content .geary-attachment-pane { + border-radius: 0 0 8px 8px; +} + +.geary-main-window.unified geary-conversation-viewer#conversation_viewer list.background.conversation-listbox.content .geary-attachment-pane actionbar.background { + background-color: transparent; +} + +.geary-main-window.unified geary-conversation-viewer#conversation_viewer list.background.conversation-listbox.content .geary-attachment-pane actionbar.background > revealer > box { + border-radius: 0 0 6px 6px; +} + +/************** + * Extensions * + **************/ +window.background.csd stack stack stack frame > border, +window.background.csd > stack > stack > box > frame > border, +window.background.csd > stack > stack > box > box > frame > border, +window.background.csd > stack > box > stack > box > frame > border, +window.background.csd > stack > box > stack > scrolledwindow > viewport frame > border, +window.background.csd > stack > box > stack > box > scrolledwindow > viewport > frame > border, +window.background.csd > stack > grid > scrolledwindow > viewport > box > frame > border { + border: none; +} + +window.background.csd > stack > box > box > list, +window.background.csd > stack > box > stack > scrolledwindow > viewport > list { + border-bottom-left-radius: 12px; +} + +window.background.csd > stack > box > .sidebar > scrolledwindow > viewport > list { + padding: 0 0; +} + +/*********** + * Dialogs * + ***********/ +dialog.background.csd > box.vertical.dialog-vbox > grid.horizontal > scrolledwindow.frame > viewport.frame list:first-child { + border-radius: 0 0 0 12px; +} + +dialog.background.csd > box.vertical.dialog-vbox > grid.horizontal > scrolledwindow.frame > viewport.frame list:last-child { + border-radius: 0 0 12px 0; +} + +dialog.background.csd > box.vertical.dialog-vbox > stack > scrolledwindow, +dialog.background.csd > box.vertical.dialog-vbox > stack > stack > scrolledwindow { + border-radius: 0 0 12px 12px; + background-color: #1a1a1a; +} + +dialog.background.csd > box.vertical.dialog-vbox > stack > scrolledwindow iconview.view:not(:hover):not(:selected):not(:active), +dialog.background.csd > box.vertical.dialog-vbox > stack > stack > scrolledwindow iconview.view:not(:hover):not(:selected):not(:active) { + background-color: transparent; +} + +dialog.background.csd > box.vertical.dialog-vbox > stack > scrolledwindow > viewport.frame > list { + border-radius: 0 0 12px 12px; +} + +dialog.background.csd > box.vertical.dialog-vbox > stack > scrolledwindow > viewport.frame > list row.activatable:not(:hover):not(:selected):not(:active) { + background-color: transparent; +} + +dialog.background.csd > box.vertical.dialog-vbox > stack toolbar.toolbar { + border-radius: 0 0 12px 12px; +} + +dialog.background.csd > box.vertical.dialog-vbox > notebook > stack { + border-radius: 0 0 12px 12px; +} + +dialog.background.csd stack scrolledwindow.frame { + border-radius: 6px; +} + +dialog.background.csd stack scrolledwindow.frame textview.view { + border-radius: 6px; +} + +dialog.background.csd stack scrolledwindow.frame textview.view > text { + background: none; +} + +dialog.background.csd stack scrolledwindow viewport.frame.view { + border-radius: 6px; +} + +window.background.csd.unified { + background-color: #1a1a1a; +} + +window.background.csd.unified headerbar { + box-shadow: inset 0 -1px rgba(255, 255, 255, 0.12); +} + +window.background.csd.unified > decoration-overlay { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +window.background.csd.unified, +window.background.csd.unified > decoration, +window.background.csd.unified > decoration-overlay { + border-radius: 12px; +} + +window.background.csd.unified.tiled > decoration-overlay, window.background.csd.unified.tiled-top > decoration-overlay, window.background.csd.unified.tiled-right > decoration-overlay, window.background.csd.unified.tiled-bottom > decoration-overlay, window.background.csd.unified.tiled-left > decoration-overlay, window.background.csd.unified.maximized > decoration-overlay, window.background.csd.unified.fullscreen > decoration-overlay { + box-shadow: none; +} + +window.background.csd.unified.tiled, +window.background.csd.unified.tiled > decoration, +window.background.csd.unified.tiled > decoration-overlay, window.background.csd.unified.tiled-top, +window.background.csd.unified.tiled-top > decoration, +window.background.csd.unified.tiled-top > decoration-overlay, window.background.csd.unified.tiled-right, +window.background.csd.unified.tiled-right > decoration, +window.background.csd.unified.tiled-right > decoration-overlay, window.background.csd.unified.tiled-bottom, +window.background.csd.unified.tiled-bottom > decoration, +window.background.csd.unified.tiled-bottom > decoration-overlay, window.background.csd.unified.tiled-left, +window.background.csd.unified.tiled-left > decoration, +window.background.csd.unified.tiled-left > decoration-overlay, window.background.csd.unified.maximized, +window.background.csd.unified.maximized > decoration, +window.background.csd.unified.maximized > decoration-overlay, window.background.csd.unified.fullscreen, +window.background.csd.unified.fullscreen > decoration, +window.background.csd.unified.fullscreen > decoration-overlay { + border-radius: 0; +} + +overlay > revealer.left > scrolledwindow.frame, overlay > revealer.right > scrolledwindow.frame { + border-style: none; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 0 15px 16px 2px rgba(0, 0, 0, 0.14), 0 6px 18px 5px rgba(0, 0, 0, 0.12); +} + +overlay > revealer.left > scrolledwindow.frame { + margin-right: 32px; +} + +overlay > revealer.right > scrolledwindow.frame { + margin-left: 32px; +} + +.terminix-session-sidebar, +.tilix-session-sidebar { + background-image: image(#2a2a2a); +} + +.terminal-titlebar button { + border-radius: 0; +} + +button.image-button.session-new-button { + min-width: 30px; +} + +notebook.tilix-background tab > box > stack { + margin: -6px; +} + +button.flat.tilix-small-button { + min-height: 20px; + min-width: 16px; +} + +.terminator-terminal-window paned > separator { + background-color: #1a1a1a; +} + +.terminator-terminal-window notebook.frame { + border-style: none; +} + +#live_installer .menubar progressbar trough { + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.12); +} + +.meld-notebook, .meld-notebook > stack { + background: none; + border-radius: 0 0 12px 12px; +} + +.meld-notebook-child { + background-color: #141414; + border-radius: 0 0 12px 12px; +} + +statusbar.meld-status-bar { + background: none; +} + +window.background > box.vertical > scrolledwindow > widget toolbar { + padding: 2px; +} + +window.background > box.vertical > scrolledwindow > widget toolbar separator, +window.background > box.vertical > scrolledwindow > widget toolbar button { + margin: 2px; +} + +window.background > box.vertical > scrolledwindow > widget toolbar button { + border-radius: 6px; +} + +window.background.chromium { + background-color: #2a2a2a; +} + +window.background.chromium entry, +window.background.chromium > button { + border: 1px solid #333333; +} + +window.background.chromium > button { + color: {{colors.primary.default.hex}}; +} + +window.background.chromium > button:disabled { + color: rgba(255, 255, 255, 0.32); +} + +window.background.chromium menubar, +window.background.chromium headerbar { + color: rgba(255, 255, 255, 0.7); +} + +window.background.chromium headerbar.titlebar { + padding: 0 12px; +} + +window.background.chromium headerbar.titlebar button:active { + background-color: alpha(currentColor, 0.12); +} + +window.background.chromium spinner { + color: {{colors.primary.default.hex}}; +} + +window.background.chromium textview.view { + background-color: transparent; +} + +window.background.chromium treeview.view.cell:selected:focus { + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; +} + +window.background.chromium treeview.view button { + border: 1px solid rgba(255, 255, 255, 0.3); + background-color: #1a1a1a; +} + +window.background.chromium menu { + border-color: #555555; +} + +window.background.chromium menu menuitem { + border-radius: 0; +} + +tooltip.background.chromium { + background-color: #191919; +} + +#MozillaGtkWidget decoration { + border: none; +} + +#MozillaGtkWidget > widget text { + background-color: #2a2a2a; +} + +#MozillaGtkWidget > widget text:selected { + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_surface.default.hex}}; +} + +#MozillaGtkWidget > widget > separator { + color: #333333; +} + +#MozillaGtkWidget > widget > scrollbar { + background-clip: border-box; +} + +#MozillaGtkWidget > widget > frame > border { + border-color: #333333; +} + +#MozillaGtkWidget > widget > entry, +#MozillaGtkWidget > widget > button > button { + border: 1px solid #333333; + border-radius: 6px; + box-shadow: none; +} + +#MozillaGtkWidget > widget > entry:disabled, +#MozillaGtkWidget > widget > button > button:disabled { + border-color: rgba(255, 255, 255, 0.12); +} + +#MozillaGtkWidget > widget > entry { + min-height: 30px; + background-color: #1a1a1a; +} + +#MozillaGtkWidget > widget > entry:focus { + border-color: {{colors.primary.default.hex}}; + box-shadow: inset 0 0 0 1px {{colors.primary.default.hex}}; +} + +#MozillaGtkWidget > widget > entry:disabled { + background-color: #141414; +} + +#MozillaGtkWidget > widget > button > button { + padding: 4px 8px; + background-size: auto; +} + +#MozillaGtkWidget > widget > button > button:hover { + box-shadow: inset 0 0 0 9999px alpha(currentColor, 0.08); +} + +#MozillaGtkWidget > widget > button > button:active { + background-image: image(alpha(currentColor, 0.12)); +} + +#MozillaGtkWidget > widget > checkbutton > check, +#MozillaGtkWidget > widget > radiobutton > radio { + margin: 0; + padding: 0; +} + +#MozillaGtkWidget > widget > checkbutton > check:not(:checked):not(:indeterminate), +#MozillaGtkWidget > widget > radiobutton > radio:not(:checked):not(:indeterminate) { + color: #464646; +} + +#MozillaGtkWidget > widget > checkbutton > check:not(:checked):not(:indeterminate):hover, #MozillaGtkWidget > widget > checkbutton > check:not(:checked):not(:indeterminate):active, +#MozillaGtkWidget > widget > radiobutton > radio:not(:checked):not(:indeterminate):hover, +#MozillaGtkWidget > widget > radiobutton > radio:not(:checked):not(:indeterminate):active { + color: #727272; +} + +#MozillaGtkWidget > widget > checkbutton > check:not(:checked):not(:indeterminate):disabled, +#MozillaGtkWidget > widget > radiobutton > radio:not(:checked):not(:indeterminate):disabled { + color: rgba(70, 70, 70, 0.5); +} + +#MozillaGtkWidget menu { + border: none; +} + +#MozillaGtkWidget > widget > menubar { + color: rgba(255, 255, 255, 0.7); +} + +#MozillaGtkWidget > widget > menubar:hover { + color: {{colors.on_surface.default.hex}}; +} + +#MozillaGtkWidget > widget > menubar:disabled { + color: rgba(255, 255, 255, 0.32); +} + +#MozillaGtkWidget > widget > frame { + color: #333333; +} + +#MozillaGtkWidget menu > separator { + color: #333333; +} + +window.background:not(.csd) > window > menu menuitem { + transition: none; +} + +#ToolboxCommon > #AuxToolbox #StyleSwatch { + font-size: smaller; +} + +#ToolboxCommon > #AuxToolbox #Kludge { + padding: 0; +} + +#ToolboxCommon > #AuxToolbox spinbutton, +#ToolboxCommon > #AuxToolbox entry { + min-height: 32px; +} + +#ToolboxCommon > #AuxToolbox button:not(.up):not(.down) { + min-height: 24px; + min-width: 16px; + padding: 4px 8px; +} + +#ToolboxCommon > #AuxToolbox spinbutton button { + border-width: 4px; +} + +#ToolboxCommon > toolbar.vertical { + margin-top: -4px; +} + +#ToolboxCommon > toolbar.vertical button { + min-height: 24px; + min-width: 24px; + padding: 4px; +} + +#TopToolbars .toolbar { + background: none; +} + +#Statusbar { + padding: 3px 0 3px 8px; +} + +#Statusbar spinbutton, #Statusbar box > button.flat { + margin: 3px 0; +} + +#CanvasTable button { + min-height: 16px; + min-width: 16px; + padding: 0; +} + +#CanvasTable #HorizontalScrollbar { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +#CanvasTable #VerticalScrollbar:dir(ltr) { + border-left: 1px solid rgba(255, 255, 255, 0.12); +} + +#CanvasTable #VerticalScrollbar:dir(rtl) { + border-right: 1px solid rgba(255, 255, 255, 0.12); +} + +#Canvas_and_Dock frame > border { + border: none; +} + +#Canvas_and_Dock widget > widget > button.flat { + min-height: 16px; + min-width: 16px; + padding: 4px; +} + +#Canvas_and_Dock widget > widget > box.horizontal image { + padding: 4px; +} + +#Canvas_and_Dock box.horizontal > box.vertical > button.flat { + min-height: 16px; + min-width: 24px; + padding: 8px 4px; +} + +window.bright.symbolic scrolledwindow > viewport.frame > notebook tabs > tab { + padding: 3px 6px; +} + +window.bright.symbolic scrolledwindow > viewport.frame > notebook tabs > tab button.close-button { + margin: 4px 0; +} + +popover#ContextMenu { + border-radius: 12px; + background-clip: content-box; +} + +popover#ContextMenu modelbutton.flat { + border-radius: 6px; +} + +window.background > grid > widget > widget > scrolledwindow > viewport > grid > box > box > frame > box { + background-color: #1a1a1a; +} + +window.background.csd > box.vertical > overlay > stack > box.vertical > box.horizontal > revealer > stack > list, +window.background.csd > box.vertical > overlay > stack > box.vertical > box.horizontal > revealer > stack > scrolledwindow > viewport.frame > list, +window.background.csd > box.vertical > overlay > stack > box.vertical > box.horizontal > revealer > stack > box.vertical > stack > scrolledwindow > viewport.frame > list { + border: none; + border-radius: 0; +} + +window.background.csd > box.vertical > overlay > stack > box.vertical > box.horizontal > revealer > stack > list > row.activatable, +window.background.csd > box.vertical > overlay > stack > box.vertical > box.horizontal > revealer > stack > scrolledwindow > viewport.frame > list > row.activatable, +window.background.csd > box.vertical > overlay > stack > box.vertical > box.horizontal > revealer > stack > box.vertical > stack > scrolledwindow > viewport.frame > list > row.activatable { + border-radius: 6px; +} + +window.background:not(.csd):not(.solid-csd) > button:not(:hover):not(:active):not(:checked):not(:disabled):not(.flat) { + background-color: #141414; +} + +window.background:not(.csd) > box > widget > widget > widget > widget > widget > widget > widget > scrolledwindow entry:focus { + background-color: #373737; +} + +/********* + * Unity * + *********/ +UnityDecoration { + -UnityDecoration-extents: 28px 0 0 0; + -UnityDecoration-input-extents: 8px; + -UnityDecoration-shadow-offset-x: 0; + -UnityDecoration-shadow-offset-y: 3px; + -UnityDecoration-active-shadow-color: rgba(0, 0, 0, 0.48); + -UnityDecoration-active-shadow-radius: 18px; + -UnityDecoration-inactive-shadow-color: rgba(0, 0, 0, 0.32); + -UnityDecoration-inactive-shadow-radius: 6px; + -UnityDecoration-glow-size: 8px; + -UnityDecoration-glow-color: {{colors.primary.default.hex}}; + -UnityDecoration-title-indent: 4px; + -UnityDecoration-title-fade: 32px; + -UnityDecoration-title-alignment: 0.0; +} + +UnityDecoration .top { + padding: 0 2px; + border-style: none; + border-radius: 12px 12px 0 0; + box-shadow: inset 0 1px rgba(255, 255, 255, 0.1); + background-color: #141414; + color: {{colors.on_surface.default.hex}}; +} + +UnityDecoration .top:backdrop { + background-color: #1a1a1a; + color: rgba(255, 255, 255, 0.7); +} + +UnityDecoration .menuitem { + color: rgba(255, 255, 255, 0.7); +} + +UnityDecoration .menuitem:hover { + box-shadow: inset 0 -2px currentColor; + background-color: transparent; + color: {{colors.on_surface.default.hex}}; +} + +.background:not(.csd) headerbar:not(.titlebar) { + border-radius: 0; + box-shadow: 0 2px 3px -2px rgba(0, 0, 0, 0.3), 0 1px 2px -1px rgba(0, 0, 0, 0.24), 0 1px 2px -1px rgba(0, 0, 0, 0.17); +} + +.background:not(.csd) headerbar:not(.titlebar).inline-toolbar { + border-style: none; +} + +UnityPanelWidget, +.unity-panel { + background-color: #0f0f0f; + color: {{colors.on_surface.default.hex}}; +} + +UnityPanelWidget:backdrop, +.unity-panel:backdrop { + color: rgba(255, 255, 255, 0.7); +} + +.unity-panel.menuitem, +.unity-panel .menuitem { + color: rgba(255, 255, 255, 0.7); +} + +.unity-panel.menubar.menuitem:hover, +.unity-panel.menubar .menuitem *:hover { + box-shadow: inset 0 -2px currentColor; + background-color: transparent; + color: {{colors.on_surface.default.hex}}; +} + +.menu IdoPlaybackMenuItem.menuitem:active { + -gtk-icon-source: -gtk-icontheme("process-working-symbolic"); + animation: spin 1s linear infinite; + color: {{colors.primary.default.hex}}; +} + +.lightdm.menu { + background-image: none; + background-color: rgba(0, 0, 0, 0.45); + border: none; + border-radius: 12px; + padding: 0; + color: white; +} + +.lightdm.menu .menuitem *, +.lightdm.menu .menuitem.check:active, +.lightdm.menu .menuitem.radio:active { + color: white; +} + +.lightdm.menubar { + color: rgba(255, 255, 255, 0.8); + background-image: none; + background-color: rgba(0, 0, 0, 0.5); +} + +.lightdm.menubar > .menuitem { + padding: 2px 6px; +} + +.lightdm-combo .menu { + background-color: #1a1a1a; + border-radius: 0; + padding: 0; + color: {{colors.on_surface.default.hex}}; +} + +/************** + * Mate-Panel * + **************/ +.mate-panel-menu-bar menubar, +#PanelApplet-window-menu-applet-button { + background-color: transparent; +} + +.mate-panel-menu-bar { + background-color: #0f0f0f; + color: rgba(255, 255, 255, 0.7); + font-weight: 500; +} + +.mate-panel-menu-bar button { + min-height: 16px; + min-width: 16px; + padding: 0; + border-radius: 0; +} + +PanelToplevel.horizontal > grid > button { + min-width: 24px; +} + +PanelToplevel.vertical > grid > button { + min-height: 24px; +} + +PanelSeparator { + color: rgba(255, 255, 255, 0.12); +} + +MatePanelAppletFrameDBus { + border-style: solid; + border-color: rgba(255, 255, 255, 0.12); +} + +.mate-panel-menu-bar.horizontal MatePanelAppletFrameDBus { + border-width: 0 1px; +} + +.mate-panel-menu-bar.vertical MatePanelAppletFrameDBus { + border-width: 1px 0; +} + +.mate-panel-menu-bar menubar > menuitem { + color: rgba(255, 255, 255, 0.7); +} + +.mate-panel-menu-bar menubar > menuitem:hover { + color: {{colors.on_surface.default.hex}}; +} + +.mate-panel-menu-bar menubar > menuitem:disabled { + color: rgba(255, 255, 255, 0.32); +} + +.mate-panel-menu-bar.horizontal menubar > menuitem { + padding: 0 8px; +} + +.mate-panel-menu-bar.vertical menubar > menuitem { + padding: 8px 0; +} + +.mate-panel-menu-bar menubar menu > menuitem { + min-height: 28px; + padding: 0 6px; +} + +.mate-panel-menu-bar #PanelApplet button { + -GtkWidget-window-dragging: true; +} + +.mate-panel-menu-bar #tasklist-button { + border-image: radial-gradient(circle closest-corner at center calc(100% - 1px), currentColor 0%, transparent 0%) 0 0 0/0 0 0px; +} + +.mate-panel-menu-bar #tasklist-button:checked { + border-image: radial-gradient(circle closest-corner at center calc(100% - 1px), currentColor 100%, transparent 0%) 0 0 2/0 0 2px; +} + +.mate-panel-menu-bar #tasklist-button image:dir(ltr), .mate-panel-menu-bar #tasklist-button label:dir(rtl) { + padding-left: 4px; +} + +.mate-panel-menu-bar #tasklist-button label:dir(ltr), .mate-panel-menu-bar #tasklist-button image:dir(rtl) { + padding-right: 4px; +} + +.mate-panel-menu-bar.vertical #tasklist-button { + min-height: 32px; +} + +.mate-panel-menu-bar.horizontal #showdesktop-button image { + min-width: 24px; + padding: 0 4px; +} + +.mate-panel-menu-bar.vertical #showdesktop-button image { + min-height: 24px; + padding: 4px 0; +} + +PanelApplet.wnck-applet .wnck-pager { + background-color: transparent; + color: {{colors.primary.default.hex}}; +} + +PanelApplet.wnck-applet .wnck-pager:hover { + background-color: alpha(currentColor, 0.08); +} + +PanelApplet.wnck-applet .wnck-pager:active { + background-color: alpha(currentColor, 0.12); +} + +PanelApplet.wnck-applet .wnck-pager:selected { + background-color: {{colors.primary.default.hex}}; +} + +.mate-panel-menu-bar.horizontal #clock-applet-button label { + padding: 0 8px; +} + +.mate-panel-menu-bar.vertical #clock-applet-button label { + padding: 8px 0; +} + +#MatePanelPopupWindow { + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 7px; + box-shadow: inset 0 1px rgba(255, 255, 255, 0.1); + background-color: #2a2a2a; +} + +#MatePanelPopupWindow frame > border { + border-style: none; +} + +#MatePanelPopupWindow calendar { + border-style: none; +} + +#MatePanelPopupWindow calendar:not(:selected) { + background-color: transparent; +} + +#MatePanelPopupWindow calendar + box { + margin-top: -5px; + padding-top: 5px; + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +#MatePanelPopupWindow expander > title { + min-height: 32px; +} + +#MatePanelPopupWindow button { + padding: 4px 16px; +} + +#MatePanelPopupWindow > frame > box > box > box > widget { + color: rgba(255, 255, 255, 0.12); +} + +na-tray-applet { + -NaTrayApplet-icon-padding: 3px; + -NaTrayApplet-icon-size: 16; +} + +.mate-panel-menu-bar { + -PanelMenuBar-icon-visible: true; +} + +.mate-panel-applet-slider { + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 7px; + box-shadow: inset 0 1px rgba(255, 255, 255, 0.1); + background-color: #2a2a2a; +} + +.mate-panel-applet-slider frame > border { + border-style: none; +} + +#PanelApplet:not(:selected) > box { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); +} + +#PanelApplet:selected > box { + background-color: alpha(currentColor, 0.1); + color: {{colors.on_surface.default.hex}}; +} + +#mate-menu { + border: 1px solid rgba(0, 0, 0, 0.25); + background-color: #2a2a2a; +} + +#mate-menu button { + min-height: 24px; + min-width: 24px; + padding: 4px 0; + color: {{colors.on_surface.default.hex}}; + font-weight: normal; +} + +#mate-menu button:not(.flat) { + background-color: alpha(currentColor, 0.1); +} + +#mate-menu button image, +#mate-menu button label + label { + color: rgba(255, 255, 255, 0.7); +} + +#mate-menu entry { + margin: 0 0 4px; +} + +#mate-menu entry image { + margin: 0; +} + +#mate-menu entry + button { + margin: 0 4px 4px; + padding: 5px; +} + +.brisk-menu { + box-shadow: inset 0 1px rgba(255, 255, 255, 0.1); + background-color: #2a2a2a; +} + +.brisk-menu entry { + margin-bottom: -2px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + border-image: none; + box-shadow: none; + background-color: transparent; +} + +.brisk-menu entry + box > box:dir(ltr) { + margin-right: -2px; + border-right: 1px solid rgba(255, 255, 255, 0.12); +} + +.brisk-menu entry + box > box:dir(rtl) { + margin-left: -2px; + border-left: 1px solid rgba(255, 255, 255, 0.12); +} + +.brisk-menu .categories-list { + padding-top: 4px; +} + +.brisk-menu .categories-list button { + margin: 0 4px; +} + +.brisk-menu .categories-list button:checked { + color: {{colors.primary.default.hex}}; +} + +.brisk-menu .session-button { + padding: 11px; +} + +.brisk-menu .frame { + border-style: none; +} + +.brisk-menu .apps-list { + padding: 4px 0; + background-color: transparent; +} + +.brisk-menu .apps-list row { + padding: 0; +} + +.brisk-menu .apps-list row:hover { + box-shadow: none; +} + +.brisk-menu .apps-list button { + border-radius: 0; + color: {{colors.on_surface.default.hex}}; + font-weight: normal; +} + +/********************* + * CAJA File manager * + *********************/ +.caja-navigation-window button.toggle.image-button { + border-radius: 6px; +} + +.caja-pathbar button { + margin: 0 -1px 0 -2px; +} + +.caja-pathbar button.slider-button { + min-width: 24px; +} + +.caja-pathbar button > widget { + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); + -GtkArrow-arrow-scaling: 1; +} + +.caja-side-pane notebook viewport.frame, +.caja-side-pane notebook widget .vertical { + background-color: #1a1a1a; +} + +.caja-side-pane notebook, +.caja-notebook { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.caja-side-pane notebook .frame, +.caja-notebook .frame { + border-style: none; +} + +.caja-canvas-item { + border-radius: 6px; +} + +.caja-desktop.view .entry, +.caja-navigation-window .view .entry { + border: none; + border-radius: 6px; + background-color: rgba(255, 255, 255, 0.04); + background-image: none; + color: {{colors.on_surface.default.hex}}; +} + +.caja-desktop.view .entry:selected, +.caja-navigation-window .view .entry:selected { + background-color: alpha(currentColor, 0.06); +} + +.caja-desktop.view .entry { + background-color: #1a1a1a; + color: {{colors.on_surface.default.hex}}; + caret-color: currentColor; +} + +.caja-desktop.view .entry:selected { + background-color: alpha(currentColor, 0.06); +} + +.caja-navigation-window statusbar { + margin: 0 -10px; + padding: 0 4px; + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.caja-notebook frame > border { + border-style: none; +} + +#caja-extra-view-widget { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; +} + +#caja-extra-view-widget > box > box > label { + font-weight: bold; +} + +/********* + * Pluma * + *********/ +.pluma-window statusbar { + margin: 0 -10px; + padding: 0 4px; + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.pluma-window statusbar frame > border { + border-style: none; +} + +.pluma-window statusbar frame button.flat { + padding: 0 4px; + border-radius: 0; +} + +.pluma-window statusbar frame button.flat widget { + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); + -GtkArrow-arrow-scaling: 1; +} + +.pluma-print-preview toolbar { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +.pluma-window paned.horizontal box.vertical box.horizontal button.flat { + margin: 1px; +} + +.pluma-window paned.horizontal box.vertical .frame { + border-style: none; +} + +.pluma-window paned.horizontal box.vertical notebook.frame { + margin-top: -1px; + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.pluma-window paned.horizontal box.vertical notebook.frame box.vertical toolbar.horizontal { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +/********* + * Atril * + *********/ +.atril-window paned.horizontal box.vertical .frame { + border-style: none; +} + +.atril-window paned.horizontal box.vertical notebook .frame { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +/* mate-screensaver lock dialog */ +.lock-dialog { + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 7px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 3px 3px 0 rgba(0, 0, 0, 0.18), 0 3px 6px 0 rgba(0, 0, 0, 0.12), inset 0 1px rgba(255, 255, 255, 0.1); + background-color: #2a2a2a; +} + +.lock-dialog frame > border { + border-style: none; +} + +.lock-dialog button:not(:disabled) { + color: {{colors.primary.default.hex}}; +} + +/* multimedia OSD */ +MsdOsdWindow.background.osd { + border-radius: 6px; + background-color: rgba(25, 25, 25, 0.9); + color: {{colors.on_surface.default.hex}}; +} + +MsdOsdWindow.background.osd .trough { + border-radius: 0; + background-color: rgba(255, 255, 255, 0.12); +} + +MsdOsdWindow.background.osd .progressbar { + border-radius: 0; + background-color: {{colors.primary.default.hex}}; +} + +/****************** + * Budgie Desktop * + ******************/ +.budgie-container { + background-color: transparent; +} + +.budgie-settings-window list.sidebar { + background: none; + border-radius: 0 0 0 12px; +} + +.budgie-settings-window buttonbox.inline-toolbar { + border: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +.budgie-settings-window buttonbox.inline-toolbar button { + border-radius: 6px; +} + +dialog.background > .dialog-vbox > scrolledwindow > viewport.frame > list { + border-right: 1px solid #333333; +} + +.budgie-popover { + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 3px 3px 0 rgba(0, 0, 0, 0.18), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.75); + background-clip: border-box; + background-color: #1a1a1a; + border-radius: 12px; +} + +.budgie-popover frame.container { + padding: 6px; + border: none; +} + +.budgie-popover frame.container .container { + padding: 0; +} + +.budgie-popover list, .budgie-popover row { + padding: 0; +} + +.budgie-popover separator.horizontal { + margin: 3px 0; +} + +.budgie-popover border { + border: none; +} + +.budgie-popover list { + background-color: transparent; +} + +.budgie-popover row:hover { + box-shadow: none; +} + +.budgie-popover scrolledwindow.sidebar:not(.categories) { + background: none; + border-right: none; +} + +.budgie-popover scrolledwindow.sidebar:not(.categories) list { + background-color: rgba(255, 255, 255, 0.04); + border-radius: 3px; + padding: 3px 0; +} + +.budgie-popover scrolledwindow.sidebar:not(.categories) list > row.activatable { + padding: 6px 8px; +} + +.budgie-popover scrolledwindow.sidebar + separator { + margin: 0; + background-color: transparent; + min-width: 0; + min-height: 0; +} + +.budgie-popover scrolledwindow.sidebar + separator + scrolledwindow { + margin-left: 3px; +} + +.budgie-popover scrolledwindow.sidebar + separator + scrolledwindow list > row.activatable { + border-radius: 3px; +} + +.budgie-popover treeview.view.sidebar { + border-right: none; + background: none; +} + +.budgie-popover treeview.view.sidebar:hover { + background-color: alpha(currentColor, 0.08); +} + +.budgie-popover treeview.view.sidebar:selected { + background-color: alpha(currentColor, 0.12); +} + +.budgie-popover.budgie-menu .container { + padding: 0; +} + +.budgie-popover.budgie-menu .sidebar, +.budgie-popover.budgie-menu scrollbar, +.budgie-popover.budgie-menu entry.search { + background-color: transparent; +} + +.budgie-popover.budgie-menu entry.search { + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + border-image: none; + border-radius: 0; + box-shadow: none; + font-size: 120%; +} + +.budgie-popover.budgie-menu scrolledwindow.sidebar.categories { + background-color: rgba(255, 255, 255, 0.04); + padding-bottom: 12px; +} + +.budgie-popover.budgie-menu scrolledwindow.sidebar.categories button.flat.radio.category-button { + border-radius: 0; +} + +.budgie-popover.budgie-menu scrolledwindow > viewport.frame > list > row.activatable > button.flat { + border-radius: 0; +} + +.budgie-popover.budgie-menu list.left-overlay-menu { + border-radius: 12px; + background-color: #1a1a1a; + padding: 6px; + margin: 6px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 3px 3px 0 rgba(0, 0, 0, 0.18), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.75); + background-clip: border-box; +} + +.budgie-popover.budgie-menu list.left-overlay-menu > row.activatable { + border-radius: 6px; +} + +.budgie-popover.budgie-menu list.left-overlay-menu > row.activatable:not(:last-child) { + margin-bottom: 3px; +} + +.budgie-popover.budgie-menu list.left-overlay-menu > row.activatable button.menuitem { + border-radius: 6px; +} + +.budgie-popover.budgie-menu .budgie-menu-footer { + border-top: 1px solid rgba(255, 255, 255, 0.12); + padding: 6px; +} + +.budgie-popover.budgie-menu .budgie-menu-footer button.flat { + padding: 3px; + border-radius: 6px; +} + +.budgie-popover.budgie-menu .budgie-menu-footer button.flat.user-icon-button { + padding-right: 9px; +} + +.budgie-popover.budgie-menu .budgie-menu-footer button.flat.image-button { + border-radius: 9999px; + padding: 6px; + min-height: 16px; + min-width: 16px; + margin-left: 9px; + background-clip: border-box; +} + +.budgie-popover.user-menu list, +.budgie-popover.user-menu row { + border: none; + background: none; + box-shadow: none; +} + +.budgie-popover.user-menu > frame.container > box.vertical row.activatable:first-child { + margin-bottom: 0; + outline-width: 0; + border-radius: 6px; +} + +.budgie-popover.user-menu > frame.container > box.vertical row.activatable:first-child button.indicator-item { + transition: none; + animation: none; +} + +.budgie-popover.sound-popover .container { + padding: 0; +} + +.budgie-popover.sound-popover separator { + margin: 3px 0; +} + +.budgie-popover.night-light-indicator .view-header { + margin: 0 6px; +} + +.budgie-popover.places-menu .name-button image:dir(ltr) { + margin-right: 3px; +} + +.budgie-popover.places-menu .name-button image:dir(rtl) { + margin-left: 3px; +} + +.budgie-popover.places-menu .unmount-button { + margin: 2px; + padding: 0; +} + +.budgie-popover.places-menu .places-list:not(.always-expand) { + margin-top: 3px; + padding-top: 3px; + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.budgie-popover.places-menu .alternative-label { + padding: 3px; + font-size: 15px; +} + +.budgie-popover.workspace-popover flowboxchild { + padding: 0; +} + +.workspace-switcher .workspace-layout { + border: 0 solid rgba(255, 255, 255, 0.12); +} + +.top .workspace-switcher .workspace-layout:dir(ltr), .bottom .workspace-switcher .workspace-layout:dir(ltr) { + border-left-width: 1px; +} + +.top .workspace-switcher .workspace-layout:dir(rtl), .bottom .workspace-switcher .workspace-layout:dir(rtl) { + border-right-width: 1px; +} + +.left .workspace-switcher .workspace-layout, .right .workspace-switcher .workspace-layout { + border-top-width: 1px; +} + +.workspace-switcher .workspace-item { + border: 0 solid rgba(255, 255, 255, 0.12); +} + +.top .workspace-switcher .workspace-item:dir(ltr), .bottom .workspace-switcher .workspace-item:dir(ltr) { + border-right-width: 1px; +} + +.top .workspace-switcher .workspace-item:dir(rtl), .bottom .workspace-switcher .workspace-item:dir(rtl) { + border-left-width: 1px; +} + +.left .workspace-switcher .workspace-item, .right .workspace-switcher .workspace-item { + border-bottom-width: 1px; +} + +.workspace-switcher .workspace-item { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); +} + +.workspace-switcher .workspace-item.current-workspace { + background-color: alpha(currentColor, 0.1); +} + +.workspace-switcher .workspace-add-button { + border-radius: 6px; +} + +.budgie-panel .workspace-switcher .workspace-add-button { + min-height: 24px; + min-width: 24px; + padding: 0; +} + +.budgie-panel .workspace-switcher .workspace-icon-button { + min-height: 24px; + min-width: 24px; + padding: 0; + border-radius: 6px; +} + +.budgie-panel { + transition: background-color 75ms cubic-bezier(0, 0, 0.2, 1); + background-color: #0f0f0f; + color: rgba(255, 255, 255, 0.7); + font-weight: 500; +} + +.budgie-panel.transparent { + background-color: rgba(33, 33, 33, 0.75); +} + +.budgie-panel .icon-tasklist button.launcher:checked, .budgie-panel .icon-tasklist button.launcher:active { + color: {{colors.on_surface.default.hex}}; +} + +.top .budgie-panel.dock-mode { + border-radius: 0 0 18px 18px; +} + +.bottom .budgie-panel.dock-mode { + border-radius: 18px 18px 0 0; +} + +.bottom .budgie-panel.dock-mode .icon-tasklist > box > revealer:first-child > button.launcher { + border-top-left-radius: 18px; +} + +.bottom .budgie-panel.dock-mode .icon-tasklist > box > revealer:last-child > button.launcher { + border-top-right-radius: 18px; +} + +.left .budgie-panel.dock-mode { + border-radius: 0 18px 18px 0; +} + +.left .budgie-panel.dock-mode .icon-tasklist .launcher:first-child { + border-top-right-radius: 18px; +} + +.left .budgie-panel.dock-mode .icon-tasklist .launcher:last-child { + border-bottom-right-radius: 18px; +} + +.right .budgie-panel.dock-mode { + border-radius: 18px 0 0 18px; +} + +.right .budgie-panel.dock-mode .icon-tasklist .launcher:first-child { + border-top-left-radius: 18px; +} + +.right .budgie-panel.dock-mode .icon-tasklist .launcher:last-child { + border-bottom-left-radius: 18px; +} + +.budgie-panel button { + color: rgba(255, 255, 255, 0.7); + min-height: 24px; + min-width: 24px; + padding: 0; + border-radius: 0; +} + +.budgie-panel button:hover { + color: {{colors.on_surface.default.hex}}; +} + +.budgie-panel button:active { + color: rgba(255, 255, 255, 0.7); +} + +.budgie-panel button.budgie-menu-launcher { + color: rgba(255, 255, 255, 0.7); +} + +.budgie-panel button.budgie-menu-launcher:focus { + box-shadow: none; + border: none; + color: {{colors.on_surface.default.hex}}; +} + +.budgie-panel button.raven-trigger { + color: rgba(255, 255, 255, 0.7); +} + +.budgie-panel.horizontal button { + padding: 0 6px; +} + +.budgie-panel.vertical button { + padding: 6px 0; +} + +.budgie-panel separator { + background-color: rgba(255, 255, 255, 0.12); +} + +.budgie-panel .alert { + color: #F44336; +} + +.budgie-panel > box > widget > widget > image, +.budgie-panel > box > widget > widget > stack > image, +.budgie-panel > box > widget > widget > box > image { + margin-left: 6px; + margin-right: 6px; +} + +.budgie-panel > box > widget > widget > box > image + label { + margin-left: -4px; +} + +.budgie-panel > box > widget > widget > box > widget > image { + margin-left: 6px; +} + +.budgie-panel > box > widget > widget > box > stack > widget > label { + margin-right: 6px; +} + +.budgie-panel > box > widget > widget > box > widget > widget > image { + margin-left: 2px; + margin-right: 2px; +} + +.budgie-panel .budgie-clock-applet > widget > box, +.budgie-panel .budgie-calendar-applet > widget > box { + padding-left: 3px; + padding-right: 3px; +} + +.budgie-panel .titlebar:not(headerbar) { + min-height: 0; + padding: 0; + box-shadow: none; + background-color: transparent; + color: {{colors.on_surface.default.hex}}; +} + +.budgie-panel .titlebar:not(headerbar) button:not(.suggested-action):not(.destructive-action) { + color: rgba(255, 255, 255, 0.7); +} + +.budgie-panel .titlebar:not(headerbar) button:not(.suggested-action):not(.destructive-action):hover, .budgie-panel .titlebar:not(headerbar) button:not(.suggested-action):not(.destructive-action):active { + color: {{colors.on_surface.default.hex}}; +} + +.budgie-panel menubar, +.budgie-panel .menubar { + color: rgba(255, 255, 255, 0.7); + box-shadow: none; + border: none; +} + +.budgie-panel menubar > menuitem, +.budgie-panel .menubar > menuitem { + color: rgba(255, 255, 255, 0.7); +} + +.budgie-panel menubar > menuitem:hover, .budgie-panel menubar > menuitem:active, +.budgie-panel .menubar > menuitem:hover, +.budgie-panel .menubar > menuitem:active { + color: {{colors.on_surface.default.hex}}; +} + +.budgie-panel menubar menu separator, +.budgie-panel .menubar menu separator { + background-color: rgba(255, 255, 255, 0.12); +} + +.budgie-panel #tasklist-button { + padding: 0 4px; +} + +.budgie-panel.vertical #tasklist-button { + min-height: 32px; +} + +.budgie-panel button.flat.launcher { + padding: 0; +} + +.budgie-panel button.flat.launcher:not(:checked) { + color: rgba(255, 255, 255, 0.5); +} + +.budgie-panel button.flat.launcher:not(:checked):hover, .budgie-panel button.flat.launcher:not(:checked):active { + color: rgba(255, 255, 255, 0.7); +} + +.budgie-panel button.flat.launcher:not(:checked):disabled { + color: rgba(255, 255, 255, 0.32); +} + +.top .budgie-panel .unpinned button.flat.launcher:checked, .top .budgie-panel .pinned button.flat.launcher.running:checked { + border-image: radial-gradient(circle closest-corner at center calc(1px), currentColor 100%, transparent 0%) 2 0 0 0/2px 0 0 0; +} + +.bottom .budgie-panel .unpinned button.flat.launcher:checked, .bottom .budgie-panel .pinned button.flat.launcher.running:checked { + border-image: radial-gradient(circle closest-corner at center calc(100% - 1px), currentColor 100%, transparent 0%) 0 0 2 0/0 0 2px 0; +} + +.left .budgie-panel .unpinned button.flat.launcher:checked, .left .budgie-panel .pinned button.flat.launcher.running:checked { + border-image: radial-gradient(circle closest-corner at calc(1px) center, currentColor 100%, transparent 0%) 0 0 0 2/0 0 0 2px; +} + +.right .budgie-panel .unpinned button.flat.launcher:checked, .right .budgie-panel .pinned button.flat.launcher.running:checked { + border-image: radial-gradient(circle closest-corner at calc(100% - 1px) center, currentColor 100%, transparent 0%) 0 2 0 0/0 2px 0 0; +} + +.top .budgie-panel #tasklist-button, .budgie-panel .top #tasklist-button { + border-image: radial-gradient(circle closest-corner at center calc(1px), currentColor 0%, transparent 0%) 0 0 0 0/0 0 0 0; +} + +.top .budgie-panel #tasklist-button:checked, .budgie-panel .top #tasklist-button:checked { + border-image: radial-gradient(circle closest-corner at center calc(1px), currentColor 100%, transparent 0%) 2 0 0 0/2px 0 0 0; +} + +.bottom .budgie-panel #tasklist-button, .budgie-panel .bottom #tasklist-button { + border-image: radial-gradient(circle closest-corner at center calc(100% - 1px), currentColor 0%, transparent 0%) 0 0 0 0/0 0 0 0; +} + +.bottom .budgie-panel #tasklist-button:checked, .budgie-panel .bottom #tasklist-button:checked { + border-image: radial-gradient(circle closest-corner at center calc(100% - 1px), currentColor 100%, transparent 0%) 0 0 2 0/0 0 2px 0; +} + +.left .budgie-panel #tasklist-button, .budgie-panel .left #tasklist-button { + border-image: radial-gradient(circle closest-corner at calc(1px) center, currentColor 0%, transparent 0%) 0 0 0 0/0 0 0 0; +} + +.left .budgie-panel #tasklist-button:checked, .budgie-panel .left #tasklist-button:checked { + border-image: radial-gradient(circle closest-corner at calc(1px) center, currentColor 100%, transparent 0%) 0 0 0 2/0 0 0 2px; +} + +.right .budgie-panel #tasklist-button, .budgie-panel .right #tasklist-button { + border-image: radial-gradient(circle closest-corner at calc(100% - 1px) center, currentColor 0%, transparent 0%) 0 0 0 0/0 0 0 0; +} + +.right .budgie-panel #tasklist-button:checked, .budgie-panel .right #tasklist-button:checked { + border-image: radial-gradient(circle closest-corner at calc(100% - 1px) center, currentColor 100%, transparent 0%) 0 2 0 0/0 2px 0 0; +} + +frame.raven-frame > border { + border-style: none; +} + +.top frame.raven-frame > border { + margin-bottom: 32px; +} + +.bottom frame.raven-frame > border { + margin-top: 32px; +} + +.left frame.raven-frame > border { + margin-right: 32px; +} + +.right frame.raven-frame > border { + margin-left: 32px; +} + +.raven { + background-color: #2a2a2a; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 0 15px 16px 2px rgba(0, 0, 0, 0.14), 0 6px 18px 5px rgba(0, 0, 0, 0.12); +} + +.raven > box { + margin-bottom: -10px; +} + +.raven stackswitcher.linked { + margin: 6px 16px; +} + +.raven stackswitcher.linked > button:focus { + box-shadow: none; +} + +.raven .raven-header { + min-height: 34px; + padding: 3px; +} + +.raven .raven-header.top { + padding: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +.raven .raven-header.top stackswitcher button { + margin: -4px 0 -5px; + padding: 0 16px; + min-height: 24px; +} + +.raven .raven-header.bottom { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.raven stack .raven-header { + margin-top: -6px; +} + +.raven stack scrolledwindow .raven-header { + margin-top: -8px; +} + +.raven .raven-background { + border-style: solid none; + border-width: 1px; + border-color: rgba(255, 255, 255, 0.12); + background-color: #1a1a1a; +} + +.raven .raven-background > overlay > widget > image { + color: rgba(255, 255, 255, 0.12); +} + +.raven scrolledwindow.raven-background { + border-bottom-style: none; +} + +.raven .powerstrip button { + margin: 2px 0 1px; + padding: 11px; + border-radius: 9999px; +} + +.raven .option-subtitle { + font-size: smaller; +} + +.raven .audio-widget scale.marks-after { + padding-top: 0; + padding-bottom: 0; +} + +.raven .audio-widget scale.marks-after label { + font-size: 90%; + padding: 0; + margin: -10px 0 0 6px; +} + +.raven .audio-widget button.flat.expander-button { + margin-top: 4px; + margin-bottom: 4px; +} + +.raven .audio-widget list.devices-list.sound-devices > row.activatable:selected, .raven .audio-widget list.devices-list.sound-devices > row.activatable:checked { + background-color: rgba(255, 255, 255, 0.06); + color: {{colors.on_surface.default.hex}}; +} + +.raven .audio-widget list.devices-list.sound-devices > row.activatable:selected label, .raven .audio-widget list.devices-list.sound-devices > row.activatable:checked label { + color: {{colors.on_surface.default.hex}}; +} + +.raven .audio-widget list.devices-list.sound-devices > row.activatable label { + padding-left: 12px; +} + +.raven levelbar, .raven levelbar trough, .raven levelbar block { + border-radius: 9999px; +} + +calendar.raven-calendar { + border-style: none; + background-color: transparent; +} + +calendar.raven-calendar:selected { + border-radius: 6px; +} + +.raven-mpris { + background-color: #141414; + color: {{colors.on_surface.default.hex}}; +} + +.raven-mpris label { + min-height: 24px; +} + +.raven-mpris button.image-button { + padding: 11px; +} + +image.raven-mpris { + background-color: rgba(255, 255, 255, 0.12); + color: rgba(255, 255, 255, 0.7); + border-radius: 6px; +} + +.raven-notifications-view > .raven-background > viewport.frame { + padding: 0; +} + +.raven-notifications-view > .raven-background > viewport.frame > list > row.activatable { + margin-left: -6px; + margin-right: -3px; +} + +.raven-notifications-view > .raven-background > viewport.frame > list > row.activatable .raven-notifications-group-header { + padding: 0 12px; +} + +.raven-notifications-view > .raven-background > viewport.frame > list > row.activatable list { + padding: 6px; + background: none; +} + +.raven-notifications-view > .raven-background > viewport.frame > list > row.activatable list > row.activatable { + border: none; + padding: 6px; + padding-left: 12px; + margin: 3px; + border-radius: 6px; + background-color: rgba(255, 255, 255, 0.04); +} + +.raven-notifications-view > .raven-background > viewport.frame > list > row.activatable list > row.activatable:hover, .raven-notifications-view > .raven-background > viewport.frame > list > row.activatable list > row.activatable:selected { + background-color: rgba(255, 255, 255, 0.12); +} + +.raven-notifications-view > .raven-background > viewport.frame > list > row.activatable:selected, .raven-notifications-view > .raven-background > viewport.frame > list > row.activatable:selected:hover, .raven-notifications-view > .raven-background > viewport.frame > list > row.activatable:hover, .raven-notifications-view > .raven-background > viewport.frame > list > row.activatable:active, .raven-notifications-view > .raven-background > viewport.frame > list > row.activatable:focus { + background: none; + box-shadow: none; +} + +.raven-notifications-group .raven-notifications-group-header { + padding-left: 6px; +} + +.raven-notifications-group list > row { + border-radius: 6px; + padding: 6px; +} + +.raven-notifications-group list > row .notification-clone { + padding-left: 6px; +} + +.drop-shadow { + margin: 12px; + padding: 6px; + border-radius: 12px; + background-color: #2a2a2a; + box-shadow: 0 2px 3px -1px rgba(0, 0, 0, 0.1), 0 4px 6px 0 rgba(0, 0, 0, 0.12), 0 1px 10px 0 rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(255, 255, 255, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.75); +} + +.budgie-notification-window, .budgie-switcher-window { + background: none; +} + +.budgie-notification .notification-title, .budgie-switcher .notification-title { + font-size: 110%; + color: {{colors.on_surface.default.hex}}; +} + +.budgie-notification .notification-body, .budgie-switcher .notification-body { + color: rgba(255, 255, 255, 0.7); +} + +.budgie-osd-window { + background: none; +} + +.budgie-osd-window > box { + border-radius: 12px; + padding: 3px; + margin: 0; +} + +.budgie-osd { + color: {{colors.on_surface.default.hex}}; +} + +.budgie-osd .budgie-osd-text { + font-size: 110%; +} + +.budgie-switcher-window .drop-shadow { + border-radius: 12px; + padding: 0; +} + +.budgie-switcher-window > box { + padding: 6px; + border-radius: 12px; +} + +.budgie-switcher-window flowbox { + color: {{colors.on_surface.default.hex}}; + padding: 0; +} + +.budgie-switcher-window flowboxchild { + padding: 6px; + margin: 0; + color: {{colors.on_surface.default.hex}}; + border-radius: 6px; + transition: background-color 75ms ease-out; +} + +.budgie-switcher-window flowboxchild:hover { + color: {{colors.on_surface.default.hex}}; + background-color: alpha(currentColor, 0.08); +} + +.budgie-switcher-window flowboxchild:active { + color: {{colors.on_surface.default.hex}}; + background-color: alpha(currentColor, 0.12); +} + +.budgie-switcher-window flowboxchild:selected { + color: {{colors.on_surface.default.hex}}; + background-color: alpha(currentColor, 0.1); +} + +.budgie-panel .lock-keys image:disabled { + color: rgba(255, 255, 255, 0.32); +} + +.budgie-session-dialog, +.budgie-polkit-dialog, +.budgie-run-dialog { + background-color: #2a2a2a; + border: none; + box-shadow: none; + padding: 0; +} + +.budgie-session-dialog > box > grid, +.budgie-polkit-dialog > box > grid, +.budgie-run-dialog > box > grid { + padding: 24px; +} + +.budgie-session-dialog.background, .budgie-session-dialog.background.csd > decoration, +.budgie-polkit-dialog.background, +.budgie-polkit-dialog.background.csd > decoration, +.budgie-run-dialog.background, +.budgie-run-dialog.background.csd > decoration { + border-radius: 12px; +} + +.budgie-session-dialog.background, +.budgie-polkit-dialog.background, +.budgie-run-dialog.background { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +.budgie-session-dialog.background.csd > decoration, +.budgie-polkit-dialog.background.csd > decoration, +.budgie-run-dialog.background.csd > decoration { + border: none; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 0 15px 16px 2px rgba(0, 0, 0, 0.14), 0 6px 18px 5px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.75); +} + +.budgie-session-dialog label:not(:last-child), +.budgie-session-dialog .dialog-title, +.budgie-polkit-dialog label:not(:last-child), +.budgie-polkit-dialog .dialog-title, +.budgie-run-dialog label:not(:last-child), +.budgie-run-dialog .dialog-title { + font-size: 110%; +} + +.budgie-session-dialog buttonbox.linked > button, +.budgie-polkit-dialog buttonbox.linked > button, +.budgie-run-dialog buttonbox.linked > button { + padding: 8px 16px; + border-top: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 0; +} + +.budgie-session-dialog buttonbox.linked > button.suggested-action:not(:disabled), +.budgie-polkit-dialog buttonbox.linked > button.suggested-action:not(:disabled), +.budgie-run-dialog buttonbox.linked > button.suggested-action:not(:disabled) { + color: {{colors.primary.default.hex}}; +} + +.budgie-session-dialog buttonbox.linked > button.destructive-action:not(:disabled), +.budgie-polkit-dialog buttonbox.linked > button.destructive-action:not(:disabled), +.budgie-run-dialog buttonbox.linked > button.destructive-action:not(:disabled) { + color: #F44336; +} + +.budgie-session-dialog buttonbox.linked > button:first-child, +.budgie-polkit-dialog buttonbox.linked > button:first-child, +.budgie-run-dialog buttonbox.linked > button:first-child { + border-bottom-left-radius: 12px; + border-top-left-radius: 0; +} + +.budgie-session-dialog buttonbox.linked > button:last-child, +.budgie-polkit-dialog buttonbox.linked > button:last-child, +.budgie-run-dialog buttonbox.linked > button:last-child { + border-bottom-right-radius: 12px; + border-top-right-radius: 0; +} + +.budgie-power-dialog .titlebar, .budgie-power-dialog .titlebar:backdrop { + background-color: transparent; + box-shadow: none; + border: none; +} + +.budgie-power-dialog.background { + border-radius: 18px; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +.budgie-power-dialog.background.csd > decoration { + border: none; + border-radius: 18px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 0 15px 16px 2px rgba(0, 0, 0, 0.14), 0 6px 18px 5px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.75); +} + +.budgie-polkit-dialog .message { + color: rgba(255, 255, 255, 0.7); +} + +.budgie-polkit-dialog .failure { + color: #F44336; +} + +.budgie-polkit-dialog > box > grid { + padding-bottom: 0; +} + +.budgie-run-dialog entry.search { + font-size: 110%; + padding: 6px 14px; + border-image: none; + box-shadow: none; + background-color: transparent; +} + +.budgie-run-dialog list .dim-label { + color: {{colors.on_surface.default.hex}}; +} + +.budgie-run-dialog scrolledwindow { + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +/************** + * Xfce4 Apps * + **************/ +.XfceHeading { + background-color: #1a1a1a; +} + +/*************** + * xfce4-panel * + ***************/ +.xfce4-panel.background { + border: none; + background-color: #0f0f0f; + color: rgba(255, 255, 255, 0.7); + font-weight: 500; +} + +.xfce4-panel.background button { + min-height: 16px; + min-width: 16px; + padding: 0 6px; + border-radius: 0; + color: rgba(255, 255, 255, 0.7); +} + +.xfce4-panel.background button:hover, .xfce4-panel.background button:active, .xfce4-panel.background button:checked { + color: {{colors.on_surface.default.hex}}; +} + +.xfce4-panel.background button:disabled { + color: rgba(255, 255, 255, 0.32); +} + +.xfce4-panel.background button.flat.toggle { + padding: 0 6px; +} + +.xfce4-panel.background .tasklist button { + padding: 0 6px; +} + +.xfce4-panel.background .tasklist button image { + padding: 4px; +} + +wnck-pager:hover { + background-color: alpha(currentColor, 0.08); +} + +wnck-pager:active { + background-color: alpha(currentColor, 0.12); +} + +wnck-pager:selected { + background-color: {{colors.primary.default.hex}}; +} + +#xfce4-mpc-plugin-26 > frame > border { + border: none; +} + +#xfce-panel-button { + -gtk-icon-style: symbolic; +} + +XfdesktopIconView.view { + border-radius: 6px; + background-color: transparent; + color: {{colors.on_surface.default.hex}}; +} + +XfdesktopIconView.view:active { + box-shadow: none; +} + +XfdesktopIconView.view .rubberband { + border-radius: 0; +} + +window#whiskermenu-window { + border-radius: 12px; + background-color: transparent; + border: none; +} + +window#whiskermenu-window entry.search:focus { + background-color: #1a1a1a; +} + +window#whiskermenu-window > frame > border { + border-radius: 12px; + padding: 6px 8px 6px 9px; + margin: 6px; + border: none; + background-color: #1a1a1a; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 3px 3px 0 rgba(0, 0, 0, 0.18), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.75), inset 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +window#whiskermenu-window box.categories > button.radio { + padding: 3px 6px; + margin: 1px 0; +} + +window#whiskermenu-window box.categories > button.radio:hover { + background-color: rgba(255, 255, 255, 0.12); +} + +window#whiskermenu-window box.categories > button.radio:checked, window#whiskermenu-window box.categories > button.radio:active { + background-color: rgba(255, 255, 255, 0.3); + color: {{colors.on_surface.default.hex}}; +} + +window#whiskermenu-window box.categories > button.radio:checked:hover, window#whiskermenu-window box.categories > button.radio:active:hover { + background-image: none; +} + +window#whiskermenu-window scrolledwindow.frame { + padding: 3px; + background-color: #1a1a1a; + border-radius: 6px; +} + +window#whiskermenu-window scrolledwindow.frame treeview.view { + border-radius: 6px; +} + +window#whiskermenu-window scrolledwindow.frame treeview.view:not(:hover):not(:selected) { + background: none; +} + +window#whiskermenu-window scrolledwindow.frame treeview.view:selected:hover { + background-color: rgba(255, 255, 255, 0.12); + color: {{colors.on_surface.default.hex}}; +} + +window#whiskermenu-window .title-area > .commands-area > button.flat.command-button:checked, window#whiskermenu-window .title-area > .commands-area > button.flat.command-button:active { + background-color: rgba(255, 255, 255, 0.3); + color: {{colors.on_surface.default.hex}}; +} + +#XfceNotifyWindow { + background-color: #1a1a1a; + border-radius: 12px; + border: 1px solid rgba(0, 0, 0, 0.75); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +#XfceNotifyWindow buttonbox { + padding: 0; +} + +#XfceNotifyWindow label#summary { + font-weight: bold; +} + +dialog.xfsm-logout-dialog { + border-radius: 12px; + background-color: rgba(44, 44, 44, 0.97); + border: 1px solid rgba(0, 0, 0, 0.75); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +#xfwm-tabwin { + padding: 12px; + border-radius: 6px; + -XfwmTabwinWidget-icon-size: 64px; + -XfwmTabwinWidget-preview-size: 64px; +} + +/********** + * Thunar * + **********/ +.thunar toolbar { + box-shadow: inset 0 -1px rgba(255, 255, 255, 0.12); +} + +.thunar .standard-view.frame { + border: none; +} + +.thunar .standard-view.frame widget.view { + border-radius: 6px; +} + +.thunar .standard-view.frame .view:hover { + color: {{colors.on_surface.default.hex}}; +} + +.thunar .standard-view.frame .view:selected { + color: {{colors.on_surface.default.hex}}; + background: {{colors.primary.default.hex}}; +} + +.thunar scrolledwindow.frame.sidebar { + border-top: none; +} + +.thunar .path-bar.linked:not(.vertical) > button.path-bar-button { + margin-left: 2px; + margin-right: 2px; +} + +.thunar statusbar { + margin: 0 -10px; + padding: 0 4px; + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.thunar > grid.horizontal > paned.horizontal > scrolledwindow.frame.sidebar.shortcuts-pane { + border-top: none; +} + +window.background.csd.thunar > grid.horizontal > paned.horizontal > scrolledwindow.frame.sidebar.shortcuts-pane { + border-bottom-left-radius: 12px; +} + +menubar.-vala-panel-appmenu-private, +menubar.-vala-panel-background { + background: none; + border: none; + box-shadow: none; + animation: none; +} + +menubar.-vala-panel-appmenu-private > menuitem, +menubar.-vala-panel-background > menuitem { + color: rgba(255, 255, 255, 0.7); + font-weight: normal; +} + +menubar.-vala-panel-appmenu-private > menuitem:hover, +menubar.-vala-panel-background > menuitem:hover { + color: {{colors.on_surface.default.hex}}; + border-radius: 0; +} + +menubar.-vala-panel-appmenu-private > menuitem:disabled, +menubar.-vala-panel-background > menuitem:disabled { + color: rgba(255, 255, 255, 0.32); +} + +/************************ + * LightDM GTK+ Greeter * + ************************/ +#panel_window { + background-color: #1a1a1a; + color: {{colors.on_surface.default.hex}}; +} + +#panel_window menubar, +#panel_window separator { + background-color: transparent; +} + +#panel_window separator { + padding: 0 4px; +} + +#panel_window separator:first-child { + padding: 0 8px; +} + +#panel_window menubar > menuitem { + color: rgba(255, 255, 255, 0.7); +} + +#panel_window menubar > menuitem:hover { + color: {{colors.on_surface.default.hex}}; +} + +#panel_window menubar > menuitem:disabled label { + color: rgba(255, 255, 255, 0.32); +} + +#login_window, +#shutdown_dialog, +#restart_dialog { + border-radius: 6px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 3px 3px 0 rgba(0, 0, 0, 0.18), 0 3px 6px 0 rgba(0, 0, 0, 0.12), inset 0 1px rgba(255, 255, 255, 0.1); + background-color: #2a2a2a; + color: {{colors.on_surface.default.hex}}; +} + +#content_frame { + padding-bottom: 16px; +} + +#login_window GtkComboBox { + background: none; +} + +#login_window GtkComboBox .button, #login_window GtkComboBox .button:hover, #login_window GtkComboBox .button:active, #login_window GtkComboBox .button:focus { + padding: 0; + background: none; + border-style: none; + box-shadow: none; +} + +#user_image { + padding: 3px; + border-radius: 3px; +} + +#user_image_border { + border-radius: 3px; +} + +#buttonbox_frame { + padding-top: 24px; +} + +#buttonbox_frame > box, +#buttonbox_frame > buttonbox { + margin: -16px; +} + +#buttonbox_frame button:not(:disabled) { + color: {{colors.primary.default.hex}}; +} + +#greeter_infobar { + font-weight: bold; +} + +/******** + * Nemo * + ********/ +.nemo-window .primary-toolbar { + background-color: #141414; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + padding: 2px 3px; +} + +.nemo-window .primary-toolbar:backdrop { + background-color: #1a1a1a; +} + +.nemo-window .primary-toolbar entry { + min-height: 0; + margin: 0; +} + +.nemo-window .primary-toolbar > toolitem > box > button.image-button, +.nemo-window .primary-toolbar > toolitem > .linked > button.image-button { + margin-left: 1px; + margin-right: 1px; +} + +.nemo-window .primary-toolbar button.text-button { + padding-left: 8px; + padding-right: 8px; + color: rgba(255, 255, 255, 0.7); +} + +.nemo-window .primary-toolbar button.text-button:hover, .nemo-window .primary-toolbar button.text-button:active, .nemo-window .primary-toolbar button.text-button:checked { + color: {{colors.on_surface.default.hex}}; +} + +.nemo-window .primary-toolbar button.text-button:disabled { + color: rgba(255, 255, 255, 0.32); +} + +.nemo-window .primary-toolbar button.text-button:backdrop { + color: rgba(255, 255, 255, 0.5); +} + +.nemo-window .primary-toolbar .path-bar.linked:not(.vertical) > button { + margin-left: 1px; + margin-right: 1px; +} + +.nemo-window .primary-toolbar .path-bar.linked:not(.vertical) > button.slider-button { + border-radius: 3px; +} + +.nemo-window .primary-toolbar .path-bar.linked:not(.vertical) > button.slider-button:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} + +.nemo-window .primary-toolbar .path-bar.linked:not(.vertical) > button.slider-button:last-child { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.nemo-window .primary-toolbar button:not(.text-button):not(.image-button) { + padding-left: 4px; + padding-right: 4px; +} + +.nemo-window scrolledwindow.frame { + border-style: none; +} + +.nemo-window scrolledwindow.frame .view:not(:selected) { + background-color: transparent; +} + +.nemo-window .nemo-inactive-pane .view:not(:selected) { + background-color: #141414; +} + +.nemo-window .nemo-window-pane widget.entry { + border-radius: 6px; + background-color: rgba(255, 255, 255, 0.04); +} + +.nemo-window .toolbar { + padding: 2px; + margin: -2px; +} + +.nemo-window .toolbar button { + margin: 3px 0; + padding: 3px; +} + +.nemo-window .toolbar separator { + margin: 6px 0; +} + +.nemo-window.background.csd .toolbar { + border-radius: 0 0 12px 12px; +} + +.nemo-window.background.csd.maximized .toolbar { + border-radius: 0; +} + +.places-treeview { + -NemoPlacesTreeView-disk-full-bg-color: #6b6b6b; + -NemoPlacesTreeView-disk-full-fg-color: {{colors.primary.default.hex}}; + -NemoPlacesTreeView-disk-full-bar-width: 2px; + -NemoPlacesTreeView-disk-full-bar-radius: 0; + -NemoPlacesTreeView-disk-full-bottom-padding: 1px; + -NemoPlacesTreeView-disk-full-max-length: 80px; + padding-top: 3px; + padding-bottom: 3px; +} + +/* GTK NAMED COLORS + ---------------- + use responsibly! */ +/* +widget text/foreground color */ +@define-color theme_fg_color {{colors.on_surface.default.hex}}; +/* +text color for entries, views and content in general */ +@define-color theme_text_color {{colors.on_surface.default.hex}}; +/* +widget base background color */ +@define-color theme_bg_color #1a1a1a; +/* +text widgets and the like base background color */ +@define-color theme_base_color #1a1a1a; +/* +base background color of selections */ +@define-color theme_selected_bg_color {{colors.primary.default.hex}}; +/* +text/foreground color of selections */ +@define-color theme_selected_fg_color {{colors.on_surface.default.hex}}; +/* +base background color of insensitive widgets */ +@define-color insensitive_bg_color #1a1a1a; +/* +text foreground color of insensitive widgets */ +@define-color insensitive_fg_color rgba(255, 255, 255, 0.5); +/* +insensitive text widgets and the like base background color */ +@define-color insensitive_base_color #141414; +/* +widget text/foreground color on backdrop windows */ +@define-color theme_unfocused_fg_color {{colors.on_surface.default.hex}}; +/* +text color for entries, views and content in general on backdrop windows */ +@define-color theme_unfocused_text_color {{colors.on_surface.default.hex}}; +/* +widget base background color on backdrop windows */ +@define-color theme_unfocused_bg_color #1a1a1a; +/* +text widgets and the like base background color on backdrop windows */ +@define-color theme_unfocused_base_color #1a1a1a; +/* +base background color of selections on backdrop windows */ +@define-color theme_unfocused_selected_bg_color {{colors.primary.default.hex}}; +/* +text/foreground color of selections on backdrop windows */ +@define-color theme_unfocused_selected_fg_color {{colors.on_surface.default.hex}}; +/* +insensitive color on backdrop windows */ +@define-color unfocused_insensitive_color rgba(255, 255, 255, 0.5); +/* +widgets main borders color */ +@define-color borders rgba(255, 255, 255, 0.12); +/* +widgets main borders color on backdrop windows */ +@define-color unfocused_borders rgba(255, 255, 255, 0.12); +/* +these are pretty self explicative */ +@define-color warning_color #FFD600; +@define-color error_color #F44336; +@define-color success_color #66BB6A; +/* +these colors are exported for the window manager and shouldn't be used in applications, +read if you used those and something break with a version upgrade you're on your own... */ +@define-color wm_title {{colors.on_surface.default.hex}}; +@define-color wm_unfocused_title rgba(255, 255, 255, 0.7); +@define-color wm_highlight rgba(255, 255, 255, 0.1); +@define-color wm_border #090909; +@define-color wm_bg #141414; +@define-color wm_unfocused_bg #1a1a1a; +@define-color wm_button_icon white; +@define-color wm_button_close_hover_bg #fd5f51; +@define-color wm_button_close_active_bg #fc2714; +@define-color wm_button_max_hover_bg #38c76a; +@define-color wm_button_max_active_bg #2b9751; +@define-color wm_button_min_hover_bg #fdbe04; +@define-color wm_button_min_active_bg #c29102; +/* +FIXME this is really an API */ +@define-color content_view_bg #1a1a1a; +@define-color placeholder_text_color silver; +/* Very contrasty background for text views (@theme_text_color foreground) */ +@define-color text_view_bg #1a1a1a; +@define-color budgie_tasklist_indicator_color rgba(255, 255, 255, 0.3); +@define-color budgie_tasklist_indicator_color_active {{colors.primary.default.hex}}; +@define-color budgie_tasklist_indicator_color_active_window #406395; +@define-color budgie_tasklist_indicator_color_attention #FFD600; +@define-color STRAWBERRY_100 #FF9262; +@define-color STRAWBERRY_300 #FF793E; +@define-color STRAWBERRY_500 #F15D22; +@define-color STRAWBERRY_700 #CF3B00; +@define-color STRAWBERRY_900 #AC1800; +@define-color ORANGE_100 #FFDB91; +@define-color ORANGE_300 #FFCA40; +@define-color ORANGE_500 #FAA41A; +@define-color ORANGE_700 #DE8800; +@define-color ORANGE_900 #C26C00; +@define-color BANANA_100 #FFFFA8; +@define-color BANANA_300 #FFFA7D; +@define-color BANANA_500 #FFCE51; +@define-color BANANA_700 #D1A023; +@define-color BANANA_900 #A27100; +@define-color LIME_100 #A2F3BE; +@define-color LIME_300 #8ADBA6; +@define-color LIME_500 #73C48F; +@define-color LIME_700 #479863; +@define-color LIME_900 #1C6D38; +@define-color BLUEBERRY_100 #94A6FF; +@define-color BLUEBERRY_300 #6A7CE0; +@define-color BLUEBERRY_500 #3F51B5; +@define-color BLUEBERRY_700 #213397; +@define-color BLUEBERRY_900 #031579; +@define-color GRAPE_100 #D25DE6; +@define-color GRAPE_300 #B84ACB; +@define-color GRAPE_500 #9C27B0; +@define-color GRAPE_700 #830E97; +@define-color GRAPE_900 #6A007E; +@define-color COCOA_100 #9F9792; +@define-color COCOA_300 #7B736E; +@define-color COCOA_500 #574F4A; +@define-color COCOA_700 #463E39; +@define-color COCOA_900 #342C27; +@define-color SILVER_100 #EEE; +@define-color SILVER_300 #CCC; +@define-color SILVER_500 #AAA; +@define-color SILVER_700 #888; +@define-color SILVER_900 #666; +@define-color SLATE_100 #888; +@define-color SLATE_300 #666; +@define-color SLATE_500 #444; +@define-color SLATE_700 #222; +@define-color SLATE_900 #111; +@define-color BLACK_100 #474341; +@define-color BLACK_300 #403C3A; +@define-color BLACK_500 #393634; +@define-color BLACK_700 #33302F; +@define-color BLACK_900 #2B2928; diff --git a/quickshell/.config/quickshell/matugen/templates/gtk3-colloid-light.css b/quickshell/.config/quickshell/matugen/templates/gtk3-colloid-light.css new file mode 100644 index 0000000..a7d53f0 --- /dev/null +++ b/quickshell/.config/quickshell/matugen/templates/gtk3-colloid-light.css @@ -0,0 +1,975 @@ +@keyframes ripple { + to { + background-size: 1000% 1000%; + } +} + +@keyframes ripple-on-slider { + to { + background-size: auto, 1000% 1000%; + } +} + +@keyframes ripple-on-headerbar { + from { + background-image: radial-gradient(circle, {{colors.primary.default.hex}} 0%, transparent 0%); + } + to { + background-image: radial-gradient(circle, {{colors.primary.default.hex}} 100%, transparent 0%); + } +} + +* { + background-clip: padding-box; + -GtkToolButton-icon-spacing: 0; + -GtkTextView-error-underline-color: #E53935; + -GtkScrolledWindow-scrollbar-spacing: 0; + -GtkToolItemGroup-expander-size: 11; + -GtkWidget-text-handle-width: 24; + -GtkWidget-text-handle-height: 24; + -GtkDialog-button-spacing: 6; + -GtkDialog-action-area-border: 6; + outline-style: solid; + outline-width: 2px; + outline-color: transparent; + outline-offset: -4px; + -gtk-outline-radius: 6px; + -gtk-secondary-caret-color: {{colors.primary.default.hex}}; +} + +*:focus { + outline-color: alpha(currentColor, 0.1); +} + +XfdesktopIconView.view:active, calendar.raven-calendar:selected, box.vertical > widget > widget:selected, calendar:selected, popover.background modelbutton.flat:selected, +popover.background .menuitem.button.flat:selected, .csd treeview.view:selected, .background.csd .view:selected { + color: rgba(0, 0, 0, 0.87); + background-color: alpha(currentColor, 0.1); +} + +.nemo-window .view selection, .nemo-window .view:selected, .nautilus-window notebook .view:not(treeview) selection, .nautilus-window notebook .view:not(treeview):selected, .nautilus-window flowboxchild:selected .icon-item-background, label selection, flowbox flowboxchild:selected { + color: {{colors.primary.default.hex}}; + background-color: {{colors.primary_container.default.hex}}; +} + +.nemo-window .nemo-window-pane widget.entry:selected, window.background.csd evview.view.content-view:selected, window.background.csd evview.view.content-view:selected:backdrop, .nautilus-window.background.csd notebook widget.view:selected, entry selection, textview text selection:focus, textview text selection, widget.view:selected, .view:selected { + color: {{colors.on_primary.default.hex}}; + background-color: {{colors.primary.default.hex}}; +} + +.linked:not(.vertical) > button, .linked:not(.vertical) > entry { + border-radius: 0; +} + +.linked:not(.vertical) > button:first-child, .linked:not(.vertical) > entry:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} + +.linked:not(.vertical) > button:last-child, .linked:not(.vertical) > entry:last-child { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.linked:not(.vertical) > button:only-child, .linked:not(.vertical) > entry:only-child { + border-radius: 6px; +} + +.linked.vertical > button, .linked.vertical > entry { + border-radius: 0; +} + +.linked.vertical > button:first-child, .linked.vertical > entry:first-child { + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} + +.linked.vertical > button:last-child, .linked.vertical > entry:last-child { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; +} + +.linked.vertical > button:only-child, .linked.vertical > entry:only-child { + border-radius: 6px; +} + +/*************** + * Base States * + ***************/ +.background { + background-color: {{colors.surface.default.hex}}; + color: {{colors.on_surface.default.hex}}; +} + +dnd { + color: {{colors.on_surface.default.hex}}; +} + +.background:backdrop { + background-color: {{colors.surface_dim.default.hex}}; + color: {{colors.on_surface_variant.default.hex}}; +} + +.csd.background { + border-radius: 12px; +} + +/************* + * Scrolling * + *************/ +scrollbar { + background-color: {{colors.surface.default.hex}}; + transition: 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +* { + -GtkScrollbar-has-backward-stepper: false; + -GtkScrollbar-has-forward-stepper: false; +} + +scrollbar.top { + border-bottom: 1px solid alpha({{colors.outline.default.hex}}, 0.3); +} + +scrollbar.bottom { + border-top: 1px solid alpha({{colors.outline.default.hex}}, 0.3); +} + +scrollbar.left { + border-right: 1px solid alpha({{colors.outline.default.hex}}, 0.3); +} + +scrollbar.right { + border-left: 1px solid alpha({{colors.outline.default.hex}}, 0.3); +} + +scrollbar:backdrop { + background-color: {{colors.surface_container_low.default.hex}}; + border-color: alpha({{colors.outline.default.hex}}, 0.3); + transition: 200ms ease-out; +} + +scrollbar slider { + min-width: 6px; + min-height: 6px; + margin: -1px; + border: 4px solid transparent; + border-radius: 8px; + background-clip: padding-box; + background-color: alpha({{colors.on_surface_variant.default.hex}}, 0.5); +} + +scrollbar slider:hover { + background-color: alpha({{colors.on_surface_variant.default.hex}}, 0.7); +} + +scrollbar slider:hover:active { + background-color: {{colors.on_surface_variant.default.hex}}; +} + +scrollbar slider:backdrop { + background-color: alpha({{colors.on_surface_variant.default.hex}}, 0.4); +} + +scrollbar slider:disabled { + background-color: transparent; +} + +scrollbar.fine-tune slider { + min-width: 4px; + min-height: 4px; +} + +scrollbar.fine-tune.horizontal slider { + border-width: 2px 4px; +} + +scrollbar.fine-tune.vertical slider { + border-width: 4px 2px; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering) { + border-color: transparent; + opacity: 0.4; + background-color: transparent; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering) slider { + margin: 0; + min-width: 3px; + min-height: 3px; + background-color: {{colors.on_surface.default.hex}}; + border: 1px solid {{colors.surface.default.hex}}; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering) button { + min-width: 5px; + min-height: 5px; + background-color: {{colors.on_surface.default.hex}}; + background-clip: padding-box; + border-radius: 100px; + border: 1px solid {{colors.surface.default.hex}}; + -gtk-icon-source: none; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering).horizontal slider { + margin: 0 2px; + min-width: 40px; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering).horizontal button { + margin: 1px 2px; + min-width: 5px; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering).vertical slider { + margin: 2px 0; + min-height: 40px; +} + +scrollbar.overlay-indicator:not(.dragging):not(.hovering).vertical button { + margin: 2px 1px; + min-height: 5px; +} + +scrollbar.overlay-indicator.dragging, scrollbar.overlay-indicator.hovering { + opacity: 0.8; +} + +scrollbar.horizontal slider { + min-width: 40px; +} + +scrollbar.vertical slider { + min-height: 40px; +} + +scrollbar button { + padding: 0; + min-width: 12px; + min-height: 12px; + border-style: none; + border-radius: 0; + transition-property: min-height, min-width, color; + border-color: transparent; + background-color: transparent; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: alpha({{colors.on_surface_variant.default.hex}}, 0.5); +} + +scrollbar button:hover { + border-color: transparent; + background-color: transparent; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: alpha({{colors.on_surface_variant.default.hex}}, 0.7); +} + +scrollbar button:active, scrollbar button:checked { + border-color: transparent; + background-color: transparent; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: {{colors.on_surface_variant.default.hex}}; +} + +scrollbar button:disabled { + border-color: transparent; + background-color: transparent; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: alpha({{colors.on_surface.default.hex}}, 0.3); +} + +scrollbar button:backdrop { + border-color: transparent; + background-color: transparent; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: alpha({{colors.on_surface_variant.default.hex}}, 0.4); +} + +scrollbar button:backdrop:disabled { + border-color: transparent; + background-color: transparent; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: alpha({{colors.on_surface.default.hex}}, 0.3); +} + +scrollbar.vertical button.down { + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); +} + +scrollbar.vertical button.up { + -gtk-icon-source: -gtk-icontheme("pan-up-symbolic"); +} + +scrollbar.horizontal button.down { + -gtk-icon-source: -gtk-icontheme("pan-end-symbolic"); +} + +scrollbar.horizontal button.up { + -gtk-icon-source: -gtk-icontheme("pan-start-symbolic"); +} + +/*********** + * Buttons * + ***********/ +@keyframes needs_attention { + from { + background-image: -gtk-gradient(radial, center center, 0, center center, 0.01, to({{colors.primary.default.hex}}), to(transparent)); + } + to { + background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to({{colors.primary.default.hex}}), to(transparent)); + } +} + +button { + min-height: 24px; + min-width: 16px; + padding: 8px 16px; + border: 1px solid; + border-radius: 6px; + transition: all 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); + border-color: alpha({{colors.outline.default.hex}}, 0.5); + background-color: {{colors.surface.default.hex}}; + color: {{colors.on_surface.default.hex}}; + text-shadow: none; + -gtk-icon-shadow: none; + box-shadow: none; +} + +button:hover { + border-color: alpha({{colors.outline.default.hex}}, 0.5); + background-color: alpha({{colors.on_surface.default.hex}}, 0.08); + color: {{colors.on_surface.default.hex}}; +} + +button:active, button:checked { + border-color: alpha({{colors.outline.default.hex}}, 0.5); + background-color: alpha({{colors.on_surface.default.hex}}, 0.12); + color: {{colors.on_surface.default.hex}}; + box-shadow: none; + transition-duration: 50ms; +} + +button:backdrop { + border-color: alpha({{colors.outline.default.hex}}, 0.3); + background-color: {{colors.surface_dim.default.hex}}; + color: {{colors.on_surface_variant.default.hex}}; + text-shadow: none; + -gtk-icon-shadow: none; +} + +button:backdrop:active, button:backdrop:checked { + border-color: alpha({{colors.outline.default.hex}}, 0.3); + background-color: alpha({{colors.on_surface_variant.default.hex}}, 0.08); + color: {{colors.on_surface_variant.default.hex}}; +} + +button:backdrop:disabled { + border-color: alpha({{colors.outline.default.hex}}, 0.12); + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: alpha({{colors.on_surface.default.hex}}, 0.38); + text-shadow: none; + -gtk-icon-shadow: none; +} + +button:backdrop:disabled:active, button:backdrop:disabled:checked { + border-color: alpha({{colors.outline.default.hex}}, 0.12); + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +button:disabled { + border-color: alpha({{colors.outline.default.hex}}, 0.12); + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: alpha({{colors.on_surface.default.hex}}, 0.38); + text-shadow: none; + -gtk-icon-shadow: none; +} + +button:disabled:active, button:disabled:checked { + border-color: alpha({{colors.outline.default.hex}}, 0.12); + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: alpha({{colors.on_surface.default.hex}}, 0.38); + box-shadow: none; +} + +button.suggested-action { + border-color: {{colors.primary.default.hex}}; + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_primary.default.hex}}; +} + +button.suggested-action.flat { + border-color: transparent; + background-color: transparent; + background-image: none; + color: {{colors.primary.default.hex}}; +} + +button.suggested-action:hover { + border-color: {{colors.primary.default.hex}}; + background-color: alpha({{colors.on_primary.default.hex}}, 0.08); + color: {{colors.on_primary.default.hex}}; +} + +button.suggested-action:active, button.suggested-action:checked { + border-color: {{colors.primary.default.hex}}; + background-color: alpha({{colors.on_primary.default.hex}}, 0.12); + color: {{colors.on_primary.default.hex}}; +} + +button.suggested-action:backdrop { + border-color: {{colors.primary.default.hex}}; + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_primary.default.hex}}; +} + +button.suggested-action:backdrop:disabled { + border-color: alpha({{colors.outline.default.hex}}, 0.12); + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +button.suggested-action:disabled { + border-color: alpha({{colors.outline.default.hex}}, 0.12); + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +button.destructive-action { + border-color: {{colors.error.default.hex}}; + background-color: {{colors.error.default.hex}}; + color: {{colors.on_error.default.hex}}; +} + +button.destructive-action.flat { + border-color: transparent; + background-color: transparent; + background-image: none; + color: {{colors.error.default.hex}}; +} + +button.destructive-action:hover { + border-color: {{colors.error.default.hex}}; + background-color: alpha({{colors.on_error.default.hex}}, 0.08); + color: {{colors.on_error.default.hex}}; +} + +button.destructive-action:active, button.destructive-action:checked { + border-color: {{colors.error.default.hex}}; + background-color: alpha({{colors.on_error.default.hex}}, 0.12); + color: {{colors.on_error.default.hex}}; +} + +button.destructive-action:backdrop { + border-color: {{colors.error.default.hex}}; + background-color: {{colors.error.default.hex}}; + color: {{colors.on_error.default.hex}}; +} + +button.destructive-action:backdrop:disabled { + border-color: alpha({{colors.outline.default.hex}}, 0.12); + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +button.destructive-action:disabled { + border-color: alpha({{colors.outline.default.hex}}, 0.12); + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +button.flat { + border-color: transparent; + background-color: transparent; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + transition: none; +} + +button.flat:hover { + transition: all 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); + transition-duration: 500ms; + border-color: transparent; + background-color: alpha({{colors.on_surface.default.hex}}, 0.08); + background-image: none; +} + +button.flat:hover:active { + transition: all 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +button.flat:backdrop { + border-color: transparent; + background-color: transparent; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: {{colors.on_surface_variant.default.hex}}; +} + +button.flat:disabled { + border-color: transparent; + background-color: transparent; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +button.flat:backdrop:disabled { + border-color: transparent; + background-color: transparent; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +button.flat:active, button.flat:checked { + transition: all 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); + border-color: transparent; + background-color: alpha({{colors.on_surface.default.hex}}, 0.12); + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; +} + +button.flat:backdrop:active, button.flat:backdrop:checked { + border-color: transparent; + background-color: alpha({{colors.on_surface_variant.default.hex}}, 0.08); + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: {{colors.on_surface_variant.default.hex}}; +} + +button.flat:disabled:active, button.flat:disabled:checked { + border-color: transparent; + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface.default.hex}}, 0); + text-shadow: none; + -gtk-icon-shadow: none; + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +button.flat.suggested-action { + color: {{colors.primary.default.hex}}; +} + +button.flat.suggested-action:hover { + border-color: transparent; + background-color: alpha({{colors.primary.default.hex}}, 0.08); + background-image: none; + color: {{colors.primary.default.hex}}; +} + +button.flat.suggested-action:active, button.flat.suggested-action:checked { + border-color: transparent; + background-color: alpha({{colors.primary.default.hex}}, 0.12); + background-image: none; + color: {{colors.primary.default.hex}}; +} + +button.flat.suggested-action:backdrop { + color: {{colors.primary.default.hex}}; +} + +button.flat.suggested-action:disabled, button.flat.suggested-action:backdrop:disabled { + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +button.flat.destructive-action { + color: {{colors.error.default.hex}}; +} + +button.flat.destructive-action:hover { + border-color: transparent; + background-color: alpha({{colors.error.default.hex}}, 0.08); + background-image: none; + color: {{colors.error.default.hex}}; +} + +button.flat.destructive-action:active, button.flat.destructive-action:checked { + border-color: transparent; + background-color: alpha({{colors.error.default.hex}}, 0.12); + background-image: none; + color: {{colors.error.default.hex}}; +} + +button.flat.destructive-action:backdrop { + color: {{colors.error.default.hex}}; +} + +button.flat.destructive-action:disabled, button.flat.destructive-action:backdrop:disabled { + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +button.image-button { + min-width: 24px; + padding-left: 8px; + padding-right: 8px; +} + +button.text-button { + padding-left: 16px; + padding-right: 16px; +} + +button.text-button.image-button { + padding-left: 12px; + padding-right: 16px; +} + +button.text-button.image-button label { + padding-left: 8px; + padding-right: 0px; +} + +button.text-button.image-button label:dir(rtl) { + padding-left: 0px; + padding-right: 8px; +} + +menubutton.circular button, button.circular { + border-radius: 9999px; + -gtk-outline-radius: 9999px; +} + +menubutton.circular button label, button.circular label { + padding: 0; +} + +button:drop(active) { + border-color: {{colors.primary.default.hex}}; + box-shadow: inset 0 0 0 1px {{colors.primary.default.hex}}; +} + +/*************** + * Header bars * + ***************/ +.nemo-window .primary-toolbar button:not(.text-button), headerbar button:not(.suggested-action):not(.destructive-action) { + color: {{colors.on_surface_variant.default.hex}}; +} + +.nemo-window .primary-toolbar .linked:not(.vertical) > button:not(.text-button), headerbar .linked:not(.vertical) > button:not(.suggested-action):not(.destructive-action) { + border-radius: 6px; +} + +.nemo-window .primary-toolbar button:focus:not(.text-button), headerbar button:focus:not(.suggested-action):not(.destructive-action), .nemo-window .primary-toolbar button:hover:not(.text-button), headerbar button:hover:not(.suggested-action):not(.destructive-action), .nemo-window .primary-toolbar button:active:not(.text-button), headerbar button:active:not(.suggested-action):not(.destructive-action), .nemo-window .primary-toolbar button:checked:not(.text-button), headerbar button:checked:not(.suggested-action):not(.destructive-action) { + color: {{colors.on_surface.default.hex}}; +} + +.nemo-window .primary-toolbar button:disabled:not(.text-button), headerbar button:disabled:not(.suggested-action):not(.destructive-action) { + color: alpha({{colors.on_surface.default.hex}}, 0.3); +} + +.nemo-window .primary-toolbar button:checked:disabled:not(.text-button), headerbar button:checked:disabled:not(.suggested-action):not(.destructive-action) { + background-color: transparent; + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +.nemo-window .primary-toolbar button:backdrop:not(.text-button), headerbar button:backdrop:not(.suggested-action):not(.destructive-action) { + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +.nemo-window .primary-toolbar button:backdrop:focus:not(.text-button), headerbar button:backdrop:focus:not(.suggested-action):not(.destructive-action), .nemo-window .primary-toolbar button:backdrop:hover:not(.text-button), headerbar button:backdrop:hover:not(.suggested-action):not(.destructive-action), .nemo-window .primary-toolbar button:backdrop:active:not(.text-button), headerbar button:backdrop:active:not(.suggested-action):not(.destructive-action) { + color: {{colors.on_surface_variant.default.hex}}; +} + +.nemo-window .primary-toolbar button:backdrop:disabled:not(.text-button), headerbar button:backdrop:disabled:not(.suggested-action):not(.destructive-action) { + color: alpha({{colors.on_surface.default.hex}}, 0.3); +} + +.nemo-window .primary-toolbar button:backdrop:checked:not(.text-button), headerbar button:backdrop:checked:not(.suggested-action):not(.destructive-action) { + color: {{colors.on_surface_variant.default.hex}}; +} + +.nemo-window .primary-toolbar button:backdrop:checked:disabled:not(.text-button), headerbar button:backdrop:checked:disabled:not(.suggested-action):not(.destructive-action) { + color: alpha({{colors.on_surface.default.hex}}, 0.3); +} + +.nemo-window .primary-toolbar entry, .titlebar entry { + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: {{colors.on_surface.default.hex}}; +} + +.nemo-window .primary-toolbar entry:disabled, .titlebar entry:disabled { + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +.nemo-window .primary-toolbar entry image, .titlebar entry image { + color: {{colors.on_surface_variant.default.hex}}; +} + +.nemo-window .primary-toolbar entry image:hover, .titlebar entry image:hover, .nemo-window .primary-toolbar entry image:active, .titlebar entry image:active { + color: {{colors.on_surface.default.hex}}; +} + +.nemo-window .primary-toolbar entry image:disabled, .titlebar entry image:disabled { + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +.titlebar { + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + background-color: {{colors.surface_container_low.default.hex}}; + color: {{colors.on_surface.default.hex}}; + border-radius: 12px 12px 0 0; + box-shadow: inset 0 -1px alpha({{colors.outline.default.hex}}, 0.12), inset 0 1px alpha({{colors.surface_bright.default.hex}}, 0.15); +} + +.titlebar:disabled { + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +.titlebar:backdrop { + color: {{colors.on_surface_variant.default.hex}}; +} + +.titlebar:backdrop:disabled { + color: alpha({{colors.on_surface.default.hex}}, 0.3); +} + +.csd .titlebar:backdrop { + background-color: {{colors.surface_container_lowest.default.hex}}; +} + +.titlebar .title { + padding: 0 12px; + font-weight: bold; +} + +.titlebar .subtitle { + padding: 0 12px; + font-size: smaller; +} + +.titlebar .subtitle, +.titlebar .dim-label { + transition: color 75ms cubic-bezier(0, 0, 0.2, 1); + color: {{colors.on_surface_variant.default.hex}}; +} + +.titlebar .subtitle:backdrop, +.titlebar .dim-label:backdrop { + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +.titlebar .titlebar { + background-color: transparent; + box-shadow: none; +} + +.titlebar + separator, .titlebar + separator.sidebar { + background-color: {{colors.surface_container_low.default.hex}}; + background-image: none; + transition: all 75ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: inset 0 -1px alpha({{colors.outline.default.hex}}, 0.12), inset 0 1px alpha({{colors.surface_bright.default.hex}}, 0.15); +} + +.titlebar + separator:backdrop, .titlebar + separator.sidebar:backdrop { + background-color: {{colors.surface_container_lowest.default.hex}}; +} + +.titlebar.selection-mode + separator, .titlebar.selection-mode + separator.sidebar, .selection-mode .titlebar + separator, .selection-mode .titlebar + separator.sidebar { + background-color: {{colors.primary.default.hex}}; +} + +.titlebar.selection-mode + separator:backdrop, .titlebar.selection-mode + separator.sidebar:backdrop, .selection-mode .titlebar + separator:backdrop, .selection-mode .titlebar + separator.sidebar:backdrop { + background-color: {{colors.primary.default.hex}}; +} + +.background.csd.unified .titlebar + separator, .background.csd.unified .titlebar + separator.sidebar { + box-shadow: inset 0 -1px alpha({{colors.outline.default.hex}}, 0.12); +} + +.titlebar .linked:not(.vertical) > entry { + border-radius: 6px; + margin-left: 3px; + margin-right: 3px; +} + +.titlebar button.suggested-action:disabled, .titlebar button.destructive-action:disabled { + background-color: alpha({{colors.on_surface.default.hex}}, 0.04); + color: alpha({{colors.on_surface.default.hex}}, 0.38); +} + +.titlebar .path-bar button:not(.suggested-action):not(.destructive-action).text-button { + min-width: 0; + padding-left: 5px; + padding-right: 5px; +} + +.titlebar.selection-mode { + transition: background-color 0.1ms 225ms, color 75ms cubic-bezier(0, 0, 0.2, 1); + animation: ripple-on-headerbar 225ms cubic-bezier(0, 0, 0.2, 1); + background-color: {{colors.primary.default.hex}}; + color: {{colors.on_primary.default.hex}}; + box-shadow: inset 0 -1px alpha({{colors.outline.default.hex}}, 0.12), inset 0 1px alpha({{colors.surface_bright.default.hex}}, 0.2); +} + +.titlebar.selection-mode:backdrop { + color: alpha({{colors.on_primary.default.hex}}, 0.7); + background-color: {{colors.primary.default.hex}}; +} + +.titlebar.selection-mode .subtitle:link { + color: {{colors.on_primary.default.hex}}; +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action) { + color: {{colors.on_primary.default.hex}}; +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):disabled { + color: alpha({{colors.on_primary.default.hex}}, 0.5); +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):checked { + color: {{colors.on_primary.default.hex}}; +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):checked:disabled { + color: alpha({{colors.on_primary.default.hex}}, 0.5); +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):backdrop:not(.titlebutton) { + color: alpha({{colors.on_primary.default.hex}}, 0.7); +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):backdrop:disabled { + color: alpha({{colors.on_primary.default.hex}}, 0.32); +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):backdrop:checked { + color: alpha({{colors.on_primary.default.hex}}, 0.7); +} + +.titlebar.selection-mode button:not(.suggested-action):not(.destructive-action):backdrop:checked:disabled { + color: alpha({{colors.on_primary.default.hex}}, 0.32); +} + +.titlebar.selection-mode .selection-menu { + padding-left: 16px; + padding-right: 16px; +} + +.titlebar.selection-mode .selection-menu arrow { + -GtkArrow-arrow-scaling: 1; +} + +.titlebar.selection-mode .selection-menu .arrow { + -gtk-icon-source: -gtk-icontheme("pan-down-symbolic"); +} + +.tiled .titlebar, .tiled-top .titlebar, .tiled-right .titlebar, .tiled-bottom .titlebar, .tiled-left .titlebar, .maximized .titlebar, .fullscreen .titlebar { + border-radius: 0; +} + +.titlebar.default-decoration { + min-height: 24px; + padding: 6px 12px; + border-radius: 12px 12px 0 0; + border: none; + background-color: {{colors.surface_container_low.default.hex}}; + background-image: none; + box-shadow: inset 0 1px alpha({{colors.surface_bright.default.hex}}, 0.15); +} + +.titlebar.default-decoration:backdrop { + background-color: {{colors.surface_container_lowest.default.hex}}; +} + +.tiled .titlebar.default-decoration, .maximized .titlebar.default-decoration, .fullscreen .titlebar.default-decoration { + box-shadow: none; + border-radius: 0; +} + +.titlebar.default-decoration button.titlebutton { + min-height: 24px; + min-width: 24px; + margin: 0; + padding: 0; +} + +.background.csd .titlebar.default-decoration { + padding: 6px; + box-shadow: none; +} + +.background:not(.csd) .titlebar.default-decoration button.titlebutton:active { + background-size: 1000% 1000%; +} + +.solid-csd .titlebar:dir(rtl), .solid-csd .titlebar:dir(ltr) { + border-radius: 0; + box-shadow: none; +} + +headerbar { + min-height: 46px; + padding: 0 6px; +} + +box.vertical headerbar { + background-color: {{colors.surface_container_low.default.hex}}; +} + +headerbar entry, +headerbar spinbutton, +headerbar button, +headerbar stackswitcher { + margin-top: 6px; + margin-bottom: 6px; +} + +headerbar button, headerbar button.image-button { + border-radius: 6px; +} + +headerbar > box.left, +headerbar > box.right { + padding: 0 4px; +} + +headerbar separator.titlebutton, headerbar separator.sidebar { + margin-top: 11.5px; + margin-bottom: 11.5px; + background-color: transparent; +} + +headerbar switch { + margin-top: 11px; + margin-bottom: 11px; +} + +headerbar spinbutton button { + margin-top: 0; + margin-bottom: 0; +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/templates/kitty.conf b/quickshell/.config/quickshell/matugen/templates/kitty.conf new file mode 100644 index 0000000..04f2213 --- /dev/null +++ b/quickshell/.config/quickshell/matugen/templates/kitty.conf @@ -0,0 +1,8 @@ +cursor {{colors.on_surface.default.hex}} +cursor_text_color {{colors.on_surface_variant.default.hex}} + +foreground {{colors.on_surface.default.hex}} +background {{colors.surface.default.hex}} +selection_foreground {{colors.on_secondary.default.hex}} +selection_background {{colors.secondary_fixed_dim.default.hex}} +url_color {{colors.primary.default.hex}} \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/templates/matugen-kcolorscheme.colors b/quickshell/.config/quickshell/matugen/templates/matugen-kcolorscheme.colors new file mode 100644 index 0000000..67b0c5c --- /dev/null +++ b/quickshell/.config/quickshell/matugen/templates/matugen-kcolorscheme.colors @@ -0,0 +1,146 @@ +[KDE] +contrast=4 + +[General] +ColorScheme=DankMatugen +Name=Dank Shell (matugen) + +[ColorEffects:Disabled] +Color={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=true +Color={{colors.outline.default.red}},{{colors.outline.default.green}},{{colors.outline.default.blue}} +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +Enable=false +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate={{colors.surface_container_high.default.red}},{{colors.surface_container_high.default.green}},{{colors.surface_container_high.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Complementary] +BackgroundAlternate={{colors.surface_container_high.default.red}},{{colors.surface_container_high.default.green}},{{colors.surface_container_high.default.blue}} +BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Header] +BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Header][Inactive] +BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Selection] +BackgroundAlternate={{colors.primary_container.default.red}},{{colors.primary_container.default.green}},{{colors.primary_container.default.blue}} +BackgroundNormal={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.on_primary.default.red}},{{colors.on_primary.default.green}},{{colors.on_primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_primary.default.red}},{{colors.on_primary.default.green}},{{colors.on_primary.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Tooltip] +BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:View] +BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Window] +BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[WM] +activeBackground={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +activeBlend={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +activeForeground={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +inactiveBackground={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +inactiveBlend={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +inactiveForeground={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/templates/niri-colors.kdl b/quickshell/.config/quickshell/matugen/templates/niri-colors.kdl new file mode 100644 index 0000000..45bc05c --- /dev/null +++ b/quickshell/.config/quickshell/matugen/templates/niri-colors.kdl @@ -0,0 +1,29 @@ +layout { + background-color "transparent" + + focus-ring { + active-color "{{colors.primary.default.hex}}" + inactive-color "{{colors.outline.default.hex}}" + urgent-color "{{colors.error.default.hex}}" + } + + border { + active-color "{{colors.primary.default.hex}}" + inactive-color "{{colors.outline.default.hex}}" + urgent-color "{{colors.error.default.hex}}" + } + + shadow { + color "{{colors.shadow.default.hex}}70" + } + + tab-indicator { + active-color "{{colors.primary.default.hex}}" + inactive-color "{{colors.outline.default.hex}}" + urgent-color "{{colors.error.default.hex}}" + } + + insert-hint { + color "{{colors.primary.default.hex}}80" + } +} \ No newline at end of file diff --git a/quickshell/.config/quickshell/matugen/templates/qtct-colors.conf b/quickshell/.config/quickshell/matugen/templates/qtct-colors.conf new file mode 100644 index 0000000..3945100 --- /dev/null +++ b/quickshell/.config/quickshell/matugen/templates/qtct-colors.conf @@ -0,0 +1,144 @@ +[ColorScheme] +active_colors={{colors.on_surface.default.hex}}, {{colors.surface_container.default.hex}}, {{colors.surface_container_high.default.hex}}, {{colors.outline.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.outline_variant.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.on_primary.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.surface.default.hex}}, {{colors.background.default.hex}}, {{colors.shadow.default.hex}}, {{colors.primary.default.hex}}, {{colors.on_primary.default.hex}}, {{colors.secondary.default.hex}}, {{colors.secondary.default.hex}}, {{colors.surface_container_low.default.hex}}, {{colors.surface_container.default.hex}}, {{colors.surface_container.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}} +disabled_colors={{colors.on_surface_variant.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.surface_container_high.default.hex}}, {{colors.outline.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.outline_variant.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.shadow.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}} +inactive_colors={{colors.on_surface_variant.default.hex}}, {{colors.surface_container.default.hex}}, {{colors.surface_container_high.default.hex}}, {{colors.outline.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.outline_variant.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.surface_container.default.hex}}, {{colors.surface_container.default.hex}}, {{colors.shadow.default.hex}}, {{colors.secondary.default.hex}}, {{colors.on_secondary.default.hex}}, {{colors.secondary.default.hex}}, {{colors.secondary.default.hex}}, {{colors.surface_container_low.default.hex}}, {{colors.surface_container.default.hex}}, {{colors.surface_container.default.hex}}, {{colors.on_surface_variant.default.hex}}, {{colors.on_surface_variant.default.hex}} + +[ColorEffects:Disabled] +Color={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=true +Color={{colors.outline.default.red}},{{colors.outline.default.green}},{{colors.outline.default.blue}} +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +Enable=false +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate={{colors.surface_container_high.default.red}},{{colors.surface_container_high.default.green}},{{colors.surface_container_high.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Complementary] +BackgroundAlternate={{colors.surface_container_high.default.red}},{{colors.surface_container_high.default.green}},{{colors.surface_container_high.default.blue}} +BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Header] +BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Header][Inactive] +BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Selection] +BackgroundAlternate={{colors.primary_container.default.red}},{{colors.primary_container.default.green}},{{colors.primary_container.default.blue}} +BackgroundNormal={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.on_primary.default.red}},{{colors.on_primary.default.green}},{{colors.on_primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_primary.default.red}},{{colors.on_primary.default.green}},{{colors.on_primary.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Tooltip] +BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:View] +BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[Colors:Window] +BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} + +[WM] +activeBackground={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +activeBlend={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +activeForeground={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +inactiveBackground={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +inactiveBlend={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +inactiveForeground={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} \ No newline at end of file diff --git a/quickshell/.config/quickshell/qmlformat-all.sh b/quickshell/.config/quickshell/qmlformat-all.sh new file mode 100755 index 0000000..d1f9b41 --- /dev/null +++ b/quickshell/.config/quickshell/qmlformat-all.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# Find and format all QML files, then fix pragma ComponentBehavior +find . -name "*.qml" -exec sh -c ' + qmlfmt -t 4 -i 4 -b 250 -w "$1" + sed -i "s/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g" "$1" +' _ {} \; \ No newline at end of file diff --git a/quickshell/.config/quickshell/scripts/gtk.sh b/quickshell/.config/quickshell/scripts/gtk.sh new file mode 100755 index 0000000..5af7d91 --- /dev/null +++ b/quickshell/.config/quickshell/scripts/gtk.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +CONFIG_DIR="$1" +IS_LIGHT="$2" +SHELL_DIR="$3" + +if [ -z "$CONFIG_DIR" ] || [ -z "$IS_LIGHT" ] || [ -z "$SHELL_DIR" ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +apply_gtk3_colors() { + local config_dir="$1" + local is_light="$2" + local shell_dir="$3" + + local gtk3_dir="$config_dir/gtk-3.0" + local dank_colors="$gtk3_dir/dank-colors.css" + local gtk_css="$gtk3_dir/gtk.css" + + if [ ! -f "$dank_colors" ]; then + echo "Error: dank-colors.css not found at $dank_colors" >&2 + echo "Run matugen first to generate theme files" >&2 + exit 1 + fi + + if [ -L "$gtk_css" ]; then + rm "$gtk_css" + elif [ -f "$gtk_css" ]; then + mv "$gtk_css" "$gtk_css.backup.$(date +%s)" + echo "Backed up existing gtk.css" + fi + + ln -s "dank-colors.css" "$gtk_css" + echo "Created symlink: $gtk_css -> dank-colors.css" +} + +apply_gtk4_colors() { + local config_dir="$1" + + local gtk4_dir="$config_dir/gtk-4.0" + local dank_colors="$gtk4_dir/dank-colors.css" + local gtk_css="$gtk4_dir/gtk.css" + local gtk4_import="@import url(\"dank-colors.css\");" + + if [ ! -f "$dank_colors" ]; then + echo "Error: GTK4 dank-colors.css not found at $dank_colors" >&2 + echo "Run matugen first to generate theme files" >&2 + exit 1 + fi + + if [ -f "$gtk_css" ]; then + sed -i '/^@import url.*dank-colors\.css.*);$/d' "$gtk_css" + sed -i "1i\\$gtk4_import" "$gtk_css" + else + echo "$gtk4_import" > "$gtk_css" + fi + echo "Updated GTK4 CSS import" +} + +mkdir -p "$CONFIG_DIR/gtk-3.0" "$CONFIG_DIR/gtk-4.0" + +apply_gtk3_colors "$CONFIG_DIR" "$IS_LIGHT" "$SHELL_DIR" +apply_gtk4_colors "$CONFIG_DIR" + +echo "GTK colors applied successfully" \ No newline at end of file diff --git a/quickshell/.config/quickshell/scripts/matugen-worker.sh b/quickshell/.config/quickshell/scripts/matugen-worker.sh new file mode 100755 index 0000000..a131276 --- /dev/null +++ b/quickshell/.config/quickshell/scripts/matugen-worker.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ $# -lt 3 ]; then + echo "Usage: $0 STATE_DIR SHELL_DIR --run" >&2 + exit 1 +fi + +STATE_DIR="$1" +SHELL_DIR="$2" + +if [ ! -d "$STATE_DIR" ]; then + echo "Error: STATE_DIR '$STATE_DIR' does not exist" >&2 + exit 1 +fi + +if [ ! -d "$SHELL_DIR" ]; then + echo "Error: SHELL_DIR '$SHELL_DIR' does not exist" >&2 + exit 1 +fi + +shift 2 # Remove STATE_DIR and SHELL_DIR from arguments + +if [[ "${1:-}" != "--run" ]]; then + echo "usage: $0 STATE_DIR SHELL_DIR --run" >&2 + exit 1 +fi + +DESIRED_JSON="$STATE_DIR/matugen.desired.json" +BUILT_KEY="$STATE_DIR/matugen.key" +LAST_JSON="$STATE_DIR/last.json" +LOCK="$STATE_DIR/matugen-worker.lock" + +exec 9>"$LOCK" +flock 9 + + +read_desired() { + [[ ! -f "$DESIRED_JSON" ]] && { echo "no desired state" >&2; exit 0; } + cat "$DESIRED_JSON" +} + +key_of() { + local json="$1" + local kind=$(echo "$json" | sed 's/.*"kind": *"\([^"]*\)".*/\1/') + local value=$(echo "$json" | sed 's/.*"value": *"\([^"]*\)".*/\1/') + local mode=$(echo "$json" | sed 's/.*"mode": *"\([^"]*\)".*/\1/') + local icon=$(echo "$json" | sed 's/.*"iconTheme": *"\([^"]*\)".*/\1/') + [[ -z "$icon" ]] && icon="System Default" + echo "${kind}|${value}|${mode}|${icon}" | sha256sum | cut -d' ' -f1 +} + +build_once() { + local json="$1" + local kind value mode icon + kind=$(echo "$json" | sed 's/.*"kind": *"\([^"]*\)".*/\1/') + value=$(echo "$json" | sed 's/.*"value": *"\([^"]*\)".*/\1/') + mode=$(echo "$json" | sed 's/.*"mode": *"\([^"]*\)".*/\1/') + icon=$(echo "$json" | sed 's/.*"iconTheme": *"\([^"]*\)".*/\1/') + [[ -z "$icon" ]] && icon="System Default" + + CONFIG_DIR="${CONFIG_DIR:-$HOME/.config}" + + TMP_CFG="$(mktemp)" + trap 'rm -f "$TMP_CFG"' RETURN + + cat "$SHELL_DIR/matugen/configs/base.toml" > "$TMP_CFG" + echo "" >> "$TMP_CFG" + if command -v niri >/dev/null 2>&1; then + cat "$SHELL_DIR/matugen/configs/niri.toml" >> "$TMP_CFG" + echo "" >> "$TMP_CFG" + fi + + if command -v qt5ct >/dev/null 2>&1; then + cat "$SHELL_DIR/matugen/configs/qt5ct.toml" >> "$TMP_CFG" + echo "" >> "$TMP_CFG" + fi + + if command -v qt6ct >/dev/null 2>&1; then + cat "$SHELL_DIR/matugen/configs/qt6ct.toml" >> "$TMP_CFG" + echo "" >> "$TMP_CFG" + fi + + if [ "$mode" = "light" ]; then + COLLOID_TEMPLATE="$SHELL_DIR/matugen/templates/gtk3-colloid-light.css" + else + COLLOID_TEMPLATE="$SHELL_DIR/matugen/templates/gtk3-colloid-dark.css" + fi + + sed -i "/\[templates\.gtk3\]/,/^$/ s|input_path = './matugen/templates/gtk-colors.css'|input_path = '$COLLOID_TEMPLATE'|" "$TMP_CFG" + sed -i "s|input_path = './matugen/templates/|input_path = '$SHELL_DIR/matugen/templates/|g" "$TMP_CFG" + + pushd "$SHELL_DIR" >/dev/null + MAT_MODE=(-m "$mode") + + case "$kind" in + image) + [[ -f "$value" ]] || { echo "wallpaper not found: $value" >&2; popd >/dev/null; return 2; } + JSON=$(matugen -c "$TMP_CFG" --json hex image "$value" "${MAT_MODE[@]}") + matugen -c "$TMP_CFG" image "$value" "${MAT_MODE[@]}" >/dev/null + ;; + hex) + [[ "$value" =~ ^#[0-9A-Fa-f]{6}$ ]] || { echo "invalid hex: $value" >&2; popd >/dev/null; return 2; } + JSON=$(matugen -c "$TMP_CFG" --json hex color hex "$value" "${MAT_MODE[@]}") + matugen -c "$TMP_CFG" color hex "$value" "${MAT_MODE[@]}" >/dev/null + ;; + *) + echo "unknown kind: $kind" >&2; popd >/dev/null; return 2;; + esac + + TMP_CONTENT_CFG="$(mktemp)" + echo "[config]" > "$TMP_CONTENT_CFG" + echo "" >> "$TMP_CONTENT_CFG" + + if command -v ghostty >/dev/null 2>&1; then + cat "$SHELL_DIR/matugen/configs/ghostty.toml" >> "$TMP_CONTENT_CFG" + sed -i "s|input_path = './matugen/templates/|input_path = '$SHELL_DIR/matugen/templates/|g" "$TMP_CONTENT_CFG" + echo "" >> "$TMP_CONTENT_CFG" + fi + + if command -v kitty >/dev/null 2>&1; then + cat "$SHELL_DIR/matugen/configs/kitty.toml" >> "$TMP_CONTENT_CFG" + sed -i "s|input_path = './matugen/templates/|input_path = '$SHELL_DIR/matugen/templates/|g" "$TMP_CONTENT_CFG" + echo "" >> "$TMP_CONTENT_CFG" + fi + + if command -v dgop >/dev/null 2>&1; then + cat "$SHELL_DIR/matugen/configs/dgop.toml" >> "$TMP_CONTENT_CFG" + sed -i "s|input_path = './matugen/templates/|input_path = '$SHELL_DIR/matugen/templates/|g" "$TMP_CONTENT_CFG" + echo "" >> "$TMP_CONTENT_CFG" + fi + + if [[ -s "$TMP_CONTENT_CFG" ]] && grep -q '\[templates\.' "$TMP_CONTENT_CFG"; then + case "$kind" in + image) + matugen -c "$TMP_CONTENT_CFG" image "$value" "${MAT_MODE[@]}" >/dev/null + ;; + hex) + matugen -c "$TMP_CONTENT_CFG" color hex "$value" "${MAT_MODE[@]}" >/dev/null + ;; + esac + fi + + rm -f "$TMP_CONTENT_CFG" + popd >/dev/null + + echo "$JSON" | grep -q '"primary"' || { echo "matugen JSON missing primary" >&2; return 2; } + printf "%s" "$JSON" > "$LAST_JSON" + + if [ "$mode" = "light" ]; then + SECTION=$(echo "$JSON" | sed -n 's/.*"light":{\([^}]*\)}.*/\1/p') + else + SECTION=$(echo "$JSON" | sed -n 's/.*"dark":{\([^}]*\)}.*/\1/p') + fi + + PRIMARY=$(echo "$SECTION" | sed -n 's/.*"primary_container":"\(#[0-9a-fA-F]\{6\}\)".*/\1/p') + HONOR=$(echo "$SECTION" | sed -n 's/.*"primary":"\(#[0-9a-fA-F]\{6\}\)".*/\1/p') + SURFACE=$(echo "$SECTION" | sed -n 's/.*"surface":"\(#[0-9a-fA-F]\{6\}\)".*/\1/p') + + if command -v ghostty >/dev/null 2>&1 && [[ -f "$CONFIG_DIR/ghostty/config-dankcolors" ]]; then + OUT=$("$SHELL_DIR/matugen/dank16.py" "$PRIMARY" $([[ "$mode" == "light" ]] && echo --light) ${HONOR:+--honor-primary "$HONOR"} ${SURFACE:+--background "$SURFACE"} 2>/dev/null || true) + if [[ -n "${OUT:-}" ]]; then + TMP="$(mktemp)" + printf "%s\n\n" "$OUT" > "$TMP" + cat "$CONFIG_DIR/ghostty/config-dankcolors" >> "$TMP" + mv "$TMP" "$CONFIG_DIR/ghostty/config-dankcolors" + fi + fi + + if command -v kitty >/dev/null 2>&1 && [[ -f "$CONFIG_DIR/kitty/dank-theme.conf" ]]; then + OUT=$("$SHELL_DIR/matugen/dank16.py" "$PRIMARY" $([[ "$mode" == "light" ]] && echo --light) ${HONOR:+--honor-primary "$HONOR"} ${SURFACE:+--background "$SURFACE"} --kitty 2>/dev/null || true) + if [[ -n "${OUT:-}" ]]; then + TMP="$(mktemp)" + printf "%s\n\n" "$OUT" > "$TMP" + cat "$CONFIG_DIR/kitty/dank-theme.conf" >> "$TMP" + mv "$TMP" "$CONFIG_DIR/kitty/dank-theme.conf" + fi + fi + COLOR_SCHEME=$([[ "$mode" == "light" ]] && echo prefer-light || echo prefer-dark) + if command -v dconf >/dev/null 2>&1; then + dconf write /org/gnome/desktop/interface/color-scheme "\"$COLOR_SCHEME\"" 2>/dev/null || true + [[ "$icon" != "System Default" && -n "$icon" ]] && dconf write /org/gnome/desktop/interface/icon-theme "\"$icon\"" 2>/dev/null || true + elif command -v gsettings >/dev/null 2>&1; then + gsettings set org.gnome.desktop.interface color-scheme "$COLOR_SCHEME" 2>/dev/null || true + [[ "$icon" != "System Default" && -n "$icon" ]] && gsettings set org.gnome.desktop.interface icon-theme "$icon" 2>/dev/null || true + fi +} + +while :; do + DESIRED="$(read_desired)" + WANT_KEY="$(key_of "$DESIRED")" + HAVE_KEY="" + [[ -f "$BUILT_KEY" ]] && HAVE_KEY="$(cat "$BUILT_KEY" 2>/dev/null || true)" + + if [[ "$WANT_KEY" == "$HAVE_KEY" ]]; then + exit 0 + fi + + if build_once "$DESIRED"; then + echo "$WANT_KEY" > "$BUILT_KEY" + else + exit 2 + fi +done + +exit 0 \ No newline at end of file diff --git a/quickshell/.config/quickshell/scripts/qt.sh b/quickshell/.config/quickshell/scripts/qt.sh new file mode 100755 index 0000000..032913b --- /dev/null +++ b/quickshell/.config/quickshell/scripts/qt.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +CONFIG_DIR="$1" + +if [ -z "$CONFIG_DIR" ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +apply_qt_colors() { + local config_dir="$1" + local color_scheme_path="$(dirname "$config_dir")/.local/share/color-schemes/DankMatugen.colors" + + if [ ! -f "$color_scheme_path" ]; then + echo "Error: Qt color scheme not found at $color_scheme_path" >&2 + echo "Run matugen first to generate theme files" >&2 + exit 1 + fi + + update_qt_config() { + local config_file="$1" + + if [ -f "$config_file" ]; then + if grep -q '^\\[Appearance\\]' "$config_file"; then + if grep -q '^custom_palette=' "$config_file"; then + sed -i 's/^custom_palette=.*/custom_palette=true/' "$config_file" + else + sed -i '/^\\[Appearance\\]/a custom_palette=true' "$config_file" + fi + + if grep -q '^color_scheme_path=' "$config_file"; then + sed -i "s|^color_scheme_path=.*|color_scheme_path=$color_scheme_path|" "$config_file" + else + sed -i "/^\\[Appearance\\]/a color_scheme_path=$color_scheme_path" "$config_file" + fi + else + echo "" >> "$config_file" + echo "[Appearance]" >> "$config_file" + echo "custom_palette=true" >> "$config_file" + echo "color_scheme_path=$color_scheme_path" >> "$config_file" + fi + else + printf '[Appearance]\\ncustom_palette=true\\ncolor_scheme_path=%s\\n' "$color_scheme_path" > "$config_file" + fi + } + + qt5_applied=false + qt6_applied=false + + if command -v qt5ct >/dev/null 2>&1; then + mkdir -p "$config_dir/qt5ct" + update_qt_config "$config_dir/qt5ct/qt5ct.conf" + echo "Applied Qt5ct configuration" + qt5_applied=true + fi + + if command -v qt6ct >/dev/null 2>&1; then + mkdir -p "$config_dir/qt6ct" + update_qt_config "$config_dir/qt6ct/qt6ct.conf" + echo "Applied Qt6ct configuration" + qt6_applied=true + fi + + if [ "$qt5_applied" = false ] && [ "$qt6_applied" = false ]; then + echo "Warning: Neither qt5ct nor qt6ct found" >&2 + echo "Install qt5ct or qt6ct for Qt application theming" >&2 + exit 1 + fi +} + +apply_qt_colors "$CONFIG_DIR" + +echo "Qt colors applied successfully" \ No newline at end of file diff --git a/quickshell/.config/quickshell/shell.qml b/quickshell/.config/quickshell/shell.qml new file mode 100644 index 0000000..8b80eef --- /dev/null +++ b/quickshell/.config/quickshell/shell.qml @@ -0,0 +1,438 @@ +//@ pragma Env QSG_RENDER_LOOP=threaded +//@ pragma UseQApplication +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import qs.Common +import qs.Modals +import qs.Modals.Clipboard +import qs.Modals.Common +import qs.Modals.Settings +import qs.Modals.Spotlight +import qs.Modules +import qs.Modules.AppDrawer +import qs.Modules.DankDash +import qs.Modules.ControlCenter +import qs.Modules.Lock +import qs.Modules.Notifications.Center +import qs.Modules.Notifications.Popup +import qs.Modules.OSD +import qs.Modules.ProcessList +import qs.Modules.Settings +import qs.Modules.TopBar +import qs.Services + +ShellRoot { + id: root + + Component.onCompleted: { + PortalService.init() + // Initialize DisplayService night mode functionality + DisplayService.nightModeEnabled + } + + WallpaperBackground {} + + Lock { + id: lock + + anchors.fill: parent + } + + Variants { + model: SettingsData.getFilteredScreens("topBar") + + delegate: TopBar { + modelData: item + } + } + + Loader { + id: dankDashPopoutLoader + + active: false + asynchronous: true + + sourceComponent: Component { + DankDashPopout { + id: dankDashPopout + } + } + } + + LazyLoader { + id: notificationCenterLoader + + active: false + + NotificationCenterPopout { + id: notificationCenter + } + } + + Variants { + model: SettingsData.getFilteredScreens("notifications") + + delegate: NotificationPopupManager { + modelData: item + } + } + + LazyLoader { + id: controlCenterLoader + + active: false + + ControlCenterPopout { + id: controlCenterPopout + + onPowerActionRequested: (action, title, message) => { + powerConfirmModalLoader.active = true + if (powerConfirmModalLoader.item) { + powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary + powerConfirmModalLoader.item.show(title, message, function () { + switch (action) { + case "logout": + SessionService.logout() + break + case "suspend": + SessionService.suspend() + break + case "reboot": + SessionService.reboot() + break + case "poweroff": + SessionService.poweroff() + break + } + }, function () {}) + } + } + onLockRequested: { + lock.activate() + } + } + } + + LazyLoader { + id: wifiPasswordModalLoader + + active: false + + WifiPasswordModal { + id: wifiPasswordModal + } + } + + LazyLoader { + id: networkInfoModalLoader + + active: false + + NetworkInfoModal { + id: networkInfoModal + } + } + + LazyLoader { + id: batteryPopoutLoader + + active: false + + BatteryPopout { + id: batteryPopout + } + } + + LazyLoader { + id: vpnPopoutLoader + + active: false + + VpnPopout { + id: vpnPopout + } + } + + LazyLoader { + id: powerMenuLoader + + active: false + + PowerMenu { + id: powerMenu + + onPowerActionRequested: (action, title, message) => { + powerConfirmModalLoader.active = true + if (powerConfirmModalLoader.item) { + powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary + powerConfirmModalLoader.item.show(title, message, function () { + switch (action) { + case "logout": + SessionService.logout() + break + case "suspend": + SessionService.suspend() + break + case "reboot": + SessionService.reboot() + break + case "poweroff": + SessionService.poweroff() + break + } + }, function () {}) + } + } + } + } + + LazyLoader { + id: powerConfirmModalLoader + + active: false + + ConfirmModal { + id: powerConfirmModal + } + } + + LazyLoader { + id: processListPopoutLoader + + active: false + + ProcessListPopout { + id: processListPopout + } + } + + SettingsModal { + id: settingsModal + } + + LazyLoader { + id: appDrawerLoader + + active: false + + AppDrawerPopout { + id: appDrawerPopout + } + } + + SpotlightModal { + id: spotlightModal + } + + ClipboardHistoryModal { + id: clipboardHistoryModalPopup + } + + NotificationModal { + id: notificationModal + } + + LazyLoader { + id: processListModalLoader + + active: false + + ProcessListModal { + id: processListModal + } + } + + LazyLoader { + id: powerMenuModalLoader + + active: false + + PowerMenuModal { + id: powerMenuModal + + onPowerActionRequested: (action, title, message) => { + powerConfirmModalLoader.active = true + if (powerConfirmModalLoader.item) { + powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary + powerConfirmModalLoader.item.show(title, message, function () { + switch (action) { + case "logout": + SessionService.logout() + break + case "suspend": + SessionService.suspend() + break + case "reboot": + SessionService.reboot() + break + case "poweroff": + SessionService.poweroff() + break + } + }, function () {}) + } + } + } + } + + IpcHandler { + function open() { + powerMenuModalLoader.active = true + if (powerMenuModalLoader.item) + powerMenuModalLoader.item.open() + + return "POWERMENU_OPEN_SUCCESS" + } + + function close() { + if (powerMenuModalLoader.item) + powerMenuModalLoader.item.close() + + return "POWERMENU_CLOSE_SUCCESS" + } + + function toggle() { + powerMenuModalLoader.active = true + if (powerMenuModalLoader.item) + powerMenuModalLoader.item.toggle() + + return "POWERMENU_TOGGLE_SUCCESS" + } + + target: "powermenu" + } + + IpcHandler { + function open(): string { + processListModalLoader.active = true + if (processListModalLoader.item) + processListModalLoader.item.show() + + return "PROCESSLIST_OPEN_SUCCESS" + } + + function close(): string { + if (processListModalLoader.item) + processListModalLoader.item.hide() + + return "PROCESSLIST_CLOSE_SUCCESS" + } + + function toggle(): string { + processListModalLoader.active = true + if (processListModalLoader.item) + processListModalLoader.item.toggle() + + return "PROCESSLIST_TOGGLE_SUCCESS" + } + + target: "processlist" + } + + IpcHandler { + function open(tab: string): string { + dankDashPopoutLoader.active = true + if (dankDashPopoutLoader.item) { + switch (tab.toLowerCase()) { + case "media": + dankDashPopoutLoader.item.currentTabIndex = 1 + break + case "wallpapers": + dankDashPopoutLoader.item.currentTabIndex = 2 + break + case "weather": + dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0 + break + default: + dankDashPopoutLoader.item.currentTabIndex = 0 + break + } + dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen) + dankDashPopoutLoader.item.dashVisible = true + return "DASH_OPEN_SUCCESS" + } + return "DASH_OPEN_FAILED" + } + + function close(): string { + if (dankDashPopoutLoader.item) { + dankDashPopoutLoader.item.dashVisible = false + return "DASH_CLOSE_SUCCESS" + } + return "DASH_CLOSE_FAILED" + } + + function toggle(tab: string): string { + dankDashPopoutLoader.active = true + if (dankDashPopoutLoader.item) { + if (dankDashPopoutLoader.item.dashVisible) { + dankDashPopoutLoader.item.dashVisible = false + } else { + switch (tab.toLowerCase()) { + case "media": + dankDashPopoutLoader.item.currentTabIndex = 1 + break + case "wallpapers": + dankDashPopoutLoader.item.currentTabIndex = 2 + break + case "weather": + dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0 + break + default: + dankDashPopoutLoader.item.currentTabIndex = 0 + break + } + dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen) + dankDashPopoutLoader.item.dashVisible = true + } + return "DASH_TOGGLE_SUCCESS" + } + return "DASH_TOGGLE_FAILED" + } + + target: "dash" + } + + Variants { + model: SettingsData.getFilteredScreens("toast") + + delegate: Toast { + modelData: item + visible: ToastService.toastVisible + } + } + + Variants { + model: SettingsData.getFilteredScreens("osd") + + delegate: VolumeOSD { + modelData: item + } + } + + Variants { + model: SettingsData.getFilteredScreens("osd") + + delegate: MicMuteOSD { + modelData: item + } + } + + Variants { + model: SettingsData.getFilteredScreens("osd") + + delegate: BrightnessOSD { + modelData: item + } + } + + Variants { + model: SettingsData.getFilteredScreens("osd") + + delegate: IdleInhibitorOSD { + modelData: item + } + } +} diff --git a/quickshell/.config/quickshell/spam-notifications.sh b/quickshell/.config/quickshell/spam-notifications.sh new file mode 100755 index 0000000..fcd9f6d --- /dev/null +++ b/quickshell/.config/quickshell/spam-notifications.sh @@ -0,0 +1,207 @@ +#!/bin/bash + +# Notification Spam Test Script - Sends 100 rapid notifications from fake apps + +echo "NOTIFICATION SPAM TEST - 100 RAPID NOTIFICATIONS" +echo "=============================================================" +echo "WARNING: This will send 100 notifications very quickly!" +echo "Press Ctrl+C to cancel, or wait 3 seconds to continue..." +sleep 3 + +# Arrays of fake app names and icons +APPS=( + "slack:mail-message-new" + "discord:internet-chat" + "teams:call-start" + "zoom:camera-video" + "spotify:audio-x-generic" + "chrome:web-browser" + "firefox:web-browser" + "vscode:text-editor" + "terminal:utilities-terminal" + "steam:applications-games" + "telegram:internet-chat" + "whatsapp:phone" + "signal:security-high" + "thunderbird:mail-client" + "calendar:office-calendar" + "notes:text-editor" + "todo:emblem-default" + "weather:weather-few-clouds" + "news:rss" + "reddit:web-browser" + "twitter:internet-web-browser" + "instagram:camera-photo" + "youtube:video-x-generic" + "netflix:media-playback-start" + "github:folder-development" + "gitlab:folder-development" + "jira:applications-office" + "notion:text-editor" + "obsidian:accessories-text-editor" + "dropbox:folder-remote" + "gdrive:folder-google-drive" + "onedrive:folder-cloud" + "backup:drive-harddisk" + "antivirus:security-high" + "vpn:network-vpn" + "torrent:network-server" + "docker:application-x-executable" + "kubernetes:applications-system" + "postgres:database" + "mongodb:database" + "redis:database" + "nginx:network-server" + "apache:network-server" + "jenkins:applications-development" + "gradle:applications-development" + "maven:applications-development" + "npm:package-x-generic" + "yarn:package-x-generic" + "pip:package-x-generic" + "apt:system-software-install" +) + +# Arrays of message types +TITLES=( + "New message" + "Update available" + "Download complete" + "Task finished" + "Build successful" + "Deployment complete" + "Sync complete" + "Backup finished" + "Security alert" + "New notification" + "Process complete" + "Upload finished" + "Connection established" + "Meeting starting" + "Reminder" + "Warning" + "Error occurred" + "Success" + "Failed" + "Pending" + "In progress" + "Scheduled" + "New activity" + "Status update" + "Alert" + "Information" + "Breaking news" + "Hot update" + "Trending" + "New release" +) + +MESSAGES=( + "Your request has been processed successfully" + "New content is available for download" + "Operation completed without errors" + "Check your inbox for updates" + "3 new items require your attention" + "Background task finished executing" + "All systems operational" + "Performance metrics updated" + "Configuration saved successfully" + "Database connection established" + "Cache cleared and rebuilt" + "Service restarted automatically" + "Logs have been rotated" + "Memory usage optimized" + "Network latency improved" + "Security scan completed - no threats" + "Automatic backup created" + "Files synchronized across devices" + "Updates installed successfully" + "New features are now available" + "Your subscription has been renewed" + "Report generated and ready" + "Analysis complete - view results" + "Queue processed: 42 items" + "Rate limit will reset in 5 minutes" + "API call successful (200 OK)" + "Webhook delivered successfully" + "Container started on port 8080" + "Build artifact uploaded" + "Test suite passed: 100/100" + "Coverage report: 95%" + "Dependencies updated to latest" + "Migration completed successfully" + "Index rebuilt for faster queries" + "SSL certificate renewed" + "Firewall rules updated" + "DNS propagation complete" + "CDN cache purged globally" + "Load balancer health check: OK" + "Cluster scaled to 5 nodes" +) + +# Urgency levels +URGENCY=("low" "normal") + +# Counter +COUNT=0 +TOTAL=100 + +echo "" +echo "Starting notification spam..." +echo "------------------------------" + +# Send notifications rapidly +for i in $(seq 1 $TOTAL); do + # Pick random app, title, message, and urgency + APP=${APPS[$RANDOM % ${#APPS[@]}]} + APP_NAME=${APP%%:*} + APP_ICON=${APP#*:} + TITLE=${TITLES[$RANDOM % ${#TITLES[@]}]} + MESSAGE=${MESSAGES[$RANDOM % ${#MESSAGES[@]}]} + URG=${URGENCY[$RANDOM % ${#URGENCY[@]}]} + + # Add some variety with random numbers and timestamps + RAND_NUM=$((RANDOM % 1000)) + TIMESTAMP=$(date +"%H:%M:%S") + + # Randomly add extra details to some messages + if [ $((RANDOM % 3)) -eq 0 ]; then + MESSAGE="[$TIMESTAMP] $MESSAGE (#$RAND_NUM)" + fi + + # Send notification with very short delay + notify-send \ + -h string:desktop-entry:$APP_NAME \ + -i $APP_ICON \ + -u $URG \ + "$APP_NAME: $TITLE" \ + "$MESSAGE" & + + # Increment counter + COUNT=$((COUNT + 1)) + + # Show progress every 10 notifications + if [ $((COUNT % 10)) -eq 0 ]; then + echo " Sent $COUNT/$TOTAL notifications..." + fi + + # Tiny delay to prevent complete system freeze + # Adjust this value: smaller = faster spam, larger = slower spam + sleep 0.01 +done + +# Wait for all background notifications to complete +wait + +echo "" +echo "Spam test complete!" +echo "=============================================================" +echo "Statistics:" +echo " Total notifications sent: $TOTAL" +echo " Apps simulated: ${#APPS[@]}" +echo " Message variations: ${#MESSAGES[@]}" +echo " Time taken: ~$(($TOTAL / 100)) seconds" +echo "" +echo "Check your notification center - it should be FULL!" +echo "Tip: You may want to clear all notifications after this test" +echo "" \ No newline at end of file diff --git a/quickshell/.config/quickshell/verify-notifications.sh b/quickshell/.config/quickshell/verify-notifications.sh new file mode 100755 index 0000000..8cd0ffa --- /dev/null +++ b/quickshell/.config/quickshell/verify-notifications.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +# Enhanced Notification System Test Script with Common Icons +# Uses icons that are more likely to be available on most systems + +echo "🔔 Testing Enhanced Notification System Features" +echo "=============================================================" + +# Check what icons are available +echo "Checking available icons..." +if [ -d "~/.local/share/icons/Papirus" ]; then + echo "✓ Icon theme found" + ICON_BASE="~/.local/share/icons/Papirus" +else + echo "! Using fallback icons" + ICON_BASE="" +fi + +# Test 1: Basic notifications with markdown +echo "📱 Test 1: Basic notifications with markdown" +notify-send -h string:desktop-entry:org.gnome.Settings -i preferences-desktop "Settings" "**Bold text** and *italic text* with [links](https://example.com) and \`code blocks\`" +sleep 2 + +# Test 2: Media notifications with rich formatting (grouping) +echo "🎵 Test 2: Media notifications with rich formatting (grouping)" +notify-send -h string:desktop-entry:spotify -i audio-x-generic "Spotify" "**Now Playing:** *Song 1* by **Artist A**\n\nAlbum: ~Greatest Hits~\nDuration: \`3:45\`" +sleep 1 +notify-send -h string:desktop-entry:spotify -i audio-x-generic "Spotify" "**Now Playing:** *Song 2* by **Artist B**\n\n> From the album: \"New Releases\"\n- Track #4\n- \`4:12\`" +sleep 1 +notify-send -h string:desktop-entry:spotify -i audio-x-generic "Spotify" "**Now Playing:** *Song 3* by **Artist C**\n\n### Recently Added\n- [View on Spotify](https://spotify.com)\n- Duration: \`2:58\`" +sleep 2 + +# Test 3: System notifications with markdown (separate groups) +echo "🔋 Test 3: System notifications with markdown (separate apps)" +notify-send -h string:desktop-entry:org.gnome.PowerStats -i battery "Power Manager" "⚠️ **Battery Low:** \`15%\` remaining\n\n### Power Saving Tips:\n- Reduce screen brightness\n- *Close unnecessary apps*\n- [Power settings](settings://power)" +sleep 1 +notify-send -h string:desktop-entry:org.gnome.NetworkDisplays -i network-wired "Network Manager" "✅ **WiFi Connected:** *HomeNetwork*\n\n**Signal Strength:** Strong (85%)\n**IP Address:** \`192.168.1.100\`\n\n> Connection established successfully" +sleep 1 +notify-send -h string:desktop-entry:org.gnome.Software -i system-software-update "Software" "📦 **Updates Available**\n\n### Pending Updates:\n- **Firefox** (v119.0)\n- *System libraries* (security)\n- \`python-requests\` (dependency)\n\n[Install All](software://updates) | [View Details](software://details)" +sleep 2 + +# Test 4: Chat notifications with complex markdown (grouping) +echo "💬 Test 4: Chat notifications with complex markdown (grouping)" +notify-send -h string:desktop-entry:discord -i internet-chat "Discord" "**#general** - User1\n\nHello everyone! 👋\n\n> Just wanted to share this cool project I'm working on:\n- Built with **React** and *TypeScript*\n- Using \`styled-components\` for styling\n- [Check it out](https://github.com/user1/project)" +sleep 1 +notify-send -h string:desktop-entry:discord -i internet-chat "Discord" "**#general** - User2\n\nHey there! That looks awesome! 🚀\n\n### Quick question:\nDo you have any tips for:\n1. **State management** patterns?\n2. *Performance optimization*?\n3. Testing with \`jest\`?\n\n> I'm still learning React" +sleep 1 +notify-send -h string:desktop-entry:discord -i internet-chat "Discord" "**Direct Message** - john_doe\n\n*Private message from John* 💬\n\n**Subject:** Weekend plans\n\nHey! Want to grab coffee this weekend?\n\n### Suggestions:\n- ☕ Local café on Main St\n- 🥐 That new bakery downtown\n- 🏠 My place (I got a new espresso machine!)\n\n[Reply](discord://dm/john_doe) | [Call](discord://call/john_doe)" +sleep 2 + +# Test 5: Urgent notifications with markdown +echo "🚨 Test 5: Urgent notifications with markdown" +notify-send -u critical -i dialog-warning "Critical Alert" "🔥 **SYSTEM OVERHEATING** 🔥\n\n### Current Status:\n- **Temperature:** \`85°C\` (Critical)\n- **CPU Usage:** \`95%\`\n- *Thermal throttling active*\n\n> **Immediate Actions Required:**\n1. Close resource-intensive applications\n2. Check cooling system\n3. Reduce workload\n\n[System Monitor](gnome-system-monitor) | [Power Options](gnome-power-statistics)" +sleep 2 + +# Test 6: Notifications with actions and markdown +echo "⚡ Test 6: Action buttons with markdown" +notify-send -h string:desktop-entry:org.gnome.Software -i system-upgrade "Software" "📦 **System Updates Available**\n\n### Ready to Install:\n- **Security patches** (High priority)\n- *Feature updates* for 3 applications\n- \`kernel\` update (5.15.0 → 5.16.2)\n\n> **Recommended:** Install now for optimal security\n\n**Estimated time:** ~15 minutes\n**Restart required:** Yes\n\n[Install Now](software://install) | [Schedule Later](software://schedule)" +sleep 2 + +# Test 7: Multiple different apps with rich markdown +echo "📊 Test 7: Multiple different apps with rich markdown" +notify-send -h string:desktop-entry:thunderbird -i mail-message-new "Thunderbird" "📧 **New Messages** (3)\n\n### Recent Emails:\n1. **Sarah Johnson** - *Project Update*\n > \"The quarterly report is ready for review...\"\n \n2. **GitHub** - \`[user/repo]\` *Pull Request*\n > New PR: Fix memory leak in parser\n \n3. **Newsletter** - *Weekly Tech Digest*\n > This week: AI advancements, new frameworks...\n\n[Open Inbox](thunderbird://inbox) | [Mark All Read](thunderbird://markread)" +sleep 0.5 +notify-send -h string:desktop-entry:org.gnome.Calendar -i office-calendar "Calendar" "📅 **Upcoming Meeting**\n\n### Daily Standup\n- **Time:** 5 minutes\n- **Location:** *Conference Room A*\n- **Attendees:** Team Alpha (8 people)\n\n#### Agenda:\n1. Yesterday's progress\n2. Today's goals \n3. Blockers discussion\n\n> **Reminder:** Prepare your status update\n\n[Join Video Call](meet://standup) | [Reschedule](calendar://reschedule)" +sleep 0.5 +notify-send -h string:desktop-entry:org.gnome.Nautilus -i folder-downloads "Files" "📁 **Download Complete**\n\n### File Details:\n- **Name:** \`document.pdf\`\n- **Size:** *2.4 MB*\n- **Location:** ~/Downloads/\n- **Type:** PDF Document\n\n> **Security:** Scanned ✅ (No threats detected)\n\n**Recent Downloads:**\n- presentation.pptx (1 hour ago)\n- backup.zip (yesterday)\n\n[Open File](file://document.pdf) | [Show in Folder](nautilus://downloads)" +sleep 2 + +# notify-send --hint=boolean:resident:true "Resident Test" "Click an action - I should stay visible!" --action="Test Action" --action="Close Me" + +echo "" +echo "✅ Notification tests completed!" +echo "" +echo "📋 Enhanced Features Tested:" +echo " • Media notification replacement" +echo " • System notification grouping" +echo " • Conversation grouping and auto-expansion" +echo " • Urgency level handling" +echo " • Action button support" +echo " • Multi-app notification handling" +echo "" +echo "🎯 Check your notification popup and notification center to see the results!" +echo "" +echo "Note: Some icons may show as fallback (checkerboard) if icon themes aren't installed." +echo "To install more icons: sudo pacman -S papirus-icon-theme adwaita-icon-theme" diff --git a/quickshell/.local/share/fonts/FiraCode-Regular.ttf b/quickshell/.local/share/fonts/FiraCode-Regular.ttf new file mode 100644 index 0000000..8537307 --- /dev/null +++ b/quickshell/.local/share/fonts/FiraCode-Regular.ttf @@ -0,0 +1 @@ +Not Found \ No newline at end of file diff --git a/quickshell/.local/share/fonts/InterVariable.ttf b/quickshell/.local/share/fonts/InterVariable.ttf new file mode 100644 index 0000000..4ab79e0 Binary files /dev/null and b/quickshell/.local/share/fonts/InterVariable.ttf differ diff --git a/quickshell/.local/share/fonts/MaterialSymbolsRounded.ttf b/quickshell/.local/share/fonts/MaterialSymbolsRounded.ttf new file mode 100644 index 0000000..cb10ab3 Binary files /dev/null and b/quickshell/.local/share/fonts/MaterialSymbolsRounded.ttf differ