19 Commits
Author SHA1 Message Date
nvrl 08146fe16f final theme 2026-08-11 02:05:46 +02:00
nvrl 105cf6b3a8 updated theme 2026-08-11 01:53:26 +02:00
nvrl 0a439d951e updated style 2026-08-11 00:22:59 +02:00
nvrl 47ff5233bd added quickshell 2026-08-10 23:57:26 +02:00
nvrl cbaf55cfc5 updated 2026-08-10 22:36:16 +02:00
nvrl ad1b8fe3a9 update 2026-05-09 07:21:33 +02:00
nvrl 667e42ba15 updated keybinds and window rules 2026-05-01 20:56:23 +02:00
nvrl 5e6d48b30a added window rules and services 2026-04-29 13:37:25 +02:00
nvrl 9e8411a176 updated configs 2026-04-06 01:17:40 +02:00
nvrl 6d5d2caeca added fluxo and hyprlock conf 2026-04-04 16:44:12 +02:00
nvrl 86c964c274 fixed style css and updated config 2026-04-04 14:42:56 +02:00
nvrl 7958f17951 updated waybar + fluxo 2026-04-04 14:07:35 +02:00
nvrl 9365615f06 updated waybar config 2026-04-04 05:10:21 +02:00
nvrl f910af8dce removed splash and increased refresh intervals 2026-04-02 18:47:11 +02:00
nvrl 74945d45e9 updated hyprlock 2026-04-02 13:46:16 +02:00
nvrl eadaaa5d52 updated themes, waybar 2026-04-02 13:10:37 +02:00
nvrl 70842dc108 changen clock color 2026-04-01 17:44:08 +02:00
nvrl 1e811035a5 updated waybar + hypr rules 2026-03-31 10:20:16 +02:00
nvrl 9dda38ceee fire/ice theme 2026-03-24 23:47:28 +01:00
136 changed files with 10269 additions and 1428 deletions
+2
View File
@@ -8,6 +8,7 @@
!hypr/
!hyprpanel/
!nvim/
!quickshell/
!waybar/
!zathura/
!mimeapps.list
@@ -25,3 +26,4 @@
!systemd/
fish/fish_variables
waybar/fluxo-rs
!fluxo/
+14 -5
View File
@@ -12,7 +12,10 @@ WINIT_X11_SCALE_FACTOR = "1.0"
position = "None"
dynamic_padding = true
decorations = "None"
opacity = 0.9
## Fully opaque, no blur. Persona surfaces are flat ink on flat ground —
## glass and translucency read as a different design language entirely.
opacity = 1.0
blur = false
startup_mode = "Windowed"
dynamic_title = true
@@ -24,10 +27,11 @@ decorations_theme_variant = "None"
columns = 82
lines = 24
## Blank space added around the window in pixels.
## Blank space added around the window in pixels. Roomier than the old 3/0 —
## the text needs to sit inside the crimson border, not against it.
[window.padding]
x = 3
y = 0
x = 12
y = 8
## SCROLLING ------------------------------------------------------
[scrolling]
@@ -103,4 +107,9 @@ ipc_socket = true
## GENERAL --------------------------------------------------------
## Import additional configuration files.
import = ["~/.config/alacritty/colors.toml", "~/.config/alacritty/fonts.toml"]
import = ["~/.config/alacritty/takemi.toml", "~/.config/alacritty/fonts.toml"]
[[keyboard.bindings]]
key = "Return"
mods = "Shift"
chars = "\u001B\r"
+86
View File
@@ -0,0 +1,86 @@
## Takemi — matched to ~/.config/quickshell/config/Theme.qml and
## ~/.config/hypr/modules/takemi.conf.
##
## Near-black ground, white text, crimson as the only saturated colour. The
## sixteen ANSI slots still have to be told apart by syntax highlighters, so the
## non-crimson hues survive — but every one of them is pulled down and
## desaturated so nothing competes with the accent. Nothing here is neon.
[colors.primary]
background = "#0a0a0c"
foreground = "#ffffff"
dim_foreground = "#6b6b70"
bright_foreground = "#ffffff"
## Crimson block cursor, black text punched out of it.
[colors.cursor]
text = "#000000"
cursor = "#ff2d40"
[colors.vi_mode_cursor]
text = "#000000"
cursor = "#ffffff"
## Selection is a solid crimson block — the same "active" signal the shell uses.
[colors.selection]
text = "#000000"
background = "#ff2d40"
[colors.search.matches]
foreground = "#000000"
background = "#c8c8ce"
[colors.search.focused_match]
foreground = "#000000"
background = "#ff2d40"
[colors.footer_bar]
foreground = "#000000"
background = "#c8c8ce"
[colors.hints.start]
foreground = "#000000"
background = "#ff2d40"
[colors.hints.end]
foreground = "#000000"
background = "#c8c8ce"
## Red is the accent, exactly. The rest are muted enough to sit behind it.
[colors.normal]
black = "#14141a"
red = "#ff2d40"
green = "#7f9b6a"
yellow = "#d6a24a"
blue = "#6f7d99"
magenta = "#c0566d"
cyan = "#7f9aa0"
white = "#c8c8ce"
[colors.bright]
black = "#3a3a44"
red = "#ff6b78"
green = "#9bb886"
yellow = "#f0c070"
blue = "#8d9bb8"
magenta = "#e07a8e"
cyan = "#9db8bd"
white = "#ffffff"
[colors.dim]
black = "#0f0f13"
red = "#8f1826"
green = "#5c704d"
yellow = "#9c7635"
blue = "#515c70"
magenta = "#8c3f4f"
cyan = "#5c7075"
white = "#6b6b70"
[[colors.indexed_colors]]
index = 16
color = "#ff6b78"
[[colors.indexed_colors]]
index = 17
color = "#ffffff"
+18 -4
View File
@@ -1,4 +1,4 @@
#? Config file for btop v.1.4.6
#? Config file for btop v.1.4.7
#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
@@ -14,6 +14,11 @@ truecolor = true
#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols.
force_tty = false
#* Option to disable presets. Either the default preset, custom presets, or all presets.
#* "Off" All presets are enabled.
#* "Default" preset is disabled.#* "Custom" presets are disabled.#* "All" presets are disabled.
disable_presets = "Off"
#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets.
#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box.
#* Use whitespace " " as separator between different presets.
@@ -24,6 +29,9 @@ presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:defaul
#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift.
vim_keys = true
#* Disable all mouse events.
disable_mouse = false
#* Rounded corners on boxes, is ignored if TTY mode is ON.
rounded_corners = true
@@ -60,7 +68,7 @@ update_ms = 1000
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct",
#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly.
proc_sorting = "cpu lazy"
proc_sorting = "memory"
#* Reverse sorting order, True or False.
proc_reversed = false
@@ -92,6 +100,9 @@ proc_left = false
#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop).
proc_filter_kernel = false
#* Should the process list follow the selected process when detailed view is open.
proc_follow_detailed = true
#* In tree-view, always accumulate child process resources in the parent process.
proc_aggregate = false
@@ -208,6 +219,9 @@ io_graph_combined = false
#* Example: "/mnt/media:100 /:20 /boot:1".
io_graph_speeds = ""
#* Swap the positions of the upload and download speed graphs. When true, upload will be on top.
swap_upload_download = false
#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False.
net_download = 100
@@ -250,11 +264,11 @@ rsmi_measure_pcie_speeds = true
#* Horizontally mirror the GPU graph.
gpu_mirror_graph = true
#* Set which GPU vendors to show. Available values are "nvidia amd intel"
#* Set which GPU vendors to show. Available values are "nvidia amd intel apple"
shown_gpus = "nvidia amd intel"
#* Custom gpu0 model name, empty string to disable.
custom_gpu_name0 = ""
custom_gpu_name0 = "Radeon Vega"
#* Custom gpu1 model name, empty string to disable.
custom_gpu_name1 = ""
+2 -2
View File
@@ -1,10 +1,10 @@
[global]
corner_radius = 4
transparency = 90
frame_color = "#94e2d5"
frame_color = "#f38ba8"
frame_width = 1
separator_color= frame
highlight = "#89b4fa"
highlight = "#89dceb"
[urgency_low]
background = "#1e1e2e"
-49
View File
@@ -1,49 +0,0 @@
# This file was created by fish when upgrading to version 4.3, to migrate
# theme variables from universal to global scope.
# Don't edit this file, as it will be written by the web-config tool (`fish_config`).
# To customize your theme, delete this file and see
# help interactive#syntax-highlighting
# or
# man fish-interactive | less +/^SYNTAX.HIGHLIGHTING
# for appropriate commands to add to ~/.config/fish/config.fish instead.
# See also the release notes for fish 4.3.0 (run `help relnotes`).
set --global fish_color_autosuggestion 6c7086
set --global fish_color_cancel f38ba8
set --global fish_color_command 89b4fa
set --global fish_color_comment 7f849c
set --global fish_color_cwd f9e2af
set --global fish_color_cwd_root red
set --global fish_color_end fab387
set --global fish_color_error f38ba8
set --global fish_color_escape eba0ac
set --global fish_color_gray 6c7086
set --global fish_color_history_current --bold
set --global fish_color_host 89b4fa
set --global fish_color_host_remote a6e3a1
set --global fish_color_keyword f38ba8
set --global fish_color_match --background=brblue
set --global fish_color_normal cdd6f4
set --global fish_color_operator f5c2e7
set --global fish_color_option a6e3a1
set --global fish_color_param f2cdcd
set --global fish_color_quote a6e3a1
set --global fish_color_redirection f5c2e7
set --global fish_color_search_match --background=313244
set --global fish_color_selection --background=313244
set --global fish_color_status f38ba8
set --global fish_color_user 94e2d5
set --global fish_color_valid_path --underline
set --global fish_pager_color_background
set --global fish_pager_color_completion cdd6f4
set --global fish_pager_color_description 6c7086
set --global fish_pager_color_prefix f5c2e7
set --global fish_pager_color_progress 6c7086
set --global fish_pager_color_secondary_background
set --global fish_pager_color_secondary_completion
set --global fish_pager_color_secondary_description
set --global fish_pager_color_secondary_prefix
set --global fish_pager_color_selected_background
set --global fish_pager_color_selected_completion
set --global fish_pager_color_selected_description
set --global fish_pager_color_selected_prefix
+50
View File
@@ -0,0 +1,50 @@
# Takemi — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/takemi.toml and ~/.config/nvim/lua/takemi.lua.
#
# Replaces the catppuccin fish_frozen_theme.fish that fish 4.3 generated on
# upgrade. Same content as themes/Takemi.theme — that copy exists only so
# `fish_config` can preview it; this one is what actually loads.
#
# Note that `fish_config` writing a theme from the web UI will recreate
# fish_frozen_theme.fish, which sorts before this file and so would lose.
set --global fish_color_normal ffffff
set --global fish_color_command ffffff --bold
set --global fish_color_keyword ff2d40
set --global fish_color_param c8c8ce
set --global fish_color_option 8d9bb8
set --global fish_color_quote 7f9b6a
set --global fish_color_escape 9db8bd
set --global fish_color_operator c8c8ce
set --global fish_color_redirection ff6b78
set --global fish_color_end 6b6b70
set --global fish_color_comment 6b6b70 --italics
set --global fish_color_error ff2d40 --bold
set --global fish_color_cancel ff2d40
set --global fish_color_gray 6b6b70
set --global fish_color_autosuggestion 4a4a52
set --global fish_color_selection 000000 --background=ff2d40
set --global fish_color_search_match 000000 --background=c8c8ce
set --global fish_color_match 000000 --background=ff2d40
set --global fish_color_history_current --bold
set --global fish_color_valid_path --underline
set --global fish_color_cwd ffffff --bold
set --global fish_color_cwd_root ff2d40 --bold
set --global fish_color_user c8c8ce
set --global fish_color_host ffffff --bold
set --global fish_color_host_remote ff6b78
set --global fish_color_status ff2d40
set --global fish_pager_color_progress 6b6b70 --background=141418
set --global fish_pager_color_background
set --global fish_pager_color_prefix ffffff --bold
set --global fish_pager_color_completion c8c8ce
set --global fish_pager_color_description 6b6b70
set --global fish_pager_color_secondary_background --background=0d0d10
set --global fish_pager_color_secondary_completion c8c8ce
set --global fish_pager_color_secondary_description 6b6b70
set --global fish_pager_color_secondary_prefix ffffff --bold
set --global fish_pager_color_selected_background --background=ff2d40
set --global fish_pager_color_selected_completion 000000
set --global fish_pager_color_selected_description 000000
set --global fish_pager_color_selected_prefix 000000 --bold
+4
View File
@@ -37,3 +37,7 @@ if status is-login && test -z "$DISPLAY" -a -z "$WAYLAND_DISPLAY"
exec uwsm start default
end
end
# Claude environments
alias claude="CLAUDE_CONFIG_DIR=~/.claude command claude"
alias claude-work="CLAUDE_CONFIG_DIR=~/.claude-work command claude"
+3
View File
@@ -0,0 +1,3 @@
function feh --description 'alias feh=feh -.'
command feh -. $argv
end
+6 -2
View File
@@ -1,9 +1,13 @@
function fish_prompt --description 'Write out the prompt'
set -l last_status $status
set -l normal (set_color normal)
set -l status_color (set_color brgreen)
# White at rest, crimson when the last command failed — the prompt
# arrow is the one place the shell says "something is wrong", so it is
# the one place that earns the accent. Was brgreen/brpurple, which
# spent colour on states that are not worth any.
set -l status_color (set_color ffffff)
set -l cwd_color (set_color $fish_color_cwd)
set -l vcs_color (set_color brpurple)
set -l vcs_color (set_color 6b6b70)
set -l prompt_status ""
# Since we display the prompt on a new line allow the directory names to be longer.
+1 -1
View File
@@ -1,3 +1,3 @@
function ls --wraps='exa --long --color=auto' --description 'alias ls=exa --long --color=auto'
exa --long --color=auto $argv;
exa -l --color=auto $argv;
end
+52
View File
@@ -0,0 +1,52 @@
# name: 'Takemi'
# url: 'local — matched to ~/.config/quickshell/config/Theme.qml'
# preferred_background: 0a0a0c
# Same rules as the shell and the editor: white does the work, crimson is the
# only saturated colour and it means either "load-bearing" (keyword, prefix,
# selected) or "wrong" (error, status, root). Everything else is grey, or a hue
# pulled far enough down that it reads as grey with a temperature.
#
# Kept in sync by hand with conf.d/takemi_theme.fish, which is what actually
# takes effect at startup. This file exists so `fish_config` can preview it.
fish_color_normal ffffff
fish_color_command ffffff --bold
fish_color_keyword ff2d40
fish_color_param c8c8ce
fish_color_option 8d9bb8
fish_color_quote 7f9b6a
fish_color_escape 9db8bd
fish_color_operator c8c8ce
fish_color_redirection ff6b78
fish_color_end 6b6b70
fish_color_comment 6b6b70 --italics
fish_color_error ff2d40 --bold
fish_color_cancel ff2d40
fish_color_gray 6b6b70
fish_color_autosuggestion 4a4a52
fish_color_selection 000000 --background=ff2d40
fish_color_search_match 000000 --background=c8c8ce
fish_color_match 000000 --background=ff2d40
fish_color_history_current --bold
fish_color_valid_path --underline
fish_color_cwd ffffff --bold
fish_color_cwd_root ff2d40 --bold
fish_color_user c8c8ce
fish_color_host ffffff --bold
fish_color_host_remote ff6b78
fish_color_status ff2d40
fish_pager_color_progress 6b6b70 --background=141418
fish_pager_color_background
fish_pager_color_prefix ffffff --bold
fish_pager_color_completion c8c8ce
fish_pager_color_description 6b6b70
fish_pager_color_secondary_background --background=0d0d10
fish_pager_color_secondary_completion c8c8ce
fish_pager_color_secondary_description 6b6b70
fish_pager_color_secondary_prefix ffffff --bold
fish_pager_color_selected_background --background=ff2d40
fish_pager_color_selected_completion 000000
fish_pager_color_selected_description 000000
fish_pager_color_selected_prefix 000000 --bold
+90
View File
@@ -0,0 +1,90 @@
# fluxo-rs configuration
# Location: ~/.config/fluxo/config.toml
#
# The hardware modules below emit raw pipe-delimited values rather than display
# strings. The Quickshell config (~/.config/quickshell/services/Sys.qml) parses
# them and does its own formatting. The previous human-readable Waybar formats
# are preserved in config.toml.waybar-bak.
[general]
menu_command = "fuzzel --dmenu --prompt \"$FLUXO_PROMPT\""
[signals]
network = 1
cpu = 2
memory = 3
gpu = 4
sys = 5
disk = 6
game = 7
audio = 8
bt = 9
power = 10
mpris = 11
backlight = 12
dnd = 13
[network]
format = "{interface}|{ip}|{rx:.3}|{tx:.3}"
[cpu]
format = "{usage:.1}|{temp:.1}"
[memory]
format = "{used:.2}|{total:.2}"
[sys]
format = "{uptime}|{load1:.2}|{load5:.2}|{load15:.2}"
[disk]
format = "{mount}|{used:.1}|{total:.1}"
[power]
format = "{percentage}"
[audio]
format_sink_unmuted = "{name} {volume:>3}% <span size='large'>{icon}</span>"
format_sink_muted = "{name} <span size='large'> {icon}</span>"
format_source_unmuted = "{name} {volume:>3}% <span size='large'>{icon}</span>"
format_source_muted = "{name} <span size='large'>{icon}</span>"
[bt]
format_plugin = "{alias} [{left}|{right}] {anc} <span size='large'>󰂰</span>"
format_connected = "{alias} <span size='large'>󰂰</span>"
format_disconnected = "Disconnected <span size='large'>󰂯</span>"
format_disabled = "<span size='large'>󰂲</span>"
[game]
format_active = "<span size='large'>󰊖</span>"
format_inactive = "<span size='large'></span>"
[mpris]
format = "{artist} - {title}"
max_length = 20
scroll = true
scroll_speed = 500
scroll_separator = " /// "
[pool]
enabled = false
format = "{used:>4.0}G / {total:>4.0}G"
[gpu]
enabled = true
format_amd = "{usage:.0}|{vram_used:.2}|{vram_total:.2}|{temp:.1}"
format_intel = "{usage:.0}|0|0|0"
format_nvidia = "{usage:.0}|{vram_used:.2}|{vram_total:.2}|{temp:.1}"
[keyboard]
enabled = false
format = "{layout}"
[backlight]
enable = true
format = "{percentage}"
[dnd]
enabled = true
format_dnd = "<span size='large'>󰂛</span>"
format_normal = "<span size='large'>󰂚</span>"
+85
View File
@@ -0,0 +1,85 @@
# fluxo-rs configuration
# Location: ~/.config/fluxo/config.toml
[general]
menu_command = "fuzzel --dmenu --prompt \"$FLUXO_PROMPT\""
[signals]
network = 1
cpu = 2
memory = 3
gpu = 4
sys = 5
disk = 6
game = 7
audio = 8
bt = 9
power = 10
mpris = 11
backlight = 12
dnd = 13
[network]
format = "{interface} ({ip}):  {rx:^4.1} MB/s  {tx:^4.1} MB/s"
[cpu]
format = "CPU: {usage:^4.1}% {temp:^4.1}C"
[memory]
format = "MEM: {used:^4.1}/{total:^4.1}GB"
[sys]
format = "UP: {uptime} LOAD: {load1:^3.1} "
[disk]
format = "{mount} {used:^3.0}/{total:^3.0}G"
[power]
format = "{percentage:>3}% {icon}"
[audio]
format_sink_unmuted = "{name} {volume:>3}% <span size='large'>{icon}</span>"
format_sink_muted = "{name} <span size='large'> {icon}</span>"
format_source_unmuted = "{name} {volume:>3}% <span size='large'>{icon}</span>"
format_source_muted = "{name} <span size='large'>{icon}</span>"
[bt]
format_plugin = "{alias} [{left}|{right}] {anc} <span size='large'>󰂰</span>"
format_connected = "{alias} <span size='large'>󰂰</span>"
format_disconnected = "Disconnected <span size='large'>󰂯</span>"
format_disabled = "<span size='large'>󰂲</span>"
[game]
format_active = "<span size='large'>󰊖</span>"
format_inactive = "<span size='large'></span>"
[mpris]
format = "{artist} - {title}"
max_length = 20
scroll = true
scroll_speed = 500
scroll_separator = " /// "
[pool]
enabled = false
format = "{used:>4.0}G / {total:>4.0}G"
[gpu]
enabled = false
format_amd = "AMD: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C"
format_intel = "iGPU: {usage:>3.0}%"
format_nvidia = "NV: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C"
[keyboard]
enabled = false
format = "{layout}"
[backlight]
enable = true
format = " {icon} {percentage}"
[dnd]
enabled = true
format_dnd = "<span size='large'>󰂛</span>"
format_normal = "<span size='large'>󰂚</span>"
+14 -8
View File
@@ -1,13 +1,19 @@
include=~/.config/fuzzel/catppuccin-fuzzel/themes/catppuccin-mocha/mauve.ini
include=~/.config/fuzzel/takemi.ini
[main]
font=FiraCode Nerd Font:size=14
# JetBrainsMono to match Theme.fontMono, so the launcher and the bar are
# visibly the same typeface.
font=JetBrainsMono Nerd Font:size=14
terminal=alacritty
dpi-aware=no
prompt="> "
icon-theme=Papirus-Dark
show-actions=yes
[border]
radius=4
width=2
width=45
lines=8
horizontal-pad=20
vertical-pad=20
inner-pad=10
prompt=" "
show-actions=no
layer=overlay
+37
View File
@@ -0,0 +1,37 @@
# Takemi — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/takemi.toml and ~/.config/nvim/lua/takemi.lua.
#
# Near-black ground, white text, crimson as the only saturated colour. The
# selection is a solid crimson block with the text punched out of it in black,
# which is the same "active" signal the bar and the terminal use.
[colors]
# Flat and fully opaque. The Persona surfaces are ink on ground — translucency
# and blur read as a different design language, so the alpha stays at ff.
background=0a0a0cff
text=c8c8ceff
input=ffffffff
placeholder=6b6b70ff
prompt=ff2d40ff
# Fuzzy-match characters in an unselected row: crimson, since they are the
# reason the row is on screen at all.
match=ff2d40ff
selection=ff2d40ff
selection-text=000000ff
# White rather than black, so the matched characters still read as marked once
# the row is already crimson.
selection-match=ffffffff
# Deliberately the same as the background — hides the match counter.
counter=0a0a0cff
border=ff2d40ff
[border]
# Square. Every corner in this style is either a right angle or a hard chamfer;
# nothing in the shell is rounded.
radius=0
# Theme.stroke.
width=2
+83
View File
@@ -0,0 +1,83 @@
<div align="center">
<h1>✦ Google-ish Hyprlock Theme ✦</h1>
<h3></h3>
</div>
## ▶️ Preview
<details>
<summary><b>Without Profile & User Info</b></summary>
<img src="https://github.com/user-attachments/assets/7bbacd47-b0d7-4132-9951-53dc0ead604c" alt="Hyprlock-Without-Profile" width="1280">
</details>
<details>
<summary><b>With Profile & User Info</b></summary>
<img src="https://github.com/user-attachments/assets/9a58a6e2-a71f-4b28-998a-8a45c8950aaf" alt="Hyprlock-With-Profile" width="1280">
</details>
<details>
<summary><b>With Profile & User Info [12H Format]</b></summary>
<img src="https://github.com/user-attachments/assets/e0049860-7511-432e-8814-8d17d448182b" alt="Hyprlock-With-Profile" width="1280">
</details>
## 📦 Installation
><i><b>❗ This configuration is based on a 1080p display, if you are using a higher screen resolution,</br>you may need to reconfigure the sizes and recoordinate all the components.</b></i>
<b>Auto-Installation :</b>
```bash
sh -c "$(curl -fsSL https://raw.githubusercontent.com/Tamarindtype/googlish-hyprlock-theme/main/install.sh)"
```
</br><b>Manual Installation :</b>
```bash
# Backup your current Hyprlock config first!
# Clone the repository
git clone https://github.com/Tamarindtype/googlish-hyprlock-theme.git
# Move all the files to the hyprland config folder
mv ./googlish-hyprlock-theme/* ~/.config/hypr/
# Go to script folder
cd $HOME/.config/hypr/hyprlock/
# Change all the scripts permission to make them executable
chmod +x *.sh
# Run the Hyprlock
hyprlock
```
## 🗄️ Directory Structure
```md
$HOME
└── .config
└── hypr
├── hyprlock
│ ├── assets
│ ├── battery.sh
│ ├── bluetooth.sh
│ ├── change_wallpaper.sh
│ ├── greeting.sh
│ ├── network.sh
│ ├── medianotif.sh
│ └── weatherinfo.sh
└── hyprlock.conf
```
## 🗨️ FAQ
| Question | Answer |
| --- | --- |
| Profile & User info does not appear? | By default, it set without profile and user info. You can enable it by uncomment the `image` @PROFILE PICTURE and `label` @USER INFO in `hyprlock.conf` |
| Battery percentage number does not appear? | Change your battery module in `battery.sh`. The default is `BAT0`, you can check it by running this command `ls /sys/class/power_supply/` |
| How to change the 24H Format to 12H Format? | Comment the 24H format and uncomment the 12H format. also uncomment the AM/PM `label` & `shape` in `hyprlock.conf` |
| null location or Unable to determine your location? | Change the IP Geolocation provider in `weatherinfo.sh` |
## 🏅 Recommendations
| Type | Name | Links |
| --- | --- | --- |
| Regular | PP Neue Machina | [Pangram Pangram](https://pangrampangram.com/products/neue-machina) |
| Nerd Font | Geist & Space Mono | [Nerd Fonts](https://github.com/ryanoasis/nerd-fonts/releases) |
| Emoji | Apple Emoji | [apple-emoji-linux](https://github.com/samuelngs/apple-emoji-linux) |
## ✨ Special Thanks & Credits
| Details | Credit |
| --- | --- |
| Battery & Playerctl Widget Scripts | @ashish-kus [minimal Hyprlock](https://gist.github.com/ashish-kus/dd562b0bf5e8488a09e0b9c289f4574c) |
| Helped Me Create Dynamic WiFi, Bluetooth, Weathercast and Greeting Widget | @OPENAI [ChatGPT](https://chatgpt.com/)|
+507 -62
View File
@@ -1,100 +1,545 @@
source = $HOME/.config/hypr/catppuccin-hyprland/themes/mocha.conf
# For more information visit https://wiki.hyprland.org/Hypr-Ecosystem/hyprlock/
source = ~/.config/hypr/catppuccin-hyprland/themes/mocha.conf
$scrPath = ~/.config/hypr/hyprlock/
$imgPath = ~/.config/hypr/hyprlock/assets/
$USER = narl # Your Alias
$host = uname -n #Your Alias
$wifi-mode = false # Change the WiFi output. true = show SSID, false = Connected.
$bt-mode = false # Change the Bluetooth output. true = show Device name, false = Connected.
# Default geolocation provider is IP-API.COM, hardcode it if the location does not match the current location.
# Hardcode your location (If hardcode does not recognize your City, change it to IP Geolocation provider in weatherinfo.sh)
$CITY =
$COUNTRY =
# WALLPAPER
# Just comment and uncomment to select
$wallpaper = screenshot
#$wallpaper = ~/your/wallpaper-path.png
$blur = 2
# ADJUST HERE
$rounding = 12
$shape-rd = -1
$font-text = GeistMono Nerd Font Bold
$font-text0 = GeistMono Nerd Font
$font-display = PP Neue Machina Ultra-Bold
$font-symbol = JetBrainsMono Nerd Font Mono
# Input Var
$weight = 2
$inner-color = rgba($baseAlpha80)
$border-color = rgba($mauveAlpha80)
# Colors
$clock-color = $text
$fg0 = $text
$fg1 = $base
$shape-col0 = $mauve
$shape-col1 = rgba($surface0Alpha80)
$shape-col2 = rgba($surface1Alpha80)
$shadow-pass = 2
$shadow-size = 3
$shadow-color = $crust
$shadow-boost = 1.2
$text-shadow-pass = 1
$text-shadow-boost = 0.5
$accent = $mauve
$accentAlpha = $mauveAlpha
$font = JetBrainsMono Nerd Font
# GENERAL
general {
no_fade_in = true
grace = 1
disable_loading_bar = false
hide_cursor = true
ignore_empty_input = true
text_trim = true
}
#BACKGROUND
background {
monitor =
path = $HOME/Pictures/mandelbrot.png
blur_passes = 0
color = $base
path = $wallpaper
blur_passes = $blur
contrast = 0.8916
brightness = 0.7172
vibrancy = 0.1696
vibrancy_darkness = 0
}
# LAYOUT
# PROFILE PICTURE
image {
monitor =
path = $imgPath/profile.jpg
size = 120 # lesser side if not 1:1 ratio
opacity = 0.25
shadow_pass = $shadow-pass
shadow_size = $shadow-size
shadow_color = $shadow-color
shadow_boost = $shadow-boost
rounding = $rounding # negative values mean circle
border_size = 4
border_color = $mauve
rotate = 0 # degrees, counter-clockwise
position = 20, -20
halign = left
valign = top
zindex = 1
}
# USER INFO
label {
monitor =
text = Layout: $LAYOUT
color = $text
font_size = 25
font_family = $font
position = 30, -30
text = cmd[update:1000] echo -e "$USER\n󰁥 $($host)\n\n$(uname -r)\nPackages: $(pacman -Q | wc -l) pacman"
shadow_passes = 1
shadow_boost = 0.5
color = $fg0
font_size = 11
font_family = $font-text
position = 170, -35
halign = left
valign = top
}
# TIME
# WEATHERCAST & LOCATION
label {
monitor =
text = $TIME
color = $text
font_size = 90
font_family = $font
position = -30, 0
halign = right
valign = top
}
text = cmd[update:1000] echo "$(bash $scrPath/weatherinfo.sh)"
# DATE
label {
monitor =
text = cmd[update:43200000] date +"%A, %d %B %Y"
color = $text
font_size = 25
font_family = $font
position = -30, -150
halign = right
valign = top
}
shadow_passes = 1
shadow_boost = 0.5
# FINGERPRINT
{
monitor = "";
text = "$FPRINTPROMPT";
color = "$text";
font_size = 14;
font_family = $font;
position = "0, -107";
halign = "center";
valign = "center";
}
color = $fg0
font_size = 11
font_family = $font-text
# USER AVATAR
image {
monitor =
path = $HOME/.face
size = 100
border_color = $accent
position = 0, 75
position = 0, -20
halign = center
valign = center
valign = top
}
# INPUT FIELD
input-field {
monitor =
size = 300, 60
outline_thickness = 4
dots_size = 0.2
dots_spacing = 0.2
size = 275, 55
rounding = $rounding
outline_thickness = $weight
outer_color = $border-color
dots_size = 0.1 # Scale of input-field height, 0.2 - 0.8
dots_spacing = 1 # Scale of dots' absolute size, 0.0 - 1.0
dots_center = true
outer_color = $accent
inner_color = $surface0
font_color = $text
inner_color = $inner-color
font_color = $fg0
fade_on_empty = false
placeholder_text = <span foreground="##$textAlpha"><i>󰌾 Logged in as </i><span foreground="##$accentAlpha">$USER</span></span>
font_family = $font-text
placeholder_text = <span foreground="##$textAlpha">󰢏 $USER</span>
hide_input = false
check_color = $accent
check_color = $mauve
fail_color = $red
fail_text = <i>$FAIL <b>($ATTEMPTS)</b></i>
capslock_color = $yellow
position = 0, -47
position = 0, -240
halign = center
valign = center
zindex = 10
}
# TIME HR
label {
monitor =
text = cmd[update:1000] echo -e "$(date +"%H")" # 24-Hour Format
#text = cmd[update:1000] echo -e "$(date +"%I")" # 12-Hour Format
color = $clock-color
shadow_pass = $shadow-pass
shadow_size = $shadow-size
shadow_color = $shadow-color
shadow_boost = $shadow-boost
font_size = 150
font_family = $font-display
position = 0, -155
halign = center
valign = top
}
# TIME MM
label {
monitor =
text = cmd[update:1000] echo -e "$(date +"%M")"
color = $clock-color
shadow_pass = $shadow-pass
shadow_size = $shadow-size
shadow_color = $shadow-color
shadow_boost = $shadow-boost
font_size = 150
font_family = $font-display
position = 0, -325
halign = center
valign = top
}
# AM/PM for 12-Hour Format
#label {
monitor =
text = cmd[update:1000] echo -e "$(date +"%p")"
color = $clock-color
shadow_pass = $shadow-pass
shadow_size = $shadow-size
shadow_color = $shadow-color
shadow_boost = $shadow-boost
font_size = 16
font_family = $font-display
position = 0, 17
halign = center
valign = center
zindex = 5
}
# AM/PM BG
#shape {
monitor =
size = 70, 40
shadow_passes = $text-shadow-pass
shadow_boost = $text-shadow-boost
color = $shape-col2
rounding = $rounding
border_size =
border_color =
position = 0, 20
halign = center
valign = center
zindex = 1
}
# GREETING
label {
monitor =
text = cmd[update:1000] echo "$(bash $scrPath/greeting.sh)"
shadow_passes = $text-shadow-pass
shadow_boost = $text-shadow-boost
color = $fg0
font_size = 11
font_family = $font-text
position = 0, -55
halign = center
valign = center
}
# TODAY IS
label {
monitor =
text = cmd[update:1000] bash -c 'day=$(date +%A); echo "Today is $day"'
shadow_passes = $text-shadow-pass
shadow_boost = $text-shadow-boost
color = $fg0
font_size = 11
font_family = $font-text
position = 0, -75
halign = center
valign = center
}
# DATE
label {
monitor =
text = cmd[update:1000] bash -c 'day=$(date +%d); case "$day" in 1) suffix="st";; 2) suffix="nd";; 3) suffix="rd";; *) suffix="th";; esac; echo -e "$(date +"%B %e")'$day'$suffix, $(date +%Y)"'
shadow_passes = $text-shadow-pass
shadow_boost = $text-shadow-boost
color = $fg0
font_size = 14
font_family = $font-text
position = 0, -115
halign = center
valign = center
}
# BATTERY
label {
monitor =
text = cmd[update:1000] echo -e "$($scrPath/battery.sh)"
color = $fg1
font_size = 12
font_family = $font-text
position = -37, 29
halign = right
valign = bottom
zindex = 5
}
# NETWORK
label {
monitor =
text = cmd[update:1000] echo -e "$($scrPath/network.sh)"
color = $fg1
font_size = 12
font_family = $font-text
position = 37, 29
halign = left
valign = bottom
zindex = 5
}
# BLUETOOTH
label {
monitor =
text = cmd[update:1000] echo -e "$(~/.config/hypr/hyprlock/bluetooth.sh)"
color = $fg1
font_size = 12
font_family = $font-text
position = 37, 85
halign = left
valign = bottom
zindex = 5
}
# BATTERY BG
shape {
monitor =
size = 90, 40
shadow_passes = $text-shadow-pass
shadow_boost = $text-shadow-boost
color = $shape-col0
rounding = $shape-rd
border_size =
border_color =
position = -20, 20
halign = right
valign = bottom
zindex = 1
}
# NETWORK BG
shape {
monitor =
size = 150, 40
shadow_passes = $text-shadow-pass
shadow_boost = $text-shadow-boost
color = $shape-col0
rounding = $shape-rd
border_size =
border_color =
position = 20, 20
halign = left
valign = bottom
zindex = 1
}
# BLUETOOTH BG
shape {
monitor =
size = 150, 40
shadow_passes = $text-shadow-pass
shadow_boost = $text-shadow-boost
color = $shape-col0
rounding = $shape-rd
border_size =
border_color =
position = 20, 75
halign = left
valign = bottom
zindex = 1
}
# MEDIA BG
image {
monitor =
path = $imgPath/media-bg-dark-25.png
size = 8%
opacity = 0.25
rounding = 5
border_size = 0
rotate = 0
position = 0, -10%
halign = center
valign = center
zindex = 1
}
shape {
monitor =
size = 35%, 8%
shadow_passes = $text-shadow-pass
shadow_boost = $text-shadow-boost
color = $shape-col1
rounding = $rounding
border_size =
border_color =
position = 0, 5%
halign = center
valign = bottom
zindex = 1
}
# PLAYER TITLE
label {
monitor =
text = cmd[update:1000] echo "$($scrPath/playerctl.sh --title)"
color = $fg0
font_size = 14
font_family = $font-text
position = 0, -40%
halign = center
valign = center
zindex = 5
}
# PLAYER ARTIST
label {
monitor =
text = cmd[update:1000] echo "$($scrPath/playerctl.sh --artist)"
color = $fg0
font_size = 11
font_family = $font-text
position = 0, -42%
halign = center
valign = center
zindex = 5
}
# PLAYER ALBUM
label {
monitor =
text = cmd[update:1000] echo "$($scrPath/playerctl.sh --album)"
color = $fg0
font_size = 11
font_family = $font-text0
position = 0, -44%
halign = center
valign = center
zindex = 5
}
# PLAYER STATUS SYMBOL
label {
monitor =
text = cmd[update:1000] echo "$($scrPath/playerctl.sh --status-symbol)"
color = $fg0
font_size = 16
font_family = $font-symbol
position = 33.5%, -38%
halign = left
valign = center
zindex = 5
}
# PLAYER STATUS
label {
monitor =
text = cmd[update:1000] echo "$($scrPath/playerctl.sh --status)"
color = $fg0
font_size = 10
font_family = $font-text
position = 35%, -38%
halign = left
valign = center
zindex = 5
}
# PLAYER SOURCE SYMBOL
label {
monitor =
text = cmd[update:1000] echo "$($scrPath/playerctl.sh --source-symbol)"
color = rgba($subtext0Alpha99)
font_size = 16
font_family = $font-symbol
position = -33.5%, -38%
halign = right
valign = center
zindex = 5
}
# PLAYER SOURCE
label {
monitor =
text = cmd[update:1000] echo "$($scrPath/playerctl.sh --source)"
color = rgba($subtext0Alpha99)
font_size = 10
font_family = $font-text
position = -35%, -38%
halign = right
valign = center
zindex = 5
}
label {
monitor =
text =
color = $fg0
font_size = 24
font_family = $font-symbol
position = 0, 1%
halign = center
valign = bottom
}
+155
View File
@@ -0,0 +1,155 @@
source = $HOME/.config/hypr/catppuccin-hyprland/themes/mocha.conf
$accent = $mauve
$accentAlpha = $mauveAlpha
$font = JetBrainsMono Nerd Font
# GENERAL
general {
hide_cursor = true
no_fade_in = false
grace = 0
disable_loading_bar = true
}
# BACKGROUND
background {
monitor =
path = $HOME/Pictures/reze/reze.png
blur_passes = 3
blur_size = 7
noise = 0.0117
contrast = 0.8916
brightness = 0.8172
vibrancy = 0.1696
vibrancy_darkness = 0.0
color = $base
}
# TIME
label {
monitor =
text = $TIME
color = $text
font_size = 120
font_family = $font
position = 0, 300
halign = center
valign = center
}
# DATE
label {
monitor =
text = cmd[update:43200000] date +"%A, %d %B %Y"
color = $text
font_size = 30
font_family = $font
position = 0, 200
halign = center
valign = center
}
# USER AVATAR (Center)
image {
monitor =
path = $HOME/.face
size = 120
border_color = rgba(255, 255, 255, 0.1)
position = 0, 50
halign = center
valign = center
}
# INPUT FIELD (Center)
input-field {
monitor =
size = 300, 60
outline_thickness = 2
dots_size = 0.2
dots_spacing = 0.2
dots_center = true
outer_color = rgba(255, 255, 255, 0.1)
inner_color = rgba(0, 0, 0, 0.2)
font_color = $text
fade_on_empty = false
placeholder_text = <span foreground="##$textAlpha"><i>󰌾 Logged in as </i><span foreground="##$accentAlpha">$USER</span></span>
hide_input = false
check_color = $accent
fail_color = $red
fail_text = <i>$FAIL <b>($ATTEMPTS)</b></i>
capslock_color = $yellow
position = 0, -60
halign = center
valign = center
}
# --- MUSIC DASHBOARD (TOP RIGHT) ---
# ALBUM ART (At the very top right, 80px high, adjusted -3px for perfect alignment)
image {
monitor =
path = /tmp/hyprlock_art.png
size = 80
rounding = 15
border_color = rgba(255, 255, 255, 0.1)
reload_time = 2
reload_cmd = ~/.config/hypr/scripts/album_art.sh
position = -20, -17
halign = right
valign = top
}
# MUSIC TEXT BOX (To the left of the image, same height)
shape {
monitor =
size = 300, 80
color = rgba(0, 0, 0, 0.3)
rounding = 15
border_size = 1
border_color = rgba(255, 255, 255, 0.1)
position = -110, -20
halign = right
valign = top
}
# MUSIC TEXT (Right aligned and vertically centered inside the 80px box)
label {
monitor =
text = cmd[update:1000] ~/.config/hypr/scripts/songdetail.sh
color = $text
font_size = 14
font_family = $font
position = -125, -37
halign = right
valign = top
text_align = right
}
# --- STATUS DASHBOARD (BOTTOM RIGHT) ---
# SYSTEM INFO BOX (Vertical stack container)
shape {
monitor =
size = 300, 100
color = rgba(0, 0, 0, 0.3)
rounding = 15
border_size = 1
border_color = rgba(255, 255, 255, 0.1)
position = -20, 20
halign = right
valign = bottom
}
# SYSTEM INFO TEXT (3 lines, top of each other, right aligned)
label {
monitor =
text = cmd[update:5000] ~/.config/hypr/scripts/status_info.sh
color = $text
font_size = 14
font_family = $font
position = -35, 35
halign = right
valign = bottom
text_align = right
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
# Get the current battery percentage
battery_percentage=$(cat /sys/class/power_supply/BAT0/capacity)
# Get the battery status (Charging or Discharging)
battery_status=$(cat /sys/class/power_supply/BAT0/status)
# Define the battery icons for each 10% segment
battery_icons=("󰂃" "󰁺" "󰁻" "󰁼" "󰁽" "󰁾" "󰁿" "󰂀" "󰂁" "󰁹")
# Define the charging icon
charging_icon="󰂄"
# Calculate the index for the icon array
# Ensure the index is within bounds (0 to 9) for battery percentages 0 to 100
icon_index=$((battery_percentage / 10))
# If the battery is 100%, use the last icon (index 9)
if [ "$battery_percentage" -eq 100 ]; then
icon_index=9
fi
# Get the corresponding icon
battery_icon=${battery_icons[$icon_index]}
# Check if the battery is charging
if [ "$battery_status" = "Charging" ]; then
battery_icon="$charging_icon"
fi
# Output the battery percentage and icon
echo "$battery_percentage% $battery_icon"
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# Path to the Hyprland configuration file
config_file="$HOME/.config/hypr/hyprlock.conf"
# Read the bt-mode value from the configuration file
bt_mode=$(grep -oP '^\$bt-mode\s*=\s*\K\S+' ~/.config/hypr/hyprlock.conf)
# Get Bluetooth power status
bluetooth_status=$(bluetoothctl show | grep "Powered:" | awk '{print $2}')
# Check if Bluetooth is powered on
if [ "$bluetooth_status" != "yes" ]; then
echo "󰂯 Bluetooth Off"
exit 0
fi
# Initialize connected devices
connected_devices=$(echo "$connected_devices" | sed 's/[[:space:]]*$//')
# Process Bluetooth devices
while read -r line; do
echo "Processing line: $line" >&2
device_mac=$(echo "$line" | awk '{print $2}')
device_name=$(echo "$line" | cut -d' ' -f3-)
echo "Device MAC: $device_mac" >&2
echo "Device Name: $device_name" >&2
if bluetoothctl info "$device_mac" | grep -q "Connected: yes"; then
connected_devices+="$device_name "
fi
echo "Finished processing $device_mac" >&2
echo "---" >&2
done < <(bluetoothctl devices)
# If no connected devices, show "No Devices"
if [ -z "$connected_devices" ]; then
echo "󰂲 No Devices"
exit 0
fi
# Display output based on bt-mode
if [ "$bt_mode" = "true" ]; then
echo "󰂯 $connected_devices"
else
echo "󰂯 Connected"
fi
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# Only works if you use SWWW for your wallpapers
SWWW_DIR="$HOME/.cache/swww/"
HYPRLOCK="$HOME/.config/hypr/hyprlock.conf"
# Get a Monitor Cache File
FIRST_FILE=$(find "$SWWW_DIR" -type f | head -n 1)
# Check if SWWW file exists
if [ -n "$FIRST_FILE" ] && [ -f "$FIRST_FILE" ]; then
WALLPAPER=$(sed -n '2p' "$FIRST_FILE")
sed -i "s|^\(\$wallpaper[[:space:]]*=[[:space:]]*\).*|\1$WALLPAPER # (screenshot or /path/to/your/wallpaper.jpg)|" "$HYPRLOCK"
echo "Wallpaper path updated to $WALLPAPER"
exit 0
fi
# Error
echo "Fehler: kein Wallpaper gefunden."
exit 1
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Set your Username
# username="TamarindX"
# Read the username alias from hyprlock.conf
username=$(grep -oP '^\$USER\s*=\s*\K\S+' ~/.config/hypr/hyprlock.conf)
# Check if the username was successfully extracted
if [ -z "$username" ]; then
echo "Username not found in hyprlock.conf."
exit 1
fi
# Get the current hour
hour=$(date +%H)
# Determine the greeting based on the time
if [ "$hour" -ge 5 ] && [ "$hour" -lt 12 ]; then
greeting="Good Morning"
elif [ "$hour" -ge 12 ] && [ "$hour" -lt 17 ]; then
greeting="Good Afternoon"
elif [ "$hour" -ge 17 ] && [ "$hour" -lt 21 ]; then
greeting="Good Evening"
elif [ "$hour" -ge 21 ] && [ "$hour" -lt 24 ]; then
greeting="Good Night"
else
greeting="GO TO SLEEP!"
fi
# Output the combined text
echo -e "Hello, $username! $greeting"
+65
View File
@@ -0,0 +1,65 @@
#!/bin/bash
# Set this variable to control the output
# Set to "true" to show SSID, "false" to show "Connected"
# show_ssid=false
# Read the wifi-mode alias from hyprlock.conf
show_ssid=$(grep -oP '^\$wifi-mode\s*=\s*\K\S+' ~/.config/hypr/hyprlock.conf)
# Check if the SSID was successfully extracted else fallback?!
if [ -z "$show_ssid" ]; then
show_ssid=false
fi
# Check if any Ethernet connection is active
ethernet_connected=$(nmcli -t -f DEVICE,TYPE,STATE dev | grep -E 'ethernet:connected')
# If Ethernet
if [ -n "$ethernet_connected" ]; then
echo "󰈀 Ethernet"
exit 0
fi
# Get Wi-Fi connection status
wifi_status=$(nmcli -t -f WIFI g)
# Check if Wi-Fi is enabled
if [ "$wifi_status" != "enabled" ]; then
echo "󰤮 Wi-Fi Off"
exit 0
fi
# Get active Wi-Fi connection details
wifi_info=$(nmcli -t -f ACTIVE,SSID,SIGNAL dev wifi | grep '^yes')
# If no active connection, show "Disconnected"
if [ -z "$wifi_info" ]; then
echo "󰤮 No Wi-Fi"
exit 0
fi
# Extract SSID
ssid=$(echo "$wifi_info" | cut -d':' -f2)
# Extract signal strength
signal_strength=$(echo "$wifi_info" | cut -d':' -f3)
# Define Wi-Fi icons based on signal strength
wifi_icons=("󰤯" "󰤟" "󰤢" "󰤥" "󰤨") # From low to high signal
# Clamp signal strength between 0 and 100
signal_strength=$((signal_strength < 0 ? 0 : (signal_strength > 100 ? 100 : signal_strength)))
# Calculate the icon index based on signal strength
icon_index=$((signal_strength / 25))
# Get the corresponding icon
wifi_icon=${wifi_icons[$icon_index]}
# Output based on show_ssid variable
if [ "$show_ssid" = true ]; then
echo "$wifi_icon $ssid"
else
echo "$wifi_icon Connected"
fi
+116
View File
@@ -0,0 +1,116 @@
#!/bin/bash
if [ $# -eq 0 ]; then
echo "Usage: $0 --title | --artist | --album | --source | --source-symbol"
exit 1
fi
# Function to get metadata using playerctl
get_metadata() {
key=$1
playerctl metadata --player=%any,chromium,firefox --format "{{ $key }}" 2>/dev/null
}
# Check for arguments
# Function to determine the source and return an icon and text
get_source_info_symbol() {
trackid=$(get_metadata "mpris:trackid")
if [[ "$trackid" == *"firefox"* ]]; then
echo -e "󰈹"
elif [[ "$trackid" == *"spotify"* ]]; then
echo -e ""
elif [[ "$trackid" == *"chromium"* ]]; then
echo -e ""
else
echo ""
fi
}
get_source_info() {
trackid=$(get_metadata "mpris:trackid")
if [[ "$trackid" == *"firefox"* ]]; then
echo -e "Firefox"
elif [[ "$trackid" == *"spotify"* ]]; then
echo -e "Spotify"
elif [[ "$trackid" == *"chromium"* ]]; then
echo -e "Chrome"
else
echo ""
fi
}
# Function to truncate text with ellipsis if necessary
truncate_with_ellipsis() {
text=$1
max_length=$2
if [ ${#text} -gt $max_length ]; then
echo "${text:0:$((max_length - 3))}..."
else
echo "$text"
fi
}
# Parse the argument
case "$1" in
--title)
title=$(get_metadata "xesam:title")
if [ -z "$title" ]; then
echo ""
else
truncate_with_ellipsis "$title" 28 # Limit the output to 50 characters
fi
;;
--artist)
artist=$(get_metadata "xesam:artist")
if [ -z "$artist" ]; then
echo ""
else
truncate_with_ellipsis "$artist" 28 # Limit the output to 50 characters
fi
;;
--status-symbol)
status=$(playerctl status 2>/dev/null)
if [[ $status == "Playing" ]]; then
echo "󰎆"
elif [[ $status == "Paused" ]]; then
echo "󰏥"
else
echo ""
fi
;;
--status)
status=$(playerctl status 2>/dev/null)
if [[ $status == "Playing" ]]; then
echo "Playing Now"
elif [[ $status == "Paused" ]]; then
echo "Paused"
else
echo ""
fi
;;
--album)
album=$(playerctl metadata --player=%any,chromium,firefox --format "{{ xesam:album }}" 2>/dev/null)
if [[ -n $album ]]; then
echo "$album"
else
status=$(playerctl status 2>/dev/null)
if [[ -n $status ]]; then
echo "Not album"
else
truncate_with_ellipsis "$album" 28 # Limit the output to 50 characters
fi
fi
;;
--source-symbol)
get_source_info_symbol
;;
--source)
get_source_info
;;
*)
echo "Invalid option: $1"
echo "Usage: $0 --title | --artist | --album | --source | --source-symbol"
exit 1
;;
esac
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# Get location data using IP Geolocation
# Recommended with API, but you can try w/o API [IPINFO.IO]
# API_TOKEN="YOUR_API_TOKEN"
# location_data=$(curl -s "https://ipinfo.io?token=$API_TOKEN" 2>/dev/null)
# location_data=$(curl -s https://ipinfo.io 2>/dev/null) #[Limited requests. Require API to send 50k request/month]
# Extract city and country code (ISO 3166-1 alpha-2 code)
# CITY=$(echo "$location_data" | jq -r '.city // empty')
# COUNTRY=$(echo "$location_data" | jq -r '.country // empty')
# IPINFO Alternative [IP-API.COM]
location_data=$(curl -s "http://ip-api.com/json/" 2>/dev/null)
# Extract city and country code
CITY=$(echo "$location_data" | jq -r '.city // empty')
COUNTRY=$(echo "$location_data" | jq -r '.countryCode // empty')
# Visit http://ip-api.com/json/ to find other variables
# HARDCODE Location
# CITY=$(grep -oP '^\$CITY\s*=\s*\K.+' ~/.config/hypr/hyprlock.conf)
# COUNTRY=$(grep -oP '^\$COUNTRY\s*=\s*\K.+' ~/.config/hypr/hyprlock.conf)
# Check if CITY and COUNTRY are valid
if [[ -n "$CITY" && -n "$COUNTRY" ]]; then
# Fetch weather info for the detected city from wttr.in
weather_info=$(curl -s "wttr.in/$CITY?format=%c+%C+%t" 2>/dev/null)
# Check if the weather info is valid
if [[ -n "$weather_info" ]]; then
echo "$COUNTRY, $CITY: $weather_info"
else
echo "Weather info unavailable for $COUNTRY, $CITY"
fi
else
echo "Unable to determine your location"
fi
+4 -2
View File
@@ -1,5 +1,7 @@
wallpaper {
monitor =
path = ~/Pictures/mandelbrot.png
fit_mode = fill
path = ~/.wallpapers/takemi_1.jpg
fit_mode = cover
}
splash = false
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# Set paths
CONFIG_DIR="$HOME/.config/hypr"
BACKUP_DIR="$CONFIG_DIR/hyprlock-backup-$(date +%Y%m%d%H%M%S)"
REPO_URL="https://github.com/Tamarindtype/googlish-hyprlock-theme.git"
TEMP_DIR="$(mktemp -d)"
read -p "Do you want to see every command executed? [y/N]: " choice
choice=${choice,,}
if [[ "$choice" == "y" ]]; then
SHOW_COMMANDS=true
else
SHOW_COMMANDS=false
fi
run_cmd() {
local desc="$1"
local cmd="$2"
if [[ "$SHOW_COMMANDS" == true ]]; then
echo -e "\n$desc"
echo "-> $cmd"
read -p "Run command? [Y/n]: " confirm
confirm=${confirm,,}
if [[ "$confirm" == "n" ]]; then
echo "⏭ Skipped."
else
eval "$cmd"
fi
else
eval "$cmd"
fi
}
# Backup existing config
if [ -d "$CONFIG_DIR/hyprlock" ] || [ -f "$CONFIG_DIR/hyprlock.conf" ]; then
read -p "Do you want to create a backup of your config? [Y/n]: " choice
choice=${choice,,}
if [[ "$choice" != "n" ]]; then
run_cmd "Creating backup folder" "mkdir -p \"$BACKUP_DIR\""
if [ -d "$CONFIG_DIR/hyprlock" ]; then
run_cmd "Backing up folder" "mv \"$CONFIG_DIR/hyprlock\" \"$BACKUP_DIR/\""
fi
if [ -f "$CONFIG_DIR/hyprlock.conf" ]; then
run_cmd "Backing up file" "mv \"$CONFIG_DIR/hyprlock.conf\" \"$BACKUP_DIR/\""
fi
fi
else
echo "No existing hyprlock config found. Skipping backup."
fi
# Clone the repo
run_cmd "Cloning repository..." "git clone \"$REPO_URL\" \"$TEMP_DIR\""
# Move files
run_cmd "Creating config directory" "mkdir -p \"$CONFIG_DIR\""
if [ -d "$TEMP_DIR/hyprlock" ]; then
run_cmd "Moving hyprlock folder" "mv \"$TEMP_DIR/hyprlock\" \"$CONFIG_DIR/\""
fi
if [ -f "$TEMP_DIR/hyprlock.conf" ]; then
run_cmd "Moving hyprlock.conf" "mv \"$TEMP_DIR/hyprlock.conf\" \"$CONFIG_DIR/\""
fi
# Make scripts executable
if [ -d "$CONFIG_DIR/hyprlock" ]; then
run_cmd "Making scripts executable..." "chmod +x \"$CONFIG_DIR/hyprlock\"/*.sh"
fi
# Optional: Run hyprlock
run_cmd "Running Hyprlock" "hyprlock"
read -p "Do you want to install the 'hyprlock-change-wallpaper' command? [Y/n]: " choice
choice=${choice,,}
if [[ "$choice" != "n" ]]; then
run_cmd "Installing hyprlock-change-wallpaper shortcut" "sudo ln -sf $CONFIG_DIR/hyprlock/change_wallpaper.sh /usr/local/bin/hyprlock-change-wallpaper"
run_cmd "Make command runable" "chmod +x $CONFIG_DIR/hyprlock/change_wallpaper.sh"
if command -v hyprlock-change-wallpaper &>/dev/null; then
echo "hyprlock-change-wallpaper was succesfully installed"
else
echo "Error while installing hyprlock-change-wallpaper."
fi
else
echo "⏭ installation of hyprlock-change-wallpaper skipped"
fi
# Cleanup
run_cmd "Cleaning up temp files..." "rm -rf \"$TEMP_DIR\""
+5 -1
View File
@@ -2,6 +2,11 @@
# Autostart necessary processes (like notifications daemons, status bars, etc.)
# Or execute your favorite apps at launch like this:
# Takemi shell — bar, popouts, notifications, OSD, power menu and lock screen.
# Config lives in ~/.config/quickshell. It owns org.freedesktop.Notifications,
# so dunst must stay masked (systemctl --user mask dunst.service).
exec-once = uwsm app -- qs
# exec-once = uwsm app -- waybar
# exec-once = uwsm app -- hyprpanel
# exec-once = uwsm app -- nextcloud --background
@@ -14,4 +19,3 @@ exec-once = uwsm app -- hyprpaper
# exec-once = uwsm app -- /usr/lib/xdg-desktop-portal-hyprland
exec-once = wl-paste --type text --watch cliphist store # Stores only text data
exec-once = wl-paste --type image --watch cliphist store # Stores only image data
exec-once = uwsm app -- ~/.cargo/bin/fluxo-rs daemon
+3 -2
View File
@@ -10,8 +10,9 @@ env = XCURSOR_THEME,Bibata-Modern-Classic
env = XDG_CURRENT_DESKTOP,Hyprland
# env = debug:full_cm_proto,true
env = QT_QPA_PLATFORMTHEME,qt6ct
# env = LIBVA_DRIVER_NAME,radeonsi
# env = VDPAU_DRIVER,radeonsi
env = LIBVA_DRIVER_NAME,radeonsi
env = VDPAU_DRIVER,radeonsi
env = AMD_VULKAN_ICD,RADV
env = MOZ_ENABLE_WAYLAND,1
env = ELECTRON_OZONE_PLATFORM_HINT,wayland
env = EDITOR,nvim
+5 -7
View File
@@ -1,13 +1,13 @@
# https://wiki.hyprland.org/Configuring/Variables/#input
input {
# index 0 = dvorak-intl (default), index 1 = us-intl
# switch with $mainMod ALT, SPACE (see keybinds.conf)
kb_layout = us,us
kb_variant = dvorak-intl,intl
# kb_variant = intl
kb_model =
kb_options =
kb_rules =
kb_options = ctrl:nocaps
kb_options = caps:backspace
follow_mouse = 0
@@ -19,16 +19,14 @@ input {
}
}
# Layouts are inherited from the global input block so that layout
# switching applies to these too. Only bind resolution differs here.
device {
name = at-translated-set-2-keyboard
resolve_binds_by_sym = 1
kb_layout = us
kb_variant = dvorak-intl
}
device {
name = topre-corporation-hhkb-professional
resolve_binds_by_sym = 1
kb_layout = us
kb_variant = dvorak-intl
}
+21 -9
View File
@@ -6,20 +6,32 @@ bindl=,switch:off:Lid Switch,exec,~/.config/hypr/scripts/lid_handler.sh open
bindr = ALT, m, exec, cliphist list | fuzzel --dmenu | cliphist decode | wl-copy
bind = , PRINT, exec, grim -g "$(slurp)" - | wl-copy
bind = SHIFT, PRINT, exec, grim -g "$(slurp)" ~/Pictures/Screenshots/$(date +'%Y-%m-%d_%H-%M-%S').png
bind = CTRL, PRINT, exec, ~/.config/hypr/scripts/screenshot_window.sh copy
bind = CTRL SHIFT, PRINT, exec, ~/.config/hypr/scripts/screenshot_window.sh save
bindr = ALT SHIFT, z, exec, ~/.config/hypr/scripts/replay-ctrl.sh save
bindr = ALT, z, exec, ~/.config/hypr/scripts/replay-ctrl.sh toggle
bindr = ALT SHIFT, c, exec, ~/.config/hypr/scripts/droidcam-ctrl.sh toggle
# general binds
bind = $mainMod, M, exec, pdfs-prompt
bind = $mainMod, B, exec, fluxo bt menu
bind = $mainMod, RETURN, exec, $terminal
bind = $mainMod SHIFT, Q, killactive,
bind = $mainMod CTRL, L, exec, hyprlock
bind = $mainMod CTRL, L, exec, qs ipc call shell lock
bind = $mainMod, E, exec, $fileManager
bind = $mainMod, V, togglefloating,
bind = $mainMod, F, fullscreen,
bind = $mainMod, SPACE, exec, $menu
bind = $mainMod, P, exec, ~/.config/hypr/scripts/powermenu.sh
bind = $mainMod, t, togglesplit, # dwindle
bind = $mainMod, P, exec, qs ipc call shell power
# Takemi shell
bind = $mainMod SHIFT, N, exec, qs ipc call shell toggleDnd
bind = $mainMod CTRL SHIFT, N, exec, qs ipc call shell clearNotifications
bind = $mainMod SHIFT, R, exec, qs ipc call shell reloadConfig
# cycle keyboard layout (dvorak-intl <-> us-intl)
bind = $mainMod ALT, SPACE, exec, hyprctl switchxkblayout all next
# bind = $mainMod, t, togglesplit, # dwindle
bind = $mainMod, n, togglegroup
# Move focus with mainMod + arrow keys
@@ -77,12 +89,12 @@ bindm = $mainMod, mouse:272, movewindow
bindm = $mainMod, mouse:273, resizewindow
# Laptop multimedia keys for volume and LCD brightness
bindel = ,XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+
bindel = ,XF86AudioLowerVolume, exec, wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%-
bindel = ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
bindel = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle
bindel = ,XF86MonBrightnessUp, exec, brightnessctl set 5%+
bindel = ,XF86MonBrightnessDown, exec, brightnessctl set 5%-
bindel = ,XF86AudioRaiseVolume, exec, fluxo vol up 5
bindel = ,XF86AudioLowerVolume, exec, fluxo vol down 5
bindel = ,XF86AudioMute, exec, fluxo vol mute
bindel = ,XF86AudioMicMute, exec, fluxo mic mute
bindel = ,XF86MonBrightnessUp, exec, brightnessctl s +10%
bindel = ,XF86MonBrightnessDown, exec, brightnessctl s 10%-
# Requires playerctl
bindl = , XF86AudioNext, exec, playerctl next
+5 -1
View File
@@ -2,7 +2,11 @@
# laptop screen
monitor = eDP-1, 1920x1080@60, 0x0, 1
# samsung home monitor
monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 2560x1440@144, 0x-1440, 1
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 1280x720@240, 0x0, 1
monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 2560x1440@240, -2560x0, 1
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 2560x1440@144, 0x0, 1
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 1920x1080@144, 0x0, 1
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 2560x1440@60, 0x0, 1
# lid_state fallback
-38
View File
@@ -1,38 +0,0 @@
exec-once = hyprpm reload -n
# plugin:hyprfocus {
# enabled = yes
# animate_floating = yes
# animate_workspacechange = yes
# focus_animation = shrink
# # Beziers for focus animations
# bezier = bezIn, 0.5,0.0,1.0,0.5
# bezier = bezOut, 0.0,0.5,0.5,1.0
# bezier = overshot, 0.05, 0.9, 0.1, 1.05
# bezier = smoothOut, 0.36, 0, 0.66, -0.56
# bezier = smoothIn, 0.25, 1, 0.5, 1
# bezier = realsmooth, 0.28,0.29,.69,1.08
# # Flash settings
# flash {
# flash_opacity = 0.95
# in_bezier = realsmooth
# in_speed = 0.5
# out_bezier = realsmooth
# out_speed = 3
# }
# # Shrink settings
# shrink {
# shrink_percentage = 0.95
# in_bezier = realsmooth
# in_speed = 1
# out_bezier = realsmooth
# out_speed = 2
# }
# }
#
# plugin:csgo-vulkan-fix {
# res_w = 1280
# res_h = 960
# class = gamescope
# fix_mouse = true
# }
+39
View File
@@ -0,0 +1,39 @@
# Takemi palette — kept in step with ~/.config/quickshell/config/Theme.qml.
#
# Crimson, black, white. Nothing else carries hue.
# ----------------------------------------------------------------- THE KNOB
# The one colour in this setup. Change this line and the window borders,
# group indicators and focus states all recolour with it.
#
# Keep it in step with `readonly property color accent` in
# ~/.config/quickshell/config/Theme.qml, which is what colours the shell.
# Those are the only two places the colour is written.
$accent = rgb(ff2d40)
$accentDim = rgb(8f1826)
$accentSoft = rgb(ff6b78)
# ------------------------------------------------------------------ neutrals
$ink = rgb(000000)
$base = rgb(0a0a0c)
$mantle = rgb(0d0d10)
$surface = rgb(141418)
$surfaceAlt = rgb(1c1c22)
$overlay = rgb(26262e)
$outline = rgb(3a3a44)
$text = rgb(ffffff)
$subtext = rgb(c8c8ce)
$muted = rgb(6b6b70)
# Retired hues, kept as aliases so existing references resolve. The shell has
# no teal any more — the wallpaper is the colour in the composition.
$primary = $text
$primaryDim = rgb(9a9aa2)
$glow = $accent
$blue = $primaryDim
$deep = $surfaceAlt
$warn = $accentSoft
$ok = $text
+82 -58
View File
@@ -1,22 +1,20 @@
# catppuccin mocha theme
source = ~/.config/hypr/catppuccin-hyprland/themes/mocha.conf
# Takemi — Persona 5 flavoured theme, matched to ~/.config/quickshell.
source = ~/.config/hypr/modules/takemi.conf
# Refer to https://wiki.hyprland.org/Configuring/Variables/
# https://wiki.hyprland.org/Configuring/Variables/#general
general {
gaps_in = 5
gaps_out = 5, 5, 5, 5
gaps_out = 6, 8, 8, 8
border_size = 3
border_size = 2
# https://wiki.hyprland.org/Configuring/Variables/#variable-types for info about colors
# col.active_border = rgba(cba6f7ee) rgba(f38ba8ee) 45deg
col.active_border = rgb($mauveAlpha) rgb($blueAlpha) 30deg
col.inactive_border = rgb($surface2Alpha)
# Crimson into its own shadow, on the diagonal. The accent is the only hue
# here — a gradient into a second colour is what put blue in the border.
col.active_border = $accent $accentDim 135deg
col.inactive_border = $outline
# Set to true enable resizing windows by clicking and dragging on borders and gaps
resize_on_border = false
# Please see https://wiki.hyprland.org/Configuring/Tearing/ before you turn this on
@@ -27,64 +25,72 @@ general {
# https://wiki.hyprland.org/Configuring/Variables/#decoration
decoration {
rounding = 4
rounding_power = 1
# Persona 5 has no rounded corners. Neither do we.
rounding = 0
# Change transparency of focused and unfocused windows
# Fully opaque, both states. Flat ink on flat ground.
active_opacity = 1.0
inactive_opacity = 1.0
shadow {
enabled = true
range = 4
range = 18
render_power = 3
color = rgba(1a1a1aee)
offset = 0 4
color = rgba(000000ee)
color_inactive = rgba(00000088)
}
# https://wiki.hyprland.org/Configuring/Variables/#blur
#
# Off. Blur is the signature of the glass-and-frost look, which is the
# opposite of what this theme is doing — every surface here is opaque, flat
# and hard-edged. With nothing translucent left to blur it also costs three
# render passes for no visible result.
blur {
enabled = true
size = 3
passes = 1
vibrancy = 0.1696
enabled = false
}
}
# https://wiki.hyprland.org/Configuring/Variables/#animations
# https://wiki.hyprland.org/Configuring/Animations/
# Quick and clean. Motion should get out of the way, not perform.
#
# The previous pass overdid it: a 1.56 overshoot on every window, an 88% popin
# scale, and a borderangle looping forever so the focused border was in constant
# motion. The bounce is dialled back to a hint, the popin barely scales, and the
# border angle no longer animates at all.
animations {
enabled = yes, please :)
# Default animations, see https://wiki.hyprland.org/Configuring/Animations/ for more
bezier = easeOutQuint,0.23,1,0.32,1
bezier = easeInOutCubic,0.65,0.05,0.36,1
bezier = snap, 0.16, 1.00, 0.30, 1.00
bezier = overshoot, 0.34, 1.12, 0.64, 1.00
bezier = hard, 0.55, 0.00, 0.90, 0.35
bezier = linear, 0, 0, 1, 1
bezier = almostLinear,0.5,0.5,0.75,1.0
bezier = quick,0.15,0,0.1,1
animation = global, 1, 10, default
animation = border, 1, 5.39, easeOutQuint
animation = windows, 1, 4.79, easeOutQuint
animation = windowsIn, 1, 4.1, easeOutQuint, popin 87%
animation = windowsOut, 1, 1.49, linear, popin 87%
animation = fadeIn, 1, 1.73, almostLinear
animation = fadeOut, 1, 1.46, almostLinear
animation = fade, 1, 3.03, quick
animation = layers, 1, 3.81, easeOutQuint
animation = layersIn, 1, 4, easeOutQuint, fade
animation = layersOut, 1, 1.5, linear, fade
animation = fadeLayersIn, 1, 1.79, almostLinear
animation = fadeLayersOut, 1, 1.39, almostLinear
animation = workspaces, 1, 1.94, almostLinear, fade
animation = workspacesIn, 1, 1.21, almostLinear, fade
animation = workspacesOut, 1, 1.94, almostLinear, fade
}
animation = global, 1, 4, snap
animation = border, 1, 5, snap
# See https://wiki.hyprland.org/Configuring/Dwindle-Layout/ for more
dwindle {
pseudotile = true # Master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below
preserve_split = true # You probably want this
animation = windows, 1, 4, snap
animation = windowsIn, 1, 4, overshoot, popin 96%
animation = windowsOut, 1, 2.6, hard, popin 97%
animation = windowsMove, 1, 3.6, snap
animation = fade, 1, 3, snap
animation = fadeIn, 1, 2.4, snap
animation = fadeOut, 1, 1.8, hard
animation = fadeSwitch, 1, 2, snap
animation = fadeShadow, 1, 3, snap
animation = fadeDim, 1, 2.4, snap
animation = layers, 1, 3, snap
animation = layersIn, 1, 3, snap, slide
animation = layersOut, 1, 2, hard, slide
animation = workspaces, 1, 3, snap, slidevert
animation = workspacesIn, 1, 3, snap, slidevert
animation = workspacesOut, 1, 2.4, hard, slidevert
animation = specialWorkspace, 1, 3, snap, slidevert
}
# See https://wiki.hyprland.org/Configuring/Master-Layout/ for more
@@ -92,22 +98,40 @@ master {
new_status = master
}
dwindle {
preserve_split = true
}
# https://wiki.hyprland.org/Configuring/Variables/#misc
misc {
force_default_wallpaper = 0 # Set to 0 or 1 to disable the anime mascot wallpapers
disable_hyprland_logo = true # If true disables the random hyprland logo / anime girl background. :(
vrr = 1
force_default_wallpaper = 0
disable_hyprland_logo = true
# 0 = off, 1 = always, 2 = fullscreen only (G7 is a 240Hz VRR panel)
vrr = 2
font_family = FiraCode Nerd Font
background_color = $base
focus_on_activate = true
}
group {
col.border_active = $accent $accentDim 135deg
col.border_inactive = $outline
groupbar {
font_size = 14
text_color = $text
text_color_inactive = $text
col.active = $maroon
col.inactive = $surface2
font_family = JetBrainsMono Nerd Font
font_size = 11
height = 24
gradients = false
# --- Active Tab ---
col.active = $accent
text_color = $ink
# --- Inactive Tab ---
col.inactive = $surface
text_color_inactive = $muted
indicator_height = 3
}
col.border_active = $maroon $teal 30deg
col.border_inactive = $surface2
}
+14 -2
View File
@@ -5,8 +5,8 @@
# VARIABLES
# ==========================================
# Dialogs and utility apps that should float in the center
$dialog_titles = ^(Open Form|Open File|Select a File|Choose a file|Open Workspace|Choose Directory|Save As|Save File|branchdialog|pinentry-gtk-2|Confirm to replace files|File Operation Progress|Open Files|Anmelden Google Konten — Zen Browser)$
$dialog_classes = ^(pavucontrol|blueman-manager|nm-connection-editor|org.pulseaudio.pavucontrol)$
$dialog_titles = ^(Open Form|Open File|Select a File|Choose a file|Open Workspace|Choose Directory.*|Save As.*|Save File.*|branchdialog|pinentry-gtk-2|Confirm to replace files|File Operation Progress|Open Files.*|Anmelden.*|File Upload.*|TRuDI-Export laden)$
$dialog_classes = ^(pavucontrol|blueman-manager|nm-connection-editor|org.pulseaudio.pavucontrol|io.narl.proton-drive-linux-prompt)$
# App classes
$steam = ^(steam)$
@@ -75,3 +75,15 @@ windowrule = no_blur 1, match:class $steam, match:title negative:^(Steam)$
# Allow tearing for games started with Gamescope
windowrule = immediate 1, match:class $gamescope
# Layer rules
# waybar blur
layerrule = blur on, match:namespace bottom
layerrule = blur on, match:namespace top
layerrule = ignore_alpha 0.5, match:namespace bottom
layerrule = ignore_alpha 0.5, match:namespace top
# launcher blur
layerrule = blur on, match:namespace launcher
layerrule = ignore_alpha 0.5, match:namespace launcher
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
url=$(playerctl metadata mpris:artUrl 2>/dev/null)
if [[ "$url" == file://* ]]; then
cp "${url#file://}" /tmp/hyprlock_art.png
echo "/tmp/hyprlock_art.png"
elif [[ "$url" == http* ]]; then
curl -s "$url" -o /tmp/hyprlock_art.png
echo "/tmp/hyprlock_art.png"
else
echo "$HOME/.config/hypr/catppuccin-hyprland/assets/mocha.webp"
fi
+3 -2
View File
@@ -5,8 +5,9 @@
LID_STATE_FILE="$HOME/.config/hypr/lid_state.conf"
restart_ui() {
# Spawn in background and detach
systemctl restart --user waybar hyprpaper
# The Quickshell bar follows monitor hotplug on its own, so only the
# wallpaper daemon needs a kick.
systemctl restart --user hyprpaper
}
if [[ "$1" == "close" ]]; then
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
# Get active window info in JSON format
WINDOW_INFO=$(hyprctl activewindow -j)
# Extract window class name (app name)
APP_NAME=$(echo "$WINDOW_INFO" | jq -r '.class')
# If app name is empty, null, or invalid, default to "unknown"
if [ -z "$APP_NAME" ] || [ "$APP_NAME" = "null" ]; then
APP_NAME="unknown"
fi
# Sanitize app name: convert to lowercase and replace characters that aren't letters, numbers, hyphens, or underscores with underscores
APP_NAME_SAFE=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/_/g')
# Extract geometry: "x,y wxh"
GEOMETRY=$(echo "$WINDOW_INFO" | jq -r '"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"')
# Check if geometry is valid
if [ -z "$GEOMETRY" ] || [ "$GEOMETRY" = "null,null nullxnull" ]; then
notify-send -u normal "Screenshot Error" "No active window found to screenshot."
exit 1
fi
ACTION=$1
case "$ACTION" in
copy)
grim -g "$GEOMETRY" - | wl-copy
notify-send -u low "Screenshot" "Active window ($APP_NAME) copied to clipboard"
;;
save)
SCREENSHOTS_DIR="$HOME/Pictures/Screenshots"
APP_DIR="$SCREENSHOTS_DIR/$APP_NAME_SAFE"
mkdir -p "$APP_DIR"
FILE_NAME="$(date +'%Y-%m-%d_%H-%M-%S').png"
FILE_PATH="$APP_DIR/$FILE_NAME"
grim -g "$GEOMETRY" "$FILE_PATH"
notify-send -u low "Screenshot" "Saved active window to $APP_NAME_SAFE/$FILE_NAME"
;;
*)
echo "Usage: $0 {copy|save}"
exit 1
;;
esac
exit 0
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
status=$(playerctl status 2>/dev/null)
if [ "$status" = "Playing" ] || [ "$status" = "Paused" ]; then
title=$(playerctl metadata title | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
artist=$(playerctl metadata artist | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
if [ ${#title} -gt 25 ]; then title="${title:0:22}..."; fi
if [ ${#artist} -gt 25 ]; then artist="${artist:0:22}..."; fi
echo "<span weight='bold' foreground='#cba6f7'>$title</span>"
echo "<span foreground='#bac2de' size='small'>$artist</span>"
else
echo "<span weight='bold' foreground='#cdd6f4'>No Media</span>"
echo "<span size='small' foreground='#a6adc8'>Idling</span>"
fi
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# 1. Network
net=$(nmcli -t -f active,ssid dev wifi | grep '^yes' | cut -d: -f2 || echo 'Offline')
if [ ${#net} -gt 15 ]; then net="${net:0:12}..."; fi
# 2. Battery
bat_cap=$(cat /sys/class/power_supply/BAT0/capacity)
bat_stat=$(cat /sys/class/power_supply/BAT0/status)
if [ "$bat_stat" = "Charging" ]; then icon="󰂄"; else icon="󰁹"; fi
# 3. Load
cpu=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}' | cut -d. -f1)
# Output 3 lines for the vertical stack
echo "󰖩 $net"
echo "$icon $bat_cap% ($bat_stat)"
echo "󰍛 Load: $cpu%"
+17 -5
View File
@@ -2,21 +2,26 @@
application/pdf=org.pwmt.zathura-pdf-poppler.desktop
image/jpeg=feh.desktop
image/png=feh.desktop
x-scheme-handler/http=zen.desktop
x-scheme-handler/https=zen.desktop
x-scheme-handler/http=brave-browser.desktop
x-scheme-handler/https=brave-browser.desktop
x-scheme-handler/chrome=zen.desktop
text/html=zen.desktop
text/html=brave-browser.desktop
application/x-extension-htm=zen.desktop
application/x-extension-html=zen.desktop
application/x-extension-shtml=zen.desktop
application/xhtml+xml=zen.desktop
application/x-extension-xhtml=zen.desktop
application/x-extension-xht=zen.desktop
x-scheme-handler/about=google-chrome.desktop
x-scheme-handler/unknown=google-chrome.desktop
x-scheme-handler/about=brave-browser.desktop
x-scheme-handler/unknown=brave-browser.desktop
application/vnd.openxmlformats-officedocument.wordprocessingml.document=libreoffice-writer.desktop
x-scheme-handler/ror2mm=r2modman.desktop
image/webp=feh.desktop
x-scheme-handler/claude-cli=claude-code-url-handler.desktop
x-scheme-handler/msteams=teams-for-linux.desktop
x-scheme-handler/slack=slack.desktop
audio/flac=mpv.desktop
x-scheme-handler/codex=ChatGPT.desktop
[Added Associations]
application/pdf=org.pwmt.zathura-pdf-poppler.desktop;
@@ -28,3 +33,10 @@ text/html=zen.desktop;
x-scheme-handler/chrome=zen.desktop;
application/vnd.openxmlformats-officedocument.wordprocessingml.document=libreoffice-writer.desktop;
image/webp=feh.desktop;
application/x-extension-htm=zen.desktop;
application/x-extension-html=zen.desktop;
application/x-extension-shtml=zen.desktop;
application/xhtml+xml=zen.desktop;
application/x-extension-xhtml=zen.desktop;
application/x-extension-xht=zen.desktop;
audio/flac=mpv.desktop;
+225
View File
@@ -0,0 +1,225 @@
# Takemi
A Persona 5 flavoured Quickshell desktop for Hyprland. Crimson, black and white,
with no gradients anywhere. Everything leans by the same 14°, corners are
chopped rather than rounded, and every surface is one continuous slab divided by
hairlines rather than a cluster of floating cards.
Replaces Waybar, dunst and hyprlock.
## Running
```sh
qs # starts the shell (Hyprland does this via autostart.conf)
qs -p lockpreview.qml # the lock screen in an ordinary window, for styling
```
Hyprland starts it from `~/.config/hypr/modules/autostart.conf`.
## Layout
Two surfaces. Status moves off the bar and onto a vertical rail, because on a
1080p panel horizontal space is cheap and vertical space is not: a 48px rail
costs 2.5% of the width, where another 48px strip along the bottom would have
cost 4.4% of the height on top of what the bar already takes.
```
┌────────────────────────────────────────────┐
│ ◆ │ 1 2 3 │ window title ♪ TRACK │ 42% │ 00:09 │ bar — 48px exclusive
├────────────────────────────────────────────┤
│ ▐█▌│ rail — 48px exclusive
│ windows ▐ ▌│
│ ▐ ▌│
│ ▐ ▌│
└────────────────────────────────────────────┘
```
- **bar** — one slab: identity and workspaces on the left; the focused window,
the player, the CPU/MEM/GPU readout and the clock on the right.
- **rail** — one slab: network, bluetooth, audio, mic, backlight, battery, tray,
notifications, power. Popouts open leftward, into the screen.
Each surface is a single continuous `P5Panel` with modules divided by sheared
hairlines. `BarPill` is chromeless by default — a module is *content inside* a
surface, not a surface of its own — and marks hover with a crimson underline.
Pass `standalone: true` for the rare thing that really is its own panel.
```
shell.qml entrypoint — bar, rail, power menu, toasts, OSD per screen
config/ palette, type scale, geometry, motion curves
components/ the P5 primitives: panels, gauges, sliders, split-ink text
services/ data sources, all singletons
bar/ the two surfaces and their modules
popouts/ hover panels hanging off rail and bar modules
overlays/ power menu, notification toasts, volume/brightness OSD
lock/ session lock
assets/ halftone and stripe tiles, generated with ImageMagick
```
`P5Panel` is the base surface everything sits on. It does not auto-size —
callers set implicit sizes from their own layout (`row.implicitWidth +
contentPad * 2`), which keeps the shape maths out of binding loops.
## Modules
| Module | Where | Left click | Right click | Middle | Scroll | Hover |
|---|---|---|---|---|---|---|
| Sigil | bar | app menu | reload shell | | | |
| Workspaces | bar | switch | | | | |
| Clock | bar | pin calendar | | | | calendar |
| Network | rail | pin panel | toggle Wi-Fi | nm-connection-editor | | networks |
| Bluetooth | rail | pin panel | toggle adapter | | | devices |
| Volume | rail | mute | pin panel | pavucontrol | volume | audio mixer |
| Mic | rail | mute | pin panel | | mic volume | audio mixer |
| Brightness | rail | | | | brightness | |
| Battery | rail | pin panel | | | | power detail |
| Tray | rail | activate | app menu | secondary | | |
| Bell | rail | history + mark read | do not disturb | | | history |
| Power | rail | power menu | | | | |
| Media | bar | play/pause | next | raise player | next/prev | full player |
| CPU/MEM/GPU | bar | btop | | | | resource dashboard |
Popouts open on a short dwell and stay while the pointer is over the trigger or
the panel. Clicking pins one open.
## Keybinds
Set in `~/.config/hypr/modules/keybinds.conf`:
- `Super+Ctrl+L` — lock
- `Super+P` — power menu
- `Super+Shift+N` — toggle do not disturb
- `Super+Ctrl+Shift+N` — clear notifications
- `Super+Shift+R` — reload the shell
All of these go through Quickshell's IPC, so they work from any script:
```sh
qs ipc call shell lock
qs ipc call shell power
qs ipc call shell toggleDnd
qs ipc call shell clearNotifications
qs ipc call shell reloadConfig
```
## Data sources
Hardware telemetry comes from `fluxo`, whose hardware modules are configured to
emit raw pipe-delimited values rather than display strings — `services/Sys.qml`
parses them and does its own formatting. The previous human-readable Waybar
formats are preserved in `~/.config/fluxo/config.toml.waybar-bak`.
Everything else is native: PipeWire for audio, BlueZ for bluetooth,
NetworkManager for Wi-Fi, UPower for battery, MPRIS for media, and Quickshell's
own notification server. These are event-driven, so the bar and OSD react the
instant a volume key is pressed instead of on the next poll.
## Lock screen
Authenticates through `/etc/pam.d/hyprlock`, which on this box is just
`auth include login`. The visuals live in `lock/LockFace.qml` so they can be
styled in an ordinary window (`qs -p lockpreview.qml`) without locking the
session.
If the shell ever dies while the session is locked, the compositor keeps the
session secured by protocol and there is no unlocker left to talk to — recovery
means restarting Hyprland from a TTY.
## What was retired
- `waybar.service` — disabled. Its config is still in `~/.config/waybar`.
- `dunst.service` — masked, so it cannot be D-Bus activated and steal
`org.freedesktop.Notifications`. Undo with
`systemctl --user unmask dunst.service`.
- `hyprlock` — no longer bound. `hyprlock.conf` is untouched.
## Optional
`power-profiles-daemon` is not installed (TLP is), so the power-profile
switching in `services/Battery.qml` stays dormant. Nothing else depends on it.
The display face is Archivo Black, set italic almost everywhere. Change
`Theme.fontDisplay` and everything follows.
## Palette discipline
Crimson, black, white. That is the whole palette.
**Changing the accent.** The colour is written in exactly three places, one per
program:
| File | Line | Covers |
|---|---|---|
| `quickshell/config/Theme.qml` | `readonly property color accent: "#ff2d40"` | the shell |
| `hypr/modules/takemi.conf` | `$accent = rgb(ff2d40)` | window borders, groups |
| `alacritty/takemi.toml` | `red`, cursor, selection | the terminal |
Everything else derives. `accentDim` and `accentSoft` come off `accent` via
`Qt.darker`/`Qt.lighter`, `glow` (the hover highlight) *is* `accent`, and every
Hyprland border, group indicator and focus state references `$accent`. Change
those lines and the whole setup recolours — swap in Persona 4's yellow or
Persona 3's blue and nothing else needs touching.
## The rest of the desktop
- **Alacritty** — `alacritty/takemi.toml`, selected by the `import` in
`alacritty.toml`. The Catppuccin files are left in place, just unreferenced.
Near-black ground, white text, crimson cursor and selection. The sixteen ANSI
slots keep their hues because syntax highlighters need to tell them apart, but
every one is desaturated so nothing competes with the accent.
- **Neovim** — `nvim/lua/takemi.lua`, loaded through `nvim/colors/takemi.lua`
so `:colorscheme takemi` and `require("takemi").lualine()` both work. Same
palette, same rule: crimson is spent only on keywords, types and errors,
functions are plain white-bold, and `terminal_color_0..15` mirror
`takemi.toml` slot for slot so `:terminal` matches the shell around it.
- **Fish** — `fish/conf.d/takemi_theme.fish`, with a preview copy in
`fish/themes/Takemi.theme` for `fish_config`. Replaced the Catppuccin
`fish_frozen_theme.fish` that fish 4.3 generated on upgrade. The prompt arrow
is white and turns crimson on a non-zero exit — the shell's one "wrong"
signal, and so the one place that earns the accent.
- **Fuzzel** — `fuzzel/takemi.ini`, selected by the `include` in `fuzzel.ini`.
Square corners rather than the stock radius, and a fully opaque ground.
- **No glass anywhere.** Alacritty is `opacity = 1.0, blur = false`, and
Hyprland's `decoration:blur` is disabled with both opacities at `1.0`. Blur is
the signature of the frosted-glass look, which is the opposite of flat ink on
flat ground — and with nothing translucent left it was costing three render
passes for no visible result.
White is the resting voice — text, icons, idle indicators. Crimson is the only
accent and always means one thing: active, or wrong. Black is structure, used as
a hard keyline rather than as a soft shadow. **There are no gradients in this
shell**; every fill is flat. `Theme.sheen` still exists as a no-op property
because a teal-to-transparent wash on every panel was the single biggest source
of the murky blue cast the design used to have.
`primary`, `glow`, `blue`, `ok` and `warn` survive in `Theme.qml` only as
aliases onto white, crimson and grey, so the existing call sites keep working —
don't introduce new uses. `warn` used to alias `accentSoft`, which put a
washed-out pink-red wherever something was merely notable; there is no
half-crimson in this palette, so it aliases the real `accent` now and anything
merely informational (brightness, most readouts) is white instead. `Theme.heat()` is a continuous white→crimson ramp, so
a CPU figure that has gone red means the same thing as a battery that has gone
red. Tray icons are force-flattened to a single colour for the same reason: app
brand colours wreck the palette faster than anything else in the shell.
The wallpaper is teal. The UI used to be teal too, and the two fought each other
into a blue-grey mush. Going monochrome-plus-red is what lets the wallpaper be
the colour in the composition.
## Geometry discipline
**One angle, one chamfer, everywhere.** `Theme.skew` is `tan(14°)` and every
edge in the shell leans by exactly that, in the same direction; every corner is
chopped by exactly `Theme.cut`. A vertical panel (the rail) leans its horizontal
edges by a fraction of its *width*, which works out to the same 14°.
This is deliberate and was learned the hard way. An earlier pass gave every
module a different lean, chamfer and height from per-index variance tables, on
the theory that Persona geometry is "chaotic". It isn't — Persona UI is
meticulously clean, and the chaos lives in composition and motion, not in each
element being a different shape. Randomised geometry just reads as noise. If you
find yourself adding a variance table, don't.
Each surface is **one continuous slab**, with modules divided by sheared
hairline rules rather than by gaps. A row of separate floating cards reads as
clutter; that is what the dividers exist to avoid.
Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

+75
View File
@@ -0,0 +1,75 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "root:/config"
import "root:/components"
// Focused window title, glitching on every change the way a Persona 5 caption
// snaps into place.
Item {
id: root
required property var screenRef
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(screenRef)
readonly property HyprlandToplevel toplevel: Hyprland.activeToplevel
// Only speak for the monitor this bar lives on.
readonly property bool mine: toplevel !== null
&& (root.monitor === null || toplevel.monitor === null
|| toplevel.monitor.id === root.monitor.id)
readonly property string title: mine && toplevel.title ? toplevel.title : "—"
// The bar shrinks this when the three clusters would otherwise collide.
property real maxWidth: 240
implicitWidth: Math.min(maxWidth, Math.max(70, label.implicitWidth + 4))
implicitHeight: 20
Behavior on implicitWidth {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutExpo }
}
Marquee {
id: label
anchors.fill: parent
text: root.title
color: root.mine ? Theme.subtext : Theme.muted
pixelSize: Theme.fsSmall
family: Theme.fontDisplay
italic: true
weight: Font.DemiBold
}
onTitleChanged: flash.restart()
// A quick crimson wipe under the title on focus change.
Rectangle {
id: wipe
anchors.bottom: parent.bottom
height: 2
width: 0
color: Theme.accent
opacity: 0
}
SequentialAnimation {
id: flash
ParallelAnimation {
NumberAnimation {
target: wipe; property: "width"
from: 0; to: root.width
duration: Theme.durBase; easing.type: Easing.OutExpo
}
NumberAnimation {
target: wipe; property: "opacity"
from: 0; to: 1; duration: Theme.durFast
}
}
NumberAnimation {
target: wipe; property: "opacity"
to: 0; duration: Theme.durSlow
}
}
}
+143
View File
@@ -0,0 +1,143 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
// One bar per monitor, and one surface.
//
// Identity and workspaces on the left, the player, metrics and clock on the
// right, all inside a single continuous slab and separated by hairline rules
// rather than by gaps. Nothing floats and nothing has its own card. Status
// lives on the rail, which is likewise one slab — two surfaces, not a dozen
// islands.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "takemi-bar"
anchors {
top: true
left: true
right: true
}
margins {
top: Theme.barMargin
left: Theme.barMargin
right: Theme.barMargin
}
implicitHeight: Theme.barHeight
exclusiveZone: Theme.barHeight + Theme.barMargin
color: "transparent"
// The slab. One panel, full width, flat fill, black keyline.
P5Panel {
id: slab
anchors.fill: parent
lean: Theme.skew
chop: Theme.cut
chopCorners: [1, 3]
padding: 0
fill: Theme.surface
fillOpacity: 1.0
border: Theme.alpha(Theme.outline, 0.9)
borderWidth: 1
slash: true
slashColor: Theme.accent
slashWidth: 5
// No offset copy on the big slabs. The 3px black silhouette behind the
// fill is a print-misregistration trick, and at this scale it stops
// reading as one: on a 1900px bar you only ever see it at the chopped
// corners, where it turns the single clean cut into a staggered double
// edge that looks like a rendering fault. The black keyline still
// carries the heavy outer contour, which is what the corner needs.
drop: false
}
readonly property real edgeInset: Theme.barHeight * Theme.skew + Theme.padM
// ------------------------------------------------------------------ left
Row {
id: leftGroup
anchors.left: parent.left
anchors.leftMargin: root.edgeInset
anchors.verticalCenter: parent.verticalCenter
spacing: 0
Sigil {
id: sigil
screenRef: root.modelData
}
Divider {}
Workspaces {
id: spaces
anchors.verticalCenter: parent.verticalCenter
screenRef: root.modelData
}
Divider {}
ActiveWindow {
anchors.verticalCenter: parent.verticalCenter
screenRef: root.modelData
maxWidth: Math.max(120, root.width
- leftGroup.x - rightGroup.width - sigil.implicitWidth
- spaces.implicitWidth - root.edgeInset * 2 - Theme.padXL)
}
}
// ----------------------------------------------------------------- right
Row {
id: rightGroup
anchors.right: parent.right
anchors.rightMargin: root.edgeInset
anchors.verticalCenter: parent.verticalCenter
spacing: 0
MediaPill {
screenRef: root.modelData
}
Divider {}
Resources {
screenRef: root.modelData
}
Divider {}
Clock {
screenRef: root.modelData
}
}
// A hairline rule between modules, sheared to the house angle. This is what
// divides the bar now — gaps between cards are what made it look scattered.
component Divider: Item {
anchors.verticalCenter: parent.verticalCenter
implicitWidth: Theme.padM * 2
implicitHeight: Theme.barHeight
Rectangle {
anchors.centerIn: parent
width: 1
height: 20
color: Theme.alpha(Theme.outline, 0.9)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 20 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
}
}
+148
View File
@@ -0,0 +1,148 @@
import QtQuick
import "root:/config"
import "root:/components"
// Shared chrome for every bar module.
//
// The bar is one continuous slab, so by default a module draws no panel of its
// own — it is content inside the bar's surface, and marks hover with a crimson
// underline rather than by lighting up a card. That is what stops the bar from
// reading as a row of floating islands.
//
// `standalone: true` brings back the individual leaning slab, for the few
// places that genuinely are separate surfaces.
Item {
id: root
property color accent: Theme.accent
property bool active: false
property bool slash: false
property real lean: Theme.skew
property bool leanLeft: false
property real padding: Theme.padM
property bool interactive: true
property bool halftone: false
property bool standalone: false
property real chop: Theme.cut
property var chopCorners: [1, 3]
readonly property bool hovered: hover.hovered
readonly property real leanInset: standalone ? panelLoader.item?.leanInset ?? 0 : 0
// The hover rule's own strip, reserved at the bottom of the module.
//
// The rule used to be drawn into the same box as the content, so anything
// taller than about 32px — the media pill's art, the metric meters — had a
// crimson line struck through its bottom edge on hover. Reserving the band
// and centring content in what is left is what keeps the rule under the
// module instead of across it.
readonly property real ruleHeight: 2
readonly property real ruleInset: 5
readonly property real ruleBand: root.standalone ? 0 : root.ruleHeight + root.ruleInset
// Total horizontal inset a module must add on top of its content width.
readonly property real contentPad: standalone
? (panelLoader.item?.contentPad ?? root.padding)
: root.padding
default property alias content: holder.data
signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitHeight: Theme.barHeight
Loader {
id: panelLoader
anchors.fill: parent
active: root.standalone
sourceComponent: P5Panel {
lean: root.lean
leanLeft: root.leanLeft
chop: root.chop
chopCorners: root.chopCorners
padding: root.padding
halftone: root.halftone
halftoneColor: root.accent
fill: root.active ? Theme.surfaceAlt : Theme.surface
fillOpacity: 1.0
border: root.hovered || root.active
? root.accent
: Theme.alpha(Theme.outline, 0.9)
borderWidth: root.hovered || root.active ? Theme.stroke : 1
slash: root.slash || root.active
slashColor: root.accent
}
}
Item {
id: holder
anchors.fill: parent
anchors.leftMargin: root.contentPad
anchors.rightMargin: root.contentPad
anchors.bottomMargin: root.ruleBand
}
// Hover mark for in-bar modules: a crimson rule under the content, wiping
// out from the leading edge. Sheared to match the one angle everything uses.
Item {
visible: !root.standalone && root.interactive
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: root.ruleInset
height: root.ruleHeight
clip: true
Rectangle {
height: parent.height
width: root.hovered || root.active ? parent.width : 0
color: root.accent
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 2 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
}
HoverHandler {
id: hover
enabled: root.interactive
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: root.interactive
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
enabled: root.interactive
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
enabled: root.interactive
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler {
enabled: root.interactive
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y)
}
}
+101
View File
@@ -0,0 +1,101 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/popouts"
// Centre-stage clock. Hovering drops the calendar.
BarPill {
id: root
required property var screenRef
accent: Theme.accent
slash: true
padding: Theme.padS
implicitWidth: layout.implicitWidth + contentPad * 2
SystemClock {
id: clock
precision: SystemClock.Seconds
}
onClicked: calendar.pinned = !calendar.pinned
Row {
id: layout
anchors.centerIn: parent
spacing: Theme.padM
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: -1
Text {
text: Qt.formatDateTime(clock.date, "ddd").toUpperCase()
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2.5
renderType: Text.NativeRendering
}
Text {
text: Qt.formatDateTime(clock.date, "dd MMM").toUpperCase()
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 2
height: 22
color: Theme.alpha(Theme.accent, 0.6)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
SplitText {
id: timeText
anchors.verticalCenter: parent.verticalCenter
text: Qt.formatDateTime(clock.date, "HH:mm")
pixelSize: Theme.fsTitle
split: 1.4
}
// Seconds, kept quiet so they read as a ticking detail rather than noise.
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 4
text: Qt.formatDateTime(clock.date, "ss")
color: Theme.alpha(Theme.primary, 0.8)
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
// Glitch the time on the hour, because it should feel like something happened.
property int lastHour: -1
Connections {
target: clock
function onDateChanged() {
const h = clock.date.getHours();
if (root.lastHour !== -1 && h !== root.lastHour) timeText.glitch();
root.lastHour = h;
}
}
CalendarPopout {
id: calendar
anchorItem: root
triggerHovered: root.hovered
clockDate: clock.date
}
}
+185
View File
@@ -0,0 +1,185 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Now-playing module. Collapses to nothing when no player is around.
BarPill {
id: root
required property var screenRef
accent: Theme.primary
padding: Theme.padS
visible: Media.hasPlayer || width > 1
implicitWidth: Media.hasPlayer
? layout.implicitWidth + contentPad * 2
: 0
Behavior on implicitWidth {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutExpo }
}
clip: true
onClicked: Media.playPause()
onRightClicked: Media.next()
onMiddleClicked: Media.raise()
onScrolled: delta => delta > 0 ? Media.next() : Media.previous()
Row {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padS
// Album art, chopped into the house shape.
Item {
id: artFrame
anchors.verticalCenter: parent.verticalCenter
width: 24
height: 24
Shape {
id: artMask
anchors.fill: parent
visible: false
layer.enabled: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "white"
strokeColor: "transparent"
PathPolyline {
path: Geom.chamfer(Geom.parallelogram(24, 24, 0.18), 5, [1, 3])
}
}
}
Image {
id: art
anchors.fill: parent
source: Media.artUrl
fillMode: Image.PreserveAspectCrop
cache: true
asynchronous: true
visible: status === Image.Ready
layer.enabled: true
layer.effect: MaskedArt { maskItem: artMask }
}
// Fallback: the source app's glyph.
Icon {
anchors.centerIn: parent
visible: art.status !== Image.Ready
text: Media.sourceIcon
font.pixelSize: Theme.fsLarge
color: Theme.primary
pulsing: Media.playing
pulseScale: 1.12
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 0
Marquee {
width: 130
height: 14
text: Media.title || Media.identity
color: Theme.text
pixelSize: Theme.fsSmall
family: Theme.fontDisplay
weight: Font.DemiBold
italic: true
}
Marquee {
width: 130
height: 12
text: Media.artist || Media.album || Media.identity
color: Theme.muted
pixelSize: Theme.fsMicro
family: Theme.fontMono
}
}
// Play state, as a two-bar equaliser that freezes when paused.
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Repeater {
model: 3
Rectangle {
required property int index
width: 3
height: 6
anchors.verticalCenter: parent.verticalCenter
color: Media.playing ? Theme.primary : Theme.muted
SequentialAnimation on height {
running: Media.playing
loops: Animation.Infinite
NumberAnimation {
to: 4 + (index * 5) % 13
duration: 320 + index * 90
easing.type: Easing.InOutSine
}
NumberAnimation {
to: 14 - (index * 4) % 9
duration: 280 + index * 70
easing.type: Easing.InOutSine
}
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
}
}
// Track progress, hairline along the bottom of the pill.
Rectangle {
anchors.bottom: parent.bottom
anchors.bottomMargin: 3
anchors.left: parent.left
anchors.leftMargin: root.leanInset
width: Math.max(0, (root.width - root.leanInset * 2) * Media.progress)
height: 2
color: Theme.accent
visible: Media.canSeek
Behavior on width {
NumberAnimation { duration: 480; easing.type: Easing.OutQuad }
}
}
onVisibleChanged: if (visible) titleGlitch.restart()
Connections {
target: Media
function onTrackChanged() { titleGlitch.restart(); }
}
SequentialAnimation {
id: titleGlitch
NumberAnimation {
target: artFrame; property: "rotation"
from: -8; to: 0; duration: Theme.durSlow; easing.type: Easing.OutBack
}
}
MediaPopout {
id: popout
anchorItem: root
triggerHovered: root.hovered && Media.hasPlayer
}
}
+75
View File
@@ -0,0 +1,75 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Notification bell with an unread badge. Click opens the history, right click
// toggles do-not-disturb.
BarPill {
id: root
required property var screenRef
accent: Notifs.dnd ? Theme.muted : Theme.primary
padding: Theme.padS
implicitWidth: Theme.barHeight - 4 + contentPad
onClicked: {
popout.pinned = !popout.pinned;
Notifs.markRead();
}
onRightClicked: Notifs.toggleDnd()
Icon {
id: bell
anchors.centerIn: parent
text: Notifs.icon
font.pixelSize: Theme.fsLarge
color: {
if (Notifs.dnd) return Theme.muted;
if (Notifs.hasUnread) return Theme.accent;
return root.hovered ? Theme.glow : Theme.primary;
}
}
// Unread badge, chopped like everything else.
Rectangle {
visible: Notifs.unread > 0 && !Notifs.dnd
anchors.right: parent.right
anchors.rightMargin: 2
anchors.top: parent.top
anchors.topMargin: 4
width: Math.max(12, badge.implicitWidth + 5)
height: 12
color: Theme.accent
Text {
id: badge
anchors.centerIn: parent
text: Notifs.unread > 9 ? "9+" : Notifs.unread
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: 8
font.weight: Font.Bold
renderType: Text.NativeRendering
}
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 12 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Connections {
target: Notifs
function onPopupRequested() { bell.bump(); }
}
NotifPopout {
id: popout
anchorItem: root
triggerHovered: root.hovered
}
}
+42
View File
@@ -0,0 +1,42 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Far right. Opens the full-screen power menu.
BarPill {
id: root
required property var screenRef
accent: Theme.accent
slash: true
padding: Theme.padS
active: Actions.powerMenuOpen
implicitWidth: Theme.barHeight - 4 + contentPad
onClicked: Actions.togglePowerMenu()
Icon {
id: glyph
anchors.centerIn: parent
text: "󰐥"
font.pixelSize: Theme.fsLarge
color: root.hovered || Actions.powerMenuOpen ? Theme.accent : Theme.subtext
}
// A crimson halo that swells under the pointer.
Rectangle {
anchors.centerIn: glyph
z: -1
width: root.hovered ? 26 : 0
height: width
radius: width / 2
color: Theme.alpha(Theme.accent, 0.18)
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
}
+231
View File
@@ -0,0 +1,231 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// The right-hand status rail — one slab, not a stack of cards.
//
// Connectivity, audio, power, tray and notifications all sit inside a single
// continuous column, divided by hairline rules the same way the bar divides its
// modules. Same lean, same chamfer, same keyline as the bar and the dock, so
// the three surfaces read as one system.
//
// On a 1080p panel this is the cheap edge: 48px of width is 2.5% of the screen,
// where the same 48px of height would be 4.4% on top of what the bar takes.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "takemi-rail"
anchors {
top: true
bottom: true
right: true
}
margins {
top: Theme.barHeight + Theme.barMargin * 2
bottom: Theme.barMargin
right: Theme.barMargin
}
implicitWidth: Theme.railWidth
exclusiveZone: Theme.railWidth + Theme.barMargin
color: "transparent"
// The slab.
P5Panel {
id: slab
anchors.fill: parent
vertical: true
lean: Theme.skew
chop: Theme.cut
chopCorners: [1, 3]
padding: 0
fill: Theme.surface
fillOpacity: 1.0
border: Theme.alpha(Theme.outline, 0.9)
borderWidth: 1
slash: true
slashColor: Theme.accent
slashWidth: 5
// Same as the bar — see the note there. The doubled corner is worse on
// the rail, because its chopped corners sit against the wallpaper's
// high-contrast stripes rather than against other chrome.
drop: false
}
readonly property real edgeInset: Theme.railWidth * Theme.skew + Theme.padM
// ------------------------------------------------------------------- top
Column {
id: upper
anchors.top: parent.top
anchors.topMargin: root.edgeInset
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0
RailIcon {
id: netIcon
icon: Net.icon
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
}
RailIcon {
id: btIcon
icon: Bt.icon
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
Divider {}
RailIcon {
id: volIcon
icon: Audio.icon
readout: Audio.muted ? "" : Audio.volumePercent + "%"
alert: Audio.muted
onClicked: Audio.toggleMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onMiddleClicked: Actions.openAudioSettings()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
RailIcon {
id: micIcon
icon: Audio.micIcon
alert: Audio.micMuted
onClicked: Audio.toggleMicMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
RailIcon {
id: brightIcon
visible: Brightness.available
icon: Brightness.icon
readout: Math.round(Brightness.percent) + "%"
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
Divider { visible: Battery.available }
RailIcon {
id: battIcon
visible: Battery.available
icon: Battery.icon
readout: Math.round(Battery.percent) + "%"
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
// ---------------------------------------------------------------- bottom
Column {
id: lower
anchors.bottom: parent.bottom
anchors.bottomMargin: root.edgeInset
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0
Tray {
id: tray
anchors.horizontalCenter: parent.horizontalCenter
}
Divider { visible: tray.visible }
RailIcon {
id: bellIcon
icon: Notifs.icon
readout: Notifs.unread > 0
? (Notifs.unread > 9 ? "9+" : String(Notifs.unread))
: ""
dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd
onClicked: {
notifPopout.pinned = !notifPopout.pinned;
Notifs.markRead();
}
onRightClicked: Notifs.toggleDnd()
}
RailIcon {
id: powerIcon
icon: "󰐥"
alert: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu()
}
}
// The rail's counterpart to the bar's divider: a horizontal hairline,
// sheared to the same angle the rail's own edges lean at.
component Divider: Item {
implicitWidth: Theme.railWidth
implicitHeight: Theme.padM + 4
Rectangle {
anchors.centerIn: parent
width: 22
height: 1
color: Theme.alpha(Theme.outline, 0.9)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, 0, 0, 0,
-Theme.skew, 1, 0, 22 * Theme.skew / 2,
0, 0, 1, 0, 0, 0, 0, 1)
}
}
}
// ------------------------------------------------------------- popouts
// All of these hang off the left edge of the rail, opening into the screen.
NetPopout {
id: netPopout
anchorItem: netIcon
triggerHovered: netIcon.hovered
fromEdge: Edges.Left
}
BtPopout {
id: btPopout
anchorItem: btIcon
triggerHovered: btIcon.hovered
fromEdge: Edges.Left
}
AudioPopout {
id: audioPopout
anchorItem: volIcon
triggerHovered: volIcon.hovered || micIcon.hovered
fromEdge: Edges.Left
}
BatteryPopout {
id: battPopout
anchorItem: battIcon
triggerHovered: battIcon.hovered
fromEdge: Edges.Left
visible: Battery.available && shown
}
NotifPopout {
id: notifPopout
anchorItem: bellIcon
triggerHovered: bellIcon.hovered
fromEdge: Edges.Left
}
}
+125
View File
@@ -0,0 +1,125 @@
import QtQuick
import "root:/config"
import "root:/components"
// One slot on the right-hand rail: a glyph with an optional micro readout
// stacked beneath it, and a crimson bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so unlike StatusIcon the readout sits
// under the glyph rather than beside it, and anything longer than a percentage
// belongs in the popout instead.
Item {
id: root
property string icon: ""
property string readout: ""
property color tint: Theme.primary
property bool alert: false
property bool dim: false
property bool pulsing: false
readonly property bool hovered: hover.hovered
// Clearance between the wipe and the rail's left edge, chosen so the bar
// stays inside the slab once `scale` has expanded the icon about its centre.
readonly property real wipeInset: 5
signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitWidth: Theme.railWidth
implicitHeight: root.readout !== "" ? 42 : 32
Column {
id: layout
anchors.centerIn: parent
spacing: 1
Icon {
id: glyph
anchors.horizontalCenter: parent.horizontalCenter
text: root.icon
pulsing: root.pulsing
font.pixelSize: Theme.fsLarge
color: {
if (root.alert) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: root.readout !== ""
text: root.readout
color: root.dim ? Theme.muted : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.DemiBold
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
// Leading-edge wipe. On the rail this runs vertically down the left side,
// mirroring the slash P5Panel puts on horizontal modules.
//
// Held off the edge by `wipeInset`: the icon spans the rail's full width, so
// at x = 0 this landed exactly on the slab's own border, and the 1.08 hover
// scale then pushed it out past the silhouette onto the desktop. The inset
// has to survive that scale, which is why it is not simply 1 or 2.
//
// It is crimson rather than `tint` because it only ever appears on hover,
// and hover is crimson everywhere else in the shell (Theme.glow). Painting
// it white put a white bar on the rail's edge for no stated reason.
Rectangle {
anchors.left: parent.left
anchors.leftMargin: root.wipeInset
anchors.verticalCenter: parent.verticalCenter
width: 2
height: root.hovered ? layout.implicitHeight : 0
color: Theme.glow
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
scale: root.hovered ? 1.08 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y)
}
}
+106
View File
@@ -0,0 +1,106 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Compact CPU / MEM / GPU readout. Each metric is a number over a segmented
// meter; hovering opens the full gauge cluster.
BarPill {
id: root
required property var screenRef
accent: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent))
padding: Theme.padM
implicitWidth: layout.implicitWidth + contentPad * 2
onClicked: Actions.openSystemMonitor()
Row {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
Metric {
label: "CPU"
value: Sys.cpuUsage
readout: Math.round(Sys.cpuUsage) + "%"
}
Divider {}
Metric {
label: "MEM"
value: Sys.memPercent
readout: Sys.memUsed.toFixed(1) + "G"
}
Divider { visible: Sys.gpuAvailable }
Metric {
visible: Sys.gpuAvailable
label: "GPU"
value: Sys.gpuUsage
readout: Math.round(Sys.gpuUsage) + "%"
}
}
component Divider: Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 18
color: Theme.alpha(Theme.outline, 0.8)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 18 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
// The CPU/MEM/GPU labels are gone: three characters of chrome per metric,
// repeated three times, in the most cramped part of the shell. The number
// over its meter is legible on its own, and dropping them buys the room to
// set the figure in display italic instead of squeezing it into mono.
component Metric: Column {
id: metric
property string label: ""
property real value: 0
property string readout: ""
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
text: metric.readout
color: Theme.heat(metric.value)
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
MeterBar {
value: metric.value
segments: 9
segmentWidth: 3
spacing: 2
height: 5
}
}
SysPopout {
id: popout
anchorItem: root
triggerHovered: root.hovered
}
}
+44
View File
@@ -0,0 +1,44 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// The house mark, far left. Click launches the app menu; right click reloads
// the shell, which is handy while tweaking the config.
BarPill {
id: root
required property var screenRef
accent: Theme.accent
slash: true
padding: Theme.padS
lean: Theme.skew
implicitWidth: Theme.barHeight + contentPad
onClicked: Actions.launch("fuzzel")
onRightClicked: Actions.reloadShell()
// The wordmark. Was the Death arcana skull (nf-md-skull, U+F068C) — a
// pictogram in a shell that otherwise says everything in heavy italic
// type. SplitText puts it in the display face with the crimson-and-black
// offset every other heading carries, so the house mark is built out of
// the same parts as the rest of the bar.
SplitText {
id: mark
anchors.centerIn: parent
text: "P5"
pixelSize: Theme.fsLarge
letterSpacing: 0
color: root.hovered ? Theme.accent : Theme.primary
}
// Kick the ghosts out and let them snap back. The skull could spin a full
// turn and still read as a skull; a wordmark upside down just reads as
// broken, so the click gets the glitch instead.
Connections {
target: root
function onClicked() { mark.glitch(); }
}
}
+117
View File
@@ -0,0 +1,117 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Network, bluetooth, audio, mic, backlight and battery, in one slab. Each
// glyph carries its own popout.
BarPill {
id: root
required property var screenRef
accent: Theme.primary
padding: Theme.padS
interactive: false
implicitWidth: layout.implicitWidth + contentPad * 2
Row {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
StatusIcon {
id: netIcon
icon: Net.icon
label: Sys.netUp ? Sys.rate(Sys.netRx + Sys.netTx) : ""
labelWidth: 62
tint: Theme.primary
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
}
StatusIcon {
id: btIcon
icon: Bt.icon
tint: Bt.connected.length > 0 ? Theme.blue : Theme.primary
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
StatusIcon {
id: volIcon
icon: Audio.icon
label: Audio.muted ? "" : Audio.volumePercent + "%"
labelWidth: Audio.muted ? 0 : 34
alert: Audio.muted
onClicked: Audio.toggleMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onMiddleClicked: Actions.openAudioSettings()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
StatusIcon {
id: micIcon
icon: Audio.micIcon
alert: Audio.micMuted
onClicked: Audio.toggleMicMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
StatusIcon {
id: brightIcon
visible: Brightness.available
icon: Brightness.icon
label: Math.round(Brightness.percent) + "%"
labelWidth: 34
// Brightness is never an alarm — it stays white and lets the
// battery be the only thing in the cluster that can go crimson.
tint: Theme.text
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
StatusIcon {
id: battIcon
visible: Battery.available
icon: Battery.icon
label: Math.round(Battery.percent) + "%"
labelWidth: 34
tint: Battery.tint
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
NetPopout {
id: netPopout
anchorItem: netIcon
triggerHovered: netIcon.hovered
}
BtPopout {
id: btPopout
anchorItem: btIcon
triggerHovered: btIcon.hovered
}
AudioPopout {
id: audioPopout
anchorItem: volIcon
triggerHovered: volIcon.hovered || micIcon.hovered
}
BatteryPopout {
id: battPopout
anchorItem: battIcon
triggerHovered: battIcon.hovered
visible: Battery.available && shown
}
}
+108
View File
@@ -0,0 +1,108 @@
import QtQuick
import "root:/config"
import "root:/components"
// One indicator inside the status cluster: a glyph, an optional readout, and a
// crimson underline that wipes in on hover.
Item {
id: root
property string icon: ""
property string label: ""
property color tint: Theme.primary
property bool alert: false
property bool dim: false
property bool pulsing: false
// Fixed label width. Readouts that change width constantly (throughput,
// percentages) would otherwise shove the whole cluster around.
property real labelWidth: 0
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitWidth: layout.implicitWidth
implicitHeight: Theme.barHeight - Theme.padS * 2
Row {
id: layout
anchors.centerIn: parent
spacing: 5
Icon {
id: glyph
anchors.verticalCenter: parent.verticalCenter
text: root.icon
pulsing: root.pulsing
color: {
if (root.alert) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
visible: root.label !== ""
text: root.label
width: root.labelWidth > 0 ? root.labelWidth : implicitWidth
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
color: root.dim ? Theme.muted : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
height: 2
width: root.hovered ? layout.implicitWidth : 0
color: root.alert ? Theme.accent : root.tint
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
scale: root.hovered ? 1.06 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y)
}
}
+103
View File
@@ -0,0 +1,103 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "root:/config"
import "root:/components"
// StatusNotifier tray, as a vertical column for the right-hand rail. Left click
// activates, right click opens the app's own menu, middle click is the
// secondary action. The surrounding slab is the caller's business — this is
// just the stack of icons.
Column {
id: root
spacing: Theme.padS + 2
visible: SystemTray.items.values.length > 0
Repeater {
model: SystemTray.items
Item {
id: entry
required property SystemTrayItem modelData
width: 18
height: 18
anchors.horizontalCenter: parent.horizontalCenter
// App icons arrive in their own brand colours, which wrecks the
// two-hue palette faster than anything else in the shell. Flatten
// every one of them to a single-colour silhouette: white at rest,
// crimson under the pointer. Detail inside the glyph is lost on
// purpose — that flattening *is* the screen-printed look.
IconImage {
id: img
anchors.fill: parent
source: entry.modelData.icon
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1.0
colorizationColor: itemHover.hovered
? Theme.accent
: Theme.text
Behavior on colorizationColor {
ColorAnimation { duration: Theme.durFast }
}
}
opacity: itemHover.hovered ? 1.0 : 0.82
Behavior on opacity {
NumberAnimation { duration: Theme.durFast }
}
}
scale: itemHover.hovered ? 1.18 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
HoverHandler {
id: itemHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: {
if (entry.modelData.onlyMenu) {
menuAnchor.open();
} else {
entry.modelData.activate();
}
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: menuAnchor.open()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: entry.modelData.secondaryActivate()
}
// Menus open to the left, into the screen rather than off its edge.
QsMenuAnchor {
id: menuAnchor
menu: entry.modelData.menu
anchor.item: entry
anchor.edges: Edges.Left
anchor.gravity: Edges.Left
anchor.margins.right: Theme.popoutGap
}
}
}
}
+149
View File
@@ -0,0 +1,149 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "root:/config"
import "root:/components"
// Workspace indicator: leaning ticks that stretch open when focused. Occupied
// workspaces glow teal, urgent ones snap to crimson.
Item {
id: root
required property var screenRef
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(screenRef)
readonly property int focusedId: monitor?.activeWorkspace?.id ?? -1
// Live workspaces on this monitor, keyed by id, ignoring special workspaces.
readonly property var occupancy: {
const map = {};
for (const w of (Hyprland.workspaces?.values ?? [])) {
if (w.id < 1) continue;
if (root.monitor && w.monitor && w.monitor.id !== root.monitor.id) continue;
map[w.id] = {
count: w.toplevels?.values?.length ?? 0,
urgent: w.urgent,
fullscreen: w.hasFullscreen
};
}
return map;
}
// Always show 1..5, plus anything beyond that which is actually in use.
readonly property var slots: {
const ids = [1, 2, 3, 4, 5];
for (const key in root.occupancy) {
const id = parseInt(key);
if (ids.indexOf(id) === -1) ids.push(id);
}
return ids.sort((a, b) => a - b);
}
implicitWidth: row.implicitWidth
implicitHeight: 24
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
spacing: 6
Repeater {
model: root.slots
Item {
id: pip
required property var modelData
required property int index
readonly property int wsId: modelData
readonly property var info: root.occupancy[wsId]
readonly property bool occupied: info !== undefined && info.count > 0
readonly property bool urgent: info !== undefined && info.urgent
readonly property bool focused: root.focusedId === wsId
readonly property bool hovered: pipHover.hovered
// Every tick is the same height and the same shear. Only the
// width changes, and only to say which workspace is focused.
width: focused ? 32 : (occupied ? 16 : 9)
height: 18
Behavior on width {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutBack
easing.overshoot: 1.6
}
}
Rectangle {
id: tick
anchors.fill: parent
// Focused is a solid crimson block. Occupied is a white
// block at half weight. Empty is an outline. Three states,
// two colours, no ambiguity.
color: {
if (pip.focused || pip.urgent) return Theme.accent;
if (pip.hovered) return Theme.text;
if (pip.occupied) return Theme.alpha(Theme.text, 0.5);
return "transparent";
}
border.color: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
border.width: 1
transform: Matrix4x4 {
matrix: Qt.matrix4x4(
1, -Theme.skew, 0, pip.height * Theme.skew,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
// The focused workspace wears its number.
Text {
anchors.centerIn: parent
opacity: pip.focused ? 1 : 0
visible: opacity > 0.01
text: pip.wsId
color: Theme.ink
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering
Behavior on opacity {
NumberAnimation { duration: Theme.durBase }
}
}
// Fullscreen marker.
Rectangle {
visible: pip.info !== undefined && pip.info.fullscreen && !pip.focused
width: 4; height: 4
color: Theme.accent
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.bottom
anchors.topMargin: 1
}
HoverHandler {
id: pipHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: Hyprland.dispatch("workspace " + pip.wsId)
}
}
}
}
}
+120
View File
@@ -0,0 +1,120 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
// Radial gauge used across the resource popout. Sweeps a 270° arc with a
// tick-marked rail behind it and the reading set in the middle.
Item {
id: root
property real value: 0 // 0..100
property string label: ""
property string readout: Math.round(value) + "%"
property string sub: ""
property color arcColor: Theme.heat(value)
property real thickness: 7
property real startAngle: 135
property real sweep: 270
property int ticks: 24
implicitWidth: 96
implicitHeight: 96
readonly property real _r: Math.min(width, height) / 2 - thickness / 2 - 4
readonly property real _cx: width / 2
readonly property real _cy: height / 2
// Rail
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: Theme.alpha(Theme.outline, 0.5)
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep
}
}
}
// Tick marks around the rail.
Repeater {
model: root.ticks
Rectangle {
required property int index
readonly property real a: (root.startAngle + root.sweep * (index / (root.ticks - 1))) * Math.PI / 180
readonly property real rr: root._r + root.thickness / 2 + 3
width: 2
height: index % 4 === 0 ? 6 : 3
color: Theme.alpha(Theme.primary, index % 4 === 0 ? 0.55 : 0.25)
x: root._cx + Math.cos(a) * rr - width / 2
y: root._cy + Math.sin(a) * rr - height / 2
rotation: (root.startAngle + root.sweep * (index / (root.ticks - 1))) + 90
}
}
// Value arc
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: root.arcColor
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
id: valueArc
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep * Math.max(0, Math.min(100, root.value)) / 100
Behavior on sweepAngle {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
}
}
}
Column {
anchors.centerIn: parent
spacing: -2
SplitText {
anchors.horizontalCenter: parent.horizontalCenter
text: root.readout
pixelSize: Theme.fsLarge
color: Theme.text
split: 1.2
splitOpacity: 0.6
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.label
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: root.sub !== ""
text: root.sub
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
+123
View File
@@ -0,0 +1,123 @@
pragma Singleton
import QtQuick
import Quickshell
// Polygon maths for the Persona 5 panel language: everything leans, and corners
// get chopped rather than rounded.
Singleton {
id: root
// A right-leaning parallelogram filling w x h. `lean` is the horizontal
// offset applied to the top edge, as a fraction of height.
function parallelogram(w: real, h: real, lean: real): var {
const off = h * lean;
return [
Qt.point(off, 0),
Qt.point(w, 0),
Qt.point(w - off, h),
Qt.point(0, h)
];
}
// Same, leaning the other way.
function parallelogramL(w: real, h: real, lean: real): var {
const off = h * lean;
return [
Qt.point(0, 0),
Qt.point(w - off, 0),
Qt.point(w, h),
Qt.point(off, h)
];
}
// Vertical counterpart: the *horizontal* edges slant instead, by a fraction
// of width. A tall rail slab has to lean this way — leaning its vertical
// edges by a fraction of its height would shear it clean off the screen.
function parallelogramV(w: real, h: real, lean: real): var {
const off = w * lean;
return [
Qt.point(0, off),
Qt.point(w, 0),
Qt.point(w, h - off),
Qt.point(0, h)
];
}
function parallelogramVL(w: real, h: real, lean: real): var {
const off = w * lean;
return [
Qt.point(0, 0),
Qt.point(w, off),
Qt.point(w, h),
Qt.point(0, h - off)
];
}
function rect(w: real, h: real): var {
return [Qt.point(0, 0), Qt.point(w, 0), Qt.point(w, h), Qt.point(0, h)];
}
// Move `dist` px from `p` toward `q`.
function toward(p: point, q: point, dist: real): point {
const dx = q.x - p.x;
const dy = q.y - p.y;
const len = Math.hypot(dx, dy);
if (len < 0.0001) return p;
const t = Math.min(dist, len / 2) / len;
return Qt.point(p.x + dx * t, p.y + dy * t);
}
// Chop the listed corners of a polygon. `corners` is an array of indices;
// pass null to chop every corner.
function chamfer(pts: var, size: real, corners: var): var {
if (size <= 0) return root.closed(pts);
const n = pts.length;
const out = [];
for (let i = 0; i < n; i++) {
const p = pts[i];
if (corners !== null && corners.indexOf(i) === -1) {
out.push(p);
continue;
}
out.push(root.toward(p, pts[(i - 1 + n) % n], size));
out.push(root.toward(p, pts[(i + 1) % n], size));
}
return root.closed(out);
}
function closed(pts: var): var {
if (pts.length === 0) return pts;
const out = pts.slice();
out.push(pts[0]);
return out;
}
// Shrink a polygon toward its centroid — used for inner strokes.
function inset(pts: var, amount: real): var {
let cx = 0, cy = 0;
for (const p of pts) { cx += p.x; cy += p.y; }
cx /= pts.length; cy /= pts.length;
return pts.map(p => {
const dx = cx - p.x, dy = cy - p.y;
const len = Math.hypot(dx, dy);
if (len < 0.0001) return p;
const t = amount / len;
return Qt.point(p.x + dx * t, p.y + dy * t);
});
}
// A jagged "torn paper" edge, the Persona 5 speech-bubble signature.
// Returns a polygon `w` x `h` whose bottom edge is serrated.
function torn(w: real, h: real, teeth: int, depth: real): var {
const out = [Qt.point(0, 0), Qt.point(w, 0)];
const step = w / teeth;
for (let i = 0; i < teeth; i++) {
const x1 = w - i * step - step / 2;
const x2 = w - (i + 1) * step;
out.push(Qt.point(x1, h));
out.push(Qt.point(x2, h - depth));
}
return out;
}
}
+45
View File
@@ -0,0 +1,45 @@
import QtQuick
import "root:/config"
// Nerd Font glyph with an optional pulse, used for every status indicator.
Text {
id: root
property bool pulsing: false
property real pulseScale: 1.18
font.family: Theme.fontIcon
font.pixelSize: Theme.fsLarge
color: Theme.primary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
SequentialAnimation on scale {
running: root.pulsing
loops: Animation.Infinite
alwaysRunToEnd: true
NumberAnimation { to: root.pulseScale; duration: 620; easing.type: Easing.OutExpo }
NumberAnimation { to: 1.0; duration: 620; easing.type: Easing.InOutQuad }
}
function bump() {
bumpAnim.restart();
}
SequentialAnimation {
id: bumpAnim
NumberAnimation {
target: root; property: "scale"
to: 1.35; duration: Theme.durFast; easing.type: Easing.OutExpo
}
NumberAnimation {
target: root; property: "scale"
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
}
}
}
+62
View File
@@ -0,0 +1,62 @@
import QtQuick
import "root:/config"
// Horizontally scrolling text that only moves when it actually overflows, and
// pauses at each end so titles stay readable.
Item {
id: root
property string text: ""
property int pixelSize: Theme.fsSmall
property string family: Theme.fontDisplay
property int weight: Theme.weightBody
property bool italic: false
property color color: Theme.text
property real speed: 26 // px per second
property int pause: 1400 // ms held at each end
property bool running: true
readonly property bool overflowing: label.implicitWidth > width + 1
clip: true
implicitHeight: label.implicitHeight
implicitWidth: label.implicitWidth
Text {
id: label
y: 0
height: root.height
text: root.text
color: root.color
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
renderType: Text.NativeRendering
}
SequentialAnimation {
id: scroll
running: root.running && root.overflowing && root.visible
loops: Animation.Infinite
PauseAnimation { duration: root.pause }
NumberAnimation {
target: label; property: "x"
from: 0; to: Math.min(0, root.width - label.implicitWidth)
duration: Math.max(1, Math.abs(root.width - label.implicitWidth) / root.speed * 1000)
easing.type: Easing.Linear
}
PauseAnimation { duration: root.pause }
NumberAnimation {
target: label; property: "x"
to: 0
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
onOverflowingChanged: if (!overflowing) label.x = 0
onTextChanged: { label.x = 0; scroll.restart(); }
}
+14
View File
@@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Effects
// Layer effect that clips whatever it is applied to into the silhouette of
// `maskItem` — used to chop album art into the shell's panel shape.
MultiEffect {
property Item maskItem
maskEnabled: maskItem !== null
maskSource: maskItem
// Hard edge — the chopped corners should read as cuts, not fades.
maskThresholdMin: 0.5
maskSpreadAtMin: 0.0
}
+59
View File
@@ -0,0 +1,59 @@
import QtQuick
import "root:/config"
// A segmented load meter — slanted ticks that light up left to right. Reads as
// a gauge at a glance and keeps the panel language consistent.
Item {
id: root
property real value: 0 // 0..100
property int segments: 14
property real segmentWidth: 4
property real spacing: 3
property color activeColor: Theme.heat(value)
property color idleColor: Theme.alpha(Theme.outline, 0.55)
property real lean: Theme.skew
readonly property int litCount: Math.round(Math.max(0, Math.min(100, value)) / 100 * segments)
implicitWidth: segments * segmentWidth + (segments - 1) * spacing
implicitHeight: 14
Row {
anchors.fill: parent
spacing: root.spacing
Repeater {
model: root.segments
Item {
required property int index
width: root.segmentWidth
height: root.height
Rectangle {
width: root.segmentWidth
height: root.height
color: parent.index < root.litCount ? root.activeColor : root.idleColor
// Peak segments glow, so a pegged CPU is visible peripherally.
opacity: parent.index < root.litCount
? (parent.index >= root.segments - 2 ? 1.0 : 0.9)
: 1.0
transform: Matrix4x4 {
matrix: Qt.matrix4x4(
1, -root.lean, 0, root.height * root.lean,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
}
}
}
+91
View File
@@ -0,0 +1,91 @@
import QtQuick
import "root:/config"
import "root:/components"
// One metric in a popout: a label and its reading on a line, with a wide
// sheared segmented meter underneath.
//
// This replaces the radial Gauge. A 270° dial was the only curved thing in a
// shell whose whole geometry is sheared slabs and hard chamfers, so it read as
// though it had been imported from another design. The meter says the same
// thing in the language the bar and the OSD already speak, and it gets far more
// resolution across a popout's width than a 88px dial did.
Item {
id: root
property string label: ""
property real value: 0 // 0..100
property string readout: Math.round(root.value) + "%"
property string sub: ""
property color tone: Theme.heat(root.value)
property int segments: 24
property real segmentSpacing: 3
implicitHeight: head.height + Theme.padS + meter.height
Item {
id: head
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: Math.max(name.implicitHeight, figure.implicitHeight)
Text {
id: name
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.label.toUpperCase()
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
// The secondary figure — temperature, used-of-total. Sits next to the
// label rather than under the number so the right edge stays a single
// clean column of readings.
Text {
anchors.left: name.right
anchors.leftMargin: Theme.padM
anchors.right: figure.left
anchors.rightMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
visible: root.sub !== ""
text: root.sub
color: Theme.muted
elide: Text.ElideRight
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
SplitText {
// Not `value` — an id by that name shadows the `value` property, and
// the defaults above then round a Text item instead of a number.
id: figure
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: root.readout
pixelSize: Theme.fsBody
color: Theme.text
split: 1
splitOpacity: 0.5
}
}
MeterBar {
id: meter
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 12
segments: root.segments
spacing: root.segmentSpacing
// Fill the row exactly rather than leaving a ragged tail on the right.
segmentWidth: (width - (root.segments - 1) * root.segmentSpacing) / root.segments
value: root.value
activeColor: root.tone
}
}
+191
View File
@@ -0,0 +1,191 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "root:/config"
import "root:/services"
// One notification, used both in the history list and as a floating toast.
Item {
id: root
required property var notif
property bool showActions: true
readonly property color tone: root.notif
? Notifs.urgencyColor(root.notif.urgency)
: Theme.primary
signal dismissed()
implicitHeight: panel.height
P5Panel {
id: panel
width: parent.width
height: layout.implicitHeight + Theme.padM * 2
lean: 0.05
chop: 12
fill: Theme.surface
fillOpacity: 0.97
border: Theme.alpha(root.tone, hover.hovered ? 0.95 : 0.5)
slash: true
slashColor: root.tone
slashWidth: 4
halftone: root.notif?.urgency === NotificationUrgency.Critical
halftoneColor: Theme.accent
halftoneOpacity: 0.12
padding: Theme.padM
Column {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 3
// App line
Row {
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Notifs.appIconFor(root.notif)
color: root.tone
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: (root.notif?.appName || "System").toUpperCase()
color: root.tone
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 2
renderType: Text.NativeRendering
}
}
Text {
width: parent.width
text: root.notif?.summary || ""
color: Theme.text
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.WordWrap
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Font.Black
font.italic: true
renderType: Text.NativeRendering
}
Text {
width: parent.width
visible: text !== ""
text: root.notif?.body || ""
color: Theme.subtext
elide: Text.ElideRight
maximumLineCount: 4
wrapMode: Text.WordWrap
textFormat: Text.StyledText
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
renderType: Text.NativeRendering
}
// Inline image, when the sender supplied one.
Image {
visible: source != "" && status === Image.Ready
source: root.notif?.image || ""
sourceSize.height: 90
fillMode: Image.PreserveAspectFit
asynchronous: true
}
Row {
visible: root.showActions && (root.notif?.actions?.length ?? 0) > 0
spacing: Theme.padS
topPadding: Theme.padS
Repeater {
model: root.notif?.actions ?? []
Item {
id: actionChip
required property var modelData
implicitWidth: actionLabel.implicitWidth + Theme.padM
implicitHeight: 22
Rectangle {
anchors.fill: parent
color: actionHover.hovered
? Theme.alpha(root.tone, 0.3)
: Theme.alpha(root.tone, 0.1)
border.width: 1
border.color: Theme.alpha(root.tone, 0.6)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Text {
id: actionLabel
anchors.centerIn: parent
text: actionChip.modelData.text
color: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMicro
font.italic: true
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
HoverHandler {
id: actionHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
actionChip.modelData.invoke();
root.dismissed();
}
}
}
}
}
}
// Close affordance.
Icon {
anchors.right: parent.right
anchors.top: parent.top
visible: hover.hovered
text: "󰅖"
color: closeHover.hovered ? Theme.accent : Theme.muted
font.pixelSize: Theme.fsBody
HoverHandler {
id: closeHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: root.dismissed()
}
}
}
HoverHandler {
id: hover
}
// Middle click dismisses, matching the old dunst muscle memory.
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.dismissed()
}
}
+103
View File
@@ -0,0 +1,103 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
import "root:/components"
// The shared ground for the two full-screen surfaces: a crimson wedge leaning
// at the house angle, a tiled stripe field that sweeps in from the left, and a
// halftone dot wash over the top.
//
// The lock screen and the power menu each used to draw their own version of
// this. They disagreed — the power menu had a wedge and a full stripe field,
// the lock had a 42px stripe band and no wedge, and the wedge leaned at 0.32
// where every panel in the shell leans at Theme.skew. Building both on one
// component is what makes them read as the same object seen twice rather than
// as two guesses at the same brief.
Item {
id: root
// 0 = withdrawn, 1 = fully present. Drives the stripe slide, the stripe
// opacity and the wedge together, so a caller animates one number instead
// of keeping three in sync. The power menu runs it both ways on open and
// close; the lock runs it once on entry.
property real reveal: 1
property color tone: Theme.accent
property bool wedge: true
property real wedgeOpacity: 0.35
// Bottom-left corner of the wedge and its span, as fractions of width. The
// wedge leans right going up, like every other edge in the shell, so its
// top edge sits `leanOffset` further right than its bottom.
property real wedgeLeft: 0
property real wedgeSpan: 0.34
property bool stripes: true
property real stripeOpacity: 0.10
property bool halftone: true
property real halftoneOpacity: 0.05
property color halftoneColor: Theme.primary
// ONE angle, everywhere — the same rule the panels follow.
readonly property real leanOffset: root.height * Theme.skew
// Diagonal stripe field, sliding in from off-screen left.
Image {
anchors.fill: parent
visible: root.stripes && root.reveal > 0
source: Theme.texStripes
fillMode: Image.Tile
opacity: root.stripeOpacity * root.reveal
smooth: false
layer.enabled: true
layer.effect: TintEffect {
tintColor: root.tone
}
transform: Translate {
x: -root.width * (1 - root.reveal)
}
}
// The wedge. Drawn after the stripes so it reads as a slab laid over them.
Shape {
anchors.fill: parent
visible: root.wedge && root.reveal > 0
opacity: root.reveal
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.alpha(Theme.accentDim, root.wedgeOpacity)
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: {
const x = root.wedgeLeft * root.width;
const span = root.wedgeSpan * root.width;
const off = root.leanOffset;
return Geom.closed([
Qt.point(x + off, 0),
Qt.point(x + off + span, 0),
Qt.point(x + span, root.height),
Qt.point(x, root.height)
]);
}
}
}
}
// Screen-printed dot wash over the whole thing.
Image {
anchors.fill: parent
visible: root.halftone
source: Theme.texHalftone
fillMode: Image.Tile
opacity: root.halftoneOpacity
smooth: false
layer.enabled: true
layer.effect: TintEffect { tintColor: root.halftoneColor }
}
}
+106
View File
@@ -0,0 +1,106 @@
import QtQuick
import "root:/config"
// Chopped, leaning button. On hover it lunges toward the pointer and flips to
// crimson-on-white; on press it snaps flat.
Item {
id: root
property string text: ""
property string icon: ""
property color accent: Theme.primary
property color hoverFill: Theme.accent
property bool destructive: false
property real lean: Theme.skew
property real lunge: 6
property bool wide: false
signal clicked()
readonly property bool hovered: hover.hovered
readonly property bool pressed: tap.pressed
implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2)
implicitHeight: 44
P5Panel {
id: panel
anchors.fill: parent
lean: root.lean
padding: Theme.padS
fill: root.hovered ? (root.destructive ? root.hoverFill : Theme.text) : Theme.surface
fillOpacity: root.hovered ? 1.0 : 0.9
border: root.hovered ? "transparent" : Theme.alpha(root.accent, 0.55)
slash: !root.hovered
slashColor: root.destructive ? Theme.accent : root.accent
halftone: root.hovered
halftoneColor: root.destructive ? Theme.text : Theme.base
halftoneOpacity: 0.14
Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast }
}
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
Icon {
anchors.verticalCenter: parent.verticalCenter
visible: root.icon !== ""
text: root.icon
font.pixelSize: Theme.fsTitle
color: root.hovered
? (root.destructive ? Theme.text : Theme.base)
: root.accent
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.text
color: root.hovered
? (root.destructive ? Theme.text : Theme.base)
: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 1.5
font.capitalization: Font.AllUppercase
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
}
}
transform: Translate {
x: root.pressed ? 0 : (root.hovered ? root.lunge : 0)
Behavior on x {
NumberAnimation {
duration: Theme.durBase
easing.type: Easing.OutBack
}
}
}
scale: root.pressed ? 0.97 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutExpo }
}
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
id: tap
onTapped: root.clicked()
}
}
+248
View File
@@ -0,0 +1,248 @@
import QtQuick
import QtQuick.Shapes
import QtQuick.Effects
import "root:/config"
import "root:/components"
// The base surface for everything in this shell: a leaning, corner-chopped slab
// with an optional halftone wash and a crimson slash along its leading edge.
Item {
id: root
property color fill: Theme.surface
property real fillOpacity: 0.94
property color border: Theme.outline
property real borderWidth: Theme.stroke
// Lean of the vertical edges, as a fraction of height. 0 = upright.
property real lean: Theme.skew
property bool leanLeft: false
// Tall panels (the right-hand rail) slant their horizontal edges instead,
// by a fraction of width, and inset their content top/bottom rather than
// left/right.
property bool vertical: false
// Corner chop size, and which corners get chopped (indices into the
// polygon, clockwise from the top-left). Default chops the two corners that
// read as "cut" against the lean.
property real chop: Theme.cut
property var chopCorners: [1, 3]
// Crimson slash down the leading edge — the Persona 5 tell.
property bool slash: false
property color slashColor: Theme.accent
property real slashWidth: 4
// Screen-printed dot wash.
property bool halftone: false
property color halftoneColor: Theme.primary
property real halftoneOpacity: 0.07
// Accepted and ignored. This used to paint a teal-to-transparent gradient
// down the top of every panel in the shell — the single biggest source of
// the murky blue cast. Fills are flat now. Kept as a no-op property so the
// call sites that still set it don't have to be chased down.
property bool sheen: false
// The screen-print stack. A pure-black copy of the silhouette sits behind
// the fill, offset down-and-right, and the fill carries a black keyline
// outside its coloured border. Together these are what stop the panel from
// reading as a flat clip-path.
property bool drop: true
property real dropOffset: Theme.dropOffset
property color dropColor: Theme.ink
property bool keyline: true
property real keylineWidth: Theme.keylineWidth
readonly property real leanInset:
(vertical ? width : height) * Math.abs(lean)
readonly property var polygon: vertical
? (leanLeft ? Geom.parallelogramVL(width, height, lean)
: Geom.parallelogramV(width, height, lean))
: (leanLeft ? Geom.parallelogramL(width, height, lean)
: Geom.parallelogram(width, height, lean))
// Child content lives inside `contentItem`, inset on both sides far enough
// to clear the slanted edges. Panels do not auto-size: callers set implicit
// sizes from their own layout (`row.implicitWidth + contentPad * 2`), which
// keeps the shape maths out of every binding loop.
default property alias content: contentHolder.data
readonly property alias contentItem: contentHolder
property real padding: Theme.padM
readonly property real contentPad: padding + leanInset
// The panel outline as a mask texture. Anything that is painted from the
// raw polygon rather than from the chamfered one has to be clipped to this,
// or it escapes at the chopped corners.
Shape {
id: silhouette
anchors.fill: parent
visible: false
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "white"
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
}
// Offset black copy of the silhouette, behind everything.
Loader {
anchors.fill: parent
active: root.drop && root.dropOffset > 0
sourceComponent: Shape {
preferredRendererType: Shape.CurveRenderer
asynchronous: false
transform: Translate {
x: root.dropOffset * (root.leanLeft ? -1 : 1)
y: root.dropOffset
}
ShapePath {
fillColor: root.dropColor
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
}
}
Shape {
id: body
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: false
// Fill, carrying the black keyline. The stroke is centred on the path,
// so half of it sits outside the silhouette — which is exactly the
// heavy outer contour the style wants.
ShapePath {
fillColor: Theme.alpha(root.fill, root.fillOpacity)
strokeColor: root.keyline ? root.dropColor : "transparent"
strokeWidth: root.keyline
? root.borderWidth + root.keylineWidth * 2
: 0
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
// Coloured border, drawn on top of the keyline so it reads as an inlay.
ShapePath {
fillColor: "transparent"
strokeColor: root.borderWidth > 0 ? root.border : "transparent"
strokeWidth: root.borderWidth
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
}
// Halftone wash, masked to the panel silhouette.
Loader {
anchors.fill: parent
active: root.halftone
asynchronous: true
sourceComponent: Image {
anchors.fill: parent
source: Theme.texHalftone
fillMode: Image.Tile
opacity: root.halftoneOpacity
smooth: false
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1.0
colorizationColor: root.halftoneColor
maskEnabled: true
maskSource: silhouette
}
}
}
// Leading-edge slash.
//
// Its polygon is built from the panel's raw corners, so on a panel with a
// chopped corner the slash used to run straight through the cut and out the
// far side — a crimson wedge sitting a few pixels outside the silhouette,
// at an angle that matched nothing. It is masked to the outline now, so the
// slash ends exactly where the chamfer does.
Loader {
anchors.fill: parent
active: root.slash
sourceComponent: Item {
Shape {
id: slashShape
anchors.fill: parent
visible: false
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: root.slashColor
strokeColor: "transparent"
PathPolyline {
path: {
const t = root.slashWidth;
// On a rail slab the slash runs along the slanted
// top edge instead of down a side.
if (root.vertical) {
const voff = root.width * root.lean;
return root.leanLeft
? Geom.closed([Qt.point(0, 0), Qt.point(root.width, voff),
Qt.point(root.width, voff + t), Qt.point(0, t)])
: Geom.closed([Qt.point(0, voff), Qt.point(root.width, 0),
Qt.point(root.width, t), Qt.point(0, voff + t)]);
}
const off = root.height * root.lean;
return root.leanLeft
? Geom.closed([Qt.point(0, 0), Qt.point(t, 0),
Qt.point(off + t, root.height), Qt.point(off, root.height)])
: Geom.closed([Qt.point(off, 0), Qt.point(off + t, 0),
Qt.point(t, root.height), Qt.point(0, root.height)]);
}
}
}
}
MultiEffect {
anchors.fill: parent
source: slashShape
maskEnabled: true
maskSource: silhouette
}
}
}
Item {
id: contentHolder
anchors.fill: parent
anchors.leftMargin: root.vertical ? root.padding : root.contentPad
anchors.rightMargin: root.vertical ? root.padding : root.contentPad
anchors.topMargin: root.vertical ? root.contentPad : root.padding
anchors.bottomMargin: root.vertical ? root.contentPad : root.padding
}
}
+99
View File
@@ -0,0 +1,99 @@
import QtQuick
import "root:/config"
// Leaning slider with a chopped handle. Drag or click anywhere on the rail.
Item {
id: root
property real value: 0 // 0..1
property color accent: Theme.primary
property bool enabledControl: true
signal moved(real value)
implicitHeight: 18
implicitWidth: 200
readonly property real _lean: Theme.skew * height
// Rail
Rectangle {
id: rail
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
height: 6
color: Theme.alpha(Theme.outline, 0.7)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 6 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
// Fill
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: Math.max(0, Math.min(1, root.value)) * root.width
height: 6
color: root.enabledControl ? root.accent : Theme.muted
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 6 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
Behavior on width {
enabled: !drag.active
NumberAnimation { duration: Theme.durFast }
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
// Handle
Rectangle {
id: handle
width: 6
height: hover.hovered || drag.active ? 18 : 14
color: root.enabledControl ? Theme.text : Theme.muted
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(1, root.value)) * root.width - width / 2
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, height * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
Behavior on height {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack }
}
Behavior on x {
enabled: !drag.active
NumberAnimation { duration: Theme.durFast }
}
}
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: event => root.moved(Math.max(0, Math.min(1, event.position.x / root.width)))
}
DragHandler {
id: drag
target: null
xAxis.enabled: true
yAxis.enabled: false
onCentroidChanged: {
if (!active) return;
root.moved(Math.max(0, Math.min(1, centroid.position.x / root.width)));
}
}
}
+157
View File
@@ -0,0 +1,157 @@
import QtQuick
import Quickshell
import "root:/config"
// Hover-driven panel that hangs off a bar item. Opens on a short dwell, stays
// open while the pointer is over either the trigger or the panel itself, and
// can be pinned with a click. Slides in from behind the bar with an overshoot.
PopupWindow {
id: root
required property Item anchorItem
property bool triggerHovered: false
property bool pinned: false
property int showDelay: 140
property int hideDelay: 200
property int fromEdge: Edges.Bottom
property real contentWidth: Theme.popoutWidth
property real contentHeight: 200
// Extra surface around the content so the entry overshoot and glow are not
// clipped by the window edge.
readonly property int bleed: 24
default property alias body: contentHolder.data
readonly property bool wantsOpen: triggerHovered || panelHover.hovered || pinned
signal dismissedFully()
// Panels hanging off the vertical rail open sideways, so the entry slide has
// to follow whichever axis the popout actually travels along, and start on
// the side nearest its trigger.
readonly property bool horizontal:
fromEdge === Edges.Left || fromEdge === Edges.Right
readonly property string slideProperty: horizontal ? "x" : "y"
readonly property real slideOrigin: bleed + (fromEdge === Edges.Left
|| fromEdge === Edges.Top ? 14 : -14)
anchor.item: anchorItem
anchor.edges: fromEdge
anchor.gravity: fromEdge
anchor.adjustment: horizontal
? PopupAdjustment.SlideY
: PopupAdjustment.SlideX
anchor.margins.top: Theme.popoutGap
anchor.margins.bottom: Theme.popoutGap
anchor.margins.left: Theme.popoutGap
anchor.margins.right: Theme.popoutGap
implicitWidth: contentWidth + bleed * 2
implicitHeight: contentHeight + bleed * 2
color: "transparent"
visible: shown
property bool shown: false
onWantsOpenChanged: {
if (wantsOpen) {
hideTimer.stop();
showTimer.restart();
} else {
showTimer.stop();
hideTimer.restart();
}
}
Timer {
id: showTimer
interval: root.showDelay
onTriggered: root.open()
}
Timer {
id: hideTimer
interval: root.hideDelay
onTriggered: root.dismiss()
}
function open() {
exitAnim.stop();
shown = true;
enterAnim.restart();
}
function dismiss() {
if (!shown) return;
enterAnim.stop();
exitAnim.restart();
}
Item {
id: contentHolder
x: root.bleed
y: root.bleed
width: root.contentWidth
height: root.contentHeight
opacity: 0
transformOrigin: {
switch (root.fromEdge) {
case Edges.Left: return Item.Right;
case Edges.Right: return Item.Left;
case Edges.Top: return Item.Bottom;
default: return Item.Top;
}
}
scale: 0.94
// The handler has to live on the content item, not the window: a
// HoverHandler parented to the PopupWindow itself latches on and the
// panel never learns the pointer left.
HoverHandler {
id: panelHover
}
}
ParallelAnimation {
id: enterAnim
NumberAnimation {
target: contentHolder; property: "opacity"
to: 1.0; duration: Theme.durBase; easing.type: Easing.OutExpo
}
NumberAnimation {
target: contentHolder; property: "scale"
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
}
NumberAnimation {
target: contentHolder; property: root.slideProperty
from: root.slideOrigin; to: root.bleed
duration: Theme.durSlow; easing.type: Easing.OutBack
}
}
ParallelAnimation {
id: exitAnim
NumberAnimation {
target: contentHolder; property: "opacity"
to: 0.0; duration: Theme.durFast; easing.type: Easing.InQuad
}
NumberAnimation {
target: contentHolder; property: "scale"
to: 0.96; duration: Theme.durFast; easing.type: Easing.InQuad
}
NumberAnimation {
target: contentHolder; property: root.slideProperty
to: root.bleed + (root.slideOrigin - root.bleed) * 0.6
duration: Theme.durFast; easing.type: Easing.InQuad
}
onFinished: {
root.shown = false;
root.pinned = false;
contentHolder.x = root.bleed;
contentHolder.y = root.bleed;
root.dismissedFully();
}
}
}
+96
View File
@@ -0,0 +1,96 @@
import QtQuick
import "root:/config"
// Display text with the wallpaper's RGB split baked in: a crimson and a cyan
// ghost sitting a pixel off the white body. Drives most headings in the shell.
Item {
id: root
property string text: ""
property int pixelSize: Theme.fsLarge
property int weight: Theme.weightDisplay
property string family: Theme.fontDisplay
property bool italic: true
property color color: Theme.text
property real split: 1.6
property real splitOpacity: 0.85
property int elide: Text.ElideNone
property int horizontalAlignment: Text.AlignLeft
property real letterSpacing: 0.5
implicitWidth: body.implicitWidth
implicitHeight: body.implicitHeight
Text {
id: ghostRed
anchors.fill: parent
anchors.leftMargin: -root.split
anchors.topMargin: root.split * 0.4
text: root.text
color: Theme.splitRed
opacity: root.splitOpacity
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
}
Text {
id: ghostCyan
anchors.fill: parent
anchors.leftMargin: root.split
anchors.topMargin: -root.split * 0.4
text: root.text
color: Theme.splitCyan
opacity: root.splitOpacity
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
}
Text {
id: body
anchors.fill: parent
text: root.text
color: root.color
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
}
// Kick the ghosts outward briefly — used on track changes, mode flips, etc.
function glitch() {
glitchAnim.restart();
}
SequentialAnimation {
id: glitchAnim
NumberAnimation {
target: root; property: "split"
to: 5.5; duration: Theme.durFast
easing.type: Easing.OutExpo
}
NumberAnimation {
target: root; property: "split"
to: 1.6; duration: Theme.durSlow
easing.type: Easing.OutBack
}
}
}
+11
View File
@@ -0,0 +1,11 @@
import QtQuick
import QtQuick.Effects
// Layer effect that recolours whatever it is applied to — used to tint the
// white texture tiles without shipping a copy per colour.
MultiEffect {
property color tintColor: "white"
colorization: 1.0
colorizationColor: tintColor
}
+160
View File
@@ -0,0 +1,160 @@
pragma Singleton
import QtQuick
import Quickshell
// Takemi palette: crimson, black, white. Nothing else.
//
// White does the work: text, icons, idle indicators. Crimson is the only accent
// and it always means the same thing — active, or wrong. Black is structure,
// used as a hard keyline rather than as a shadow. There are no gradients in
// this shell; every fill is flat.
//
// The wallpaper is teal. The UI used to be teal too, and the two fought each
// other into a murky blue-grey. Letting the UI go monochrome-plus-red is what
// lets the wallpaper read as the colour in the composition.
Singleton {
id: root
// ---------------------------------------------------------------- colors
readonly property color ink: "#000000"
readonly property color base: "#0a0a0c"
readonly property color mantle: "#0d0d10"
readonly property color surface: "#141418"
readonly property color surfaceAlt: "#1c1c22"
readonly property color overlay: "#26262e"
readonly property color outline: "#3a3a44"
readonly property color text: "#ffffff"
readonly property color subtext: "#c8c8ce"
readonly property color muted: "#6b6b70"
// ------------------------------------------------------------ THE KNOB
// The one colour in this shell. Change this line and the entire accent
// recolours — everything below is derived from it, and nothing else in the
// palette carries hue at all.
//
// Keep it in step with `$accent` at the top of
// ~/.config/hypr/modules/takemi.conf, which is what colours the Hyprland
// window borders. Those are the only two places the colour is written.
readonly property color accent: "#ff2d40"
readonly property color accentDim: Qt.darker(root.accent, 1.9)
readonly property color accentSoft: Qt.lighter(root.accent, 1.35)
// `primary` is the resting voice of the UI, which is now simply white.
// Kept under the old name so the existing call sites don't all have to move.
readonly property color primary: root.text
readonly property color primaryDim: "#9a9aa2"
readonly property color deep: root.surfaceAlt
// `glow` is the hover highlight. It used to be a brighter teal, which now
// that everything rests at white would have made hover invisible — so it
// resolves to crimson. Pointing at a thing lights it up red.
readonly property color glow: root.accent
// Retired hues. Aliases only — do not introduce new uses.
//
// `warn` used to be accentSoft, which put a washed-out pink-red on screen
// wherever something was merely notable. That reads as a faded accent, not
// as a decision. There is no half-crimson in this palette: a thing is wrong
// (accent) or it is not (text). `warn` now resolves to the real accent, so
// low battery shouts properly and everything merely informational is white.
readonly property color blue: root.primaryDim
readonly property color ok: root.text
readonly property color warn: root.accent
// Display text carries a crimson and a black offset rather than an RGB
// split. Same trick, correct palette.
readonly property color splitRed: root.accent
readonly property color splitCyan: root.ink
function alpha(c: color, a: real): color {
return Qt.rgba(c.r, c.g, c.b, a);
}
function mix(a: color, b: color, t: real): color {
const k = Math.max(0, Math.min(1, t));
return Qt.rgba(a.r + (b.r - a.r) * k,
a.g + (b.g - a.g) * k,
a.b + (b.b - a.b) * k,
a.a + (b.a - a.a) * k);
}
// Load readouts sit white until the number actually matters, then bleed to
// crimson. Not a hue cycle — red always means the same thing.
function heat(pct: real): color {
if (pct <= 55) return root.text;
return root.mix(root.text, root.accent, (pct - 55) / 45);
}
// ----------------------------------------------------------------- fonts
readonly property string fontDisplay: "Archivo Black"
readonly property string fontMono: "JetBrainsMono Nerd Font"
readonly property string fontIcon: "JetBrainsMono Nerd Font"
readonly property int weightDisplay: Font.Black
readonly property int weightBody: Font.Medium
readonly property int fsMicro: 9
readonly property int fsSmall: 11
readonly property int fsBody: 12
readonly property int fsLarge: 15
readonly property int fsTitle: 20
readonly property int fsHuge: 34
readonly property int fsMega: 92
// -------------------------------------------------------------- geometry
readonly property int barHeight: 40
readonly property int barMargin: 8
readonly property int barGap: 8
// The right-hand rail uses the same lean and chamfer as the bar — a
// vertical panel leans its horizontal edges by a fraction of its width,
// which works out to the same 14 degrees.
readonly property int railWidth: 48
// ONE angle, everywhere. tan(14deg). Persona geometry is disciplined, not
// random: every edge in the shell leans by exactly this much in exactly the
// same direction, and every corner is chopped by exactly `cut`. Varying
// these per module reads as noise, not as style.
readonly property real skew: 0.249
readonly property int cut: 10
readonly property int stroke: 2
// Pure-black keyline and offset copy behind panels. This is the one piece of
// layering the style needs; it is not a soft shadow and never blurs.
readonly property real dropOffset: 3
readonly property real keylineWidth: 2
readonly property int padS: 6
readonly property int padM: 12
readonly property int padL: 20
readonly property int padXL: 32
readonly property int popoutWidth: 380
readonly property int popoutGap: 10
// ------------------------------------------------------------ animation
// P5 motion is snappy and slightly overshooting — never soft.
readonly property int durFast: 110
readonly property int durBase: 190
readonly property int durSlow: 340
readonly property int durLazy: 620
readonly property var easeSnap: [0.16, 1.0, 0.3, 1.0, 1, 1] // outExpo-ish
readonly property var easeBack: [0.34, 1.56, 0.64, 1.0, 1, 1] // overshoot
readonly property var easeIn: [0.55, 0.0, 0.9, 0.35, 1, 1]
// -------------------------------------------------------------- textures
readonly property url texHalftone: "root:/assets/halftone.png"
readonly property url texStripes: "root:/assets/stripes.png"
readonly property url texScanline: "root:/assets/scanline.png"
// Last resort only. The live wallpaper comes from the Wallpaper singleton,
// which asks hyprpaper what is actually on screen — a path written down
// here goes stale the moment the wallpaper is switched, which is exactly
// what happened to the lock screen.
readonly property url fallbackWallpaper: "file:///home/narl/.wallpapers/takemi_1.jpg"
}
+29
View File
@@ -0,0 +1,29 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/services"
import "root:/lock"
// Session lock. Replaces hyprlock; authenticates through the existing
// /etc/pam.d/hyprlock stack, which is just `auth include login`.
//
// The visuals live in LockFace so they can be exercised in an ordinary window
// (see lockpreview.qml) without locking the session to test a colour.
WlSessionLock {
id: lock
locked: Actions.lockRequested
WlSessionLockSurface {
id: lockSurface
color: "black"
LockFace {
anchors.fill: parent
// So each screen blurs its own wallpaper rather than all of them
// sharing whichever one hyprpaper happened to list first.
screenName: lockSurface.screen ? lockSurface.screen.name : ""
onUnlocked: Actions.lockRequested = false
}
}
}
+429
View File
@@ -0,0 +1,429 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Pam
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/lock"
// The lock screen's whole face, kept separate from WlSessionLockSurface so it
// can be instantiated in an ordinary window for development without actually
// locking the session.
Item {
id: surface
// Emitted once PAM has accepted the password.
signal unlocked()
property string entry: ""
property string status: ""
property bool failed: false
property bool busy: pam.active
Rectangle {
anchors.fill: parent
color: Theme.base
}
// ------------------------------------------------------- background
// Whatever hyprpaper is actually displaying, not a path written down once
// and left to rot. `screenName` lets a multi-monitor lock show each screen
// its own picture; it falls back to the first wallpaper hyprpaper reports.
property string screenName: ""
Image {
id: wall
anchors.fill: parent
source: surface.screenName !== ""
? Wallpaper.forMonitor(surface.screenName)
: Wallpaper.source
fillMode: Image.PreserveAspectCrop
asynchronous: false
cache: true
layer.enabled: true
layer.effect: MultiEffect {
blurEnabled: true
blur: 0.55
blurMax: 48
saturation: -0.15
brightness: -0.35
}
}
// Wash pulling the photo down toward the shell's ground. Was described as
// a teal wash back when the UI was teal; the colours it actually uses are
// the neutral surfaces, which is what it should have been all along.
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: Theme.alpha(Theme.deep, 0.55) }
GradientStop { position: 0.6; color: Theme.alpha(Theme.base, 0.75) }
GradientStop { position: 1.0; color: Theme.alpha(Theme.base, 0.92) }
}
}
// The same wedge-and-stripes ground the power menu stands on. The wedge
// sits under the clock, which is where the power menu puts it under the
// title — so the two screens land the crimson mass in the same place.
P5Backdrop {
id: backdrop
anchors.fill: parent
reveal: 0
wedgeSpan: 0.30
wedgeOpacity: 0.30
stripeOpacity: 0.07
}
// ------------------------------------------------------------ clock
SystemClock {
id: clock
precision: SystemClock.Minutes
}
// The house mark, same wordmark the bar carries far left. Small, above the
// clock — the lock's hero is the time, not the branding.
SplitText {
id: mark
anchors.left: timeBlock.left
anchors.bottom: timeBlock.top
anchors.bottomMargin: 6
text: "P5"
pixelSize: Theme.fsTitle
split: 2
letterSpacing: 0
opacity: timeBlock.opacity
}
Column {
id: timeBlock
anchors.left: parent.left
anchors.leftMargin: parent.width * 0.1
anchors.top: parent.top
anchors.topMargin: parent.height * 0.18
spacing: -18
SplitText {
text: Qt.formatDateTime(clock.date, "HH")
pixelSize: Theme.fsMega * 1.7
split: 5
splitOpacity: 0.85
}
SplitText {
text: Qt.formatDateTime(clock.date, "mm")
pixelSize: Theme.fsMega * 1.7
color: Theme.primary
split: 5
splitOpacity: 0.6
}
Text {
text: Qt.formatDateTime(clock.date, "dddd, dd MMMM").toUpperCase()
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsBody
font.letterSpacing: 6
topPadding: 34
renderType: Text.NativeRendering
}
}
// ------------------------------------------------------ status rail
Column {
anchors.right: parent.right
anchors.rightMargin: parent.width * 0.1
anchors.top: parent.top
anchors.topMargin: parent.height * 0.2
spacing: Theme.padM
width: 300
RailRow {
width: parent.width
visible: Battery.available
glyph: Battery.icon
label: Math.round(Battery.percent) + "%"
detail: Battery.timeLabel
tone: Battery.tint
}
RailRow {
width: parent.width
glyph: Net.icon
label: Net.label
detail: Sys.netUp ? Sys.netIp : "no link"
tone: Sys.netUp ? Theme.primary : Theme.muted
}
RailRow {
width: parent.width
visible: Media.hasPlayer
glyph: Media.playing ? "󰝚" : "󰏤"
label: Media.title
detail: Media.artist
tone: Theme.primary
}
RailRow {
width: parent.width
visible: Notifs.count > 0
glyph: "󱅫"
label: Notifs.count + " notification" + (Notifs.count === 1 ? "" : "s")
detail: Notifs.history.length > 0 ? Notifs.history[0].summary : ""
tone: Theme.accent
}
}
// ------------------------------------------------------- auth panel
Item {
id: authBlock
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: parent.height * 0.14
width: 460
height: 150
transform: Translate { id: shakeShift }
SplitText {
id: prompt
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
text: surface.failed
? (surface.status || "WRONG").toUpperCase()
: (surface.busy ? "CHECKING…" : "WHO GOES THERE")
color: surface.failed ? Theme.accent : Theme.text
pixelSize: Theme.fsTitle
letterSpacing: 4
horizontalAlignment: Text.AlignHCenter
split: surface.failed ? 4 : 1.6
}
// Password field: one chopped block per character.
P5Panel {
id: field
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: prompt.bottom
anchors.topMargin: Theme.padL
width: 380
height: 56
lean: 0.12
chop: 16
fill: Theme.mantle
fillOpacity: 0.94
border: surface.failed
? Theme.accent
: (input.activeFocus ? Theme.primary : Theme.outline)
borderWidth: 2
slash: true
slashColor: surface.failed ? Theme.accent : Theme.primary
slashWidth: 6
halftone: true
halftoneColor: surface.failed ? Theme.accent : Theme.primary
halftoneOpacity: 0.08
Row {
anchors.centerIn: parent
spacing: 6
visible: surface.entry.length > 0
Repeater {
model: Math.min(surface.entry.length, 22)
Rectangle {
required property int index
width: 10
height: 20
color: surface.failed ? Theme.accent : Theme.primary
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 20 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
// Each new block snaps in.
Component.onCompleted: pop.start()
NumberAnimation {
id: pop
target: parent
property: "scale"
from: 0.2; to: 1
duration: Theme.durBase
easing.type: Easing.OutBack
}
}
}
}
Text {
anchors.centerIn: parent
visible: surface.entry.length === 0 && !surface.busy
text: "PASSWORD"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 5
renderType: Text.NativeRendering
}
// Busy sweep.
Rectangle {
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
width: 60
height: 3
color: Theme.primary
visible: surface.busy
XAnimator on x {
running: surface.busy
loops: Animation.Infinite
from: 20
to: field.width - 80
duration: 700
easing.type: Easing.InOutQuad
}
}
// The real input, invisible but focused.
TextInput {
id: input
anchors.fill: parent
opacity: 0
focus: true
echoMode: TextInput.Password
enabled: !surface.busy
activeFocusOnTab: true
onTextChanged: {
surface.entry = text;
if (surface.failed && text.length === 0) surface.failed = false;
}
onAccepted: surface.submit()
Keys.onEscapePressed: {
input.text = "";
surface.failed = false;
}
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: field.bottom
anchors.topMargin: Theme.padM
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: "󰌾"
color: Theme.muted
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: Quickshell.env("USER") || "narl"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 3
renderType: Text.NativeRendering
}
}
}
// Keep focus on the field no matter what.
Timer {
interval: 250
running: true
repeat: true
onTriggered: if (!input.activeFocus && !surface.busy) input.forceActiveFocus()
}
function submit() {
if (surface.busy || surface.entry.length === 0) return;
surface.failed = false;
surface.status = "";
pam.start();
}
PamContext {
id: pam
config: "hyprlock"
user: Quickshell.env("USER") || "narl"
onPamMessage: {
if (pam.responseRequired) pam.respond(surface.entry);
else if (pam.message) surface.status = pam.message;
}
onCompleted: result => {
if (result === PamResult.Success) {
surface.unlocked();
input.text = "";
surface.entry = "";
surface.failed = false;
} else {
surface.failed = true;
if (surface.status === "") surface.status = "Access denied";
input.text = "";
surface.entry = "";
shake.restart();
}
}
onError: err => {
surface.failed = true;
surface.status = "PAM error";
input.text = "";
surface.entry = "";
shake.restart();
}
}
SequentialAnimation {
id: shake
loops: 2
NumberAnimation {
target: shakeShift; property: "x"
to: -16; duration: 55; easing.type: Easing.OutQuad
}
NumberAnimation {
target: shakeShift; property: "x"
to: 16; duration: 55; easing.type: Easing.OutQuad
}
NumberAnimation {
target: shakeShift; property: "x"
to: 0; duration: 90; easing.type: Easing.OutBack
}
}
// Entry flourish when the lock comes up.
Component.onCompleted: intro.start()
ParallelAnimation {
id: intro
// The stripes sweep in under everything, the same move the power menu
// opens with.
NumberAnimation {
target: backdrop; property: "reveal"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation {
target: timeBlock; property: "opacity"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation {
target: authBlock; property: "opacity"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation {
target: shakeShift; property: "y"
from: 40; to: 0; duration: Theme.durLazy; easing.type: Easing.OutBack
}
}
}
+64
View File
@@ -0,0 +1,64 @@
import QtQuick
import "root:/config"
import "root:/components"
// One line of the lock screen's status rail.
Item {
id: rail
property string glyph: ""
property string label: ""
property string detail: ""
property color tone: Theme.primary
implicitHeight: visible ? 38 : 0
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: 26
color: rail.tone
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: rail.glyph
color: rail.tone
font.pixelSize: Theme.fsLarge
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: -2
Text {
text: rail.label
color: Theme.text
elide: Text.ElideRight
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
Text {
visible: rail.detail !== ""
text: rail.detail
color: Theme.muted
elide: Text.ElideRight
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/lock"
// Development harness: renders the lock screen in an ordinary window so it can
// be styled and screenshotted without locking the session.
//
// qs -p lockpreview.qml
//
// PAM still runs for real here, so a correct password simply closes the window.
ShellRoot {
FloatingWindow {
implicitWidth: 1600
implicitHeight: 900
color: Theme.base
LockFace {
anchors.fill: parent
onUnlocked: console.log("PAM accepted — the real lock would release here")
}
}
}
+199
View File
@@ -0,0 +1,199 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
// Volume / mic / brightness heads-up, bottom centre. Appears on change and
// fades out on its own.
PanelWindow {
id: root
required property var modelData
screen: modelData
readonly property bool primaryScreen: Quickshell.screens.length === 0
|| Quickshell.screens[0] === modelData
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-osd"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors { bottom: true }
margins.bottom: 90
implicitWidth: 300
implicitHeight: 78
color: "transparent"
exclusionMode: ExclusionMode.Ignore
visible: primaryScreen && (shown || fade.running)
property bool shown: false
property string kind: "volume"
// Suppress the burst of change signals that fires while services warm up.
property bool armed: false
Timer {
interval: 1500
running: true
onTriggered: root.armed = true
}
readonly property string glyph: {
switch (root.kind) {
case "mic": return Audio.micIcon;
case "brightness": return Brightness.icon;
default: return Audio.icon;
}
}
readonly property string title: {
switch (root.kind) {
case "mic": return Audio.micMuted ? "Mic muted" : "Microphone";
case "brightness": return "Brightness";
default: return Audio.muted ? "Muted" : "Volume";
}
}
readonly property real level: {
switch (root.kind) {
case "mic": return Audio.micMuted ? 0 : Audio.micVolume;
case "brightness": return Brightness.percent / 100;
default: return Audio.muted ? 0 : Audio.volume;
}
}
readonly property color tone: {
switch (root.kind) {
case "mic": return Audio.micMuted ? Theme.accent : Theme.blue;
// Turning the screen up is not a warning. White, like volume.
case "brightness": return Theme.text;
default: return Audio.muted ? Theme.accent : Theme.primary;
}
}
function flash(k: string) {
if (!root.armed) return;
root.kind = k;
root.shown = true;
glyphIcon.bump();
hold.restart();
}
Timer {
id: hold
interval: 1600
onTriggered: root.shown = false
}
Connections {
target: Audio
function onVolumeBumped() { root.flash("volume"); }
function onMicBumped() { root.flash("mic"); }
}
Connections {
target: Brightness
function onBumped() { root.flash("brightness"); }
}
P5Panel {
id: panel
anchors.fill: parent
anchors.margins: 4
fill: Theme.mantle
fillOpacity: 0.97
border: Theme.alpha(root.tone, 0.85)
borderWidth: Theme.stroke
slash: true
slashColor: root.tone
slashWidth: 5
halftone: true
halftoneColor: root.tone
halftoneOpacity: 0.08
sheen: true
lean: 0.09
chop: 14
padding: Theme.padM
opacity: root.shown ? 1 : 0
scale: root.shown ? 1 : 0.9
Behavior on opacity {
NumberAnimation { id: fade; duration: Theme.durBase }
}
Behavior on scale {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutBack }
}
Icon {
id: glyphIcon
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.glyph
color: root.tone
font.pixelSize: 30
width: 36
}
Column {
anchors.left: glyphIcon.right
anchors.leftMargin: Theme.padM
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 4
// Title left, readout hard right. This used to be a Row with a
// `parent.width - 90` spacer, which assumed the two labels never
// needed more than 90px between them — "BRIGHTNESS" plus "100%"
// does, and the percentage fell off the right edge. Anchoring the
// readout to the right and letting the title elide into whatever
// is left makes the widest case the safe one.
Item {
width: parent.width
height: Math.max(titleLabel.implicitHeight, readout.implicitHeight)
Text {
id: titleLabel
anchors.left: parent.left
anchors.right: readout.left
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
text: root.title.toUpperCase()
color: Theme.subtext
elide: Text.ElideRight
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
SplitText {
id: readout
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
// Natural width, not a fixed one — SplitText offsets its
// ghosts with left/right anchor margins, so pinning the
// width and right-aligning the text would flatten the RGB
// split away. The title's left edge is anchored, so a
// widening readout only moves its own left edge.
text: Math.round(root.level * 100) + "%"
pixelSize: Theme.fsBody
split: 1
splitOpacity: 0.5
}
}
MeterBar {
width: parent.width
height: 10
segments: 20
segmentWidth: (width - 19 * 3) / 20
spacing: 3
value: root.level * 100
activeColor: root.tone
}
}
}
}
+274
View File
@@ -0,0 +1,274 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
// Full-screen power menu. Crimson stripes sweep in behind a stack of leaning
// buttons; Escape or a click on the backdrop dismisses it.
PanelWindow {
id: root
required property var modelData
screen: modelData
visible: Actions.powerMenuOpen || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-power"
WlrLayershell.keyboardFocus: Actions.powerMenuOpen
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
readonly property bool open: Actions.powerMenuOpen
onOpenChanged: {
if (open) {
openAnim.restart();
} else {
closeAnim.restart();
}
}
// ------------------------------------------------------------- backdrop
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.82)
opacity: 0
TapHandler {
onTapped: Actions.closePowerMenu()
}
}
// Stripes and wedge, shared with the lock screen. The wedge used to lean at
// 0.32 here, which was its own angle and nothing else's; P5Backdrop leans
// it at Theme.skew like every other edge in the shell.
P5Backdrop {
id: ground
anchors.fill: parent
reveal: 0
stripeOpacity: 0.10
wedgeOpacity: 0.35
wedgeSpan: 0.34
// The live desktop is still visible behind this, so a dot wash would be
// laid over other people's windows rather than over a ground of our
// own. The lock owns the halftone; here it just muddies things.
halftone: false
}
// ---------------------------------------------------------------- title
Column {
id: title
anchors.left: parent.left
anchors.leftMargin: root.width * 0.12
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -60
spacing: -14
opacity: 0
SplitText {
text: "TAKE"
pixelSize: Theme.fsMega
split: 4
splitOpacity: 0.9
}
SplitText {
text: "YOUR"
pixelSize: Theme.fsMega
color: Theme.accent
split: 4
splitOpacity: 0.5
}
SplitText {
text: "TIME."
pixelSize: Theme.fsMega
split: 4
splitOpacity: 0.9
}
Text {
text: "— TAE TAKEMI"
color: Theme.primary
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 5
topPadding: 24
renderType: Text.NativeRendering
}
}
// -------------------------------------------------------------- actions
Column {
id: actions
anchors.right: parent.right
anchors.rightMargin: root.width * 0.12
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padM
// The widest slab in the stack. Fixed rather than derived so the ragged
// x offsets below cannot feed back into the Column's own width.
readonly property int stackWidth: 312
width: stackWidth
Repeater {
id: buttonRepeater
model: [
{ label: "Lock", icon: "󰌾", danger: false, act: "lock" },
{ label: "Suspend", icon: "󰤄", danger: false, act: "suspend" },
{ label: "Hibernate", icon: "󰒲", danger: false, act: "hibernate" },
{ label: "Log Out", icon: "󰗽", danger: true, act: "logout" },
{ label: "Reboot", icon: "󰜉", danger: true, act: "reboot" },
{ label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" }
]
// A carrier item owns the staggered slide so the button keeps its
// own hover transform.
Item {
id: entry
required property var modelData
required property int index
readonly property int slabWidth: actions.stackWidth
width: entry.slabWidth
height: 52
opacity: 0
transform: [
Translate { id: slide; x: 90 },
Rotation {
id: tilt
angle: -3
origin.x: entry.slabWidth / 2
origin.y: 26
}
]
P5Button {
anchors.fill: parent
text: entry.modelData.label
icon: entry.modelData.icon
destructive: entry.modelData.danger
accent: entry.modelData.danger ? Theme.accent : Theme.primary
lean: Theme.skew
lunge: -10
onClicked: root.invoke(entry.modelData.act)
}
// Staggered entry, one button after another.
Connections {
target: root
function onOpenChanged() {
if (root.open) entryAnim.restart();
else entry.opacity = 0;
}
}
SequentialAnimation {
id: entryAnim
PauseAnimation { duration: 40 + entry.index * 55 }
ParallelAnimation {
NumberAnimation {
target: entry; property: "opacity"
to: 1; duration: Theme.durBase
}
NumberAnimation {
target: slide; property: "x"
from: 90; to: 0
duration: Theme.durSlow
easing.type: Easing.OutBack
}
// Slabs land out of true and snap square. Sliding alone
// reads like a slide deck; the tilt is what makes it
// land like a card being thrown down. Same direction
// every time — the motion is the flourish, the geometry
// stays disciplined.
NumberAnimation {
target: tilt; property: "angle"
from: -3; to: 0
duration: Theme.durSlow
easing.type: Easing.OutBack
easing.overshoot: 2.2
}
}
}
}
}
}
// Hint line.
Text {
anchors.bottom: parent.bottom
anchors.bottomMargin: 40
anchors.horizontalCenter: parent.horizontalCenter
text: "ESC TO CANCEL"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 4
opacity: backdrop.opacity
renderType: Text.NativeRendering
}
function invoke(what: string) {
Actions.closePowerMenu();
switch (what) {
case "lock": Actions.lock(); break;
case "suspend": Actions.suspend(); break;
case "hibernate": Actions.hibernate(); break;
case "logout": Actions.logout(); break;
case "reboot": Actions.reboot(); break;
case "shutdown": Actions.shutdown(); break;
}
}
// Escape closes. The layer takes exclusive focus while open.
Item {
anchors.fill: parent
focus: root.open
Keys.onEscapePressed: Actions.closePowerMenu()
}
ParallelAnimation {
id: openAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad
}
// One number now drives the slide, the stripe opacity and the wedge.
NumberAnimation {
target: ground; property: "reveal"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation {
target: title; property: "opacity"
to: 1; duration: Theme.durSlow
}
}
ParallelAnimation {
id: closeAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: ground; property: "reveal"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: title; property: "opacity"
to: 0; duration: Theme.durFast
}
}
}
+148
View File
@@ -0,0 +1,148 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
import "root:/config"
import "root:/components"
import "root:/services"
// Floating notification stack under the bar, top right. Replaces dunst.
PanelWindow {
id: root
required property var modelData
screen: modelData
// Only the primary bar screen shows toasts, to avoid duplicates.
readonly property bool primaryScreen: Quickshell.screens.length === 0
|| Quickshell.screens[0] === modelData
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-toasts"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors { top: true; right: true }
margins {
top: Theme.barHeight + Theme.barMargin * 2
right: Theme.barMargin
}
implicitWidth: 400
implicitHeight: Math.max(1, stack.implicitHeight)
color: "transparent"
exclusionMode: ExclusionMode.Ignore
visible: primaryScreen && stack.children.length > 0
// Toasts currently on screen, newest first.
property list<Notification> live: []
property int maxLive: 5
Connections {
target: Notifs
enabled: root.primaryScreen
function onPopupRequested(notif) {
root.live = [notif, ...root.live].slice(0, root.maxLive);
}
}
function retire(notif) {
const idx = root.live.indexOf(notif);
if (idx === -1) return;
const next = root.live.slice();
next.splice(idx, 1);
root.live = next;
}
Column {
id: stack
anchors.right: parent.right
anchors.top: parent.top
width: parent.width
spacing: Theme.padS
Repeater {
model: root.live
Item {
id: toast
required property var modelData
width: stack.width
height: card.implicitHeight
NotifCard {
id: card
width: parent.width
notif: toast.modelData
onDismissed: {
Notifs.dismiss(toast.modelData);
root.retire(toast.modelData);
}
}
// Countdown rule that drains along the bottom edge.
Rectangle {
id: fuse
anchors.bottom: parent.bottom
anchors.left: parent.left
height: 2
width: parent.width
color: Theme.alpha(card.tone, 0.7)
visible: life.running
}
NumberAnimation {
id: life
target: fuse
property: "width"
from: toast.width
to: 0
running: false
duration: Notifs.timeoutFor(toast.modelData)
easing.type: Easing.Linear
onFinished: root.retire(toast.modelData)
}
// Slide in from the right, Persona-style.
opacity: 0
transform: Translate { id: shove; x: 60 }
Component.onCompleted: {
enter.start();
const ms = Notifs.timeoutFor(toast.modelData);
if (ms > 0) life.start();
}
ParallelAnimation {
id: enter
NumberAnimation {
target: toast; property: "opacity"
to: 1; duration: Theme.durBase
}
NumberAnimation {
target: shove; property: "x"
from: 60; to: 0
duration: Theme.durSlow
easing.type: Easing.OutBack
}
}
// Hovering a toast pauses its fuse.
HoverHandler {
id: toastHover
onHoveredChanged: {
if (!life.duration) return;
if (hovered) life.pause();
else if (life.paused) life.resume();
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.retire(toast.modelData)
}
}
}
}
}
+275
View File
@@ -0,0 +1,275 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import "root:/config"
import "root:/components"
import "root:/services"
// Output and input levels, the sink picker, and a per-app mixer.
Popout {
id: root
contentWidth: 360
contentHeight: surface.implicitContentHeight + Theme.padM * 2
PopoutSurface {
id: surface
anchors.fill: parent
tone: Theme.primary
readonly property real implicitContentHeight: header.height + column.implicitHeight + Theme.padM * 2
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Audio"
icon: Audio.icon
accent: Theme.primary
subtitle: Audio.sinkName
}
Column {
id: column
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padM
// ------------------------------------------------------- output
LevelRow {
width: parent.width
icon: Audio.icon
label: "Output"
readout: Audio.muted ? "MUTED" : Audio.volumePercent + "%"
value: Audio.volume
muted: Audio.muted
accent: Theme.primary
onMoved: v => Audio.setVolume(v)
onToggled: Audio.toggleMute()
}
// -------------------------------------------------------- input
LevelRow {
width: parent.width
icon: Audio.micIcon
label: "Input"
readout: Audio.micMuted ? "MUTED" : Audio.micPercent + "%"
value: Audio.micVolume
muted: Audio.micMuted
accent: Theme.blue
onMoved: v => Audio.setMicVolume(v)
onToggled: Audio.toggleMicMute()
}
SectionRule {
width: parent.width
text: "Outputs"
visible: Audio.sinks.length > 1
}
Repeater {
model: Audio.sinks.length > 1 ? Audio.sinks : []
PickRow {
required property var modelData
width: column.width
label: modelData.nickname || modelData.description || modelData.name
icon: "󰓃"
selected: Audio.sink === modelData
onClicked: Audio.setSink(modelData)
}
}
SectionRule {
width: parent.width
text: "Applications"
visible: Audio.streams.length > 0
}
Repeater {
model: Audio.streams
LevelRow {
required property var modelData
width: column.width
icon: "󰝚"
label: Audio.streamName(modelData)
readout: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : ""
value: modelData.audio?.volume ?? 0
muted: modelData.audio?.muted ?? false
accent: Theme.glow
onMoved: v => { if (modelData.audio) modelData.audio.volume = v; }
onToggled: { if (modelData.audio) modelData.audio.muted = !modelData.audio.muted; }
}
}
P5Button {
text: "Mixer"
icon: "󰕾"
onClicked: Actions.openAudioSettings()
}
}
}
// ------------------------------------------------------------- pieces
component LevelRow: Item {
id: levelRow
property string icon: ""
property string label: ""
property string readout: ""
property real value: 0
property bool muted: false
property color accent: Theme.primary
signal moved(real value)
signal toggled()
implicitHeight: 42
Row {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padS
Icon {
id: levelIcon
anchors.verticalCenter: parent.verticalCenter
text: levelRow.icon
color: levelRow.muted ? Theme.accent : levelRow.accent
font.pixelSize: Theme.fsLarge
TapHandler { onTapped: levelRow.toggled() }
HoverHandler { cursorShape: Qt.PointingHandCursor }
}
Text {
anchors.verticalCenter: parent.verticalCenter
width: levelRow.width - levelIcon.width - readoutText.width - Theme.padS * 3
text: levelRow.label
elide: Text.ElideRight
color: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
}
Text {
id: readoutText
anchors.verticalCenter: parent.verticalCenter
text: levelRow.readout
color: levelRow.muted ? Theme.accent : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
renderType: Text.NativeRendering
}
}
P5Slider {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
value: levelRow.value
accent: levelRow.accent
enabledControl: !levelRow.muted
onMoved: v => levelRow.moved(v)
}
}
component SectionRule: Item {
property string text: ""
implicitHeight: visible ? 18 : 0
Text {
id: sectionLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: parent.text.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: sectionLabel.right
anchors.leftMargin: Theme.padS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.6)
}
}
component PickRow: Item {
id: pickRow
property string label: ""
property string icon: ""
property bool selected: false
signal clicked()
implicitHeight: 26
Rectangle {
anchors.fill: parent
color: pickHover.hovered
? Theme.alpha(Theme.primary, 0.14)
: (pickRow.selected ? Theme.alpha(Theme.primary, 0.07) : "transparent")
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: pickRow.selected ? 18 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: pickRow.icon
color: pickRow.selected ? Theme.primary : Theme.muted
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: pickRow.label
color: pickRow.selected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
renderType: Text.NativeRendering
}
}
HoverHandler {
id: pickHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: pickRow.clicked()
}
}
}
+143
View File
@@ -0,0 +1,143 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Charge level, time estimate, draw rate and health.
Popout {
id: root
contentWidth: 310
// Derived: the charge block is a different height from the dial it
// replaced, and the health line comes and goes.
contentHeight: header.height + Theme.padM + readout.height + Theme.padM
+ bar.height + Theme.padM + detailText.implicitHeight + Theme.padL
+ suspendButton.height + Theme.padM * 2
PopoutSurface {
anchors.fill: parent
tone: Battery.tint
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Power"
icon: Battery.icon
accent: Battery.tint
subtitle: Battery.charging ? "CHARGING" : (Battery.onBattery ? "ON BATTERY" : "AC")
}
// The dial that used to sit here duplicated the meter below it and was
// the only curve in the shell. The charge is now simply set large, and
// the MeterBar underneath is left to carry the level on its own.
Item {
id: readout
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
height: Math.max(charge.height, detail.height)
Column {
id: charge
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
// No "CHARGE" caption under the number. It landed directly on
// top of the meter, and between the header and a full-width
// charge bar there was nothing left for it to disambiguate.
SplitText {
text: Math.round(Battery.percent) + "%"
pixelSize: Theme.fsHuge
color: Battery.tint
split: 2.5
splitOpacity: 0.7
}
}
Column {
id: detail
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
SplitText {
anchors.right: parent.right
horizontalAlignment: Text.AlignRight
text: Battery.timeLabel
pixelSize: Theme.fsBody
split: 1.1
splitOpacity: 0.55
}
Text {
text: Battery.changeRate > 0
? Battery.changeRate.toFixed(1) + " W " + (Battery.charging ? "in" : "draw")
: "idle"
anchors.right: parent.right
horizontalAlignment: Text.AlignRight
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
renderType: Text.NativeRendering
}
Text {
visible: Battery.health > 0
text: "Health " + Math.round(Battery.health) + "%"
anchors.right: parent.right
horizontalAlignment: Text.AlignRight
color: Battery.health < 70 ? Theme.warn : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
MeterBar {
id: bar
anchors.top: readout.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
height: 12
segments: 20
segmentWidth: (width - 19 * 3) / 20
spacing: 3
value: Battery.percent
activeColor: Battery.tint
}
Text {
id: detailText
anchors.top: bar.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
text: Battery.detail
wrapMode: Text.WordWrap
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
// Follows the detail line rather than being pinned to the bottom of a
// fixed-height panel, which is what left a dead band above it.
P5Button {
id: suspendButton
anchors.top: detailText.bottom
anchors.topMargin: Theme.padL
anchors.left: parent.left
text: "Suspend"
icon: "󰤄"
onClicked: {
root.pinned = false;
Actions.suspend();
}
}
}
}
+192
View File
@@ -0,0 +1,192 @@
import QtQuick
import Quickshell
import Quickshell.Bluetooth
import "root:/config"
import "root:/components"
import "root:/services"
// Adapter toggle, scan, and the device list with battery levels where BlueZ
// reports them.
Popout {
id: root
contentWidth: 340
contentHeight: 280
// Discovery is expensive; run it only while the panel is open.
onShownChanged: {
if (!shown && Bt.discovering && Bt.adapter) Bt.adapter.discovering = false;
}
PopoutSurface {
anchors.fill: parent
tone: Bt.enabled ? Theme.blue : Theme.muted
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Bluetooth"
icon: Bt.icon
accent: Theme.blue
subtitle: Bt.adapter ? Bt.adapter.name : "NO ADAPTER"
}
Row {
id: controls
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
spacing: Theme.padS
P5Button {
text: Bt.enabled ? "On" : "Off"
icon: Bt.enabled ? "󰂯" : "󰂲"
accent: Bt.enabled ? Theme.blue : Theme.muted
onClicked: Bt.toggle()
}
P5Button {
text: Bt.discovering ? "Scanning" : "Scan"
icon: "󰐷"
accent: Bt.discovering ? Theme.accent : Theme.primary
onClicked: Bt.toggleScan()
}
}
Text {
id: listLabel
anchors.top: controls.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
text: Bt.devices.length > 0 ? "DEVICES" : "NO DEVICES"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
ListView {
anchors.top: listLabel.bottom
anchors.topMargin: Theme.padS
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
clip: true
spacing: 2
model: Bt.devices
delegate: Item {
id: devRow
required property BluetoothDevice modelData
width: ListView.view.width
height: 34
Rectangle {
anchors.fill: parent
color: devHover.hovered
? Theme.alpha(Theme.blue, 0.16)
: (devRow.modelData.connected ? Theme.alpha(Theme.blue, 0.08) : "transparent")
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: devRow.modelData.connected ? 22 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Bt.deviceIcon(devRow.modelData)
color: devRow.modelData.connected ? Theme.blue : Theme.subtext
font.pixelSize: Theme.fsLarge
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: -2
Text {
text: Bt.deviceLabel(devRow.modelData)
color: devRow.modelData.connected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: devRow.modelData.connected ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Text {
text: {
if (devRow.modelData.pairing) return "pairing…";
if (devRow.modelData.connected) return "connected";
if (devRow.modelData.paired || devRow.modelData.bonded) return "paired";
return devRow.modelData.address;
}
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
renderType: Text.NativeRendering
}
}
}
// Device battery, where BlueZ exposes it.
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: 3
visible: devRow.modelData.batteryAvailable
Icon {
anchors.verticalCenter: parent.verticalCenter
text: "󰥉"
color: devRow.modelData.battery > 0.25 ? Theme.ok : Theme.warn
font.pixelSize: Theme.fsSmall
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: Math.round(devRow.modelData.battery * 100) + "%"
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
HoverHandler {
id: devHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: Bt.toggleDevice(devRow.modelData)
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: if (devRow.modelData.paired) devRow.modelData.forget()
}
}
}
}
}
+260
View File
@@ -0,0 +1,260 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Month grid with the current day slashed in crimson, plus uptime and a couple
// of world clocks.
Popout {
id: root
property date clockDate: new Date()
contentWidth: 320
contentHeight: 352
// Month currently on display, as an offset from the real one.
property int monthOffset: 0
onShownChanged: if (!shown) monthOffset = 0
readonly property date viewDate: {
const d = new Date(root.clockDate);
d.setDate(1);
d.setMonth(d.getMonth() + root.monthOffset);
return d;
}
readonly property int viewYear: viewDate.getFullYear()
readonly property int viewMonth: viewDate.getMonth()
// Monday-first grid, 6 rows of 7.
readonly property var cells: {
const first = new Date(root.viewYear, root.viewMonth, 1);
const lead = (first.getDay() + 6) % 7;
const start = new Date(root.viewYear, root.viewMonth, 1 - lead);
const out = [];
for (let i = 0; i < 42; i++) {
const d = new Date(start);
d.setDate(start.getDate() + i);
out.push({
day: d.getDate(),
inMonth: d.getMonth() === root.viewMonth,
today: d.toDateString() === root.clockDate.toDateString()
});
}
return out;
}
PopoutSurface {
anchors.fill: parent
tone: Theme.accent
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: Qt.formatDateTime(root.viewDate, "MMMM")
icon: "󰃭"
accent: Theme.accent
subtitle: Qt.formatDateTime(root.viewDate, "yyyy")
}
// Month stepper.
Row {
anchors.top: header.bottom
anchors.topMargin: Theme.padS
anchors.right: parent.right
spacing: Theme.padS
z: 2
StepButton {
glyph: "󰅁"
onClicked: root.monthOffset--
}
StepButton {
glyph: "󰅂"
onClicked: root.monthOffset++
}
}
// Weekday header, clear of the month stepper above it.
Row {
id: dayNames
anchors.top: header.bottom
anchors.topMargin: 36
anchors.left: parent.left
anchors.right: parent.right
Repeater {
model: ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]
Text {
required property var modelData
required property int index
width: dayNames.width / 7
horizontalAlignment: Text.AlignHCenter
text: modelData
color: index >= 5 ? Theme.alpha(Theme.accent, 0.8) : Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1
renderType: Text.NativeRendering
}
}
}
Grid {
id: grid
anchors.top: dayNames.bottom
anchors.topMargin: Theme.padS
anchors.left: parent.left
anchors.right: parent.right
columns: 7
Repeater {
model: root.cells
Item {
id: cell
required property var modelData
width: grid.width / 7
height: 30
// Today gets a leaning crimson slab.
Rectangle {
anchors.centerIn: parent
width: 26
height: 24
visible: cell.modelData.today
color: Theme.accent
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 24 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Text {
anchors.centerIn: parent
text: cell.modelData.day
color: {
if (cell.modelData.today) return Theme.text;
if (!cell.modelData.inMonth) return Theme.alpha(Theme.muted, 0.45);
return Theme.subtext;
}
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: cell.modelData.today ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
}
}
}
// Footer: uptime and a second time zone.
Row {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
Column {
width: parent.width / 2
spacing: -2
Text {
text: "UPTIME"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
Text {
text: Sys.uptime
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
}
Column {
width: parent.width / 2
spacing: -2
Text {
anchors.right: parent.right
text: "TOKYO"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
Text {
anchors.right: parent.right
text: {
// Tokyo is UTC+9 year-round.
const utc = root.clockDate.getTime()
+ root.clockDate.getTimezoneOffset() * 60000;
const tokyo = new Date(utc + 9 * 3600000);
return Qt.formatDateTime(tokyo, "HH:mm");
}
color: Theme.primary
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
}
}
}
component StepButton: Item {
id: step
property string glyph: ""
signal clicked()
implicitWidth: 22
implicitHeight: 22
Rectangle {
anchors.fill: parent
color: stepHover.hovered ? Theme.alpha(Theme.accent, 0.25) : "transparent"
border.width: 1
border.color: Theme.alpha(Theme.outline, 0.7)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Icon {
anchors.centerIn: parent
text: step.glyph
color: stepHover.hovered ? Theme.text : Theme.subtext
font.pixelSize: Theme.fsBody
}
HoverHandler {
id: stepHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: step.clicked()
}
}
}
+337
View File
@@ -0,0 +1,337 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Full player: cover art, transport, seek bar, and a picker when more than one
// player is running.
Popout {
id: root
contentWidth: 400
contentHeight: 200 + (Media.players.length > 1 ? 34 : 0)
PopoutSurface {
id: surface
anchors.fill: parent
tone: Theme.primary
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Now Playing"
icon: Media.sourceIcon
subtitle: Media.identity
}
// Cover art, chopped.
Item {
id: artFrame
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
width: 108
height: 108
Shape {
id: artMask
anchors.fill: parent
visible: false
layer.enabled: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "white"
strokeColor: "transparent"
PathPolyline {
path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3])
}
}
}
Image {
id: art
anchors.fill: parent
source: Media.artUrl
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
visible: status === Image.Ready
layer.enabled: true
layer.effect: MaskedArt { maskItem: artMask }
}
// Placeholder when the player exposes no artwork.
Shape {
anchors.fill: parent
visible: art.status !== Image.Ready
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.surface
strokeColor: Theme.alpha(Theme.primary, 0.5)
strokeWidth: 2
PathPolyline {
path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3])
}
}
}
Icon {
anchors.centerIn: parent
visible: art.status !== Image.Ready
text: Media.sourceIcon
font.pixelSize: 42
color: Theme.alpha(Theme.primary, 0.7)
}
// Spinning rule around the art while playing.
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
width: 30
height: 3
color: Theme.accent
visible: Media.playing
}
}
Column {
id: info
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: artFrame.right
anchors.leftMargin: Theme.padM
anchors.right: parent.right
spacing: 2
Marquee {
width: parent.width
height: 24
text: Media.title || "Nothing playing"
color: Theme.text
pixelSize: Theme.fsLarge
family: Theme.fontDisplay
weight: Font.Black
italic: true
}
Marquee {
width: parent.width
height: 16
text: Media.artist
color: Theme.primary
pixelSize: Theme.fsSmall
family: Theme.fontDisplay
italic: true
weight: Font.DemiBold
}
Marquee {
width: parent.width
height: 14
text: Media.album
color: Theme.muted
pixelSize: Theme.fsMicro
family: Theme.fontMono
}
Item { width: 1; height: Theme.padS }
// Transport
Row {
spacing: Theme.padM
TransportButton {
glyph: "󰒮"
enabledControl: Media.canGoPrevious
onClicked: Media.previous()
}
TransportButton {
glyph: Media.playing ? "󰏤" : "󰐊"
primary: true
enabledControl: Media.hasPlayer
onClicked: Media.playPause()
}
TransportButton {
glyph: "󰒭"
enabledControl: Media.canGoNext
onClicked: Media.next()
}
TransportButton {
glyph: "󰐒"
enabledControl: Media.hasPlayer
onClicked: Media.raise()
}
}
}
// Seek bar across the bottom.
Item {
id: seekArea
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: picker.visible ? picker.top : parent.bottom
anchors.bottomMargin: Theme.padS
height: 26
visible: Media.hasPlayer
Text {
id: elapsed
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: Media.timeString(Media.position)
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
Text {
id: total
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: Media.timeString(Media.length)
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
P5Slider {
anchors.left: elapsed.right
anchors.right: total.left
anchors.leftMargin: Theme.padS
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
value: Media.progress
accent: Theme.accent
enabledControl: Media.canSeek
onMoved: v => Media.seekTo(v)
}
}
// Player picker, only when it matters.
Row {
id: picker
anchors.left: parent.left
anchors.bottom: parent.bottom
spacing: Theme.padS
visible: Media.players.length > 1
Repeater {
model: Media.players
Item {
id: chip
required property var modelData
readonly property bool current: Media.active === modelData
implicitWidth: chipLabel.implicitWidth + Theme.padM
implicitHeight: 22
Rectangle {
anchors.fill: parent
color: chip.current
? Theme.alpha(Theme.primary, 0.22)
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent")
border.width: 1
border.color: chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Text {
id: chipLabel
anchors.centerIn: parent
text: chip.modelData.identity
color: chip.current ? Theme.text : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
HoverHandler {
id: chipHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: Media.select(chip.modelData)
}
}
}
}
}
component TransportButton: Item {
id: tb
property string glyph: ""
property bool primary: false
property bool enabledControl: true
signal clicked()
implicitWidth: primary ? 38 : 30
implicitHeight: primary ? 38 : 30
Rectangle {
anchors.fill: parent
color: {
if (!tb.enabledControl) return "transparent";
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.alpha(Theme.primary, 0.22);
return tb.primary ? Theme.alpha(Theme.primary, 0.18) : "transparent";
}
border.width: tb.primary ? 2 : 1
border.color: tb.enabledControl
? (tbHover.hovered ? Theme.text : Theme.alpha(Theme.primary, 0.6))
: Theme.alpha(Theme.outline, 0.4)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, tb.height * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Icon {
anchors.centerIn: parent
text: tb.glyph
font.pixelSize: tb.primary ? Theme.fsTitle : Theme.fsLarge
color: {
if (!tb.enabledControl) return Theme.alpha(Theme.muted, 0.5);
if (tbHover.hovered && tb.primary) return Theme.text;
return Theme.primary;
}
}
scale: tbHover.hovered && tb.enabledControl ? 1.12 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
HoverHandler {
id: tbHover
enabled: tb.enabledControl
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: tb.enabledControl
onTapped: tb.clicked()
}
}
}
+252
View File
@@ -0,0 +1,252 @@
import QtQuick
import Quickshell
import Quickshell.Networking
import "root:/config"
import "root:/components"
import "root:/services"
// Link state, throughput, and the Wi-Fi picker.
Popout {
id: root
contentWidth: 360
contentHeight: 300
// Scan only while the popout is actually on screen.
onShownChanged: Net.setScanning(shown)
PopoutSurface {
anchors.fill: parent
tone: Sys.netUp ? Theme.primary : Theme.muted
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Network"
icon: Net.icon
subtitle: Sys.netUp ? Sys.netIp : "OFFLINE"
}
// Throughput
Row {
id: rates
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padM
RateBlock {
width: (parent.width - Theme.padM) / 2
glyph: "󰇚"
label: "Down"
value: Sys.rate(Sys.netRx)
tone: Theme.primary
}
RateBlock {
width: (parent.width - Theme.padM) / 2
glyph: "󰕒"
label: "Up"
value: Sys.rate(Sys.netTx)
tone: Theme.accent
}
}
Row {
id: controls
anchors.top: rates.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
spacing: Theme.padS
P5Button {
text: Net.wifiEnabled ? "Wi-Fi On" : "Wi-Fi Off"
icon: Net.wifiEnabled ? "󰖩" : "󰖪"
accent: Net.wifiEnabled ? Theme.primary : Theme.muted
onClicked: Net.toggleWifi()
}
P5Button {
text: "Settings"
icon: "󰒓"
onClicked: Actions.openNetworkSettings()
}
}
Text {
id: apsLabel
anchors.top: controls.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
text: Net.scanning ? "SCANNING…" : "NETWORKS"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
ListView {
anchors.top: apsLabel.bottom
anchors.topMargin: Theme.padS
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
clip: true
spacing: 2
model: Net.wifiNetworks
delegate: Item {
id: apRow
required property var modelData
width: ListView.view.width
height: 30
Rectangle {
anchors.fill: parent
color: apHover.hovered
? Theme.alpha(Theme.primary, 0.14)
: (apRow.modelData.connected ? Theme.alpha(Theme.primary, 0.07) : "transparent")
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: apRow.modelData.connected ? 20 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Net.signalIcon(apRow.modelData.signalStrength ?? 0)
color: apRow.modelData.connected ? Theme.primary : Theme.subtext
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: apRow.modelData.name
color: apRow.modelData.connected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: apRow.modelData.connected ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Icon {
anchors.verticalCenter: parent.verticalCenter
visible: Net.secured(apRow.modelData)
text: "󰌾"
color: Theme.muted
font.pixelSize: Theme.fsMicro
}
}
Text {
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
text: apRow.modelData.known ? "SAVED" : ""
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1
renderType: Text.NativeRendering
}
HoverHandler {
id: apHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
if (apRow.modelData.connected) {
apRow.modelData.disconnect();
} else {
// Saved networks connect directly; new secured ones
// need a passphrase, which NetworkManager's own
// agent will prompt for.
apRow.modelData.connect();
}
}
}
}
}
}
component RateBlock: Item {
id: rateBlock
property string glyph: ""
property string label: ""
property string value: ""
property color tone: Theme.primary
implicitHeight: 40
Rectangle {
anchors.fill: parent
color: Theme.alpha(rateBlock.tone, 0.08)
border.width: 1
border.color: Theme.alpha(rateBlock.tone, 0.35)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, rateBlock.height * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Row {
anchors.centerIn: parent
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: rateBlock.glyph
color: rateBlock.tone
font.pixelSize: Theme.fsLarge
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: -1
Text {
text: rateBlock.label.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
Text {
text: rateBlock.value
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
}
}
}
}
+113
View File
@@ -0,0 +1,113 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "root:/config"
import "root:/components"
import "root:/services"
// Notification history and the do-not-disturb switch.
Popout {
id: root
contentWidth: 400
contentHeight: 380
PopoutSurface {
anchors.fill: parent
tone: Notifs.dnd ? Theme.muted : Theme.primary
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Notifications"
icon: Notifs.icon
accent: Notifs.dnd ? Theme.muted : Theme.primary
subtitle: Notifs.count + " HELD"
}
Row {
id: controls
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
spacing: Theme.padS
P5Button {
text: Notifs.dnd ? "Silenced" : "Silence"
icon: Notifs.dnd ? "󰂛" : "󰂚"
accent: Notifs.dnd ? Theme.accent : Theme.primary
onClicked: Notifs.toggleDnd()
}
P5Button {
text: "Clear"
icon: "󰎟"
destructive: true
onClicked: Notifs.clearAll()
}
}
Text {
anchors.centerIn: parent
visible: Notifs.count === 0
text: "NOTHING TO REPORT"
color: Theme.muted
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.italic: true
font.weight: Font.Black
font.letterSpacing: 3
renderType: Text.NativeRendering
}
ListView {
anchors.top: controls.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
clip: true
spacing: Theme.padS
model: Notifs.history
delegate: NotifCard {
required property var modelData
width: ListView.view.width
notif: modelData
onDismissed: Notifs.dismiss(modelData)
}
add: Transition {
NumberAnimation {
properties: "x"
from: 40
duration: Theme.durSlow
easing.type: Easing.OutBack
}
NumberAnimation {
property: "opacity"
from: 0; to: 1
duration: Theme.durBase
}
}
remove: Transition {
NumberAnimation {
property: "opacity"
to: 0
duration: Theme.durFast
}
}
displaced: Transition {
NumberAnimation {
properties: "y"
duration: Theme.durBase
easing.type: Easing.OutExpo
}
}
}
}
}
+73
View File
@@ -0,0 +1,73 @@
import QtQuick
import "root:/config"
import "root:/components"
// Title bar for a popout: glyph, split-ink heading, right-aligned status text,
// and a crimson rule with a chopped tail.
Item {
id: root
property string title: ""
property string subtitle: ""
property string icon: ""
property color accent: Theme.primary
implicitHeight: 34
Row {
anchors.left: parent.left
anchors.top: parent.top
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
visible: root.icon !== ""
text: root.icon
color: root.accent
font.pixelSize: Theme.fsTitle
}
SplitText {
anchors.verticalCenter: parent.verticalCenter
text: root.title.toUpperCase()
pixelSize: Theme.fsLarge
letterSpacing: 1.5
split: 1.3
splitOpacity: 0.7
}
}
Text {
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 6
visible: root.subtitle !== ""
text: root.subtitle
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 22
height: 2
color: Theme.alpha(root.accent, 0.8)
}
Rectangle {
anchors.bottom: parent.bottom
anchors.right: parent.right
width: 16
height: 2
color: Theme.accent
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -1.4, 0, 2.8,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import QtQuick
import "root:/config"
import "root:/components"
// The slab every popout sits on. Pure preset — it declares no children of its
// own, so P5Panel's default content property stays usable by callers.
P5Panel {
property color tone: Theme.primary
fill: Theme.mantle
fillOpacity: 0.97
border: Theme.alpha(tone, 0.75)
borderWidth: Theme.stroke
halftone: true
halftoneColor: tone
halftoneOpacity: 0.06
sheen: true
slash: true
slashColor: tone
slashWidth: 5
lean: 0.06
chop: 16
padding: Theme.padM
}
+149
View File
@@ -0,0 +1,149 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// The resource dashboard: four gauges, then the details that do not fit on one.
Popout {
id: root
contentWidth: 420
// Derived rather than a fixed number: the meters are taller than the dials
// they replaced, and the GPU block disappears entirely on machines without
// one. A hardcoded height would either clip or leave a hole.
contentHeight: header.height + Theme.padM
+ gauges.implicitHeight + Theme.padL
+ details.implicitHeight + Theme.padM * 2
PopoutSurface {
anchors.fill: parent
tone: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent))
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "System"
icon: "󰍛"
accent: Theme.primary
subtitle: "UP " + Sys.uptime + " · " + Sys.procs + " PROCS"
}
Column {
id: gauges
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padM
MetricRow {
width: parent.width
value: Sys.cpuUsage
label: "CPU"
sub: Math.round(Sys.cpuTemp) + "°C"
}
MetricRow {
width: parent.width
value: Sys.memPercent
label: "MEM"
sub: Sys.gb(Sys.memUsed) + " / " + Sys.gb(Sys.memTotal)
}
MetricRow {
width: parent.width
visible: Sys.gpuAvailable
value: Sys.gpuUsage
label: "GPU"
sub: Math.round(Sys.gpuTemp) + "°C"
}
MetricRow {
width: parent.width
value: Sys.diskPercent
label: "DISK"
sub: Sys.diskFree + " free"
}
}
Column {
id: details
anchors.top: gauges.bottom
anchors.topMargin: Theme.padL
anchors.left: parent.left
anchors.right: parent.right
spacing: 3
InfoRow {
width: parent.width
key: "Processor"
value: Sys.cpuModel
}
InfoRow {
width: parent.width
key: "Load"
value: Sys.load1.toFixed(2) + " " + Sys.load5.toFixed(2) + " " + Sys.load15.toFixed(2)
}
InfoRow {
width: parent.width
visible: Sys.gpuAvailable
key: "VRAM"
value: Sys.gb(Sys.gpuVramUsed) + " / " + Sys.gb(Sys.gpuVramTotal)
}
InfoRow {
width: parent.width
key: "Root"
value: Sys.gb(Sys.diskUsed) + " of " + Sys.gb(Sys.diskTotal) + " used"
}
InfoRow {
width: parent.width
key: "Network"
value: Sys.netUp
? Sys.netInterface + " ↓ " + Sys.rate(Sys.netRx) + " ↑ " + Sys.rate(Sys.netTx)
: "offline"
}
}
}
component InfoRow: Item {
id: infoRow
property string key: ""
property string value: ""
implicitHeight: visible ? 17 : 0
Text {
id: keyLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: infoRow.key.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
Text {
anchors.right: parent.right
anchors.left: keyLabel.right
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
text: infoRow.value
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
+69
View File
@@ -0,0 +1,69 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import "root:/services"
// Shell-wide UI state and the handful of external commands the shell fires.
// Also exposes an IPC surface so Hyprland keybinds can drive the same actions
// (`qs ipc call shell lock`, `qs ipc call shell power`, ...).
Singleton {
id: root
// ------------------------------------------------------------- UI state
property bool powerMenuOpen: false
property bool lockRequested: false
property bool sidebarOpen: false
signal osdRequested(string kind)
function togglePowerMenu() { root.powerMenuOpen = !root.powerMenuOpen; }
function closePowerMenu() { root.powerMenuOpen = false; }
function lock() { root.powerMenuOpen = false; root.lockRequested = true; }
// ------------------------------------------------------------- commands
// Each slot gets its own Process so a slow one cannot cancel another.
Process { id: slotA }
Process { id: slotB }
Process { id: slotC }
property int _slot: 0
function run(argv: var) {
const slots = [slotA, slotB, slotC];
const p = slots[root._slot % slots.length];
root._slot++;
p.running = false;
p.command = argv;
p.running = true;
}
function launch(app: string) {
root.run(["uwsm", "app", "--", app]);
}
// --------------------------------------------------------------- power
function logout() { root.run(["uwsm", "stop"]); }
function reboot() { root.run(["systemctl", "reboot"]); }
function shutdown() { root.run(["systemctl", "poweroff"]); }
function suspend() { root.lock(); root.run(["systemctl", "suspend"]); }
function hibernate() { root.lock(); root.run(["systemctl", "hibernate"]); }
function reloadShell() { Quickshell.reload(true); }
// ------------------------------------------------------------- helpers
function openAudioSettings() { root.launch("pavucontrol"); }
function openSystemMonitor() { root.run(["alacritty", "-e", "btop"]); }
function openNetworkSettings() { root.run(["nm-connection-editor"]); }
function copyText(s: string) { root.run(["wl-copy", "--", s]); }
IpcHandler {
target: "shell"
function lock(): void { root.lock(); }
function power(): void { root.togglePowerMenu(); }
function reloadConfig(): void { root.reloadShell(); }
function toggleDnd(): void { Notifs.toggleDnd(); }
function clearNotifications(): void { Notifs.clearAll(); }
}
}
+99
View File
@@ -0,0 +1,99 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
// Live audio state straight from PipeWire, so the bar and OSD react the instant
// a volume key is pressed rather than on the next poll.
Singleton {
id: root
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
// Binding node.audio.* only works while the node is tracked.
PwObjectTracker {
objects: [root.sink, root.source].filter(n => n !== null)
}
readonly property bool ready: sink !== null && sink.audio !== null
readonly property real volume: sink?.audio?.volume ?? 0
readonly property bool muted: sink?.audio?.muted ?? true
readonly property int volumePercent: Math.round(volume * 100)
readonly property string sinkName: sink?.nickname || sink?.description || sink?.name || "No output"
readonly property real micVolume: source?.audio?.volume ?? 0
readonly property bool micMuted: source?.audio?.muted ?? true
readonly property int micPercent: Math.round(micVolume * 100)
readonly property string sourceName: source?.nickname || source?.description || source?.name || "No input"
// Every sink on the box, for the output picker in the audio popout.
readonly property var sinks: Pipewire.nodes.values.filter(n =>
n.isSink && !n.isStream && n.audio !== null)
// Application streams, for the per-app mixer.
readonly property var streams: Pipewire.nodes.values.filter(n =>
n.isStream && n.audio !== null && n.isSink === false)
PwObjectTracker {
objects: root.streams
}
signal volumeBumped()
signal micBumped()
onVolumePercentChanged: volumeBumped()
onMutedChanged: volumeBumped()
onMicPercentChanged: micBumped()
onMicMutedChanged: micBumped()
function setVolume(v: real) {
if (!sink?.audio) return;
sink.audio.volume = Math.max(0, Math.min(1.5, v));
}
function changeVolume(delta: real) {
setVolume(volume + delta);
}
function toggleMute() {
if (!sink?.audio) return;
sink.audio.muted = !sink.audio.muted;
}
function setMicVolume(v: real) {
if (!source?.audio) return;
source.audio.volume = Math.max(0, Math.min(1.5, v));
}
function toggleMicMute() {
if (!source?.audio) return;
source.audio.muted = !source.audio.muted;
}
function setSink(node: PwNode) {
Pipewire.preferredDefaultAudioSink = node;
}
function streamName(node: PwNode): string {
if (!node) return "";
return node.properties?.["application.name"]
|| node.properties?.["media.name"]
|| node.nickname
|| node.description
|| node.name;
}
// Nerd Font glyph for the current output level.
readonly property string icon: {
if (muted) return "󰝟";
if (volumePercent === 0) return "󰕿";
if (volumePercent < 34) return "󰕿";
if (volumePercent < 67) return "󰖀";
return "󰕾";
}
readonly property string micIcon: micMuted ? "󰍭" : "󰍬"
}

Some files were not shown because too many files have changed in this diff Show More