26 Commits
Author SHA1 Message Date
nvrl 32d5683fad feat(quickshell): group the notification log by sender
The log lists one entry per app, ordered by each app's newest
notification. An app with several shows a header with its name, a
count and a clear mark for that app alone, then its newest entry and a
'+ N more' line; clicking either unfolds the rest. Entries under a
header drop their own sender line and keep the urgency dot.
2026-10-04 23:45:57 +02:00
nvrl ec76f8dec3 chore(hypr): drop screenshot_window.sh, replaced by shot.sh 2026-10-04 23:40:22 +02:00
nvrl 299291eaf6 chore(quickshell): drop the unused halftone, noise and stripe tiles
Only dots.png and scanline.png are referenced (Theme.texDots and
Theme.texScanline).
2026-10-04 23:40:11 +02:00
nvrl add12ccc76 refactor(quickshell): drop the Persona names from the primitives
P5Panel, P5Button, P5Slider and P5Tooltip become Panel, ActionButton,
LevelSlider and Tooltip. ActionButton and LevelSlider avoid clashing
with QtQuick.Controls, which the ask panel imports.
2026-10-04 23:39:51 +02:00
nvrl 7b7cd3e575 style: one sans and one mono, and no stretched text
GTK, Qt and Hyprland's own text move to the shell's pair: Noto Sans for
interface text and JetBrainsMono Nerd Font for figures and code. They
had been Adwaita Sans, Sans Serif and FiraCode.

The tray menu now slides and fades in without scaling, and a pressed
button sinks one pixel instead of shrinking, so no label is ever
stretched mid-animation.
2026-10-04 23:38:46 +02:00
nvrl 31b57f4f8c feat: screenshots answer with a thumbnail notification
All four Print binds go through hypr/scripts/shot.sh. Every capture
lands on the clipboard and posts a notification showing the picture,
with Open, Save (for clipboard captures, which wait in the runtime dir)
and Edit when satty is installed. Cancelling the region picker does
nothing.

Notification cards now load images passed as a path hint: Quickshell
routes those through the icon provider, which returned a placeholder.
2026-10-04 23:36:56 +02:00
nvrl 373584f34e feat(quickshell): quick switches under the dashboard
A row of pill switches under the dashboard's columns: Wi-Fi,
Bluetooth, do not disturb and stay awake, plus night light through
hyprsunset when it is installed. On is the light slab with ink type;
off is a hairline.
2026-10-04 23:34:27 +02:00
nvrl f7551006b3 feat(quickshell): privacy indicator at the head of the rail
A lit icon tops the rail while another program records from a
microphone, uses a camera, or shares the screen. It is read from
PipeWire: capture streams for the mic, links out of video sources for
the camera and screencasts. The tooltip names the recording apps.
2026-10-04 23:31:39 +02:00
nvrl dc2a8fc883 feat(quickshell): scratchpad ring in the bar, keyboard layout in the OSD
The Super+S scratchpad gets a ring after the workspace pips while it
holds windows, filled while it is open; clicking it toggles it.
Switching keyboard layout flashes the OSD with the short name (US,
Dvorak) and the full keymap underneath.
2026-10-04 23:29:42 +02:00
nvrl b69f4bcf0d feat(quickshell): Alt+Tab overview of every monitor and workspace
Each monitor is a row of workspace cards at its own aspect ratio, with
live captures of the windows where they sit, including windows on
hidden workspaces. Alt+Tab opens it with the previous window picked,
further Tabs walk the focus history, and releasing Alt switches.
Super+W opens it without the hold. Hover, click, Enter and the
workspace digits work too.

Group cycling moves from Alt+Tab to Super+Tab and Super+Shift+Tab.
2026-10-04 23:27:48 +02:00
nvrl e9735d1129 feat(quickshell): "Take your time." glows and flickers
A new GlowText component lights a line from inside: a soft two-layer
halo that breathes slowly, and every 4-14 seconds a short, uneven
stutter like a tube catching. Only opacity and the halo move, so the
glyphs stay on the pixel grid. The lock screen and the power menu use
it; the power menu's stops while it is closed.
2026-10-04 23:12:50 +02:00
nvrl c36a74e861 feat(quickshell): idle chain, drifting wallpaper, and a few fixes
- Idle is handled in the shell instead of hypridle: the screen dims
  slowly at 4 minutes, locks at 5, powers down at 6, and suspends at 15
  on battery. Idle inhibitors hold every stage off; Super+Shift+I does
  the same by hand and puts a cup on the rail.
- The shell draws the wallpaper (backdrop/), so hyprpaper is gone. The
  picture is drawn a little larger than the screen and drifts with the
  workspace a beat after the windows, and dims a shade while windows are
  open. The path lives in quickshell/wallpaper.
- The lock screen and power menu say "Take your time." instead of the
  NieR slogan.
- hyprpaper is no longer launched twice, the media module keeps the
  player that played last instead of an empty browser tab, and grouped
  windows show thin untitled tabs with their place in the group (2/3)
  in the bar title.
- qt6ct uses Darkly; the package list gains adw-gtk-theme and darkly-bin.
2026-10-04 23:10:19 +02:00
nvrl 864420281b chore: drop waybar, hyprlock and the catppuccin leftovers
None of these are referenced any more: the shell replaced waybar and
hyprlock, and btop, zathura and the rest now use the YoRHa themes. The
hyprpaper unit is disabled because the shell draws the wallpaper.
2026-10-04 23:10:08 +02:00
nvrlandClaude Opus 5.5 b86e584b02 fix(quickshell): keep text sharp under zoom and on the lock screen
Native-rendered glyphs are rasterised at rest size, so anything moved
with `scale` stretched a bitmap and came out blurred and stepped.

- Icons render as curves, so their pulse and bump stay smooth.
- The lock face is laid out at uiScale instead of being drawn at
  1080p size and scaled by up to 1.3, and it sits on whole pixels. Its
  native labels now land on the pixel grid.
- The OSD rises into place instead of scaling in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:58:00 +02:00
nvrlandClaude Opus 5.5 f1d16e5175 theme: zathura takes the YoRHa palette
A local zathura/yorha replaces the catppuccin include. Pages are
recoloured onto the hull with bone text while images keep their
colours, the index and completion select with the light slab, and
search hits use a bone wash with the current hit in sand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:48:28 +02:00
nvrlandClaude Opus 5.5 485ee5c0ca theme: GTK and Qt take the YoRHa palette
GTK moves from Catppuccin Mocha to adw-gtk3-dark, recoloured through the
libadwaita named colours in gtk-3.0/gtk.css and as CSS variables in
gtk-4.0/gtk.css, so Nautilus and other libadwaita apps follow too. The
gtk-4.0 symlinks into the Catppuccin theme are gone, since libadwaita
would load its gtk-dark.css over the overrides.

Qt gets qt6ct/colors/yorha.conf: the same palette in QPalette roles,
with the light slab as the highlight and ink as its text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:17:52 +02:00
nvrlandClaude Opus 5.5 9af24ccf87 theme: workspaces drift behind the hull instead of over the bar
The hull moves to the top layer, with its transparent opening and empty input
region, so windows sliding out of the opening during a workspace switch pass
behind the frame like things behind a porthole rim rather than drawing over
the bar. A layer rule keeps the bar and the rail above it on that layer.

The animation tree is redone around that: workspaces drift 18% vertically
and fade on a slow, jolt-free "drift" curve; the SUPER+S scratchpad slides in
whole from under the bar, the same gesture as the dashboard and popouts;
windows and layers keep their calm resolve and fade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:13:11 +02:00
nvrlandClaude Opus 5.5 63a910bf14 theme: calm YoRHa lock screen and power menu; nvim and btop follow
The lock screen and the power menu lose the last Persona motion: no split
display type, no grid sweeping in behind a dark column, no tilted slabs, no
skewed password blocks or hard shake. Both are now one small group of type in
the dark — the porthole mark, a line of light display type, a status line, and
either a rounded password field that fills with dots or a NieR menu whose
selected row is a pill of light (rust for the session-ending three). The lock
keeps the wallpaper almost whole; a wrong password nudges the field once and
turns it to rust. P5Backdrop and the grid texture had no users left and go.

Neovim moves to a local yorha colorscheme (nvim submodule) and btop gets a
yorha theme with hairline boxes and grey-to-light gradients.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:09:07 +02:00
nvrlandClaude Opus 5.5 a8c19961e2 theme: soften the YoRHa shapes; SUPER+D opens the dashboard
The square markers, block meters and hard-cornered fills read as boxy
next to the porthole. The desktop is now soft throughout: the porthole
is the shape everything is built around and every smaller surface echoes
it.

- Theme gains radiusS / radiusM / radiusL. radiusL is frameRadius - padS,
  so anything inset padS inside one of the frame's curves is concentric
  with it.
- P5Panel and Skew draw rounded rectangles (Geom.roundedRect) whenever
  they neither lean nor are chopped, which is always now; the polygon
  path is kept for shapes brought back on purpose.
- Markers are dots; workspace pips, toggles, transport and media chips,
  notification actions and today's date are pills or discs; the slider
  handle is round. Meters are one continuous rounded line that glides to
  its level instead of a row of segments.
- Album art is rounded with ClippingRectangle in the bar, the media
  popout and the dashboard.
- Free-standing panels (launcher, ask) drop the corner brackets and take
  the frame radius.
- Fix: a hovered notification's wash was a full-bleed square, so in the
  drawer's bottom corner it ran past the drawer's curve. It is inset by
  padS/2 and rounded by radiusL, which keeps it inside the curve.

Hyprland: the discord special workspace and its window rule are gone,
SUPER+S stays the only scratchpad, and SUPER+D now opens the dashboard
(it was on SUPER+I).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:56:43 +02:00
nvrlandClaude Opus 5.5 d6de04eaaf feat(quickshell): notifications as a NieR message log in a corner drawer
Notifications were still Persona: separate floating cards with an urgency
slash down the leading edge, action chips that filled with the accent on
hover, and a 60px shove in from the right.

NotifCard is now laid out like an entry in NieR's message log: a square
urgency marker and the sender's name running into a hairline, the summary
and body set in from the marker, and hairline action buttons with their
own small marker that lift on hover instead of inverting. It draws no
panel; rest is bare and hover lifts the row onto the surface tone.
Critical entries take the red into the marker and the sender line only.

The toast stack is one hull drawer docked into the opening's top-right
corner — off the bar through one fillet, onto the rail through the other,
the same construction as a corner-docked popout. It grows with its
entries and slides back behind the frame when the last one goes. Each
entry's countdown is a hairline along its foot, and new entries fade in
place. The history popout lists the same entries flush.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:48:30 +02:00
nvrlandClaude Opus 5.5 ae6539218e theme: strip the remaining Persona punch from Hyprland and the popouts
Hyprland:
- Windows round at 8px, concentric with the porthole's 18px corners at
  the 10px gap, instead of square corners inside a rounded opening.
- Group tabs are solid hull slabs; the titles used to float over the
  wallpaper. The current tab is lifted a step with its title in the light.
- Motion rewritten to glide and settle: no overshoot curve, gentler
  popin, workspaces slide-fade a short way instead of sweeping the whole
  screen, and layers only fade so they never stack on quickshell's own
  slides.

Quickshell:
- Album art in the bar and the media popout is square in a hairline frame,
  not masked to a leaning chamfered shape; the accent slab under the
  popout art is gone, and a track change dips the title instead of
  tilting the art.
- The bar's equaliser is a five-hairline signal meter that rests as dots.
- Segment meters (system, battery, OSD) are 4px rows instead of 10-12px
  blocks; the toast fuse is a hairline; the clock's accent slab is a
  neutral divider.
- No hover pops: rail icons, tray icons, media transport buttons and
  P5Button no longer scale up or lunge sideways; the icon bump is reduced.

Terminal: ANSI bright black drops to #464d44. TUIs use it as a panel fill,
and at the outline grey it read as a flat light slab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:45:24 +02:00
nvrlandClaude Opus 5.5 a27dd6f93b fix(quickshell): rail popouts no longer land short of the rail
Popouts anchored to their trigger item, and Quickshell maps an anchor
rect given in item coordinates through the item's transform. Rail icons
scale to 1.08 on hover, so a panel placed ~360px left of the icon landed
up to ~29px further left still, leaving a strip of desktop between it and
the rail — by however far the hover scale had got when the dwell timer
fired, which is why it only happened sometimes.

Anchor to the trigger window's content item instead, which never
transforms, and place in window coordinates from the trigger's centre,
which a scale about the centre leaves where it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:38:06 +02:00
nvrlandClaude Opus 5.5 196abc2311 feat(quickshell): popouts grow out of the frame
Hover panels used to float a gap away from their trigger as free-standing
cards. They are now part of the hull: flush against the frame edge they
open from, painted in the hull colour, joined to it through concave
fillets, with the porthole rim carried round their open side, and they
slide out from behind the frame instead of fading and scaling in.

Popout places itself rather than relying on the compositor's slide
adjustment. A panel is centred on its trigger and, if that lands it near
one of the opening's corners, docked into the corner — attached to the
bar and the rail at once — so it never leaves a thin strip of opening
between itself and the rail. The calendar, network and audio panels dock
top-right; notifications dock bottom-right.

PopoutSurface leaves fill, border and corner ticks to the hull when it is
inside a Popout, and keeps them in the launcher and the ask panel. Its
attached fill opacity is 0 rather than a transparent colour, because
P5Panel re-applies fillOpacity to the colour and turned "transparent"
into 97% black.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:34:17 +02:00
nvrlandClaude Opus 5.5 ecc7d2884c feat(quickshell): dashboard drawer that drops from the bar
A drawer lowered out of the underside of the bar, painted in the hull
colour and joined to it by two concave fillets so it reads as more of the
hull rather than as a floating card — caelestia's dashboard idea, in the
YoRHa language. Four columns divided by hairlines: operator and clock, the
month with today as an inverted square, the system meters, and the active
player with a hairline timeline and square transport buttons.

Opened by clicking the bar's clock (which used to pin the calendar), by
SUPER+I, or by `qs ipc call shell dashboard`. Escape or moving focus away
closes it through a HyprlandFocusGrab; Actions.dashboardClosedAt keeps the
clock click that cleared the grab from reopening it. The clock's calendar
hover is suppressed while the drawer is down, since it shows the month.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:19:30 +02:00
nvrlandClaude Opus 5.5 18091a7109 theme: YoRHa dark — NieR:Automata interface on the space-1 wallpaper
Replaces the Persona x Mocha rice. NieR's menu language is kept whole —
plain rectangles, hairline rules, letter-spaced upright capitals, a square
marker on the current row, corner ticks round panels, and selection shown
by inverting the row — but the colours come from ~/.wallpapers/space-1.jpg
rather than from the game's beige: the grey-green void as the ground, the
suit's sage grey as text, the planet's white as the one accent, and the
hull's red indicator lights as the alarm.

The Persona geometry is switched off rather than ripped out: Theme.skew
and Theme.cut are 0, so P5Panel, Skew and Geom collapse to rectangles.
Overshoot easing, italics and black weights are gone shell-wide.

New, borrowed from caelestia's screen border: bar/Frame.qml draws the bar,
the rail and a thin left/bottom band as one hull on the bottom layer, with
the desktop as a rounded opening cut out of it. Frame also owns all four
exclusive zones, so the bar and rail are placed from the screen edge.

Hyprland moves to 1px borders lit from the planet white into grey, a soft
wide shadow and gaps measured from the opening. Alacritty, fish and dunst
get matching YoRHa palettes. Neovim, GTK/Qt and btop are still Mocha.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:10:53 +02:00
nvrl 5be22d86a9 updated autostart 2026-10-04 19:31:19 +02:00
128 changed files with 4747 additions and 3608 deletions

No files matched your search

+3
View File
@@ -44,3 +44,6 @@ gtk-4.0/*
!qt6ct/
qt6ct/*
!qt6ct/qt6ct.conf
!qt6ct/colors/
qt6ct/colors/*
!qt6ct/colors/yorha.conf
-9
View File
@@ -1,24 +1,15 @@
[submodule "waybar/catppuccin-waybar"]
path = waybar/catppuccin-waybar
url = https://github.com/catppuccin/waybar.git
[submodule "hypr/catppuccin-hyprland"]
path = hypr/catppuccin-hyprland
url = https://github.com/catppuccin/hyprland.git
[submodule "hypr/catppuccin-hyprlock"]
path = hypr/catppuccin-hyprlock
url = https://github.com/catppuccin/hyprlock.git
[submodule "zathura/catppuccin-zathura"]
path = zathura/catppuccin-zathura
url = https://github.com/catppuccin/zathura.git
[submodule "nvim/pack/nvim/start/mini.nvim"]
path = nvim/pack/nvim/start/mini.nvim
url = https://github.com/echasnovski/mini.nvim
[submodule "nvim/pack/nvim/start/nvim-lspconfig"]
path = nvim/pack/nvim/start/nvim-lspconfig
url = https://github.com/neovim/nvim-lspconfig
[submodule "btop/catppuccin-btop"]
path = btop/catppuccin-btop
url = https://github.com/catppuccin/btop.git
[submodule "nvim"]
path = nvim
url = https://git.narl.io/nvrl/nvim-config.git
+3 -3
View File
@@ -13,7 +13,7 @@ position = "None"
dynamic_padding = true
decorations = "None"
## Fully opaque, no blur. Persona surfaces are flat ink on flat ground —
## Fully opaque, no blur. YoRHa surfaces are flat and opaque —
## glass and translucency read as a different design language entirely.
opacity = 1.0
blur = false
@@ -28,7 +28,7 @@ columns = 82
lines = 24
## 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.
## the text needs to sit inside the border, not against it.
[window.padding]
x = 12
y = 8
@@ -107,7 +107,7 @@ ipc_socket = true
## GENERAL --------------------------------------------------------
## Import additional configuration files.
import = ["~/.config/alacritty/catppuccin-mocha.toml", "~/.config/alacritty/fonts.toml"]
import = ["~/.config/alacritty/yorha.toml", "~/.config/alacritty/fonts.toml"]
[[keyboard.bindings]]
key = "Return"
+67
View File
@@ -0,0 +1,67 @@
# YoRHa dark — matched to ~/.config/quickshell/config/Theme.qml.
#
# The ground and the text are the space-1 wallpaper's void and sage grey. The
# eight hues are the same few steps of earth NieR's world is painted in —
# rust, moss, sand, dusk — pulled down to sit in that void without glowing.
# Selection and search invert, the way NieR's cursor does.
[colors.primary]
background = "#0f110e"
foreground = "#ccd2cb"
dim_foreground = "#79847a"
bright_foreground = "#eef1ea"
[colors.cursor]
text = "#0f110e"
cursor = "#eef1ea"
[colors.vi_mode_cursor]
text = "#0f110e"
cursor = "#bcc1bf"
[colors.search.matches]
foreground = "#0f110e"
background = "#9fa7a2"
[colors.search.focused_match]
foreground = "#0f110e"
background = "#eef1ea"
[colors.footer_bar]
foreground = "#0f110e"
background = "#9fa7a2"
[colors.hints.start]
foreground = "#0f110e"
background = "#c9b27c"
[colors.hints.end]
foreground = "#0f110e"
background = "#9fa7a2"
[colors.selection]
text = "#0f110e"
background = "#bcc1bf"
[colors.normal]
black = "#20241f"
red = "#c8684f"
green = "#8fa37e"
yellow = "#c9b27c"
blue = "#7f9aa8"
magenta = "#a48ba0"
cyan = "#86a8a0"
white = "#bcc1bf"
# Bright black doubles as a panel fill in TUIs (Claude Code's side pane, fzf
# previews), so it sits one step under the outline grey rather than at it:
# light enough for comments, dark enough not to read as a slab.
[colors.bright]
black = "#464d44"
red = "#d98068"
green = "#a5b893"
yellow = "#d8c491"
blue = "#96afbc"
magenta = "#b8a0b4"
cyan = "#9cbdb4"
white = "#eef1ea"
+1 -1
View File
@@ -2,7 +2,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"
color_theme = "/home/narl/.config/btop/themes/catppuccin_mocha.theme"
color_theme = "/home/narl/.config/btop/themes/yorha.theme"
#* If the theme set background should be shown, set to False if you want terminal background transparency.
theme_background = false
-83
View File
@@ -1,83 +0,0 @@
# Main background, empty for terminal default, need to be empty if you want transparent background
theme[main_bg]="#1e1e2e"
# Main text color
theme[main_fg]="#cdd6f4"
# Title color for boxes
theme[title]="#cdd6f4"
# Highlight color for keyboard shortcuts
theme[hi_fg]="#89b4fa"
# Background color of selected item in processes box
theme[selected_bg]="#45475a"
# Foreground color of selected item in processes box
theme[selected_fg]="#89b4fa"
# Color of inactive/disabled text
theme[inactive_fg]="#7f849c"
# Color of text appearing on top of graphs, i.e uptime and current network graph scaling
theme[graph_text]="#f5e0dc"
# Background color of the percentage meters
theme[meter_bg]="#45475a"
# Misc colors for processes box including mini cpu graphs, details memory graph and details status text
theme[proc_misc]="#f5e0dc"
# CPU, Memory, Network, Proc box outline colors
theme[cpu_box]="#cba6f7" #Mauve
theme[mem_box]="#a6e3a1" #Green
theme[net_box]="#eba0ac" #Maroon
theme[proc_box]="#89b4fa" #Blue
# Box divider line and small boxes line color
theme[div_line]="#6c7086"
# Temperature graph color (Green -> Yellow -> Red)
theme[temp_start]="#a6e3a1"
theme[temp_mid]="#f9e2af"
theme[temp_end]="#f38ba8"
# CPU graph colors (Teal -> Lavender)
theme[cpu_start]="#94e2d5"
theme[cpu_mid]="#74c7ec"
theme[cpu_end]="#b4befe"
# Mem/Disk free meter (Mauve -> Lavender -> Blue)
theme[free_start]="#cba6f7"
theme[free_mid]="#b4befe"
theme[free_end]="#89b4fa"
# Mem/Disk cached meter (Sapphire -> Lavender)
theme[cached_start]="#74c7ec"
theme[cached_mid]="#89b4fa"
theme[cached_end]="#b4befe"
# Mem/Disk available meter (Peach -> Red)
theme[available_start]="#fab387"
theme[available_mid]="#eba0ac"
theme[available_end]="#f38ba8"
# Mem/Disk used meter (Green -> Sky)
theme[used_start]="#a6e3a1"
theme[used_mid]="#94e2d5"
theme[used_end]="#89dceb"
# Download graph colors (Peach -> Red)
theme[download_start]="#fab387"
theme[download_mid]="#eba0ac"
theme[download_end]="#f38ba8"
# Upload graph colors (Green -> Sky)
theme[upload_start]="#a6e3a1"
theme[upload_mid]="#94e2d5"
theme[upload_end]="#89dceb"
# Process box color gradient for threads, mem and cpu usage (Sapphire -> Mauve)
theme[process_start]="#74c7ec"
theme[process_mid]="#b4befe"
theme[process_end]="#cba6f7"
+60
View File
@@ -0,0 +1,60 @@
# YoRHa dark — matched to ~/.config/quickshell/config/Theme.qml and
# ~/.config/alacritty/yorha.toml.
#
# The void ground and sage text of the space-1 wallpaper. Boxes are hairline
# grey, not four competing hues; the light marks what is selected. Every
# gradient climbs from the suit's grey up to the light, and only temperature
# and load run on into the hull's rust, the shell's one colour for "wrong".
theme[main_bg]="#0f110e"
theme[main_fg]="#ccd2cb"
theme[title]="#eef1ea"
theme[hi_fg]="#eef1ea"
theme[selected_bg]="#20241f"
theme[selected_fg]="#eef1ea"
theme[inactive_fg]="#545c52"
theme[graph_text]="#9fa7a2"
theme[meter_bg]="#20241f"
theme[proc_misc]="#9fa7a2"
theme[cpu_box]="#383e35"
theme[mem_box]="#383e35"
theme[net_box]="#383e35"
theme[proc_box]="#383e35"
theme[div_line]="#2a2f28"
theme[temp_start]="#8e978d"
theme[temp_mid]="#c9b27c"
theme[temp_end]="#c8684f"
theme[cpu_start]="#79847a"
theme[cpu_mid]="#bcc1bf"
theme[cpu_end]="#c8684f"
theme[free_start]="#545c52"
theme[free_mid]="#79847a"
theme[free_end]="#9fa7a2"
theme[cached_start]="#545c52"
theme[cached_mid]="#7f9aa8"
theme[cached_end]="#96afbc"
theme[available_start]="#545c52"
theme[available_mid]="#86a8a0"
theme[available_end]="#9cbdb4"
theme[used_start]="#79847a"
theme[used_mid]="#bcc1bf"
theme[used_end]="#eef1ea"
theme[download_start]="#545c52"
theme[download_mid]="#8fa37e"
theme[download_end]="#a5b893"
theme[upload_start]="#545c52"
theme[upload_mid]="#c9b27c"
theme[upload_end]="#d8c491"
theme[process_start]="#79847a"
theme[process_mid]="#9fa7a2"
theme[process_end]="#eef1ea"
+10 -10
View File
@@ -1,22 +1,22 @@
[global]
corner_radius = 4
corner_radius = 0
transparency = 90
frame_color = "#89b4fa"
frame_color = "#545c52"
frame_width = 1
separator_color= frame
highlight = "#cba6f7"
highlight = "#eef1ea"
[urgency_low]
background = "#1e1e2e"
foreground = "#cdd6f4"
background = "#0f110e"
foreground = "#ccd2cb"
timeout = 4
[urgency_normal]
background = "#1e1e2e"
foreground = "#cdd6f4"
background = "#0f110e"
foreground = "#ccd2cb"
timeout = 6
[urgency_critical]
background = "#1e1e2e"
foreground = "#cdd6f4"
frame_color = "#f38ba8"
background = "#0f110e"
foreground = "#ccd2cb"
frame_color = "#c8684f"
-52
View File
@@ -1,52 +0,0 @@
# Mocha — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/catppuccin-mocha.toml and ~/.config/nvim/lua/takemi.lua.
#
# Catppuccin Mocha's own fish theme (themes/Catppuccin Mocha.theme), with the
# selection and pager rows carried over from the Takemi layout: a solid accent
# block with crust punched out of it, rather than a tinted surface. The shell
# selects with a blue slab and marks with mauve; the prompt should not
# disagree.
#
# 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 cdd6f4
set --global fish_color_command 89b4fa
set --global fish_color_keyword f38ba8
set --global fish_color_param f2cdcd
set --global fish_color_option a6e3a1
set --global fish_color_quote a6e3a1
set --global fish_color_escape eba0ac
set --global fish_color_operator f5c2e7
set --global fish_color_redirection f5c2e7
set --global fish_color_end fab387
set --global fish_color_comment 7f849c --italics
set --global fish_color_error f38ba8 --bold
set --global fish_color_cancel f38ba8
set --global fish_color_gray 6c7086
set --global fish_color_autosuggestion 6c7086
set --global fish_color_selection 11111b --background=89b4fa
set --global fish_color_search_match 11111b --background=cba6f7
set --global fish_color_match 11111b --background=89b4fa
set --global fish_color_history_current --bold
set --global fish_color_valid_path --underline
set --global fish_color_cwd f9e2af --bold
set --global fish_color_cwd_root f38ba8 --bold
set --global fish_color_user 94e2d5
set --global fish_color_host 89b4fa --bold
set --global fish_color_host_remote a6e3a1
set --global fish_color_status f38ba8
set --global fish_pager_color_progress 6c7086 --background=313244
set --global fish_pager_color_background
set --global fish_pager_color_prefix cba6f7 --bold
set --global fish_pager_color_completion cdd6f4
set --global fish_pager_color_description 6c7086
set --global fish_pager_color_secondary_background --background=181825
set --global fish_pager_color_secondary_completion cdd6f4
set --global fish_pager_color_secondary_description 6c7086
set --global fish_pager_color_secondary_prefix cba6f7 --bold
set --global fish_pager_color_selected_background --background=89b4fa
set --global fish_pager_color_selected_completion 11111b
set --global fish_pager_color_selected_description 11111b
set --global fish_pager_color_selected_prefix 11111b --bold
+51
View File
@@ -0,0 +1,51 @@
# YoRHa dark — matched to ~/.config/quickshell/config/Theme.qml and
# ~/.config/alacritty/yorha.toml.
#
# Commands are the planet light, arguments the sage text, strings moss,
# anything wrong the hull red. Selection and the pager's current row invert —
# a light slab with the void punched out of it, the same cursor the shell
# draws.
#
# 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 ccd2cb
set --global fish_color_command eef1ea --bold
set --global fish_color_keyword c9b27c
set --global fish_color_param bcc1bf
set --global fish_color_option 9cbdb4
set --global fish_color_quote 8fa37e
set --global fish_color_escape c9b27c
set --global fish_color_operator 9fa7a2
set --global fish_color_redirection 9fa7a2
set --global fish_color_end 79847a
set --global fish_color_comment 545c52
set --global fish_color_error c8684f --bold
set --global fish_color_cancel c8684f
set --global fish_color_gray 545c52
set --global fish_color_autosuggestion 545c52
set --global fish_color_selection 0f110e --background=bcc1bf
set --global fish_color_search_match 0f110e --background=eef1ea
set --global fish_color_match 0f110e --background=bcc1bf
set --global fish_color_history_current --bold
set --global fish_color_valid_path --underline
set --global fish_color_cwd c9b27c --bold
set --global fish_color_cwd_root c8684f --bold
set --global fish_color_user 9cbdb4
set --global fish_color_host eef1ea --bold
set --global fish_color_host_remote 8fa37e
set --global fish_color_status c8684f
set --global fish_pager_color_progress 79847a --background=161915
set --global fish_pager_color_background
set --global fish_pager_color_prefix eef1ea --bold
set --global fish_pager_color_completion ccd2cb
set --global fish_pager_color_description 79847a
set --global fish_pager_color_secondary_background --background=0c0d0b
set --global fish_pager_color_secondary_completion ccd2cb
set --global fish_pager_color_secondary_description 79847a
set --global fish_pager_color_secondary_prefix eef1ea --bold
set --global fish_pager_color_selected_background --background=eef1ea
set --global fish_pager_color_selected_completion 0f110e
set --global fish_pager_color_selected_description 0f110e
set --global fish_pager_color_selected_prefix 0f110e --bold
+57 -4
View File
@@ -1,4 +1,57 @@
/* Catppuccin Mocha Blue carries the whole GTK3 palette itself, so there is
* nothing to override here — the Takemi branch only needed this file because
* it was painting Fluent's greys black. Kept as a hook for the next time the
* rice disagrees with stock Catppuccin. */
/* YoRHa colours for adw-gtk3 — kept in step with
* ~/.config/quickshell/config/Theme.qml and gtk-4.0/gtk.css.
*
* adw-gtk3 draws the libadwaita shapes (soft corners, flat cards) and takes
* every surface from these named colours, so the whole palette lives here.
* Selection is the shell's light slab with ink text; red only for destructive
* and failed. */
@define-color accent_color #eef1ea;
@define-color accent_bg_color #bcc1bf;
@define-color accent_fg_color #090a08;
@define-color destructive_color #c8684f;
@define-color destructive_bg_color #c8684f;
@define-color destructive_fg_color #090a08;
@define-color error_color #c8684f;
@define-color error_bg_color #c8684f;
@define-color error_fg_color #090a08;
@define-color warning_color #c9b27c;
@define-color warning_bg_color #c9b27c;
@define-color warning_fg_color #090a08;
@define-color success_color #8fa37e;
@define-color success_bg_color #8fa37e;
@define-color success_fg_color #090a08;
@define-color window_bg_color #0f110e;
@define-color window_fg_color #ccd2cb;
@define-color view_bg_color #0c0d0b;
@define-color view_fg_color #ccd2cb;
@define-color headerbar_bg_color #161915;
@define-color headerbar_fg_color #ccd2cb;
@define-color headerbar_border_color #2a2f28;
@define-color headerbar_backdrop_color #0f110e;
@define-color headerbar_shade_color rgba(0, 0, 0, 0.36);
@define-color sidebar_bg_color #161915;
@define-color sidebar_fg_color #ccd2cb;
@define-color sidebar_backdrop_color #0f110e;
@define-color sidebar_shade_color rgba(0, 0, 0, 0.36);
@define-color secondary_sidebar_bg_color #0f110e;
@define-color secondary_sidebar_fg_color #ccd2cb;
@define-color secondary_sidebar_backdrop_color #0c0d0b;
@define-color card_bg_color #161915;
@define-color card_fg_color #ccd2cb;
@define-color card_shade_color rgba(0, 0, 0, 0.36);
@define-color dialog_bg_color #161915;
@define-color dialog_fg_color #ccd2cb;
@define-color popover_bg_color #20241f;
@define-color popover_fg_color #ccd2cb;
@define-color popover_shade_color rgba(0, 0, 0, 0.36);
@define-color thumbnail_bg_color #20241f;
@define-color thumbnail_fg_color #ccd2cb;
@define-color shade_color rgba(0, 0, 0, 0.36);
@define-color scrollbar_outline_color rgba(0, 0, 0, 0.5);
+2 -2
View File
@@ -1,7 +1,7 @@
[Settings]
gtk-theme-name=catppuccin-mocha-blue-standard+default
gtk-theme-name=adw-gtk3-dark
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Adwaita Sans 11
gtk-font-name=Noto Sans 11
gtk-cursor-theme-name=Bibata-Modern-Classic
gtk-cursor-theme-size=24
gtk-application-prefer-dark-theme=1
+95 -4
View File
@@ -1,4 +1,95 @@
/* Catppuccin Mocha Blue, straight from the installed theme. The Takemi branch
* overrode Fluent's greys with black here; Mocha needs no override — its own
* surfaces are the palette the rest of the rice is built on. */
@import url("/usr/share/themes/catppuccin-mocha-blue-standard+default/gtk-4.0/gtk.css");
/* YoRHa colours for libadwaita and adw-gtk3 — kept in step with
* ~/.config/quickshell/config/Theme.qml and gtk-3.0/gtk.css.
*
* libadwaita reads its palette from CSS variables; the named colours below
* the variables cover GTK4 apps that still go through the adw-gtk3 theme. */
:root {
--accent-color: #eef1ea;
--accent-bg-color: #bcc1bf;
--accent-fg-color: #090a08;
--destructive-color: #c8684f;
--destructive-bg-color: #c8684f;
--destructive-fg-color: #090a08;
--error-color: #c8684f;
--error-bg-color: #c8684f;
--error-fg-color: #090a08;
--warning-color: #c9b27c;
--warning-bg-color: #c9b27c;
--warning-fg-color: #090a08;
--success-color: #8fa37e;
--success-bg-color: #8fa37e;
--success-fg-color: #090a08;
--window-bg-color: #0f110e;
--window-fg-color: #ccd2cb;
--view-bg-color: #0c0d0b;
--view-fg-color: #ccd2cb;
--headerbar-bg-color: #161915;
--headerbar-fg-color: #ccd2cb;
--headerbar-border-color: #2a2f28;
--headerbar-backdrop-color: #0f110e;
--headerbar-shade-color: rgba(0, 0, 0, 0.36);
--sidebar-bg-color: #161915;
--sidebar-fg-color: #ccd2cb;
--sidebar-backdrop-color: #0f110e;
--sidebar-shade-color: rgba(0, 0, 0, 0.36);
--secondary-sidebar-bg-color: #0f110e;
--secondary-sidebar-fg-color: #ccd2cb;
--secondary-sidebar-backdrop-color: #0c0d0b;
--card-bg-color: #161915;
--card-fg-color: #ccd2cb;
--card-shade-color: rgba(0, 0, 0, 0.36);
--dialog-bg-color: #161915;
--dialog-fg-color: #ccd2cb;
--popover-bg-color: #20241f;
--popover-fg-color: #ccd2cb;
--popover-shade-color: rgba(0, 0, 0, 0.36);
--thumbnail-bg-color: #20241f;
--thumbnail-fg-color: #ccd2cb;
--shade-color: rgba(0, 0, 0, 0.36);
--scrollbar-outline-color: rgba(0, 0, 0, 0.5);
}
@define-color accent_color #eef1ea;
@define-color accent_bg_color #bcc1bf;
@define-color accent_fg_color #090a08;
@define-color destructive_color #c8684f;
@define-color destructive_bg_color #c8684f;
@define-color destructive_fg_color #090a08;
@define-color error_color #c8684f;
@define-color error_bg_color #c8684f;
@define-color error_fg_color #090a08;
@define-color warning_color #c9b27c;
@define-color warning_bg_color #c9b27c;
@define-color warning_fg_color #090a08;
@define-color success_color #8fa37e;
@define-color success_bg_color #8fa37e;
@define-color success_fg_color #090a08;
@define-color window_bg_color #0f110e;
@define-color window_fg_color #ccd2cb;
@define-color view_bg_color #0c0d0b;
@define-color view_fg_color #ccd2cb;
@define-color headerbar_bg_color #161915;
@define-color headerbar_fg_color #ccd2cb;
@define-color headerbar_border_color #2a2f28;
@define-color headerbar_backdrop_color #0f110e;
@define-color headerbar_shade_color rgba(0, 0, 0, 0.36);
@define-color sidebar_bg_color #161915;
@define-color sidebar_fg_color #ccd2cb;
@define-color sidebar_backdrop_color #0f110e;
@define-color sidebar_shade_color rgba(0, 0, 0, 0.36);
@define-color secondary_sidebar_bg_color #0f110e;
@define-color secondary_sidebar_fg_color #ccd2cb;
@define-color secondary_sidebar_backdrop_color #0c0d0b;
@define-color card_bg_color #161915;
@define-color card_fg_color #ccd2cb;
@define-color card_shade_color rgba(0, 0, 0, 0.36);
@define-color dialog_bg_color #161915;
@define-color dialog_fg_color #ccd2cb;
@define-color popover_bg_color #20241f;
@define-color popover_fg_color #ccd2cb;
@define-color popover_shade_color rgba(0, 0, 0, 0.36);
@define-color thumbnail_bg_color #20241f;
@define-color thumbnail_fg_color #ccd2cb;
@define-color shade_color rgba(0, 0, 0, 0.36);
@define-color scrollbar_outline_color rgba(0, 0, 0, 0.5);
+2 -2
View File
@@ -1,7 +1,7 @@
[Settings]
gtk-theme-name=catppuccin-mocha-blue-standard+default
gtk-theme-name=adw-gtk3-dark
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Adwaita Sans 11
gtk-font-name=Noto Sans 11
gtk-cursor-theme-name=Bibata-Modern-Classic
gtk-cursor-theme-size=24
gtk-application-prefer-dark-theme=1
-545
View File
@@ -1,545 +0,0 @@
# 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
# 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 = $wallpaper
blur_passes = $blur
contrast = 0.8916
brightness = 0.7172
vibrancy = 0.1696
vibrancy_darkness = 0
}
# 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 = 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
}
# WEATHERCAST & LOCATION
label {
monitor =
text = cmd[update:1000] echo "$(bash $scrPath/weatherinfo.sh)"
shadow_passes = 1
shadow_boost = 0.5
color = $fg0
font_size = 11
font_family = $font-text
position = 0, -20
halign = center
valign = top
}
# INPUT FIELD
input-field {
monitor =
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
inner_color = $inner-color
font_color = $fg0
fade_on_empty = false
font_family = $font-text
placeholder_text = <span foreground="##$textAlpha">󰢏 $USER</span>
hide_input = false
check_color = $mauve
fail_color = $red
fail_text = <i>$FAIL <b>($ATTEMPTS)</b></i>
capslock_color = $yellow
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
@@ -1,155 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

-33
View File
@@ -1,33 +0,0 @@
#!/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
@@ -1,47 +0,0 @@
#!/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
@@ -1,22 +0,0 @@
#!/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
@@ -1,32 +0,0 @@
#!/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
@@ -1,65 +0,0 @@
#!/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
@@ -1,116 +0,0 @@
#!/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
@@ -1,39 +0,0 @@
#!/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
+1 -1
View File
@@ -1,6 +1,6 @@
wallpaper {
monitor =
path = ~/.wallpapers/arch.png
path = ~/.wallpapers/space-1.jpg
fit_mode = cover
}
-97
View File
@@ -1,97 +0,0 @@
#!/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\""
+52 -27
View File
@@ -1,11 +1,25 @@
-- https://wiki.hypr.land/Configuring/Advanced-and-Cool/Animations/
--
-- Quick and clean. Motion should get out of the way, not perform.
-- YoRHa motion: calm and mechanical. Things glide in, settle and stop; nothing
-- overshoots, nothing pops. Matches Theme.easeSnap / easeGlide in
-- ~/.config/quickshell/config/Theme.qml.
--
-- An earlier 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 does not animate at all.
-- The screen is a porthole: the hull (quickshell/bar/Frame.qml) sits on the
-- top layer, so anything that moves past the edge of the opening passes
-- behind the frame instead of over the bar.
--
-- windows resolve in from a hair smaller and fade, the way a NieR menu
-- panel appears rather than arrives; leave the same way, faster.
-- workspaces drift: the old one sinks a little way behind the rim as it
-- fades, the new one rises into its place. A short, slow
-- vertical drift rather than a full-screen sweep, which on a
-- 2560px panel is a lot of motion for "you are somewhere else
-- now". Direction follows workspace order.
-- scratchpad a drawer: SUPER+S lowers it out from under the bar, the same
-- gesture as the dashboard and the popouts, and raises it back.
-- layers only fade; quickshell animates its own surfaces.
--
-- Nothing overshoots and the border angle never animates.
hl.config({
animations = {
@@ -14,34 +28,45 @@ hl.config({
})
-- ------------------------------------------------------------------- curves
hl.curve("snap", { type = "bezier", points = { { 0.16, 1.00 }, { 0.30, 1.00 } } })
hl.curve("overshoot", { type = "bezier", points = { { 0.34, 1.12 }, { 0.64, 1.00 } } })
hl.curve("hard", { type = "bezier", points = { { 0.55, 0.00 }, { 0.90, 0.35 } } })
-- outQuint: fast off the mark, long quiet settle.
hl.curve("glide", { type = "bezier", points = { { 0.22, 1.00 }, { 0.36, 1.00 } } })
-- Material standard, for things that move from one place to another.
hl.curve("standard", { type = "bezier", points = { { 0.40, 0.00 }, { 0.20, 1.00 } } })
-- Leaving: ease in, so the exit gathers pace and is gone.
hl.curve("exit", { type = "bezier", points = { { 0.55, 0.00 }, { 0.90, 0.35 } } })
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
-- easeInOutSine-ish with a long tail: starts without a jolt, settles slowly.
-- For motion big enough that a fast start would read as a shove.
hl.curve("drift", { type = "bezier", points = { { 0.35, 0.00 }, { 0.15, 1.00 } } })
-- --------------------------------------------------------------------- tree
-- speed is in ds (1ds = 100ms). Unset leaves inherit their parent.
hl.animation({ leaf = "global", enabled = true, speed = 4, bezier = "snap" })
hl.animation({ leaf = "border", enabled = true, speed = 5, bezier = "snap" })
hl.animation({ leaf = "global", enabled = true, speed = 4, bezier = "glide" })
hl.animation({ leaf = "border", enabled = true, speed = 4, bezier = "glide" })
hl.animation({ leaf = "borderangle", enabled = false })
hl.animation({ leaf = "windows", enabled = true, speed = 4, bezier = "snap" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4, bezier = "overshoot", style = "popin 96%" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 2.6, bezier = "hard", style = "popin 97%" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 3.6, bezier = "snap" })
hl.animation({ leaf = "windows", enabled = true, speed = 4, bezier = "glide" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4, bezier = "glide", style = "popin 94%" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 2.4, bezier = "exit", style = "popin 96%" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 4, bezier = "standard" })
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "snap" })
hl.animation({ leaf = "fadeIn", enabled = true, speed = 2.4, bezier = "snap" })
hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.8, bezier = "hard" })
hl.animation({ leaf = "fadeSwitch", enabled = true, speed = 2, bezier = "snap" })
hl.animation({ leaf = "fadeShadow", enabled = true, speed = 3, bezier = "snap" })
hl.animation({ leaf = "fadeDim", enabled = true, speed = 2.4, bezier = "snap" })
hl.animation({ leaf = "fade", enabled = true, speed = 3.4, bezier = "glide" })
hl.animation({ leaf = "fadeIn", enabled = true, speed = 3.4, bezier = "glide" })
hl.animation({ leaf = "fadeOut", enabled = true, speed = 2.2, bezier = "exit" })
hl.animation({ leaf = "fadeSwitch", enabled = true, speed = 3, bezier = "glide" })
hl.animation({ leaf = "fadeShadow", enabled = true, speed = 3.4, bezier = "glide" })
hl.animation({ leaf = "fadeDim", enabled = true, speed = 3, bezier = "glide" })
hl.animation({ leaf = "layers", enabled = true, speed = 3, bezier = "snap" })
hl.animation({ leaf = "layersIn", enabled = true, speed = 3, bezier = "snap", style = "slide" })
hl.animation({ leaf = "layersOut", enabled = true, speed = 2, bezier = "hard", style = "slide" })
-- Quickshell animates its own surfaces; the compositor only fades them, so the
-- two motions never stack.
hl.animation({ leaf = "layers", enabled = true, speed = 2.6, bezier = "glide" })
hl.animation({ leaf = "layersIn", enabled = true, speed = 2.6, bezier = "glide", style = "fade" })
hl.animation({ leaf = "layersOut", enabled = true, speed = 2, bezier = "exit", style = "fade" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 3, bezier = "snap", style = "slidevert" })
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 3, bezier = "snap", style = "slidevert" })
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 2.4, bezier = "hard", style = "slidevert" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 5.5, bezier = "drift", style = "slidefadevert 18%" })
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 5.5, bezier = "drift", style = "slidefadevert 18%" })
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 4.5, bezier = "drift", style = "slidefadevert 18%" })
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 3, bezier = "snap", style = "slidevert" })
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 4.5, bezier = "glide", style = "slidevert" })
hl.animation({ leaf = "specialWorkspaceIn", enabled = true, speed = 4.5, bezier = "glide", style = "slidevert" })
hl.animation({ leaf = "specialWorkspaceOut", enabled = true, speed = 3.5, bezier = "exit", style = "slidevert" })
+3 -2
View File
@@ -10,7 +10,8 @@ hl.on("hyprland.start", function()
hl.exec_cmd("uwsm app -- qs")
hl.exec_cmd("uwsm app -- nm-applet --indicator")
hl.exec_cmd("uwsm app -- hyprpaper")
-- hyprpaper is not started here: its systemd unit is enabled for
-- graphical-session.target, and a second copy here ran alongside it.
-- clipboard history, text and images
hl.exec_cmd("wl-paste --type text --watch cliphist store")
@@ -19,7 +20,7 @@ hl.on("hyprland.start", function()
-- pdfs-prompt has no hidden/resident mode, so the first search of the
-- session is cold. Run one throwaway query once the Proton Drive daemon
-- is up to pull the binary into the page cache and warm the daemon's index.
hl.exec_cmd("sh -c 'sleep 15; pdfs-prompt --feed a >/dev/null 2>&1'")
hl.exec_cmd("uwsm app -- pdfs-prompt --preload")
-- hl.exec_cmd("uwsm app -- nextcloud --background")
-- hl.exec_cmd("uwsm app -- rclone mount google_drive: ~/gdrive")
+7 -4
View File
@@ -5,10 +5,13 @@ local scripts = os.getenv("HOME") .. "/.config/hypr/scripts/"
-- ------------------------------------------------------------- screenshots
-- PRINT selects a region, CTRL grabs the focused window, SHIFT saves to disk
-- instead of the clipboard.
hl.bind("PRINT", hl.dsp.exec_cmd('grim -g "$(slurp)" - | wl-copy'))
hl.bind("SHIFT + PRINT", hl.dsp.exec_cmd('grim -g "$(slurp)" ~/Pictures/Screenshots/$(date +\'%Y-%m-%d_%H-%M-%S\').png'))
hl.bind("CTRL + PRINT", hl.dsp.exec_cmd(scripts .. "screenshot_window.sh copy"))
hl.bind("CTRL + SHIFT + PRINT", hl.dsp.exec_cmd(scripts .. "screenshot_window.sh save"))
-- All four go through scripts/shot.sh, which also posts a notification with
-- the capture as its thumbnail (Open, Edit with satty, Save).
local shot = scripts .. "shot.sh "
hl.bind("PRINT", hl.dsp.exec_cmd(shot .. "region copy"), { description = "screenshot an area to the clipboard" })
hl.bind("SHIFT + PRINT", hl.dsp.exec_cmd(shot .. "region save"), { description = "screenshot an area to a file" })
hl.bind("CTRL + PRINT", hl.dsp.exec_cmd(shot .. "window copy"), { description = "screenshot the window to the clipboard" })
hl.bind("CTRL + SHIFT + PRINT", hl.dsp.exec_cmd(shot .. "window save"), { description = "screenshot the window to a file" })
-- ------------------------------------------------------- replay and camera
-- On release, so the shortcut is not caught mid-chord.
+10
View File
@@ -9,7 +9,17 @@ end
shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" })
shell("power", { key = mod .. " + P", description = "power menu" })
shell("ask", { key = mod .. " + A", description = "ask claude" })
shell("dashboard", { key = mod .. " + D", description = "dashboard" })
shell("overview", { key = mod .. " + W", description = "overview of every workspace" })
-- Alt+Tab: hold Alt, tap Tab through focus history, let go to switch. The
-- overview sees the release itself; the release bind is a backstop for the
-- case where Alt comes up before the overview has the keyboard.
shell("overviewNext", { key = "ALT + Tab", description = "switch windows" })
shell("overviewPrev", { key = "ALT + SHIFT + Tab", description = "switch windows, backwards" })
hl.bind("ALT + ALT_L", hl.dsp.exec_cmd("qs ipc call shell overviewCommit"), { release = true })
shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
shell("toggleStayAwake", { key = mod .. " + SHIFT + I", description = "keep the screen awake" })
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
+4 -2
View File
@@ -18,8 +18,10 @@ end
-- ------------------------------------------------------------------- groups
hl.bind(mod .. " + n", hl.dsp.group.toggle())
hl.bind("ALT + Tab", hl.dsp.group.next())
-- hl.bind("ALT + Tab", hl.dsp.group.prev())
-- Alt+Tab belongs to the overview (lua/binds/shell.lua), so tabs in a group
-- cycle on Super+Tab.
hl.bind(mod .. " + Tab", hl.dsp.group.next(), { description = "next window in group" })
hl.bind(mod .. " + SHIFT + Tab", hl.dsp.group.prev(), { description = "previous window in group" })
-- -------------------------------------------------------------------- mouse
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
+4 -8
View File
@@ -1,4 +1,4 @@
-- Workspaces, including the two scratchpads.
-- Workspaces, including the scratchpad.
local mod = "SUPER"
@@ -13,10 +13,6 @@ end
hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
-- Scratchpads: S holds anything virtual, D holds chat.
local special = { S = "virtual", D = "discord" }
for key, name in pairs(special) do
hl.bind(mod .. " + " .. key, hl.dsp.workspace.toggle_special(name))
hl.bind(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = "special:" .. name }))
end
-- Scratchpad: S holds anything virtual. Chat opens wherever it is launched.
hl.bind(mod .. " + S", hl.dsp.workspace.toggle_special("virtual"))
hl.bind(mod .. " + SHIFT + S", hl.dsp.window.move({ workspace = "special:virtual" }))
+1 -8
View File
@@ -79,9 +79,6 @@ local function reconcile(ignore)
return
end
-- First decision of the session, and it agrees with what lua/monitors.lua
-- already applied: nothing actually moved, so no daemon needs poking.
local settled = panel_off == nil and not want_off
panel_off = want_off
if want_off then
@@ -91,11 +88,7 @@ local function reconcile(ignore)
end
monitors.apply_external()
-- Quickshell follows monitor hotplug by itself; only the wallpaper daemon
-- needs a kick.
if not settled then
hl.exec_cmd("systemctl restart --user hyprpaper")
end
-- Quickshell follows monitor hotplug by itself, wallpaper included.
end
-- ------------------------------------------------------------------ triggers
+49 -34
View File
@@ -1,5 +1,5 @@
-- Persona × Mocha — Persona 5 shapes on the Catppuccin Mocha palette, matched
-- to ~/.config/quickshell.
-- YoRHa dark — NieR:Automata's interface language on the space-1 wallpaper's
-- palette, matched to ~/.config/quickshell.
--
-- Everything static about how the desktop is painted. Motion lives next door
-- in lua/animations.lua.
@@ -9,17 +9,20 @@ local c = require("lua/palette")
hl.config({
-- https://wiki.hypr.land/Configuring/Basics/Variables/
general = {
-- Measured from the quickshell hull's opening, not the screen edge: the
-- hull reserves its own exclusive zones. 10px clears the opening's 18px
-- corner radius, so a tiled window never touches the rim.
gaps_in = 5,
gaps_out = { top = 6, right = 8, bottom = 8, left = 8 },
gaps_out = 10,
border_size = 2,
border_size = 1,
col = {
active_border = c.activeBorder,
-- Surface-level, not outline: an idle window's frame is structure,
-- Overlay, not outline: an idle window's frame is structure,
-- and the outline grey was bright enough to compete with the
-- focused border.
inactive_border = c.surfaceAlt,
inactive_border = c.overlay,
},
resize_on_border = false,
@@ -32,29 +35,32 @@ hl.config({
},
decoration = {
-- Persona 5 has no rounded corners. Neither do we.
rounding = 0,
-- Concentric with the porthole: the opening's corners are 18px and
-- the windows sit 10px inside them, so 8px keeps every gap the same
-- width all the way round the curve. Matches Theme.frameRadius -
-- gaps_out; change one and the other has to follow.
rounding = 8,
rounding_power = 2,
-- Fully opaque, both states. Flat ink on flat ground.
active_opacity = 1.0,
inactive_opacity = 1.0,
-- A hard crust offset, the same printed drop the shell's panels
-- carry, rather than a soft blur. Sharp with a 1px range leaves a
-- clean band down the right and bottom edges only.
-- A soft, wide shadow: windows float in the opening rather than
-- being printed onto it. Dark against the planet light, invisible
-- against the void, which is how a shadow behaves out there.
shadow = {
enabled = true,
sharp = true,
range = 1,
offset = { 4, 4 },
color = c.ink,
color_inactive = c.ink,
sharp = false,
range = 24,
render_power = 3,
offset = { 0, 6 },
color = "rgba(000000aa)",
color_inactive = "rgba(00000066)",
},
-- 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.
-- Off. Every surface here is opaque, so there is nothing to blur, and
-- blur would cost three render passes for no visible result.
blur = {
enabled = false,
},
@@ -65,35 +71,44 @@ hl.config({
disable_hyprland_logo = true,
-- 0 = off, 1 = always, 2 = fullscreen only (G7 is a 240Hz VRR panel)
vrr = 0,
font_family = "FiraCode Nerd Font",
font_family = "JetBrainsMono Nerd Font",
background_color = c.base,
focus_on_activate = true,
-- The shell powers displays down after idle (quickshell/services/Idle.qml)
-- and back up when idle ends; these wake them even if it is not running.
key_press_enables_dpms = true,
mouse_move_enables_dpms = true,
},
group = {
col = {
border_active = c.activeBorder,
border_inactive = c.surfaceAlt,
border_inactive = c.overlay,
},
-- Tabs are thin rounded lines, not titled slabs. The bar already
-- writes the focused title and its place in the group ("2/3", in
-- quickshell/bar/ActiveWindow.qml), so titles here only repeated it in
-- a second font. Hyprland paints a tab's indicator in the same colour
-- as its fill, so with no fill the current tab is simply the light one.
groupbar = {
font_family = "JetBrainsMono Nerd Font",
font_size = 11,
height = 24,
render_titles = false,
gradients = false,
-- --- Active tab ---
indicator_height = 3,
indicator_gap = 0,
rounding = 2,
round_only_edges = false,
keep_upper_gap = false,
gaps_in = 4,
gaps_out = 0,
col = {
active = c.accent,
inactive = c.surface,
inactive = c.overlay,
locked_active = c.alarm,
locked_inactive = c.overlay,
},
text_color = c.ink,
-- --- Inactive tab ---
text_color_inactive = c.muted,
indicator_height = 3,
},
},
+2 -3
View File
@@ -49,10 +49,9 @@ local function mirror_all(panel)
end
end
-- The bar and popouts stay on the outputs they were created for, so
-- respawn the shell the same way lua/autostart.lua starts it.
-- The bar, popouts and wallpaper stay on the outputs they were created for,
-- so respawn the shell the same way lua/autostart.lua starts it.
local function restart_clients()
hl.exec_cmd("systemctl restart --user hyprpaper")
hl.exec_cmd("sh -c 'qs kill; exec uwsm app -- qs'")
end
+24 -22
View File
@@ -1,6 +1,7 @@
-- Mocha palette — kept in step with ~/.config/quickshell/config/Theme.qml.
-- YoRHa dark palette — kept in step with ~/.config/quickshell/config/Theme.qml.
--
-- Catppuccin Mocha ground, blue accent, mauve alarm. Nothing else carries hue.
-- The wallpaper's grey-green void for the ground, its planet light as the
-- accent, the hull's red indicator as the alarm. Nothing else carries hue.
--
-- Returned as a table, so every module that paints something does
-- local c = require("lua/palette")
@@ -9,44 +10,45 @@
local c = {}
-- ---------------------------------------------------------------- THE KNOBS
-- Two colours in this setup: blue for anything live (window borders, group
-- indicators, focus), mauve for anything wrong. Change these lines and every
-- Two colours in this setup: the planet light for anything live (window
-- borders, group indicators, focus), the hull red for anything wrong. Change these lines and every
-- painted edge recolours with them.
--
-- Keep them in step with `accent` / `alarm` in
-- ~/.config/quickshell/config/Theme.qml, which is what colours the shell.
-- Those are the only places the colours are written.
c.accent = "rgb(89b4fa)"
c.alarm = "rgb(cba6f7)"
c.accent = "rgb(eef1ea)"
c.alarm = "rgb(c8684f)"
c.accentDim = "rgb(5a7dc0)"
c.accentSoft = "rgb(b4befe)"
c.accentDim = "rgb(8e978d)"
c.accentSoft = "rgb(bcc1bf)"
-- ------------------------------------------------------------------ neutrals
c.ink = "rgb(11111b)"
c.ink = "rgb(090a08)"
c.base = "rgb(1e1e2e)"
c.mantle = "rgb(181825)"
c.surface = "rgb(313244)"
c.surfaceAlt = "rgb(45475a)"
c.overlay = "rgb(585b70)"
c.outline = "rgb(6c7086)"
c.base = "rgb(0f110e)"
c.mantle = "rgb(0c0d0b)"
c.surface = "rgb(161915)"
c.surfaceAlt = "rgb(20241f)"
c.overlay = "rgb(383e35)"
c.outline = "rgb(545c52)"
c.text = "rgb(cdd6f4)"
c.subtext = "rgb(a6adc8)"
c.muted = "rgb(7f849c)"
c.text = "rgb(ccd2cb)"
c.subtext = "rgb(9fa7a2)"
c.muted = "rgb(79847a)"
-- Semantic aliases, so existing references resolve without inventing hues.
c.primary = c.text
c.primaryDim = "rgb(9399b2)"
c.primaryDim = "rgb(8e978d)"
c.glow = c.accent
c.blue = c.accent
c.deep = c.surfaceAlt
c.warn = c.alarm
c.ok = c.text
-- Blue into lavender, on the diagonal. Both ends are the accent's own family,
-- so the border never reads as two colours arguing.
c.activeBorder = { colors = { c.accent, c.accentSoft }, angle = 135 }
-- The focused window is lit by the planet: full light at the top-left, the
-- suit's grey at the far corner. One family, so it reads as light falling
-- across the frame rather than as two colours.
c.activeBorder = { colors = { c.accent, c.accentDim }, angle = 135 }
return c
+10
View File
@@ -11,3 +11,13 @@ for _, namespace in ipairs({ "bottom", "top", "takemi-launcher" }) do
ignore_alpha = 0.5,
})
end
-- The hull shares the top layer with the bar and the rail, which draw on it,
-- so they are ordered above it whatever order they map in. See
-- quickshell/bar/Frame.qml.
for _, namespace in ipairs({ "takemi-bar", "takemi-rail" }) do
hl.layer_rule({
match = { namespace = namespace },
order = -1,
})
end
-1
View File
@@ -54,7 +54,6 @@ hl.window_rule({ match = { class = GAMESCOPE }, workspace = "1" })
-- Special workspaces
hl.window_rule({ match = { class = "^(Spotify|spotify)$" }, workspace = "special:virtual" })
hl.window_rule({ match = { class = "^(discord|vesktop)$" }, workspace = "special:discord" })
-- ==========================================
-- STEAM & GAMING
-49
View File
@@ -1,49 +0,0 @@
#!/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
+73
View File
@@ -0,0 +1,73 @@
#!/bin/sh
# Screenshots with a receipt: every capture lands on the clipboard and comes
# back as a notification with its own thumbnail, so a keypress that used to
# vanish silently now shows what it took.
#
# shot.sh region copy pick an area, copy it
# shot.sh region save pick an area, save it to ~/Pictures/Screenshots
# shot.sh window copy the focused window, copy it
# shot.sh window save the focused window, save it
#
# A copy is kept in $XDG_RUNTIME_DIR only so the notification has a picture
# to show; its Save action moves it into ~/Pictures/Screenshots. Edit opens
# satty when it is installed. Cancelling the region picker does nothing.
set -eu
target=${1:-region}
mode=${2:-copy}
dir="$HOME/Pictures/Screenshots"
stamp=$(date +'%Y-%m-%d_%H-%M-%S')
case "$target" in
region)
geometry=$(slurp) || exit 0
name="$stamp.png"
;;
window)
info=$(hyprctl activewindow -j)
geometry=$(printf '%s' "$info" | jq -r 'if .at then "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])" else empty end')
if [ -z "$geometry" ]; then
notify-send -a Screenshot "Screenshot" "No window is focused."
exit 1
fi
app=$(printf '%s' "$info" | jq -r '.class // "unknown"' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/_/g')
name="${app}_$stamp.png"
;;
*)
echo "usage: shot.sh region|window copy|save" >&2
exit 2
;;
esac
if [ "$mode" = save ]; then
mkdir -p "$dir"
file="$dir/$name"
else
file="${XDG_RUNTIME_DIR:-/tmp}/shot-$name"
fi
grim -g "$geometry" "$file"
wl-copy --type image/png < "$file"
title=$([ "$mode" = save ] && echo "Screenshot saved" || echo "Screenshot copied")
body=$([ "$mode" = save ] && echo "${file#"$HOME"/}" || echo "On the clipboard")
set -- --action=open=Open
command -v satty >/dev/null 2>&1 && set -- "$@" --action=edit=Edit
[ "$mode" = copy ] && set -- "$@" --action=save=Save
# notify-send waits for the notification to close and prints the action
# that was chosen, so run it in the background and act on the answer.
(
choice=$(notify-send -a Screenshot -i "$file" -h "string:image-path:$file" \
"$@" "$title" "$body") || exit 0
case "$choice" in
open) xdg-open "$file" ;;
edit) satty --filename "$file" --output-filename "$dir/edited_$name" --copy-command wl-copy ;;
save) mkdir -p "$dir" && mv "$file" "$dir/$name" \
&& notify-send -a Screenshot -i "$dir/$name" -h "string:image-path:$dir/$name" \
"Screenshot saved" "${dir#"$HOME"/}/$name" ;;
esac
) >/dev/null 2>&1 &
+1 -1
Submodule nvim updated: 4736228253...19e0db60a1.
+2 -1
View File
@@ -3,6 +3,7 @@ aalib
abseil-cpp
acl
acpi
adw-gtk-theme
adwaita-cursors
adwaita-fonts
adwaita-icon-theme
@@ -87,7 +88,6 @@ calibre
cargo-audit
catppuccin-gtk-theme-mocha
catppuccin-qt5ct-git
catppuccinifier-cli-git
cbindgen
cblas
cdparanoia
@@ -111,6 +111,7 @@ cups-pk-helper
curl
cxxbridge
cython
darkly-bin
dav1d
db5.3
dbus
+4
View File
@@ -0,0 +1,4 @@
[ColorScheme]
active_colors=#ffccd2cb, #ff20241f, #ff383e35, #ff2a2f28, #ff0c0d0b, #ff161915, #ffccd2cb, #ffeef1ea, #ffccd2cb, #ff0c0d0b, #ff0f110e, #ff090a08, #ffbcc1bf, #ff090a08, #ffeef1ea, #ff9fa7a2, #ff161915, #ff0f110e, #ff20241f, #ffccd2cb, #ff79847a, #ffbcc1bf
inactive_colors=#ffccd2cb, #ff20241f, #ff383e35, #ff2a2f28, #ff0c0d0b, #ff161915, #ffccd2cb, #ffeef1ea, #ffccd2cb, #ff0c0d0b, #ff0f110e, #ff090a08, #ff383e35, #ffccd2cb, #ffeef1ea, #ff9fa7a2, #ff161915, #ff0f110e, #ff20241f, #ffccd2cb, #ff79847a, #ff383e35
disabled_colors=#ff545c52, #ff161915, #ff2a2f28, #ff20241f, #ff0c0d0b, #ff161915, #ff545c52, #ff79847a, #ff545c52, #ff0c0d0b, #ff0f110e, #ff090a08, #ff20241f, #ff545c52, #ff79847a, #ff545c52, #ff161915, #ff0f110e, #ff20241f, #ff545c52, #ff464d44, #ff20241f
+4 -4
View File
@@ -1,13 +1,13 @@
[Appearance]
color_scheme_path=/usr/share/qt6ct/colors/catppuccin-mocha-blue.conf
color_scheme_path=/home/narl/.config/qt6ct/colors/yorha.conf
custom_palette=true
icon_theme=Papirus-Dark
standard_dialogs=default
style=Fusion
style=Darkly
[Fonts]
fixed="Sans Serif,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
general="Sans Serif,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
fixed="JetBrainsMono Nerd Font,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
general="Noto Sans,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
[Interface]
activate_item_on_single_click=1
+270 -104
View File
@@ -1,10 +1,12 @@
# Takemi — Persona × Mocha
# Takemi — YoRHa dark
A Persona 5 flavoured Quickshell desktop for Hyprland, painted in Catppuccin
Mocha. Blue for anything live, mauve for anything wrong, the Mocha greys for
everything else, and 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.
A NieR:Automata flavoured Quickshell desktop for Hyprland, kept dark and lit
like its wallpaper: a lone figure at a porthole, the void outside, one planet's
light. The planet white for anything live, the hull red for anything wrong,
grey-green for everything else. Hairlines, small dots, soft rounded corners,
letter-spaced capitals, and selection shown by inverting the row. The bar, the
rail and a thin border are one hull around the desktop, and the desktop is the
porthole cut out of it.
Replaces Waybar, dunst and hyprlock.
@@ -25,57 +27,105 @@ 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 ▐ ▌│
│ ▐ ▌│
│ ▐ ▌│
└────────────────────────────────────────────┘
████████████████████████████████████████████████
█ ◉ │ 1 2 3 │ window title ♪ TRACK │ CPU │ 20:04 █ bar — 40px, part of the hull
█╭──────────────────────────────────────────╮ ▐▌█
█│ │ ▐▌█ rail — 48px, part of the hull
█│ windows │ ▐▌█
█│ │ ▐▌█
█╰──────────────────────────────────────────╯ █ frame — 8px left and bottom
████████████████████████████████████████████████
```
- **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.
- **hull** — `bar/Frame.qml`: one surface round the whole screen, with the
desktop as a rounded opening. The bar and the rail sit on it.
- **bar** — identity and workspaces on the left (a ring after them while the
`Super+S` scratchpad holds windows, filled while it is open); the focused window, the
player, the CPU/MEM/GPU readout and the clock on the right.
- **rail** — network, bluetooth, audio, mic, backlight, battery, tray,
notifications, power. While something records the mic, uses a camera or
shares the screen, a lit icon heads the rail (`services/Privacy.qml`, read
from PipeWire). Popouts grow leftward out of it, into the screen.
Each permanent surface is a single continuous `ChromeSurface` 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 an
accent underline. Pass `standalone: true` for the rare thing that really is its
own panel.
Modules are divided by hairlines. `BarPill` is chromeless by default — a module
is *content inside* the hull, not a surface of its own — and marks hover with
an accent underline. Pass `standalone: true` for the rare thing that really is
its own panel.
The shell uses three deliberate surface tiers. **Chrome** (`ChromeSurface`) is
the calm, texture-free bar, rail, and tooltip layer. **Transient**
The shell uses three deliberate surface tiers. **Chrome** (the hull and
`ChromeSurface`) is the calm, texture-free bar, rail, and tooltip layer. **Transient**
(`PopoutSurface`) is reserved for expressive menus and detail panels, with one
clear composition and stronger separation from the desktop. **Cinematic**
(`P5Backdrop`) belongs only to scene-scale experiences such as power and lock;
it is never used as decoration inside ordinary chrome or popouts.
clear composition and stronger separation from the desktop. **Scene**
surfaces — the lock screen and the power menu — have no panel at all: the
desktop or the wallpaper dims, and one small group of type (the mark, a line of
thin display type, a short log, a menu or a field) stands in the dark.
```
shell.qml entrypoint — bar, rail, power menu, toasts, OSD per screen
shell.qml entrypoint — hull, bar, rail, dashboard, power menu, toasts, OSD per screen
config/ palette, type scale, geometry, motion curves
components/ the P5 primitives: panels, sliders, list rows, print-shadowed text
components/ the primitives: panels, sliders, list rows, display 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
overlays/ power menu, notification toasts, the volume/brightness/keyboard OSD, idle dim
lock/ session lock
assets/ halftone and stripe tiles, generated with ImageMagick
overview/ every monitor, workspace and window at once
backdrop/ the wallpaper, drawn by the shell
assets/ the dot and scanline tiles, generated with ImageMagick
```
`P5Panel` is the base surface everything sits on. It does not auto-size —
`Panel` 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.
## Popouts
Hover panels (`components/Popout.qml`) are part of the hull, not cards
floating near their trigger. Each one sits flush against the frame edge it
opens from, is painted in the hull colour, joins the frame through concave
fillets, carries the rim round its open side, and slides out from behind the
frame. `PopoutSurface` notices it is inside a `Popout` and leaves the fill and
the frame to the hull; in the launcher and the ask panel it is still a
free-standing panel with corner ticks.
Placement is explicit: centred on the trigger, then — if that would land the
panel within `dockReach` of one of the opening's corners — docked into the
corner instead, attached to the bar and the rail at once (`dock` is `top`,
`right`, `topRight` or `bottomRight`). Without docking, a panel near the rail
left a thin strip of opening between itself and the rail.
## Notifications
The shell is the notification server (`services/Notifs.qml`). New ones arrive
as toasts in a corner drawer (`overlays/Toasts.qml`); the bell on the rail
opens the log (`popouts/NotifPopout.qml`), which is grouped by sender. A
sender with one notification is just that entry. A sender with several gets a
header — its name, a count, a mark to clear only that sender — over its
newest entry and a `+ 2 more` line; a click on either opens the rest.
## Dashboard
`dashboard/Dashboard.qml` is a drawer that slides down out of the underside of
the bar — clock click, `SUPER + D`, or `qs ipc call shell dashboard`. It is
painted in the hull colour and joins the bar through two concave fillets, so it
reads as more of the hull rather than as a floating card (caelestia's idea).
Four columns divided by hairlines: operator and time, the month, the system
meters, and what is playing. Escape or focusing anything else puts it away; a
`HyprlandFocusGrab` handles the latter, and `Actions.dashboardClosedAt` stops
the clock click that cleared the grab from opening it straight back up.
Under the columns, past a hairline, a row of switches: Wi-Fi, Bluetooth, do
not disturb and stay awake, plus night light when `hyprsunset` is installed
(it runs at 4200 K for as long as the switch is on). On is the light slab
with ink type, the same as a selection everywhere else; off is a hairline.
## Modules
| Module | Where | Left click | Right click | Middle | Scroll | Hover |
|---|---|---|---|---|---|---|
| Sigil | bar | app menu | reload shell | | | |
| Workspaces | bar | switch | | | | |
| Clock | bar | pin calendar | | | | calendar |
| Clock | bar | dashboard | | | | calendar |
| Network | rail | pin details | | | | state + networks |
| Bluetooth | rail | pin details | | | | state + devices |
| Volume | rail | pin audio | | | volume | state + scroll hint + audio |
@@ -112,6 +162,10 @@ Set in `~/.config/hypr/lua/binds/shell.lua`:
- `Super+Ctrl+L` — lock
- `Super+P` — power menu
- `Super+Shift+N` — toggle do not disturb
- `Alt+Tab` / `Alt+Shift+Tab` — overview, walking focus history; let go of Alt to switch
- `Super+W` — overview, without the hold
- `Super+Tab` / `Super+Shift+Tab` — next / previous window in a group
- `Super+Shift+I` — stay awake (no dim, lock or screen-off until toggled back)
- `Super+Ctrl+Shift+N` — clear notifications
- `Super+A` — ask panel
- `Super+Shift+R` — reload the shell
@@ -122,6 +176,8 @@ All of these go through Quickshell's IPC, so they work from any script:
qs ipc call shell lock
qs ipc call shell power
qs ipc call shell toggleDnd
qs ipc call shell toggleStayAwake
qs ipc call shell wallpaper ~/.wallpapers/space-1.jpg
qs ipc call shell ask
qs ipc call shell askAbout "why is the sky blue"
qs ipc call shell clearNotifications
@@ -180,6 +236,55 @@ 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.
## Overview
`overview/Overview.qml`. Each monitor is a row of small portholes, one per
workspace at that monitor's aspect ratio, with live captures of the windows
where they sit (`ScreencopyView`, so windows on hidden workspaces show too).
The monitor's name heads its row once there is more than one. A group shows
the member in front with a `1/3` tag; picking a hidden member puts it in
front.
`Alt+Tab` opens it with the previous window picked, more Tabs walk back
through focus history, and letting go of Alt switches. Hyprland keeps the
Alt+Tab bind while the overview has the keyboard, so those presses arrive
over IPC (`overviewNext` / `overviewPrev`); the Alt release comes to the
overview itself, with a release bind on `ALT_L` as a backstop. Hover picks
only once the pointer moves, so a resting pointer cannot override the
keyboard. Enter, a click, or `1`–`9` for a workspace also go; Escape or a
click on the dark cancels.
Focus is dispatched a moment after the overview lets go of the keyboard:
Hyprland ignores focus changes while a layer holds it exclusively.
## Wallpaper
`backdrop/Backdrop.qml` draws it on the background layer; there is no
hyprpaper. The picture is drawn 8% larger than the screen and drifts with the
workspace: 1 sits at the top of it and each step down moves it a fixed
distance, a little slower than Hyprland's own slide, so the windows arrive
first and the scene behind catches up. It never moves on its own, so an idle
desktop costs the compositor nothing. While the workspace holds windows it
settles a shade darker.
The choice is one line in `quickshell/wallpaper` (`~` allowed), watched live.
`qs ipc call shell wallpaper <path>` rewrites it.
## Idle
`services/Idle.qml`, on Wayland's idle-notify protocol instead of hypridle:
| After | Does |
|---|---|
| 4 min | the screen dims slowly (`overlays/IdleDim.qml`); any input lifts it |
| 5 min | lock |
| 6 min | displays off; any input wakes them |
| 15 min | suspend, on battery only |
Every stage respects idle inhibitors, so a fullscreen video holds them all
off. `Super+Shift+I` does the same by hand and puts a cup on the rail while it
is on.
## Lock screen
Authenticates through `/etc/pam.d/hyprlock`, which on this box is just
@@ -187,6 +292,14 @@ Authenticates through `/etc/pam.d/hyprlock`, which on this box is just
styled in an ordinary window (`qs -p lockpreview.qml`) without locking the
session.
It is the wallpaper, barely softened, with the face set in the dark of the
porthole: the mark, the time in light display type, the date, "Take your
time." glowing faintly with the odd flicker (`components/GlowText.qml`), a
status log
(battery, network, what is playing, unread notices), and a rounded field that
fills with dots as you type. The dots breathe while PAM checks; a wrong
password turns them and the field's rim to rust and nudges the field once.
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.
@@ -194,6 +307,9 @@ means restarting Hyprland from a TTY.
## What was retired
- `waybar.service` — disabled. Its config is still in `~/.config/waybar`.
- `hyprpaper` — replaced by `backdrop/`. Turn its unit off with
`systemctl --user disable --now hyprpaper`.
- `hypridle` — never installed; `services/Idle.qml` does the job.
- `dunst.service` — masked, so it cannot be D-Bus activated and steal
`org.freedesktop.Notifications`. Undo with
`systemctl --user unmask dunst.service`.
@@ -204,40 +320,45 @@ means restarting Hyprland from a TTY.
`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.
The display face is Noto Sans, upright and widely tracked — the nearest free
face to NieR's FOT-Rodin that is already installed. Change `Theme.fontDisplay`
and everything follows.
## Palette discipline
Catppuccin Mocha, spent on two hues. **Hue is meaning**; nothing gets a colour
for decoration.
YoRHa dark: NieR:Automata's interface language, painted from the wallpaper
(`~/.wallpapers/space-1.jpg`) rather than from the game's beige. Two hues, and
**hue is meaning**; nothing gets a colour for decoration.
- **Blue (`accent`, Mocha blue)** means live: the focused workspace, the
selected row, the hovered control, a channel that is on, an unread badge.
- **Mauve (`alarm`, Mocha mauve)** means wrong or destructive: critical battery,
an urgent workspace, a critical notification, reboot and shut down.
- **Grey** is everything else, and that includes *off*. A muted speaker, a
muted mic and Do Not Disturb drop to `muted` grey with their off glyph. They
used to be painted in the accent, which made a silenced channel look the most
alive thing on the rail.
- **Crust (`ink`)** is structure: the hard keyline, the offset under display
type and panels, the text on an accent slab.
- **Light (`accent`, `#eef1ea`)** means live: the focused workspace, the
selected row, the hovered control, a channel that is on, an unread badge. It
is the planet's white, barely a colour at all — NieR marks attention with
brightness and inversion, not with hue.
- **Red (`alarm`, `#c8684f`)** means wrong or destructive: critical battery, an
urgent workspace, a critical notification, reboot and shut down. It is the
hull's indicator lights, the only warm thing in the wallpaper.
- **Grey-green** is everything else, and that includes *off*. A muted speaker,
a muted mic and Do Not Disturb drop to `muted` grey with their off glyph.
- **Ink** is the text on an inverted slab and the faint drop under display
type.
**Selection is an inverted slab.** Focus and selection fill with the accent and
print their text in crust (about 9:1). Hover stays on the surface tone with an
accent mark. Destructive-active is the same slab in mauve. The `treatment*`
tokens in `Theme.qml` encode all of this; use them rather than picking colours
per widget.
**Selection is the NieR cursor.** Focus and selection invert the row: a slab of
the light with ink punched out of it. Hover lifts the surface one step and
brings the marker up to full light, but never inverts. Destructive-active is
the same slab in red. The `treatment*` tokens in `Theme.qml` encode all of
this; use them rather than picking colours per widget.
**Print, not blur.** Display text (`SplitText`) carries one hard crust offset.
The blue and mauve ghosts appear only while `glitch()` runs. Drawn permanently
1.6px off the body, they read as a focus problem on Mocha's pastels rather than
as misregistered print. The lock screen and the power menu are the exceptions;
they are the cinematic surfaces and keep their split.
**Dots, not slashes.** The current row, a button under the pointer, the end of
a header rule and a notification's urgency carry a small dot (`Theme.marker`).
Borders are structure and stay neutral.
**Texture is tiered.** Chrome (bar, rail, tooltips) and transient panels
(popouts, OSD, toasts) are flat. Stripes and halftone live only in
`P5Backdrop`, behind the lock screen and the power menu.
**Flicker, not print.** Display text (`SplitText`) is upright, tracked by
`Theme.tracking`, on a one-pixel soft drop. The two ghosts either side of it
appear only while `glitch()` runs — NieR's brief data-corruption flicker.
**Texture is rare.** Chrome (bar, rail, tooltips), transient panels (popouts,
OSD, toasts) and the two scene surfaces are flat. Depth comes from the hull's
dark, the rim hairline and the wallpaper, not from patterns.
**Type has a floor.** Nothing renders below `Theme.fsMicro` (10px).
@@ -245,60 +366,105 @@ they are the cinematic surfaces and keep their split.
| File | Covers |
|---|---|
| `quickshell/config/Theme.qml` | the shell: `accent`, `alarm` and the neutral ramp |
| `hypr/lua/palette.lua` | window borders, groups, shadows |
| `alacritty/catppuccin-mocha.toml` | the terminal (stock Mocha, cursor in the accent) |
| `quickshell/config/Theme.qml` | the shell: `accent`, `alarm`, the neutral ramp, the hull |
| `hypr/lua/palette.lua` | window borders, groups |
| `alacritty/yorha.toml` | the terminal |
| `fish/conf.d/yorha_theme.fish` | the prompt and pager |
| `nvim/lua/yorha.lua` | the editor and its statusline |
| `btop/themes/yorha.theme` | the resource monitor |
| `gtk-3.0/gtk.css`, `gtk-4.0/gtk.css` | GTK and libadwaita apps |
| `qt6ct/colors/yorha.conf` | Qt apps |
| `zathura/yorha` | the document viewer |
Everything else derives. `accentDim` and `accentSoft` come off `accent`, `glow`
*is* `accent`, and `primary` is plain `text`. `Theme.heat()` is a continuous
text → blue → mauve ramp, so a CPU figure that has gone mauve means the same
thing as a battery that has gone mauve. Tray icons are flattened to one colour
(subtext, the accent on hover) for the same reason: app brand colours wreck the
palette faster than anything else in the shell. nm-applet and blueman are
hidden from the tray, since the rail already shows network and bluetooth, but
keep running as the secret and pairing agents.
Everything else derives. `glow` *is* `accent`, and `primary` is plain `text`.
`Theme.heat()` is a continuous text → light → red ramp, so a CPU figure that
has gone red means the same thing as a battery that has gone red. Tray icons
are flattened to one colour (subtext, the accent on hover) for the same reason:
app brand colours wreck the palette faster than anything else in the shell.
nm-applet and blueman are hidden from the tray, since the rail already shows
network and bluetooth, but keep running as the secret and pairing agents.
## The rest of the desktop
- **Hyprland** — `hypr/lua/look.lua`. No rounding, no blur, both opacities at
`1.0`. The active border runs blue into lavender and the inactive border is
`surfaceAlt`. The window shadow is a sharp 4px crust offset, the same printed
drop the shell's panels carry, not a soft glow.
- **Alacritty** — `alacritty/catppuccin-mocha.toml`, selected by the `import`
in `alacritty.toml`. Stock Mocha except for the cursor, which is the accent.
`takemi.toml` is left in place, unreferenced.
- **Neovim** — the `catppuccin` plugin, flavour mocha, configured in
`nvim/lua/plugins/ui.lua`. Blue is the accent there too. `colors/takemi.lua`
is left in place, unused.
- **Fish** — `fish/conf.d/takemi_theme.fish` is Catppuccin Mocha's fish theme
with the Takemi selection rows: a solid accent block with crust punched out.
- **Hyprland** — `hypr/lua/look.lua`. Windows round at 8px, concentric with
the opening's 18px corners at a 10px gap (`Theme.frameRadius - gaps_out`);
change one and the others follow. No blur, both opacities at `1.0`, 1px
borders running from the light into the suit's grey. The shadow is soft and
wide, so windows float in the opening. Group tabs are thin rounded lines with
no titles, the current one in the light; the bar writes the title once and
adds the window's place in the group (`2/3`). Motion
(`hypr/lua/animations.lua`) glides and settles with no overshoot: windows
resolve in from a hair smaller, workspaces drift a short way vertically and
fade (sliding behind the hull), the `SUPER+S` scratchpad lowers out from
under the bar like the dashboard, and layers only fade, since quickshell
animates its own surfaces.
- **Alacritty** — `alacritty/yorha.toml`, selected by the `import` in
`alacritty.toml`. Muted earth hues on the void; selection inverts.
Alacritty only live-reloads when `alacritty.toml` itself changes, so touch
it after editing the palette.
The Catppuccin and `takemi.toml` files are left in place, unreferenced.
- **Neovim** — `nvim/lua/yorha.lua` (loaded by `colors/yorha.lua`, with a
matching lualine theme). Syntax in the terminal's earth hues, floats as hull
surfaces with rounded rims, menus selecting with the light slab.
- **btop** — `btop/themes/yorha.theme`. Hairline boxes; gradients climb from
grey to the light, and only temperature and CPU load run on into rust.
- **GTK** — the `adw-gtk3-dark` theme (package `adw-gtk-theme`) for the
libadwaita shapes, recoloured by `gtk-3.0/gtk.css` and `gtk-4.0/gtk.css`.
libadwaita apps such as Nautilus read the same overrides as CSS variables.
Selection is the light slab with ink text; only destructive actions are red.
The theme name is also in `xsettingsd/xsettingsd.conf` and has to be set with
`gsettings` for portal-aware apps.
- **Type** — one sans and one mono everywhere: Noto Sans for interface text
and JetBrainsMono Nerd Font for figures and code, in the shell
(`Theme.fontDisplay` / `fontMono`), GTK (`settings.ini` and the gsettings
`font-name` / `monospace-font-name`), qt6ct and Hyprland's own text.
- **Qt** — qt6ct with `qt6ct/colors/yorha.conf`, the same palette in
`QPalette` roles, drawn with the Darkly style (package `darkly-bin`) for
rounded controls. Fusion is the fallback if Darkly is missing.
- **zathura** — `zathura/yorha`, included from `zathurarc`. Pages are
recoloured onto the hull with text in bone; images keep their colours.
Search hits are a soft bone wash, and the current hit is sand.
- **Fish** — `fish/conf.d/yorha_theme.fish`.
- **The launcher** — no external theming to keep in step, because it is part of
this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for
`fuzzel --dmenu` where a script needs a picker.
- **No glass anywhere.** Alacritty is `opacity = 1.0, blur = false`, and
Hyprland's `decoration:blur` is disabled. 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.
Hyprland's `decoration:blur` is disabled.
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 once the biggest source of murk in the design.
There are **no gradients in the chrome**; every fill is flat. `Theme.sheen`
still exists as a no-op property so old call sites need not be chased down.
## 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°.
**The hull.** Borrowed from caelestia's screen border: the bar, the rail and a
thin band down the left and along the bottom (`Theme.frame`) are one surface,
drawn by `bar/Frame.qml`, and the desktop is a rounded opening cut out of it
(`Theme.frameRadius`). Read against the wallpaper, you are inside the ship and
the windows sit in the porthole. The hull is on the top layer, over the
windows, with a transparent opening and no input region, so a window moving
past the opening's edge — a workspace switch, the scratchpad drawer — passes
behind the frame rather than over the bar. `hypr/lua/rules/layers.lua` orders
the bar and the rail above it. The bar and the rail draw
no surface of their own and ignore exclusive zones; Frame reserves all four
zones itself, so every piece is placed from the screen edge.
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.
**Soft, everywhere else.** The porthole is the shape the desktop is built
around, and every smaller surface is a gentler echo of it. `Theme.skew` and
`Theme.cut` are `0`; `Panel` and `Skew` draw `Geom.roundedRect` with
`Theme.radiusS` (pills, chips, meter tracks), `radiusM` (rows, buttons) or
`radiusL`. `radiusL` is `frameRadius - padS`: anything inset by `padS` inside
one of the frame's curves — a hovered notification in the drawer's corner —
gets a corner concentric with it, so the gap stays even and nothing pokes past
the curve. Free-standing panels (launcher, ask) take `frameRadius` itself.
Toggles, transport buttons, workspace pips and today's date are pills or
discs; album art is rounded via `ClippingRectangle`.
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.
**Motion glides.** No overshoot anywhere: things ease in on `OutCubic` or
`OutExpo` and settle. Nothing scales up or nudges sideways on hover either;
hover brightens and marks, the way NieR's menus do. Meters are one continuous
rounded line that glides to its level, and the bar's player shows a
five-line signal meter.
Each surface is **one continuous slab**, with modules divided by hairline rules
rather than by gaps. A row of separate floating cards reads as clutter; that is
what the dividers exist to avoid.
+1 -1
View File
@@ -394,7 +394,7 @@ PanelWindow {
NumberAnimation {
target: drop; property: "y"
from: -shell.height; to: 0
duration: Theme.durSlow; easing.type: Easing.OutBack
duration: Theme.durSlow; easing.type: Easing.OutCubic
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 B

+90
View File
@@ -0,0 +1,90 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import "root:/config"
import "root:/services"
// The wallpaper, seen through the porthole. It is drawn a little larger than
// the screen and drifts when the workspace changes: windows slide past
// vertically (hypr/lua/animations.lua) and the scene behind them moves a few
// pixels the same way and a little later, the way distant things do out of a
// moving window. Only on switches — it never moves on its own, so an idle
// desktop costs the compositor nothing.
//
// It also settles a shade darker while the workspace holds windows, so the
// band of wallpaper round them reads as the far side of the glass rather than
// as part of the desktop.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.namespace: "takemi-backdrop"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
anchors { top: true; bottom: true; left: true; right: true }
color: Theme.ink
mask: Region {}
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
readonly property HyprlandWorkspace workspace: root.monitor?.activeWorkspace ?? null
// Overscan: how much bigger than the screen the picture is drawn, which is
// the room it has to drift in.
readonly property real overscan: 1.08
readonly property real slack: root.height * (root.overscan - 1)
// Workspaces 1–10 spread across the slack, so 1 sits at the top of the
// picture and each step down moves it a fixed distance. Special and
// named workspaces (negative ids) leave it where it was.
property real drift: 0
readonly property int wsId: root.workspace?.id ?? 1
onWsIdChanged: if (root.wsId > 0) root.drift = Math.min(root.wsId - 1, 9) / 9
readonly property bool occupied: (root.workspace?.toplevels?.values?.length ?? 0) > 0
Image {
id: picture
width: root.width * root.overscan
height: root.height * root.overscan
x: -(width - root.width) / 2
y: -root.drift * root.slack
source: Wallpaper.forMonitor(root.screen?.name ?? "")
sourceSize: Qt.size(width, height)
fillMode: Image.PreserveAspectCrop
asynchronous: true
smooth: true
cache: false
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: Theme.durLazy; easing.type: Easing.OutCubic }
}
// Longer than Hyprland's 550ms workspace slide, on the same curve, so
// the windows arrive first and the scene catches up.
Behavior on y {
NumberAnimation {
duration: 900
easing.type: Easing.BezierSpline
easing.bezierCurve: [0.35, 0.0, 0.15, 1.0, 1.0, 1.0]
}
}
}
Rectangle {
anchors.fill: parent
color: Theme.ink
opacity: root.occupied ? 0.18 : 0
Behavior on opacity {
NumberAnimation { duration: Theme.durLazy; easing.type: Easing.InOutSine }
}
}
Component.onCompleted: root.drift = Math.min(Math.max(root.wsId, 1) - 1, 9) / 9
}
+43 -6
View File
@@ -20,6 +20,31 @@ Item {
|| toplevel.monitor.id === root.monitor.id)
readonly property string incomingTitle: mine && toplevel.title ? toplevel.title : "—"
// Where the focused window sits in its group, as "2/3", or "" when it is
// not grouped. Hyprland's group bar draws only indicator lines (see
// hypr/lua/look.lua), so the bar is the one place the title is written.
readonly property string groupLabel: {
const ipc = root.mine ? root.toplevel.lastIpcObject : null;
const members = ipc?.grouped ?? [];
if (members.length < 2) return "";
const bare = a => String(a).replace(/^0x/, "");
const at = members.findIndex(m => bare(m) === bare(root.toplevel.address));
return (at < 0 ? "?" : at + 1) + "/" + members.length;
}
// lastIpcObject is a snapshot, refreshed only on request. Ask again
// whenever focus or group membership may have changed.
Connections {
target: Hyprland
function onRawEvent(event) {
const n = event.name;
if (n === "activewindowv2" || n === "togglegroup"
|| n === "moveintogroup" || n === "moveoutofgroup"
|| n === "closewindow" || n === "openwindow")
Hyprland.refreshToplevels();
}
}
property string displayedTitle: "—"
// Which toplevel the landing transition last played for. The landing is a
@@ -30,7 +55,8 @@ Item {
// 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))
implicitWidth: Math.min(maxWidth,
Math.max(70, label.implicitWidth + 4 + (groupTag.visible ? groupTag.width + Theme.padS : 0)))
implicitHeight: 20
Behavior on implicitWidth {
@@ -45,24 +71,23 @@ Item {
Text {
id: label
anchors.verticalCenter: parent.verticalCenter
width: parent.width
width: parent.width - (groupTag.visible ? groupTag.width + Theme.padS : 0)
text: root.displayedTitle
color: root.mine ? Theme.subtext : Theme.muted
elide: Text.ElideRight
font.pixelSize: Theme.fsSmall
font.family: Theme.fontDisplay
font.italic: true
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
// Accent duplicate: visible only during the landing transition. The
// small opposing offset gives the title Persona's rough print snap
// small opposing offset gives the title NieR's brief corruption flicker
// without leaving permanent motion in the bar.
Text {
id: ghost
anchors.verticalCenter: parent.verticalCenter
width: parent.width
width: label.width
text: root.displayedTitle
color: Theme.accent
opacity: 0
@@ -70,6 +95,18 @@ Item {
font: label.font
renderType: Text.NativeRendering
}
Text {
id: groupTag
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: root.groupLabel !== ""
text: root.groupLabel
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
// Adopt the settled caption, and stamp it only when focus actually moved.
@@ -144,7 +181,7 @@ Item {
NumberAnimation {
target: label; property: "x"
from: -10; to: 0
duration: Theme.durBase; easing.type: Easing.OutBack
duration: Theme.durBase; easing.type: Easing.OutCubic
}
NumberAnimation {
target: label; property: "opacity"
+13 -33
View File
@@ -27,29 +27,16 @@ PanelWindow {
right: true
}
margins {
top: Theme.barMargin
left: Theme.barMargin
right: Theme.barMargin
}
// Flush with the screen edge and drawn on nothing: the hull behind it is
// painted by Frame, which is what joins the bar, the rail and the border
// into one surface. Exclusive zones are Frame's job too, so every piece
// of the hull is placed from the screen edge rather than from whichever
// piece the compositor happened to arrange first.
exclusionMode: ExclusionMode.Ignore
implicitHeight: Theme.barHeight
exclusiveZone: Theme.barHeight + Theme.barMargin
color: "transparent"
// Calm permanent chrome: one opaque slab with no texture or offset copy.
ChromeSurface {
id: slab
anchors.fill: parent
lean: Theme.skew
chop: Theme.cut
chopCorners: [1, 3]
padding: 0
active: true
slashWidth: 5
}
readonly property real edgeInset: Theme.barHeight * Theme.skew + Theme.padM
readonly property real edgeInset: Theme.padM
// ------------------------------------------------------------------ left
Row {
@@ -108,25 +95,18 @@ PanelWindow {
}
}
// 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.
// A hairline rule between modules. This is what divides the bar — gaps
// between cards are what made it look scattered.
component Divider: Item {
anchors.verticalCenter: parent.verticalCenter
implicitWidth: Theme.padM * 2
implicitHeight: Theme.barHeight
// A leaning hairline needs a box wide enough to hold the lean: at 20px
// tall the top edge sits 20 * skew ≈ 5px right of the bottom one, so a
// 1px-wide Skew folded into a bowtie instead. Width is the lean plus the
// stroke the rule should actually read as.
readonly property real ruleWeight: 1.5
readonly property real ruleHeight: 20
Skew {
Rectangle {
anchors.centerIn: parent
width: parent.ruleHeight * Theme.skew + parent.ruleWeight
height: parent.ruleHeight
color: Theme.alpha(Theme.outline, 0.5)
width: 1
height: 16
color: Theme.alpha(Theme.outline, 0.6)
}
}
}
+1 -1
View File
@@ -73,7 +73,7 @@ Item {
anchors.fill: parent
active: root.standalone
sourceComponent: P5Panel {
sourceComponent: Panel {
lean: root.lean
leanLeft: root.leanLeft
chop: root.chop
+12 -11
View File
@@ -3,8 +3,10 @@ import Quickshell
import "root:/config"
import "root:/components"
import "root:/popouts"
import "root:/services"
// Centre-stage clock. Hovering drops the calendar.
// Centre-stage clock. Hovering drops the calendar; clicking drops the
// dashboard, which carries the calendar too.
BarPill {
id: root
@@ -20,7 +22,7 @@ BarPill {
precision: SystemClock.Minutes
}
onClicked: calendar.pinned = !calendar.pinned
onClicked: Actions.toggleDashboard()
Row {
id: layout
@@ -51,16 +53,15 @@ BarPill {
}
}
// Declared with room for its own lean, so the slash sits inside the box
// the Row gave it instead of hanging several px into the date column.
Skew {
// A hairline between the date and the time.
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 22 * Theme.skew + 3
height: 22
color: Theme.alpha(Theme.accent, 0.6)
width: 1
height: 18
color: Theme.alpha(Theme.outline, 0.6)
}
// Archivo Black's digits are not all the same width, so a clock sized to
// The display face's digits are not all the same width, so a clock sized to
// its own text changed width as the minutes rolled over — 11:11 is
// several px narrower than 08:48 — and shoved the rest of the bar
// around. Reserve the widest possible reading and centre inside it.
@@ -70,7 +71,6 @@ BarPill {
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsTitle
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5
}
@@ -117,7 +117,8 @@ BarPill {
CalendarPopout {
id: calendar
anchorItem: root
triggerHovered: root.hovered
// Not while the dashboard is down: it already shows the month.
triggerHovered: root.hovered && !Actions.dashboardOpen
clockDate: clock.date
}
}
+124
View File
@@ -0,0 +1,124 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import "root:/config"
// The hull: one surface for the bar, the rail and the thin band down the left
// and along the bottom, with the desktop as a rounded opening cut out of it.
// The idea is caelestia's screen border; the reading is the wallpaper's — you
// are inside the ship, and the windows sit in the porthole.
//
// The hull lives on the top layer, over every window, and its opening is
// transparent with no input region, so it covers nothing inside the opening
// and catches no clicks. Being above the windows is the point: a workspace
// switch slides windows *behind* the frame, the way things pass behind the
// rim of a porthole, instead of drawing over the bar. A fullscreen window
// still covers it, since Hyprland draws fullscreen above the top layer.
// hypr/lua/rules/layers.lua orders it under the bar and the rail.
//
// The bar and the rail draw no surface of their own and ignore exclusive
// zones, so every piece is placed from the screen edge. The four zones are
// reserved here instead, by windows that draw nothing — that way the usable
// area is exactly the opening, whichever surface the compositor arranges
// first.
Scope {
id: root
required property var modelData
PanelWindow {
id: hull
screen: root.modelData
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "takemi-frame"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
anchors {
top: true
bottom: true
left: true
right: true
}
color: "transparent"
mask: Region {}
// The opening, in window coordinates.
readonly property real x0: Theme.frame
readonly property real y0: Theme.barHeight
readonly property real x1: width - Theme.railWidth
readonly property real y1: height - Theme.frame
readonly property real r: Theme.frameRadius
readonly property string opening: {
const x0 = hull.x0, y0 = hull.y0, x1 = hull.x1, y1 = hull.y1, r = hull.r;
return `M ${x0 + r} ${y0} H ${x1 - r} A ${r} ${r} 0 0 1 ${x1} ${y0 + r}`
+ ` V ${y1 - r} A ${r} ${r} 0 0 1 ${x1 - r} ${y1}`
+ ` H ${x0 + r} A ${r} ${r} 0 0 1 ${x0} ${y1 - r}`
+ ` V ${y0 + r} A ${r} ${r} 0 0 1 ${x0 + r} ${y0} Z`;
}
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
// The hull itself: the whole screen, minus the opening.
ShapePath {
fillColor: Theme.hull
fillRule: ShapePath.OddEvenFill
strokeColor: "transparent"
PathSvg {
path: `M 0 0 H ${hull.width} V ${hull.height} H 0 Z ` + hull.opening
}
}
// The rim — a hairline round the opening, the lit edge of the
// porthole in the wallpaper.
ShapePath {
fillColor: "transparent"
strokeColor: Theme.rim
strokeWidth: 1
PathSvg {
path: hull.opening
}
}
}
}
ExclusionZone {
anchors.top: true
exclusiveZone: Theme.barHeight
}
ExclusionZone {
anchors.right: true
exclusiveZone: Theme.railWidth
}
ExclusionZone {
anchors.bottom: true
exclusiveZone: Theme.frame
}
ExclusionZone {
anchors.left: true
exclusiveZone: Theme.frame
}
component ExclusionZone: PanelWindow {
screen: root.modelData
WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.namespace: "takemi-frame-zone"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
mask: Region {}
implicitWidth: 1
implicitHeight: 1
}
}
+28 -32
View File
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Shapes
import QtQuick.Window
import Quickshell
import Quickshell.Widgets
import "root:/config"
import "root:/components"
import "root:/services"
@@ -41,40 +42,28 @@ BarPill {
anchors.left: parent.left
spacing: Theme.padS
// Album art, chopped into the house shape.
Item {
// Album art, rounded, in a hairline frame.
ClippingRectangle {
id: artFrame
anchors.verticalCenter: parent.verticalCenter
width: 24
height: 24
Shape {
id: artMask
anchors.fill: parent
visible: false
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "white"
strokeColor: "transparent"
PathPolyline {
path: Geom.chamfer(Geom.parallelogram(24, 24, 0.18), 5, [1, 3])
}
}
}
radius: Theme.radiusS
color: Theme.surface
border.width: 1
border.color: Theme.overlay
Image {
id: art
anchors.fill: parent
anchors.margins: 1
source: Media.artUrl
fillMode: Image.PreserveAspectCrop
cache: true
asynchronous: true
visible: status === Image.Ready
layer.enabled: true
layer.smooth: true
layer.effect: MaskedArt { maskItem: artMask }
sourceSize.width: 48
sourceSize.height: 48
}
// Fallback: the source app's glyph.
@@ -109,7 +98,6 @@ BarPill {
font.pixelSize: Theme.fsSmall
font.family: Theme.fontDisplay
font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
}
@@ -125,8 +113,10 @@ BarPill {
}
}
// Compact play-state equaliser. It moves only while audio is playing
// and freezes into three quiet bars when paused.
// Signal meter: five thin rounded bars, NieR's audio readout rather than a
// chunky equaliser. It moves only while audio is playing and settles
// into a row of dots when paused — the channel is open, there
// is just nothing on it.
//
// Sampled from a timer rather than tweened by an infinite
// NumberAnimation. DP-1 runs at 240 Hz, and a frame-synced animation
@@ -145,7 +135,7 @@ BarPill {
Row {
id: eq
anchors.verticalCenter: parent.verticalCenter
spacing: 2
spacing: 3
// Sampling interval — the one knob trading smoothness against cost.
// Frames are the whole expense: a full-bar repaint runs about 0.3% of
@@ -164,8 +154,8 @@ BarPill {
// sine would, is three times the motion and reads far busier than
// this meter is supposed to.
function level(i: int): real {
const lo = 4 + (i * 5) % 13; // old first NumberAnimation `to`
const hi = 14 - (i * 4) % 9; // old second NumberAnimation `to`
const lo = 2 + (i * 3) % 5;
const hi = 14 - (i * 4) % 7;
const d1 = 320 + i * 90; // ...and their durations
const d2 = 280 + i * 70;
@@ -189,12 +179,13 @@ BarPill {
}
Repeater {
model: 3
model: 5
Rectangle {
required property int index
width: 3
height: Media.playing ? eq.level(index) : 6
width: 2
radius: 1
height: Media.playing ? eq.level(index) : 2
anchors.verticalCenter: parent.verticalCenter
color: Media.playing ? Theme.primary : Theme.muted
@@ -213,11 +204,16 @@ BarPill {
function onTrackChanged() { titleGlitch.restart(); }
}
// A new track resolves in, the way NieR text does: a brief dip and back.
SequentialAnimation {
id: titleGlitch
NumberAnimation {
target: artFrame; property: "rotation"
from: -8; to: 0; duration: Theme.durSlow; easing.type: Easing.OutBack
target: meta; property: "opacity"
to: 0.25; duration: 60
}
NumberAnimation {
target: meta; property: "opacity"
to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
}
}
+36 -35
View File
@@ -30,30 +30,14 @@ PanelWindow {
right: true
}
margins {
top: Theme.barHeight + Theme.barMargin * 2
bottom: Theme.barMargin
right: Theme.barMargin
}
// Hangs from the underside of the bar, flush with the screen edge. Like
// the bar it draws no surface of its own; see Frame.
margins.top: Theme.barHeight
exclusionMode: ExclusionMode.Ignore
implicitWidth: Theme.railWidth
exclusiveZone: Theme.railWidth + Theme.barMargin
color: "transparent"
// The same calm permanent-chrome preset used by the top bar.
ChromeSurface {
id: slab
anchors.fill: parent
vertical: true
lean: Theme.skew
chop: Theme.cut
chopCorners: [1, 3]
padding: 0
active: true
slashWidth: 5
}
readonly property real edgeInset: Theme.railWidth * Theme.skew + Theme.padM
readonly property real edgeInset: Theme.padM
// ------------------------------------------------------------------- top
Column {
@@ -63,6 +47,19 @@ PanelWindow {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0
// Something is recording or watching. First on the rail and in the
// light, because it is the one thing here you did not ask to see.
RailIcon {
id: privacyIcon
visible: Privacy.active
icon: Privacy.screen ? "󰹑" : (Privacy.camera ? "󰄀" : "󰍬")
label: "In use"
statusText: Privacy.detail
live: true
}
Divider { visible: Privacy.active }
RailIcon {
id: netIcon
icon: Net.icon
@@ -153,6 +150,19 @@ PanelWindow {
Divider { visible: tray.visible }
// Only there while the idle chain is held off by hand, so it is
// obvious why the screen never locked.
RailIcon {
id: awakeIcon
visible: Idle.stayAwake
icon: "󰅶"
label: "Stay awake"
statusText: "Screen will not dim or lock"
hint: "Click to allow sleep"
live: true
onClicked: Idle.toggleStayAwake()
}
RailIcon {
id: bellIcon
icon: Notifs.icon
@@ -183,25 +193,16 @@ PanelWindow {
}
}
// The rail's counterpart to the bar's divider: a horizontal hairline,
// sheared to the same angle the rail's own edges lean at.
// The rail's counterpart to the bar's divider: a horizontal hairline.
component Divider: Item {
implicitWidth: Theme.railWidth
implicitHeight: Theme.padM + 4
// Same correction as the bar's divider, on the other axis: a 22px rule
// rises 22 * skew ≈ 5px across its run, so the box has to be that tall
// plus the weight of the stroke. At height 1 the polygon inverted and
// the rule rendered as a stubby wedge sitting left of centre.
readonly property real ruleWeight: 1.5
readonly property real ruleWidth: 22
Skew {
Rectangle {
anchors.centerIn: parent
width: parent.ruleWidth
height: parent.ruleWidth * Theme.skew + parent.ruleWeight
vertical: true
color: Theme.alpha(Theme.outline, 0.5)
width: 16
height: 1
color: Theme.alpha(Theme.outline, 0.6)
}
}
+5 -10
View File
@@ -122,12 +122,10 @@ Item {
}
// Leading-edge wipe. On the rail this runs vertically down the left side,
// mirroring the slash P5Panel puts on horizontal modules.
// mirroring the accent bar Panel 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.
// at x = 0 this landed exactly on the slab's own border.
//
// It is the accent rather than `tint` because it only ever appears on
// hover, and hover is the accent everywhere else in the shell (Theme.glow).
@@ -137,7 +135,8 @@ Item {
anchors.leftMargin: root.wipeInset
anchors.verticalCenter: parent.verticalCenter
width: 2
height: root.hovered ? root.height - 4 : 0
radius: 1
height: root.hovered ? Theme.marker * 2 : 0
color: Theme.glow
Behavior on height {
@@ -145,10 +144,6 @@ Item {
}
}
scale: root.hovered ? 1.08 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlarmChanged: if (alarm) glyph.bump()
onLiveChanged: if (live) glyph.bump()
@@ -167,7 +162,7 @@ Item {
onWheel: event => root.scrolled(event.angleDelta.y)
}
P5Tooltip {
Tooltip {
anchorItem: root
triggerHovered: root.hovered
title: root.label
-1
View File
@@ -121,7 +121,6 @@ BarPill {
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5
renderType: Text.NativeRendering
+3 -21
View File
@@ -12,33 +12,15 @@ BarPill {
required property var screenRef
accent: Theme.accent
slash: true
padding: Theme.padS
lean: Theme.skew
implicitWidth: Theme.barHeight + contentPad
onClicked: Actions.toggleLauncher()
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 ink print
// 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
Porthole {
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(); }
size: 18
lit: root.hovered
}
}
-4
View File
@@ -77,10 +77,6 @@ Column {
}
}
scale: itemHover.hovered ? 1.18 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
HoverHandler {
id: itemHover
+57 -4
View File
@@ -4,7 +4,7 @@ import Quickshell.Hyprland
import "root:/config"
import "root:/components"
// Workspace indicator: leaning ticks that stretch open when focused. Occupied
// Workspace indicator: soft pills that stretch open when focused. Occupied
// workspaces fill in, urgent ones snap to the alarm colour.
Item {
id: root
@@ -75,14 +75,14 @@ Item {
Behavior on width {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutBack
easing.overshoot: 1.6
easing.type: Easing.OutCubic
}
}
Skew {
id: tick
anchors.fill: parent
radius: height / 2
// Focused is a solid accent block. Urgent is a solid alarm
// block — it used to be the accent too, which made an
@@ -122,7 +122,6 @@ Item {
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering
Behavior on opacity {
@@ -134,6 +133,7 @@ Item {
Rectangle {
visible: pip.info !== undefined && pip.info.fullscreen && !pip.focused
width: 4; height: 4
radius: 2
color: Theme.accent
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.bottom
@@ -155,5 +155,58 @@ Item {
}
}
}
// The Super+S scratchpad, after a gap: a ring while it holds windows,
// filled with the light while it is open on this monitor. Absent when
// it is empty, which is most of the time.
Item {
id: scratch
readonly property var ws: {
for (const w of (Hyprland.workspaces?.values ?? []))
if (w.name === "special:virtual") return w;
return null;
}
readonly property int count: scratch.ws?.toplevels?.values?.length ?? 0
readonly property bool shown: (root.monitor?.lastIpcObject?.specialWorkspace?.name ?? "")
=== "special:virtual"
visible: scratch.count > 0
width: 8 + 10
height: 14
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 10
height: 10
radius: 5
color: scratch.shown ? Theme.accent : "transparent"
border.width: scratch.shown ? 0 : 1
border.color: scratchHover.hovered ? Theme.text : Theme.subtext
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
HoverHandler {
id: scratchHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: Hyprland.dispatch("hl.dsp.workspace.toggle_special(\"virtual\")")
}
}
}
// The monitor snapshot carries which special workspace is open; it is only
// refreshed on request.
Connections {
target: Hyprland
function onRawEvent(event) {
if (event.name === "activespecial" || event.name === "activespecialv2")
Hyprland.refreshMonitors();
}
}
}
@@ -1,7 +1,8 @@
import QtQuick
import "root:/config"
// Chopped, leaning button with one activation path for pointer and keyboard.
// NieR menu button: a hairline box with a square marker that lights when the
// button is pointed at or chosen. One activation path for pointer and keyboard.
FocusScope {
id: root
@@ -11,7 +12,9 @@ FocusScope {
property bool destructive: false
property bool selected: false
property real lean: Theme.skew
property real lunge: 6
// Sideways nudge on hover. Zero: NieR buttons do not move, the marker
// and the inversion carry the state.
property real lunge: 0
property bool wide: false
signal clicked()
@@ -24,7 +27,7 @@ FocusScope {
// Keyboard focus and roving selection invert the slab to solid accent.
// Pointer hover stays a keyline change, so the two never look alike and a
// mouse user never sees the "selected" treatment by accident. Destructive
// buttons keep their own mauve inversion for both.
// buttons keep their own red inversion for both.
readonly property bool inverted: root.enabled && !root.destructive && root.focusedOrSelected
// Pointer activation must not turn a transient action into the black/white
@@ -45,7 +48,7 @@ FocusScope {
implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2)
implicitHeight: 44
P5Panel {
Panel {
id: panel
anchors.fill: parent
lean: root.lean
@@ -70,8 +73,6 @@ FocusScope {
return Theme.alpha(root.accent, 0.55);
}
borderWidth: Theme.stroke
slash: !root.highlighted
slashColor: root.destructive ? Theme.alarm : root.accent
Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast }
@@ -83,6 +84,21 @@ FocusScope {
anchors.left: parent.left
spacing: Theme.padM
// The marker. Dim at rest, full strength under the pointer or the
// cursor, in whatever colour the label is wearing.
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: Theme.marker
height: Theme.marker
radius: width / 2
color: label.color
opacity: root.highlighted ? 1.0 : 0.35
Behavior on opacity {
NumberAnimation { duration: Theme.durFast }
}
}
Icon {
anchors.verticalCenter: parent.verticalCenter
visible: root.icon !== ""
@@ -100,6 +116,7 @@ FocusScope {
}
Text {
id: label
anchors.verticalCenter: parent.verticalCenter
text: root.text
color: {
@@ -114,8 +131,7 @@ FocusScope {
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 1.5
font.letterSpacing: Theme.tracking
font.capitalization: Font.AllUppercase
renderType: Text.NativeRendering
@@ -126,21 +142,20 @@ FocusScope {
}
}
// A press sinks the button one whole pixel instead of shrinking it, so
// its label stays on the pixel grid.
transform: Translate {
x: root.pressed ? 0 : (root.highlighted ? root.lunge : 0)
y: root.pressed ? 1 : 0
Behavior on x {
NumberAnimation {
duration: Theme.durBase
easing.type: Easing.OutBack
easing.type: Easing.OutCubic
}
}
}
scale: root.pressed ? 0.97 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutExpo }
}
HoverHandler {
id: hover
+4 -9
View File
@@ -3,8 +3,8 @@ import "root:/config"
import "root:/components"
// Calm, permanent-chrome preset. It is deliberately flat and opaque: one
// neutral keyline, no texture, no offset copy, and no accent unless active.
P5Panel {
// neutral hairline, no texture, no ticks, and no accent unless active.
Panel {
property bool active: false
property color tone: Theme.treatmentActiveMark
@@ -12,16 +12,11 @@ P5Panel {
fillOpacity: 1.0
border: Theme.treatmentRestMark
borderWidth: 1
// Keep the hard ink contour. Removing both texture and keyline made the
// permanent slabs read as generic broadcast HUD chrome.
keyline: true
drop: false
halftone: false
sheen: false
slash: active
slashColor: tone
slashWidth: 4
lean: 0.06
chop: Theme.cut
slashWidth: 2
padding: Theme.padS
radius: Theme.radiusM
}
+24 -1
View File
@@ -3,7 +3,11 @@ pragma Singleton
import QtQuick
import Quickshell
// Polygon maths for the Persona 5 panel language: everything leans, and corners
// Shape maths for panels. Under the YoRHa theme nothing leans and nothing is
// chopped (Theme.skew and Theme.cut are 0); surfaces are rounded rectangles
// (roundedRect). The polygon helpers are kept so a shape can be brought back
// on purpose.
// Originally the Persona 5 panel language: everything leans, and corners
// get chopped rather than rounded.
Singleton {
id: root
@@ -86,6 +90,25 @@ Singleton {
return root.closed(out);
}
// A polygon as an SVG path string, for PathSvg.
function svg(pts: var): string {
if (pts.length === 0) return "";
let d = `M ${pts[0].x} ${pts[0].y}`;
for (let i = 1; i < pts.length; i++) d += ` L ${pts[i].x} ${pts[i].y}`;
return d + " Z";
}
// A w x h rectangle with every corner rounded by `r`, clamped so a short
// piece becomes a pill rather than folding over itself.
function roundedRect(w: real, h: real, r: real): string {
const k = Math.max(0, Math.min(r, w / 2, h / 2));
if (k <= 0) return root.svg([Qt.point(0, 0), Qt.point(w, 0), Qt.point(w, h), Qt.point(0, h)]);
return `M ${k} 0 H ${w - k} A ${k} ${k} 0 0 1 ${w} ${k}`
+ ` V ${h - k} A ${k} ${k} 0 0 1 ${w - k} ${h}`
+ ` H ${k} A ${k} ${k} 0 0 1 0 ${h - k}`
+ ` V ${k} A ${k} ${k} 0 0 1 ${k} 0 Z`;
}
function closed(pts: var): var {
if (pts.length === 0) return pts;
const out = pts.slice();
+110
View File
@@ -0,0 +1,110 @@
import QtQuick
import QtQuick.Effects
import "root:/config"
// A line of type lit from inside, like a tube in a dark console: a soft halo
// that breathes slowly, and every so often a short, uneven stutter where the
// light catches — two or three dips, then steady again for a while. The
// stutters come at random intervals so it never settles into a rhythm you
// could count.
//
// Only the halo and the opacity move, never the geometry, so the glyphs stay
// on the pixel grid. Set `active` false while the text is off screen to stop
// the animation.
Item {
id: root
property string text: ""
property color color: Theme.accent
property color glowColor: root.color
property string family: Theme.fontDisplay
property int pixelSize: Theme.fsLarge
property int weight: Font.Light
property real letterSpacing: 0
property bool active: true
// 0..1, how lit the tube is at this instant.
property real lit: 1
// Slow breathing on the halo alone, between `low` and 1.
property real breath: 1
implicitWidth: label.implicitWidth
implicitHeight: label.implicitHeight
Text {
id: label
visible: false
text: root.text
color: root.color
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.letterSpacing: root.letterSpacing
// Greyscale antialiasing: the glyphs are drawn into the effect's
// texture, and native subpixel rendering there leaves colour fringes.
renderType: Text.CurveRendering
}
// A wide, faint halo under a tight, brighter one, so the light both
// spreads into the dark and hugs the strokes.
MultiEffect {
source: label
anchors.fill: label
autoPaddingEnabled: true
opacity: root.lit
shadowEnabled: true
shadowColor: root.glowColor
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
shadowBlur: 1.0
blurMax: Math.max(32, Math.round(root.pixelSize * 2))
shadowOpacity: 0.7 * root.breath
shadowScale: 1.02
}
MultiEffect {
source: label
anchors.fill: label
autoPaddingEnabled: true
opacity: 0.55 + 0.45 * root.lit
shadowEnabled: true
shadowColor: root.glowColor
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
shadowBlur: 1.0
blurMax: Math.max(8, Math.round(root.pixelSize * 0.35))
shadowOpacity: 0.9 * root.lit
}
SequentialAnimation on breath {
running: root.active
loops: Animation.Infinite
NumberAnimation { to: 0.6; duration: 2600; easing.type: Easing.InOutSine }
NumberAnimation { to: 1.0; duration: 2600; easing.type: Easing.InOutSine }
}
// One stutter: uneven dips of uneven length, then back to full.
SequentialAnimation {
id: stutter
NumberAnimation { target: root; property: "lit"; to: 0.25; duration: 40 }
NumberAnimation { target: root; property: "lit"; to: 0.9; duration: 60 }
PauseAnimation { duration: 70 }
NumberAnimation { target: root; property: "lit"; to: 0.4; duration: 30 }
NumberAnimation { target: root; property: "lit"; to: 1.0; duration: 180; easing.type: Easing.OutCubic }
}
Timer {
id: nextStutter
running: root.active
repeat: true
interval: 6000
onTriggered: {
stutter.restart();
nextStutter.interval = 4000 + Math.random() * 10000;
}
}
onActiveChanged: if (!root.active) { stutter.stop(); root.lit = 1; }
}
+7 -3
View File
@@ -13,7 +13,11 @@ Text {
color: Theme.primary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
// Curve glyphs, not native ones. Native glyphs are rasterised once at rest
// size and the bitmap is stretched when `scale` moves, so every pulse and
// bump came out blurred and stepped. Curves are drawn from the outline at
// whatever size the icon is at that frame.
renderType: Text.CurveRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
@@ -35,11 +39,11 @@ Text {
id: bumpAnim
NumberAnimation {
target: root; property: "scale"
to: 1.35; duration: Theme.durFast; easing.type: Easing.OutExpo
to: 1.12; duration: Theme.durFast; easing.type: Easing.OutExpo
}
NumberAnimation {
target: root; property: "scale"
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutCubic
}
}
}
@@ -2,7 +2,7 @@ import QtQuick
import "root:/config"
import "root:/components"
// Leaning slider with a chopped handle. Drag or click anywhere on the rail.
// Hairline slider with a round handle. Drag or click anywhere on the rail.
Item {
id: root
@@ -30,7 +30,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
height: root.activeFocus ? 10 : 6
height: root.activeFocus ? 4 : 2
// Focus widens the rail and rings it in the accent rather than filling
// it: the level fill is often the accent itself, and a solid accent
// rail swallowed it.
@@ -50,7 +50,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: Math.max(0, Math.min(1, root.value)) * root.width
height: 6
height: root.activeFocus ? 4 : 2
color: root.enabled ? root.accent : Theme.treatmentDisabledText
Behavior on width {
@@ -66,14 +66,15 @@ Item {
// Handle
Skew {
id: handle
width: 6
height: root.activeFocus || hover.hovered || drag.active ? 18 : 14
width: height
radius: height / 2
height: root.activeFocus || hover.hovered || drag.active ? 12 : 10
color: root.enabled ? Theme.text : Theme.treatmentDisabledText
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(1, root.value)) * root.width - width / 2
Behavior on height {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack }
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutCubic }
}
Behavior on x {
enabled: !drag.active
+10 -8
View File
@@ -7,8 +7,8 @@ import "root:/config"
// came out as a faint milky film. This is the one row they share now.
//
// Rest is bare. Hover lifts the row onto the surface tone. The current item —
// connected, in use — carries an accent tick down its leading edge and an
// accent glyph, and its label goes to full weight.
// connected, in use — carries a dot at its leading edge and
// an accent glyph, and its label goes to full weight.
Item {
id: root
@@ -30,6 +30,7 @@ Item {
Rectangle {
anchors.fill: parent
radius: Theme.radiusM
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
Behavior on color {
@@ -40,12 +41,14 @@ Item {
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.current ? root.height - 10 : 0
anchors.leftMargin: 2
width: root.current ? Theme.marker : 0
height: width
radius: width / 2
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic }
}
}
@@ -76,8 +79,7 @@ Item {
color: root.current || root.hovered ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: root.current ? Font.Black : Font.Medium
font.weight: root.current ? Font.DemiBold : Font.Medium
renderType: Text.NativeRendering
}
+24 -29
View File
@@ -2,8 +2,12 @@ import QtQuick
import "root:/config"
import "root:/components"
// 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.
// A load meter: a soft rounded track with the level filling it from the left.
// It used to be a row of separate segments; one continuous line reads calmer,
// and a level that glides reads as a quantity rather than as a count.
//
// `segments`, `segmentWidth` and `spacing` are accepted and ignored, so the
// call sites that still size a segmented meter do not have to be chased down.
Item {
id: root
@@ -12,42 +16,33 @@ Item {
property real segmentWidth: 4
property real spacing: 3
property color activeColor: Theme.heat(value)
property color idleColor: Theme.alpha(Theme.outline, 0.55)
property color idleColor: Theme.alpha(Theme.outline, 0.35)
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
implicitHeight: 4
Row {
readonly property real fraction: Math.max(0, Math.min(100, root.value)) / 100
Rectangle {
anchors.fill: parent
spacing: root.spacing
radius: height / 2
color: root.idleColor
}
Repeater {
model: root.segments
Item {
required property int index
width: root.segmentWidth
height: root.height
Skew {
width: root.segmentWidth
height: root.height
lean: root.lean
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
Rectangle {
height: parent.height
// Never narrower than its own height, so a near-zero level is a dot
// rather than a sliver.
width: root.fraction > 0 ? Math.max(height, parent.width * root.fraction) : 0
radius: height / 2
color: root.activeColor
Behavior on width {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutCubic }
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
}
}
}
+3 -3
View File
@@ -2,8 +2,8 @@ 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.
// One metric in a popout: a label and its reading on a line, with a thin
// 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
@@ -78,7 +78,7 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 12
height: 4
segments: root.segments
spacing: root.segmentSpacing
// Fill the row exactly rather than leaving a ragged tail on the right.
+164 -71
View File
@@ -1,96 +1,167 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import "root:/config"
import "root:/components"
import "root:/services"
// One notification, used both in the history list and as a floating toast.
// One notification, used both in the history list and in the toast drawer.
//
// Laid out like an entry in NieR's message log rather than as a card: a dot
// and the sender's name running into a hairline, the summary, the body, and
// pill buttons for any actions. There is no panel of its own — both places it
// appears are already hull surfaces — so rest is bare, hover lifts the entry
// onto a soft rounded wash, and entries are divided by the hairline in their
// own header.
//
// Urgency is the dot: the light for normal, grey for low, red for critical.
// A critical entry also takes the red into its sender line, and nothing else.
Item {
id: root
required property var notif
property bool showActions: true
// Off inside a group in the log, whose header already names the sender:
// the entry keeps only its urgency dot, the hairline and the close mark.
property bool showSender: true
readonly property bool critical: root.notif?.urgency === NotificationUrgency.Critical
readonly property color tone: root.notif
? Notifs.urgencyColor(root.notif.urgency)
: Theme.primary
// Kept for callers that size things to the card's edges; there is no lean
// or chop under the YoRHa theme.
readonly property real lean: 0
readonly property real chop: 0
readonly property bool hovered: hover.hovered
signal dismissed()
// The card's own geometry, published so anything drawn along its edges —
// the toast countdown rule — can stay inside the chopped corners instead of
// running out past them.
readonly property real lean: 0.05
readonly property real chop: 12
implicitHeight: layout.implicitHeight + Theme.padM * 2
implicitHeight: panel.height
// Hover wash. Inset by padS and rounded by radiusL, which is the frame's
// radius less that inset: an entry in the bottom corner of the drawer gets
// a corner concentric with the drawer's own, instead of a square one that
// ran out past the drawer's curve.
Rectangle {
anchors.fill: parent
anchors.margins: Theme.padS / 2
radius: Theme.radiusL
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
P5Panel {
id: panel
width: parent.width
height: layout.implicitHeight + Theme.padM * 2
lean: root.lean
chop: root.chop
fill: Theme.surface
fillOpacity: 0.97
// Urgency lives in the slash, and in the frame only when critical. A
// tone-tinted frame on every card drew a pale outline round ordinary
// notifications, and the critical halftone was painted in the accent,
// so the most urgent card wore the colour that means "live".
border: root.notif?.urgency === NotificationUrgency.Critical
? root.tone
: (hover.hovered ? Theme.treatmentHoverMark : Theme.surfaceAlt)
slash: true
slashColor: root.tone
slashWidth: 4
padding: Theme.padM
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Column {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 3
anchors.top: parent.top
anchors.margins: Theme.padM
spacing: 4
// App line
Row {
spacing: Theme.padS
// Sender: marker, glyph, name, then the hairline to the edge.
Item {
width: parent.width
height: 16
Rectangle {
id: marker
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Theme.marker
height: Theme.marker
radius: width / 2
color: root.tone
}
Icon {
id: glyph
visible: root.showSender && text !== ""
anchors.left: marker.right
anchors.leftMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
text: Notifs.appIconFor(root.notif)
color: root.tone
color: root.critical ? Theme.alarm : Theme.muted
font.pixelSize: Theme.fsBody
}
Text {
id: sender
anchors.left: glyph.visible ? glyph.right : marker.right
anchors.leftMargin: root.showSender ? Theme.padS : 0
anchors.verticalCenter: parent.verticalCenter
width: root.showSender ? implicitWidth : 0
visible: root.showSender
text: (root.notif?.appName || "System").toUpperCase()
color: root.tone
color: root.critical ? Theme.alarm : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: sender.right
anchors.leftMargin: Theme.padS
anchors.right: close.left
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.5)
}
// Close affordance: always there, quiet until the row is hovered.
Icon {
id: close
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: "󰅖"
opacity: root.hovered ? 1 : 0.0
color: closeHover.hovered ? Theme.accent : Theme.muted
font.pixelSize: Theme.fsBody
Behavior on opacity {
NumberAnimation { duration: Theme.durFast }
}
HoverHandler {
id: closeHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: root.dismissed()
}
}
}
Text {
width: parent.width
leftPadding: Theme.marker + Theme.padS
text: root.notif?.summary || ""
color: Theme.text
color: root.hovered ? Theme.accent : 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
font.weight: Theme.weightDisplay
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Text {
width: parent.width
leftPadding: Theme.marker + Theme.padS
visible: text !== ""
text: root.notif?.body || ""
color: Theme.subtext
@@ -100,51 +171,93 @@ Item {
textFormat: Text.StyledText
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
lineHeight: 1.1
renderType: Text.NativeRendering
}
// Inline image, when the sender supplied one.
// Inline image, when the sender supplied one, rounded in a hairline frame.
ClippingRectangle {
x: Theme.marker + Theme.padS
visible: inline.source != "" && inline.status === Image.Ready
width: inline.width + 2
height: inline.height + 2
radius: Theme.radiusM
color: "transparent"
border.width: 1
border.color: Theme.overlay
Image {
visible: source != "" && status === Image.Ready
source: root.notif?.image || ""
id: inline
x: 1
y: 1
// A path given as an image hint (notify-send -h image-path:…)
// comes back routed through the icon provider, which treats
// it as an icon name and returns a placeholder. Load the file.
source: {
const img = root.notif?.image || "";
if (img.startsWith("image://icon//")) return "file://" + img.slice("image://icon/".length);
return img.startsWith("/") ? "file://" + img : img;
}
sourceSize.height: 90
fillMode: Image.PreserveAspectFit
asynchronous: true
}
}
Row {
x: Theme.marker + Theme.padS
visible: root.showActions && (root.notif?.actions?.length ?? 0) > 0
spacing: Theme.padS
topPadding: Theme.padS
topPadding: 4
Repeater {
model: root.notif?.actions ?? []
// A hairline pill. Hover lifts and lights it; it never inverts.
Item {
id: actionChip
required property var modelData
implicitWidth: actionLabel.implicitWidth + Theme.padM
implicitHeight: 22
implicitWidth: chipRow.implicitWidth + Theme.padM * 2
implicitHeight: 24
Skew {
Rectangle {
anchors.fill: parent
color: actionHover.hovered ? Theme.accent : Theme.surfaceAlt
borderWidth: 1
borderColor: actionHover.hovered ? Theme.accent : Theme.overlay
radius: height / 2
color: actionHover.hovered ? Theme.treatmentHoverFill : "transparent"
border.width: 1
border.color: actionHover.hovered ? Theme.treatmentHoverMark : Theme.overlay
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Row {
id: chipRow
anchors.centerIn: parent
spacing: Theme.padS
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 4
height: 4
radius: 2
color: actionLabel.color
opacity: actionHover.hovered ? 1 : 0.4
}
Text {
id: actionLabel
anchors.centerIn: parent
text: actionChip.modelData.text
color: actionHover.hovered ? Theme.ink : Theme.text
anchors.verticalCenter: parent.verticalCenter
text: actionChip.modelData.text.toUpperCase()
color: actionHover.hovered ? Theme.accent : Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMicro
font.italic: true
font.weight: Font.DemiBold
font.letterSpacing: Theme.tracking
renderType: Text.NativeRendering
}
}
HoverHandler {
id: actionHover
@@ -162,26 +275,6 @@ Item {
}
}
// 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
}
-105
View File
@@ -1,105 +0,0 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
import "root:/components"
// The shared ground for the two full-screen surfaces: an accent 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
// Tuned down from 0.10 for Mocha. Crust stripes on the blue-grey base read
// far heavier than they did on black, and buzzed against the text.
property real stripeOpacity: 0.07
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 }
}
}
@@ -4,8 +4,12 @@ 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 accent slash along its leading edge.
// The base surface for everything in this shell. Under the YoRHa theme it is a
// rounded rectangle with a hairline border, an optional dot wash, an optional
// accent bar down its leading edge, and optional corner ticks.
//
// The lean and chop knobs still exist and still work, so a caller can bring a
// shape back on purpose, but Theme.skew and Theme.cut are both zero.
Item {
id: root
@@ -29,12 +33,16 @@ Item {
property real chop: Theme.cut
property var chopCorners: [1, 3]
// Tone slash down the leading edge — the Persona 5 tell.
// Corner radius. Used whenever the panel neither leans nor is chopped,
// which under the YoRHa theme is always.
property real radius: Theme.radiusM
// Accent bar down the leading edge.
property bool slash: false
property color slashColor: Theme.accent
property real slashWidth: 4
// Screen-printed dot wash.
// Dot-grid wash.
property bool halftone: false
property color halftoneColor: Theme.primary
property real halftoneOpacity: 0.07
@@ -45,19 +53,28 @@ Item {
// 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
// Optional print stack: an ink copy of the silhouette offset behind the
// fill, and an ink keyline outside the border. Both off by default — on a
// dark ground they only thicken the edge.
property bool drop: false
property real dropOffset: Theme.dropOffset
property color dropColor: Theme.ink
property bool keyline: true
property bool keyline: false
property real keylineWidth: Theme.keylineWidth
// Corner ticks: a short L at each corner, just outside the border.
property bool ticks: false
property color tickColor: Theme.outline
property real tickLength: Theme.tick
readonly property real leanInset:
(vertical ? width : height) * Math.abs(lean)
// The outline every layer below is drawn from.
readonly property string outline: root.lean === 0 && root.chop <= 0
? Geom.roundedRect(root.width, root.height, root.radius)
: Geom.svg(Geom.chamfer(root.polygon, root.chop, root.chopCorners).slice(0, -1))
readonly property var polygon: vertical
? (leanLeft ? Geom.parallelogramVL(width, height, lean)
: Geom.parallelogramV(width, height, lean))
@@ -90,8 +107,8 @@ Item {
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
PathSvg {
path: root.outline
}
}
}
@@ -114,8 +131,8 @@ Item {
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
PathSvg {
path: root.outline
}
}
}
@@ -139,8 +156,8 @@ Item {
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
PathSvg {
path: root.outline
}
}
@@ -152,8 +169,8 @@ Item {
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
PathSvg {
path: root.outline
}
}
}
@@ -165,7 +182,7 @@ Item {
asynchronous: true
sourceComponent: Image {
anchors.fill: parent
source: Theme.texHalftone
source: Theme.texDots
fillMode: Image.Tile
opacity: root.halftoneOpacity
smooth: false
@@ -237,6 +254,44 @@ Item {
}
}
// Corner ticks. Two hairlines per corner, sitting 3px outside the panel so
// they read as a frame around it rather than as part of its border.
Loader {
anchors.fill: parent
anchors.margins: -3
active: root.ticks
sourceComponent: Item {
Repeater {
model: 4
Item {
required property int index
readonly property bool atRight: index === 1 || index === 2
readonly property bool atBottom: index >= 2
x: atRight ? parent.width - width : 0
y: atBottom ? parent.height - height : 0
width: root.tickLength
height: root.tickLength
Rectangle {
y: parent.atBottom ? parent.height - 1 : 0
width: parent.width
height: 1
color: root.tickColor
}
Rectangle {
x: parent.atRight ? parent.width - 1 : 0
width: 1
height: parent.height
color: root.tickColor
}
}
}
}
}
Item {
id: contentHolder
anchors.fill: parent
+237 -76
View File
@@ -1,10 +1,22 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import "root:/config"
// Hover-driven panel that hangs off a bar item. Opens on a short dwell, stays
// Hover-driven panel that grows out of the hull. 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.
// can be pinned with a click.
//
// A popout is not a card floating near its trigger; it is more of the frame.
// It sits flush against the edge it opens from — the underside of the bar, or
// the inner edge of the rail — is painted in the hull colour, joins that edge
// through two concave fillets, and carries the porthole's rim round its open
// side. It slides out from behind the frame rather than fading in. This is
// caelestia's idea, and the same construction as the dashboard drawer.
//
// The panel is placed explicitly rather than by the compositor's slide
// adjustment: centred on its trigger, then clamped so the fillets never run
// into the opening's rounded corners or onto the rail.
PopupWindow {
id: root
@@ -19,9 +31,14 @@ PopupWindow {
property Item initialFocusItem: null
property bool remappingPinned: false
// Extra surface around the content so the entry overshoot and glow are not
// clipped by the window edge.
readonly property int bleed: 24
// Fillet radius, and the radius of the panel's own free corners — the same
// as the opening's, so a popout meets the frame the way the opening does.
readonly property real fillet: Theme.frameRadius
readonly property real corner: Theme.frameRadius
// How far the panel reaches back over the frame. One pixel, which covers
// the rim the hull draws along the edge so the seam disappears.
readonly property real seam: 1
default property alias body: contentHolder.data
@@ -31,28 +48,122 @@ PopupWindow {
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.
// Panels hanging off the vertical rail open sideways, out of its inner
// edge; everything else drops from the underside of the bar.
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
// Which edges of the frame the panel is attached to. A panel that would
// land within reach of the opening's corner docks into the corner instead,
// attached to the bar and the rail at once — otherwise it leaves a thin
// sliver of opening between itself and the rail, two walls a few pixels
// apart.
//
// "top" hangs from the bar fillets left and right
// "right" juts from the rail fillets above and below
// "topRight" fills the top-right corner fillets left and below
// "bottomRight" fills the bottom-right one fillets above and left
property string dock: horizontal ? "right" : "top"
implicitWidth: contentWidth + bleed * 2
implicitHeight: contentHeight + bleed * 2
// How close to a corner a panel may come before it docks into it.
readonly property real dockReach: 48
// The window: the content, plus a fillet beside each attached edge, plus
// the seam reaching back over the frame.
readonly property real windowWidth: {
switch (dock) {
case "top": return contentWidth + fillet * 2;
case "right": return contentWidth + seam;
default: return contentWidth + fillet + seam;
}
}
readonly property real windowHeight: {
switch (dock) {
case "top": return contentHeight + seam;
case "right": return contentHeight + fillet * 2;
default: return contentHeight + fillet + seam;
}
}
// Where the content sits inside the window.
readonly property real contentX: dock === "right" ? 0 : fillet
readonly property real contentY: dock === "top" || dock === "topRight" ? seam : fillet
// 0 = tucked behind the frame, 1 = fully out.
property real reveal: 0
// Placement, in the trigger window's coordinates. Worked out once per open
// in place(): item positions are not bindable, and a popout should not
// drift while it is up anyway.
property real placeX: 0
property real placeY: 0
// The popout anchors to the trigger's window, not to the trigger. Rail
// icons scale up on hover, and an anchor rect given in an item's own
// coordinates is mapped through that item's transform: a panel placed
// 360px left of a 1.08-scaled icon landed 29px further left still, off
// the rail — by however far the hover scale had got when the popout
// opened. The window's content item never transforms.
readonly property Item anchorFrame: anchorItem?.Window.contentItem ?? null
function place() {
if (!anchorItem) return;
// The trigger's centre. A transform about the centre leaves it where
// it is, so this is the same whatever the hover scale is doing.
const c = anchorItem.mapToItem(null, anchorItem.width / 2, anchorItem.height / 2);
const winW = anchorItem.Window.width;
const winH = anchorItem.Window.height;
const f = root.fillet, R = Theme.frameRadius;
if (root.horizontal) {
// The rail's window starts under the bar at the opening's right
// edge, so its x = 0 is that edge and its y = 0 is the bar's
// underside.
const h = root.contentHeight + f * 2;
const y = c.y - h / 2;
const lo = R, hi = winH - Theme.frame - R - h;
if (y < lo + root.dockReach) {
root.dock = "topRight";
root.placeY = -root.seam;
} else if (y > hi - root.dockReach) {
root.dock = "bottomRight";
root.placeY = winH - Theme.frame + root.seam - root.windowHeight;
} else {
root.dock = "right";
root.placeY = y;
}
root.placeX = root.seam - root.windowWidth;
} else {
// The bar's window is the full screen width, so the opening runs
// from `frame` to `width - railWidth`.
const w = root.contentWidth + f * 2;
const x = c.x - w / 2;
const lo = Theme.frame + R - f;
const hi = winW - Theme.railWidth - R + f - w;
if (x > hi - root.dockReach) {
root.dock = "topRight";
root.placeX = winW - Theme.railWidth + root.seam - root.windowWidth;
} else {
root.dock = "top";
root.placeX = Math.max(lo, x);
}
root.placeY = winH - root.seam;
}
}
anchor.item: anchorFrame
anchor.rect.x: root.placeX
anchor.rect.y: root.placeY
anchor.rect.w: 1
anchor.rect.h: 1
anchor.edges: Edges.Top | Edges.Left
anchor.gravity: Edges.Bottom | Edges.Right
anchor.adjustment: PopupAdjustment.None
implicitWidth: windowWidth
implicitHeight: windowHeight
color: "transparent"
visible: shown
grabFocus: pinned && shown
@@ -97,6 +208,7 @@ PopupWindow {
function open() {
PopupCoordinator.claim(root);
root.place();
exitAnim.stop();
shown = true;
enterAnim.restart();
@@ -152,10 +264,7 @@ PopupWindow {
remappingPinned = false;
pinned = false;
shown = false;
contentHolder.opacity = 0;
contentHolder.scale = 0.94;
contentHolder.x = root.bleed;
contentHolder.y = root.bleed;
root.reveal = 0;
PopupCoordinator.release(root);
root.dismissedFully();
}
@@ -178,33 +287,107 @@ PopupWindow {
}
}
// Everything slides as one sheet, clipped to the window, so the panel
// appears to come out from behind the frame.
Item {
anchors.fill: parent
clip: true
Item {
id: sheet
width: root.windowWidth
height: root.windowHeight
x: root.horizontal ? root.windowWidth * (1 - root.reveal) : 0
y: root.horizontal ? 0 : -root.windowHeight * (1 - root.reveal)
Shape {
id: silhouette
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
readonly property real w: width
readonly property real h: height
readonly property real f: root.fillet
readonly property real r: root.corner
// The open side only: from the frame, round the panel, back to
// the frame. The rim follows this; the fill closes it back
// along the frame with `closing`.
readonly property string edge: {
switch (root.dock) {
case "right":
// Down the top fillet, round the two free corners on
// the left, out through the bottom fillet.
return `M ${w} 0 A ${f} ${f} 0 0 1 ${w - f} ${f}`
+ ` H ${r} A ${r} ${r} 0 0 0 0 ${f + r}`
+ ` V ${h - f - r} A ${r} ${r} 0 0 0 ${r} ${h - f}`
+ ` H ${w - f} A ${f} ${f} 0 0 1 ${w} ${h}`;
case "topRight":
// Off the bar through the left fillet, round the one
// free corner, onto the rail through the bottom fillet.
return `M 0 0 A ${f} ${f} 0 0 1 ${f} ${f}`
+ ` V ${h - f - r} A ${r} ${r} 0 0 0 ${f + r} ${h - f}`
+ ` H ${w - f} A ${f} ${f} 0 0 1 ${w} ${h}`;
case "bottomRight":
// Off the rail through the top fillet, round the one
// free corner, onto the bottom band through the left.
return `M ${w} 0 A ${f} ${f} 0 0 1 ${w - f} ${f}`
+ ` H ${f + r} A ${r} ${r} 0 0 0 ${f} ${f + r}`
+ ` V ${h - f} A ${f} ${f} 0 0 1 0 ${h}`;
default:
// Hanging from the bar: the dashboard's silhouette.
return `M 0 0 A ${f} ${f} 0 0 1 ${f} ${f}`
+ ` V ${h - r} A ${r} ${r} 0 0 0 ${f + r} ${h}`
+ ` H ${w - f - r} A ${r} ${r} 0 0 0 ${w - f} ${h - r}`
+ ` V ${f} A ${f} ${f} 0 0 1 ${w} 0`;
}
}
// Back along the frame to the start, through the corner the
// panel is docked into if it is docked into one.
readonly property string closing: {
switch (root.dock) {
case "topRight": return ` L ${w} 0 Z`;
case "bottomRight": return ` L ${w} ${h} Z`;
default: return " Z";
}
}
ShapePath {
fillColor: Theme.hull
strokeColor: "transparent"
PathSvg { path: silhouette.edge + silhouette.closing }
}
ShapePath {
fillColor: "transparent"
strokeColor: Theme.rim
strokeWidth: 1
PathSvg { path: silhouette.edge }
}
}
Item {
id: contentHolder
focus: root.pinned
x: root.bleed
y: root.bleed
x: root.contentX
y: root.contentY
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
// Read by PopoutSurface, which leaves the fill and the frame to
// the hull when it finds itself in here.
readonly property bool frameAttached: true
// 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.
// 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.
//
// Disabled while the popout is down, which clears `hovered`. An unmapped
// surface gets no leave event, so a handler that was hovered when the
// window went away stayed hovered — `wantsOpen` never went false again
// and the panel came back up and refused to close.
// Disabled while the popout is down, which clears `hovered`. An
// unmapped surface gets no leave event, so a handler that was
// hovered when the window went away stayed hovered — `wantsOpen`
// never went false again and the panel came back up and refused
// to close.
HoverHandler {
id: panelHover
enabled: root.shown
@@ -217,44 +400,22 @@ PopupWindow {
}
}
}
}
}
ParallelAnimation {
NumberAnimation {
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
}
target: root; property: "reveal"
to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
}
ParallelAnimation {
NumberAnimation {
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
}
target: root; property: "reveal"
to: 0; duration: Theme.durBase; easing.type: Easing.InCubic
onFinished: {
root.shown = false;
root.pinned = false;
contentHolder.x = root.bleed;
contentHolder.y = root.bleed;
PopupCoordinator.release(root);
root.dismissedFully();
}
+43
View File
@@ -0,0 +1,43 @@
import QtQuick
import "root:/config"
// The house mark: a porthole with a planet low in it — the wallpaper's one
// composition, drawn in a hairline ring and a disc. The bar's sigil and the
// lock screen both draw it, so the two are the same mark at two sizes.
Item {
id: root
property real size: 18
property bool lit: false
implicitWidth: root.size
implicitHeight: root.size
Rectangle {
anchors.fill: parent
radius: width / 2
color: "transparent"
border.width: Math.max(1, Math.round(root.size / 18))
border.color: root.lit ? Theme.accent : Theme.primaryDim
Behavior on border.color {
ColorAnimation { duration: Theme.durFast }
}
}
// The planet's limb, sitting on the ring's lower edge. Always the planet
// white; `lit` only lets it rise a little.
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: root.size / 6
width: root.size * (root.lit ? 0.44 : 0.34)
height: width
radius: width / 2
color: Theme.accent
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic }
}
}
}
+14 -6
View File
@@ -3,7 +3,8 @@ import QtQuick.Shapes
import "root:/config"
import "root:/components"
// A leaning block — the small-scale counterpart to P5Panel.
// A small block — the small-scale counterpart to Panel. Upright and rounded
// under the YoRHa theme; it still leans if given a lean.
//
// Every tick, rule, meter segment and slider rail in this shell leans at
// Theme.skew. Shearing a plain Rectangle with a Matrix4x4 does that, but Qt
@@ -22,15 +23,20 @@ Shape {
property color borderColor: "transparent"
property real borderWidth: 0
// Lean of the vertical edges as a fraction of height, matching P5Panel.
// Lean of the vertical edges as a fraction of height, matching Panel.
property real lean: Theme.skew
property bool leanLeft: false
// Wide, short pieces — the rail's hairline rules — slant their horizontal
// edges by a fraction of width instead, exactly as P5Panel does for the
// edges by a fraction of width instead, exactly as Panel does for the
// rail slab.
property bool vertical: false
// Corner radius for an upright piece, clamped to a pill. Small by default,
// so a meter segment or a hover wash reads soft rather than as a block;
// a hairline stays a hairline.
property real radius: Theme.radiusS
// NOTE ON SIZING. The lean shifts one edge by `lean * height` (or by
// `lean * width` when vertical), and the piece that actually gets painted is
// what is left: a Skew declared 1px wide and 20px tall does not draw a 1px
@@ -55,13 +61,15 @@ Shape {
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
PathSvg {
path: {
const w = root.width, h = root.height, l = root.lean;
if (l === 0)
return Geom.roundedRect(w, h, root.radius);
if (root.vertical)
return Geom.closed(root.leanLeft ? Geom.parallelogramVL(w, h, l)
return Geom.svg(root.leanLeft ? Geom.parallelogramVL(w, h, l)
: Geom.parallelogramV(w, h, l));
return Geom.closed(root.leanLeft ? Geom.parallelogramL(w, h, l)
return Geom.svg(root.leanLeft ? Geom.parallelogramL(w, h, l)
: Geom.parallelogram(w, h, l));
}
}
+11 -12
View File
@@ -1,14 +1,13 @@
import QtQuick
import "root:/config"
// Display text, screen-printed: the body sits on a hard crust copy knocked a
// pixel and a half down-right — the same offset `P5Panel` puts behind every
// slab. Drives most headings in the shell.
// Display text: upright, widely tracked, sitting on a soft ink copy a pixel
// down-right — the faint drop NieR puts under every line of its interface.
// Drives most headings in the shell.
//
// This used to carry a permanent RGB split, a blue and a mauve ghost either
// side of the body. On crimson-and-black that read as misregistered print; on
// Mocha's pastels it read as blur, and it turned every heading in the shell
// soft. The two ghosts still exist, but only for the moment `glitch()` runs.
// The two ghosts either side of the body are the data-corruption flicker NieR
// uses when something changes under you. They are invisible at rest and only
// appear for the moment `glitch()` runs.
Item {
id: root
@@ -16,16 +15,16 @@ Item {
property int pixelSize: Theme.fsLarge
property int weight: Theme.weightDisplay
property string family: Theme.fontDisplay
property bool italic: true
property bool italic: false
property color color: Theme.text
property int elide: Text.ElideNone
property int horizontalAlignment: Text.AlignLeft
property real letterSpacing: 0.5
property real letterSpacing: Theme.tracking
// Print shadow. Scales gently with size so a 34px heading does not sit on
// Drop shadow. Scales gently with size so a 34px heading does not sit on
// the same hairline offset as an 11px label.
property real shadowOffset: Math.max(1, Math.round(root.pixelSize / 10))
property color shadowColor: Theme.ink
property real shadowOffset: Math.max(1, Math.round(root.pixelSize / 16))
property color shadowColor: Theme.alpha(Theme.ink, 0.85)
// Ghost offset and resting strength. Resting strength is zero, so the
// ghosts are invisible until `glitch()` kicks them out. A caller that sets
@@ -69,7 +69,6 @@ PopupWindow {
ChromeSurface {
anchors.fill: parent
padding: root.pad
lean: 0.06
Column {
id: textColumn
@@ -84,7 +83,6 @@ PopupWindow {
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering
}
+123 -95
View File
@@ -3,85 +3,93 @@ pragma Singleton
import QtQuick
import Quickshell
// Mocha palette: Catppuccin Mocha ground, blue as the accent, mauve as the
// alarm. Persona geometry, softer colour.
// YoRHa dark: the NieR:Automata interface language, lit the way
// ~/.wallpapers/space-1.jpg is lit.
//
// The Takemi branch ran monochrome-plus-crimson, where one hue carried both
// "active" and "wrong". Mocha has room for two: blue is every resting accent —
// focus, selection, the thing you are pointing at — and mauve is reserved for
// the things that bite, so destructive and error still read at a glance
// without a third colour entering the shell.
// NieR's menus are sand paper and graphite — monochrome, hairline rules,
// letter-spaced capitals, a small square marking where you are, and selection
// shown by inverting the row rather than by colouring it. That language is
// kept whole. The colours are not NieR's: the game's beige is a lit room, and
// this rice is the dark outside the porthole. The ground is the wallpaper's
// grey-green void, the text is the same sage grey the suit catches, and the
// one bright thing on screen is the planet's white — so focus and selection
// are a slab of that light with the void punched out of it.
//
// Mauve rather than Mocha's pink: pink is a pastel, and a slab of it over this
// ground goes chalky. Mauve is saturated enough to hold an edge against the
// near-black surfaces the Persona layout is built from.
//
// Everything else is Mocha's own neutral ramp. Fills stay flat and keylines
// stay hard; only the hues moved.
// There is still no third colour. The only warm thing in the wallpaper is the
// tiny red of the hull's indicator lights, and that is what `alarm` is.
Singleton {
id: root
// ---------------------------------------------------------------- colors
// Crust, not pure black: it is the darkest thing in Mocha, so it keeps its
// job as the hard keyline and as text punched out of an accent fill.
readonly property color ink: "#11111b"
// Darker than the ground: text punched out of a light fill, and the
// shadow under display text.
readonly property color ink: "#090a08"
readonly property color base: "#1e1e2e"
readonly property color mantle: "#181825"
readonly property color surface: "#313244"
readonly property color surfaceAlt: "#45475a"
readonly property color overlay: "#585b70"
readonly property color outline: "#6c7086"
// The void. Green-black rather than neutral so the shell and the
// wallpaper read as one dark, not two.
readonly property color base: "#0f110e"
readonly property color mantle: "#0c0d0b"
readonly property color surface: "#161915"
readonly property color surfaceAlt: "#20241f"
readonly property color overlay: "#383e35"
readonly property color text: "#cdd6f4"
readonly property color subtext: "#a6adc8"
readonly property color muted: "#7f849c"
// The hull the desktop is framed by. Darker than anything inside the
// opening, so the frame reads as the inside of the ship and the windows as
// what you are looking out at.
readonly property color hull: root.mantle
readonly property color rim: "#2a2f28"
readonly property color outline: "#545c52"
readonly property color text: "#ccd2cb"
readonly property color subtext: "#9fa7a2"
readonly property color muted: "#79847a"
// ------------------------------------------------------------ THE KNOBS
// Two colours, two meanings, and nothing else in the shell carries hue.
//
// `accent` is blue: anything live, focused or selected. `alarm` is mauve:
// `accent` is the planet light: anything live, focused or selected. It is
// barely a colour at all, which is the point — NieR marks attention with
// brightness and inversion, not with hue. `alarm` is the hull's red:
// anything destructive, failed or critical. Things that are merely off —
// muted, disabled, do-not-disturb — carry no hue at all; see the `Off`
// treatment below. Swap either line and everything derived
// from it recolours.
// muted, disabled, do-not-disturb — stay in the grey ramp; see the `Off`
// treatment below.
//
// Keep them in step with `c.accent` / `c.alarm` in
// ~/.config/hypr/lua/palette.lua, which is what colours the Hyprland
// window borders. Those are the only places the colours are written.
readonly property color accent: "#89b4fa"
readonly property color alarm: "#cba6f7"
readonly property color accent: "#eef1ea"
readonly property color alarm: "#c8684f"
readonly property color accentDim: "#5a7dc0"
readonly property color accentSoft: "#b4befe"
readonly property color accentDim: "#8e978d"
readonly property color accentSoft: "#bcc1bf"
// `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.
// `primary` is the resting voice of the UI — the sage text grey. 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: "#9399b2"
readonly property color primaryDim: "#8e978d"
readonly property color deep: root.surfaceAlt
// `glow` is the hover highlight. Pointing at a thing lights it up blue —
// the same signal as focus, because it is the same statement about where
// your attention is.
// `glow` is the hover highlight: pointing at a thing brightens it toward
// the planet light, the same signal as focus at a lower volume.
readonly property color glow: root.accent
// ------------------------------------------------ semantic treatments
// These tokens describe interaction meaning, not new palette entries. Each
// state is composed only from crust, the neutral ramp, blue and mauve, so
// state is composed only from ink, the grey ramp, the light and the red, so
// callers do not invent one-off state colours.
readonly property color treatmentRestFill: root.surface
readonly property color treatmentRestMark: root.outline
readonly property color treatmentRestMark: root.overlay
readonly property color treatmentRestText: root.text
readonly property color treatmentHoverFill: root.surface
// Hover lifts the surface a step and brings the marker up to full light.
// It never inverts: inversion is the cursor, and the pointer resting on
// something is not the same statement as the keyboard choosing it.
readonly property color treatmentHoverFill: root.surfaceAlt
readonly property color treatmentHoverMark: root.accent
readonly property color treatmentHoverText: root.text
readonly property color treatmentHoverText: root.accent
// Focus and selection are an inverted slab: solid blue with crust punched
// out of it (9:1). This used to be a white fill, which was right against
// Takemi's black but glares as a lavender-white block on Mocha — and it
// made "selected" a third look that was neither hover nor accent.
// Focus and selection are the NieR cursor: the row inverted, a slab of the
// planet light with ink punched out of it.
readonly property color treatmentFocusFill: root.accent
readonly property color treatmentFocusMark: root.ink
readonly property color treatmentFocusText: root.ink
@@ -89,16 +97,16 @@ Singleton {
readonly property color treatmentSelectedMark: root.treatmentFocusMark
readonly property color treatmentSelectedText: root.treatmentFocusText
readonly property color treatmentActiveFill: root.surface
readonly property color treatmentActiveFill: root.surfaceAlt
readonly property color treatmentActiveMark: root.accent
readonly property color treatmentActiveText: root.text
readonly property color treatmentActiveText: root.accent
// Mauve carries destructive/error meaning — never blue, so a dangerous
// Red carries destructive/error meaning — never the light, so a dangerous
// button can never be mistaken for a merely focused one.
//
// At rest it is a keyline and a label, not a fill. The solid mauve is held
// At rest it is a hairline and a label, not a fill. The solid red is held
// back for the moment the pointer or the selection is actually on the
// thing — then it lands hard, with crust punched out of it at 9.1:1.
// thing, with ink punched out of it.
readonly property color treatmentDestructiveFill: root.surface
readonly property color treatmentDestructiveMark: root.alarm
readonly property color treatmentDestructiveText: root.alarm
@@ -115,12 +123,11 @@ Singleton {
readonly property color treatmentUrgentText: root.text
readonly property color treatmentDisabledFill: root.surface
readonly property color treatmentDisabledMark: root.outline
readonly property color treatmentDisabledMark: root.overlay
readonly property color treatmentDisabledText: root.muted
// Off is a state the user chose — muted, do-not-disturb, radio switched
// off. It is neither live nor wrong, so it gets no hue: painting a muted
// speaker blue said "active", and painting it mauve would say "broken".
// off. It is neither live nor wrong, so it sinks into the grey ramp.
readonly property color treatmentOffFill: root.surface
readonly property color treatmentOffMark: root.overlay
readonly property color treatmentOffText: root.muted
@@ -138,27 +145,30 @@ Singleton {
a.a + (b.a - a.a) * k);
}
// Load readouts sit at text until the number actually matters, then climb
// through the accent before they reach the alarm. Going straight from text
// to mauve passes through a washed-out mid-tone that reads as a faded red
// rather than as a rising number; routing through blue keeps every step of
// the ramp a colour the shell already uses.
// Load readouts sit at text until the number actually matters, brighten
// toward the light, and only then warm into the red. Every step of the ramp
// is a colour the shell already uses.
function heat(pct: real): color {
if (pct <= 45) return root.text;
if (pct <= 75) return root.mix(root.text, root.accent, (pct - 45) / 30);
return root.mix(root.accent, root.alarm, (pct - 75) / 25);
if (pct <= 55) return root.text;
if (pct <= 80) return root.mix(root.text, root.accent, (pct - 55) / 25);
return root.mix(root.accent, root.alarm, (pct - 80) / 20);
}
// ----------------------------------------------------------------- fonts
readonly property string fontDisplay: "Archivo Black"
// NieR sets its UI in FOT-Rodin, a plain Japanese gothic, upright and
// widely tracked. Noto Sans is the closest free face that is already on
// the machine. Display text is never italic and never heavy; the wide
// tracking is what makes it read as a heading.
readonly property string fontDisplay: "Noto Sans"
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 weightDisplay: Font.Medium
readonly property int weightBody: Font.Normal
// Letter-spacing for uppercase labels and headings.
readonly property real tracking: 2.5
// Nothing in the shell renders below this. 9px mono was legible on
// crimson-on-black; on Mocha's lower-contrast ramp it was not.
readonly property int fsMicro: 10
readonly property int fsSmall: 11
readonly property int fsBody: 12
@@ -170,27 +180,44 @@ Singleton {
readonly property int fsRailIcon: 17
// -------------------------------------------------------------- geometry
// The hull. The bar along the top, the rail down the right and a thin band
// down the left and along the bottom are one continuous surface, and the
// desktop is the rounded opening cut out of it — the porthole the
// wallpaper is looking through. `frame` is the band's thickness where
// there is no bar or rail, `frameRadius` the opening's corner.
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
readonly property int frame: 8
readonly property int frameRadius: 18
// 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
// Nothing leans and nothing is chopped. Both knobs are kept so the shape
// code still has one place to ask.
readonly property real skew: 0
readonly property int cut: 0
readonly property int stroke: 1
// Crust 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
// No printed drop and no ink contour: on a dark ground they only thicken
// the edge. The shadow under display text is the one offset left.
readonly property real dropOffset: 0
readonly property real keylineWidth: 0
// Corners. Everything is soft — the porthole is the shape the whole
// desktop is built around, and every smaller surface is a gentler echo of
// it rather than a hard block.
//
// radiusS pills and small chips: buttons, tags, meter tracks
// radiusM rows and panels inside a surface
// radiusL anything that sits a padS inside the frame's own curve:
// frameRadius - padS, so its corner is concentric with the
// corner it sits in and the gap between them stays even
readonly property int radiusS: 6
readonly property int radiusM: 10
readonly property int radiusL: frameRadius - padS
// The dot that marks the current row, and the length of the corner
// ticks a panel can still ask for.
readonly property int marker: 6
readonly property int tick: 8
readonly property int padS: 6
readonly property int padM: 12
@@ -201,24 +228,25 @@ Singleton {
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
// NieR motion is calm and mechanical: things glide in and settle, they
// never bounce.
readonly property int durFast: 120
readonly property int durBase: 220
readonly property int durSlow: 380
readonly property int durLazy: 700
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 easeSnap: [0.22, 1.0, 0.36, 1.0, 1, 1] // outQuint
readonly property var easeGlide: [0.4, 0.0, 0.2, 1.0, 1, 1] // standard
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"
// A 1px dot every 8px, the ground NieR's menus are printed on.
readonly property url texDots: "root:/assets/dots.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/arch.png"
readonly property url fallbackWallpaper: "file:///home/narl/.wallpapers/space-1.jpg"
}
+773
View File
@@ -0,0 +1,773 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import "root:/config"
import "root:/components"
import "root:/services"
// The dashboard: a drawer that slides down out of the underside of the bar.
//
// The idea is caelestia's — the drawer is not a floating card but more of the
// hull, so it is painted in the hull colour and joins the bar through two
// concave fillets, and the porthole's rim runs round it without a break. Read
// against the wallpaper it is a panel lowered from the ceiling of the ship.
//
// Four columns divided by hairlines: who and when, the month, the machine,
// and what is playing. The clock in the bar toggles it, as does
// `qs ipc call shell dashboard`; Escape or a click anywhere else puts it away.
PanelWindow {
id: root
required property var modelData
screen: modelData
readonly property bool open: Actions.dashboardOpen
&& Actions.dashboardScreen === modelData.name
// 0 = tucked under the bar, 1 = fully down.
property real reveal: root.open ? 1 : 0
Behavior on reveal {
NumberAnimation {
duration: Theme.durSlow
easing.type: root.open ? Easing.OutCubic : Easing.InCubic
}
}
visible: root.open || root.reveal > 0
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-dashboard"
WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.OnDemand
: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
// One pixel up, so the drawer covers the rim the hull draws along the
// underside of the bar instead of sitting below it.
anchors.top: true
margins.top: Theme.barHeight - 1
// The fillets are the same radius as the porthole's corners, so the drawer
// meets the bar the way the opening meets the frame.
readonly property real fillet: Theme.frameRadius
readonly property real radius: Theme.frameRadius
readonly property real pad: Theme.padL
readonly property real bodyWidth: columns.implicitWidth + root.pad * 2
readonly property real bodyHeight: columns.implicitHeight + toggles.implicitHeight
+ Theme.padL + 1 + root.pad * 3
implicitWidth: root.bodyWidth + root.fillet * 2
implicitHeight: root.bodyHeight + 1
color: "transparent"
mask: Region { item: body }
HyprlandFocusGrab {
active: root.open
windows: [root]
onCleared: Actions.closeDashboard()
}
// Who is at the console. Read once; it does not change under a session.
readonly property string user: Quickshell.env("USER") ?? ""
FileView {
id: hostFile
path: "/etc/hostname"
}
readonly property string host: hostFile.text().trim()
SystemClock {
id: clock
precision: SystemClock.Seconds
}
// Everything below slides as one sheet, clipped to the window so it
// appears to come out from behind the bar rather than fading in over it.
Item {
anchors.fill: parent
clip: true
Item {
id: sheet
width: parent.width
height: parent.height
y: -height * (1 - root.reveal)
// ------------------------------------------------------ silhouette
Shape {
id: silhouette
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
readonly property real w: width
readonly property real h: height
readonly property real f: root.fillet
readonly property real r: root.radius
// Down the left fillet, round the bottom, up the right fillet.
readonly property string edge:
`M 0 0 A ${f} ${f} 0 0 1 ${f} ${f}`
+ ` V ${h - r} A ${r} ${r} 0 0 0 ${f + r} ${h}`
+ ` H ${w - f - r} A ${r} ${r} 0 0 0 ${w - f} ${h - r}`
+ ` V ${f} A ${f} ${f} 0 0 1 ${w} 0`
ShapePath {
fillColor: Theme.hull
strokeColor: "transparent"
PathSvg { path: silhouette.edge + " Z" }
}
// The rim, carried on round the drawer from the opening.
ShapePath {
fillColor: "transparent"
strokeColor: Theme.rim
strokeWidth: 1
PathSvg { path: silhouette.edge }
}
}
// ---------------------------------------------------------- body
FocusScope {
id: body
x: root.fillet
width: root.bodyWidth
height: parent.height
focus: root.open
Keys.onEscapePressed: Actions.closeDashboard()
Row {
id: columns
x: root.pad
y: root.pad
spacing: 0
// Who and when: the house mark, the operator, a large clock.
Item {
implicitWidth: 230
implicitHeight: root.columnHeight
Row {
id: who
spacing: Theme.padM
Porthole {
anchors.verticalCenter: parent.verticalCenter
size: 30
lit: true
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 1
Text {
text: root.user.toUpperCase()
color: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.letterSpacing: Theme.tracking
renderType: Text.NativeRendering
}
Text {
text: root.host !== "" ? "@ " + root.host : ""
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
}
}
Row {
id: time
anchors.top: who.bottom
anchors.topMargin: Theme.padL
spacing: Theme.padS
SplitText {
id: hhmm
text: Qt.formatDateTime(clock.date, "HH:mm")
pixelSize: 54
weight: Font.Light
letterSpacing: 2
color: Theme.accent
}
Text {
anchors.baseline: hhmm.bottom
anchors.baselineOffset: -12
text: Qt.formatDateTime(clock.date, "ss")
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsBody
renderType: Text.NativeRendering
}
}
Text {
anchors.top: time.bottom
anchors.topMargin: Theme.padS
text: Qt.formatDateTime(clock.date, "dddd · dd MMMM").toUpperCase()
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Column {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padS
Fact { label: "UPTIME"; value: Sys.uptime || "—" }
Fact {
visible: Battery.available
label: "POWER"
value: Math.round(Battery.percent) + "%"
+ (Battery.timeLabel !== "" ? " · " + Battery.timeLabel : "")
}
Fact { label: "NOTICES"; value: Notifs.count > 0 ? Notifs.count + " held" : "none" }
}
}
Rule { height: root.columnHeight }
// The month, Monday first. Today is the NieR cursor: an inverted disc.
Item {
id: month
implicitWidth: 238
implicitHeight: root.columnHeight
// Keyed by the day, not the clock: the clock ticks every
// second, and rebuilding 42 cells that often is waste.
readonly property string dayKey: Qt.formatDateTime(clock.date, "yyyy-MM-dd")
readonly property var cells: {
const now = new Date(month.dayKey + "T12:00:00");
const y = now.getFullYear(), m = now.getMonth();
const lead = (new Date(y, m, 1).getDay() + 6) % 7;
const out = [];
for (let i = 0; i < 42; i++) {
const d = new Date(y, m, 1 - lead + i);
out.push({
day: d.getDate(),
inMonth: d.getMonth() === m,
today: d.toDateString() === now.toDateString()
});
}
return out;
}
SectionRule {
id: monthHead
anchors.left: parent.left
anchors.right: parent.right
text: Qt.formatDateTime(clock.date, "MMMM yyyy")
}
Row {
id: weekdays
anchors.top: monthHead.bottom
anchors.topMargin: Theme.padS
Repeater {
model: ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]
Text {
required property var modelData
required property int index
width: month.width / 7
horizontalAlignment: Text.AlignHCenter
text: modelData
color: index >= 5 ? Theme.subtext : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
Grid {
anchors.top: weekdays.bottom
anchors.topMargin: Theme.padS
columns: 7
Repeater {
model: month.cells
Item {
required property var modelData
width: month.width / 7
height: 26
Rectangle {
anchors.centerIn: parent
width: 22
height: 22
radius: 11
visible: parent.modelData.today
color: Theme.treatmentSelectedFill
}
Text {
anchors.centerIn: parent
text: parent.modelData.day
color: parent.modelData.today ? Theme.treatmentSelectedText
: parent.modelData.inMonth ? Theme.text
: Theme.alpha(Theme.muted, 0.5)
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: parent.modelData.today ? Font.Bold : Font.Normal
renderType: Text.NativeRendering
}
}
}
}
}
Rule { height: root.columnHeight }
// The machine: the same meters the bar's readout opens onto.
Item {
implicitWidth: 238
implicitHeight: root.columnHeight
SectionRule {
id: sysHead
anchors.left: parent.left
anchors.right: parent.right
text: "System"
}
Column {
anchors.top: sysHead.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"
}
}
}
Rule { height: root.columnHeight }
// What is playing. With no player it says so, in the voice NieR's
// interface uses for an empty channel.
Item {
implicitWidth: 280
implicitHeight: root.columnHeight
SectionRule {
id: playHead
anchors.left: parent.left
anchors.right: parent.right
text: Media.hasPlayer ? Media.identity : "Audio"
}
Text {
anchors.centerIn: parent
visible: !Media.hasPlayer
text: "NO SIGNAL"
color: Theme.muted
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.letterSpacing: Theme.tracking * 2
renderType: Text.NativeRendering
}
Item {
anchors.top: playHead.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
visible: Media.hasPlayer
ClippingRectangle {
id: artFrame
width: 96
height: 96
radius: Theme.radiusL
color: Theme.surface
border.width: 1
border.color: Theme.overlay
Image {
id: art
anchors.fill: parent
anchors.margins: 1
source: Media.artUrl
fillMode: Image.PreserveAspectCrop
asynchronous: true
sourceSize.width: 192
sourceSize.height: 192
}
Icon {
anchors.centerIn: parent
visible: art.status !== Image.Ready
text: "󰝚"
color: Theme.muted
font.pixelSize: Theme.fsHuge
}
}
Column {
anchors.left: artFrame.right
anchors.leftMargin: Theme.padM
anchors.right: parent.right
anchors.verticalCenter: artFrame.verticalCenter
spacing: 3
Text {
width: parent.width
text: Media.title || "Untitled"
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.Wrap
color: Theme.accent
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsLarge
font.weight: Theme.weightDisplay
renderType: Text.NativeRendering
}
Text {
width: parent.width
text: Media.artist
elide: Text.ElideRight
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
renderType: Text.NativeRendering
}
}
// Position: a hairline with a square riding it.
Item {
id: timeline
anchors.top: artFrame.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
height: Theme.marker
visible: Media.hasTimeline
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 1
color: Theme.overlay
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width * Media.progress
height: 1
color: Theme.accent
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
x: parent.width * Media.progress - width / 2
width: Theme.marker
height: Theme.marker
radius: width / 2
color: Theme.accent
}
}
Row {
anchors.bottom: parent.bottom
anchors.left: parent.left
spacing: Theme.padS
Transport {
glyph: "󰒮"
enabled: Media.canGoPrevious
onClicked: Media.previous()
}
Transport {
glyph: Media.playing ? "󰏤" : "󰐊"
onClicked: Media.playPause()
}
Transport {
glyph: "󰒭"
enabled: Media.canGoNext
onClicked: Media.next()
}
}
Text {
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.right: parent.right
visible: Media.hasTimeline
text: Media.timeString(Media.position) + " / " + Media.timeString(Media.length)
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
}
// A hairline, then the switches you reach for most.
Rectangle {
id: togglesRule
x: root.pad
y: columns.y + columns.implicitHeight + Theme.padL
width: root.bodyWidth - root.pad * 2
height: 1
color: Theme.alpha(Theme.outline, 0.5)
}
Row {
id: toggles
x: root.pad
y: togglesRule.y + 1 + root.pad
spacing: Theme.padS
readonly property int count: nightLight.available ? 5 : 4
readonly property real chipWidth:
(root.bodyWidth - root.pad * 2 - toggles.spacing * (toggles.count - 1)) / toggles.count
Toggle {
width: toggles.chipWidth
glyph: Net.wifiEnabled ? "󰖩" : "󰖪"
label: "Wi-Fi"
on: Net.wifiEnabled
onClicked: Net.toggleWifi()
}
Toggle {
width: toggles.chipWidth
enabled: Bt.available
glyph: Bt.enabled ? "󰂯" : "󰂲"
label: "Bluetooth"
on: Bt.enabled
onClicked: Bt.toggle()
}
Toggle {
width: toggles.chipWidth
glyph: Notifs.dnd ? "󰂛" : "󰂚"
label: "Do not disturb"
on: Notifs.dnd
onClicked: Notifs.toggleDnd()
}
Toggle {
width: toggles.chipWidth
glyph: "󰅶"
label: "Stay awake"
on: Idle.stayAwake
onClicked: Idle.toggleStayAwake()
}
Toggle {
width: toggles.chipWidth
visible: nightLight.available
glyph: "󰖔"
label: "Night light"
on: nightLight.running
onClicked: nightLight.running = !nightLight.running
}
}
}
}
}
// ------------------------------------------------------------ columns
// A vertical hairline between columns, the same rule the bar divides its
// modules with, at the drawer's scale.
component Rule: Item {
implicitWidth: Theme.padL * 2 + 1
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: Theme.padS
anchors.bottomMargin: Theme.padS
width: 1
color: Theme.alpha(Theme.outline, 0.5)
}
}
readonly property real columnHeight: 214
// A warm screen for the evening, through hyprsunset if it is installed.
// It runs for as long as night light is on; stopping it restores the
// screen. The chip only appears when the binary exists.
Process {
id: nightLight
property bool available: false
command: ["hyprsunset", "--temperature", "4200"]
}
Process {
running: true
command: ["sh", "-c", "command -v hyprsunset"]
onExited: code => nightLight.available = code === 0
}
// A switch: a pill with a glyph and a name. On is the light slab with ink
// type, the same as a selection everywhere else; off is a hairline.
component Toggle: Item {
id: tg
property string glyph: ""
property string label: ""
property bool on: false
signal clicked()
height: 38
opacity: tg.enabled ? 1 : 0.35
Rectangle {
anchors.fill: parent
radius: height / 2
color: tg.on ? Theme.accentSoft : (tgHover.hovered ? Theme.treatmentHoverFill : "transparent")
border.width: tg.on ? 0 : 1
border.color: tgHover.hovered ? Theme.treatmentHoverMark : Theme.overlay
Behavior on color { ColorAnimation { duration: Theme.durBase } }
}
Row {
anchors.centerIn: parent
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: tg.glyph
font.pixelSize: Theme.fsLarge
color: tg.on ? Theme.ink : (tgHover.hovered ? Theme.accent : Theme.text)
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: tg.label.toUpperCase()
color: tg.on ? Theme.ink : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMicro
font.weight: Font.DemiBold
font.letterSpacing: Theme.tracking
renderType: Text.NativeRendering
}
}
HoverHandler {
id: tgHover
enabled: tg.enabled
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: tg.enabled
onTapped: tg.clicked()
}
}
// A label and its reading on one line, label left, reading right.
component Fact: Item {
property string label: ""
property string value: ""
width: parent.width
implicitHeight: 14
Text {
anchors.left: parent.left
text: parent.label
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Text {
anchors.right: parent.right
text: parent.value
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
// A round hairline button. Hover lifts it; nothing here inverts, since
// inversion is reserved for the keyboard's cursor.
component Transport: Item {
id: tb
property string glyph: ""
signal clicked()
width: 30
height: 30
opacity: tb.enabled ? 1 : 0.35
Rectangle {
anchors.fill: parent
radius: width / 2
color: tbHover.hovered ? Theme.treatmentHoverFill : "transparent"
border.width: 1
border.color: tbHover.hovered ? Theme.treatmentHoverMark : Theme.overlay
Behavior on color { ColorAnimation { duration: Theme.durFast } }
}
Icon {
anchors.centerIn: parent
text: tb.glyph
color: tbHover.hovered ? Theme.accent : Theme.text
}
HoverHandler {
id: tbHover
enabled: tb.enabled
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: tb.enabled
onTapped: tb.clicked()
}
}
}
+1 -1
View File
@@ -527,7 +527,7 @@ PanelWindow {
}
NumberAnimation {
target: rise; property: "y"
from: 24; to: 0; duration: Theme.durSlow; easing.type: Easing.OutBack
from: 24; to: 0; duration: Theme.durSlow; easing.type: Easing.OutCubic
}
}
+2 -1
View File
@@ -28,6 +28,7 @@ Item {
anchors.fill: parent
anchors.leftMargin: -Theme.padS
anchors.rightMargin: -Theme.padS
radius: Theme.radiusM
color: root.selected ? Theme.treatmentSelectedFill : "transparent"
opacity: root.selected ? 1 : 0
@@ -46,7 +47,7 @@ Item {
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack }
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutCubic }
}
}
+254 -219
View File
@@ -10,6 +10,13 @@ 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.
//
// Quiet on purpose. The wallpaper stays almost whole — the porthole, the
// planet, the girl looking out — and the face is one small group of type set
// in the dark of the porthole: the mark, the time, the date, a short status
// log, and a rounded field to type into. Nothing sweeps, splits or shakes;
// the group resolves in, and a wrong password nudges it once and turns its
// dots to rust.
Item {
id: surface
@@ -20,12 +27,12 @@ Item {
property string status: ""
property bool failed: false
property bool busy: pam.active
readonly property real uiScale: Math.max(0.72, Math.min(1.15,
readonly property real uiScale: Math.max(0.72, Math.min(1.3,
Math.min(width / 1920, height / 1080)))
Rectangle {
anchors.fill: parent
color: Theme.base
color: Theme.ink
}
// ------------------------------------------------------- background
@@ -43,247 +50,269 @@ Item {
fillMode: Image.PreserveAspectCrop
asynchronous: false
cache: true
opacity: 0
// Barely softened: enough that the scene steps back behind the type,
// not so much that it stops being a place.
layer.enabled: true
layer.effect: MultiEffect {
blurEnabled: true
blur: 0.55
blurMax: 48
saturation: -0.15
brightness: -0.35
blur: 0.2
blurMax: 32
saturation: -0.1
brightness: -0.18
}
}
// 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.
// Shade deepening toward the left, where the type sits in the porthole's
// dark, and a breath of it along the foot.
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) }
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: Theme.alpha(Theme.ink, 0.72) }
GradientStop { position: 0.45; color: Theme.alpha(Theme.ink, 0.28) }
GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.18) }
}
}
// 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 accent mass in the same place.
P5Backdrop {
id: backdrop
Rectangle {
anchors.fill: parent
reveal: 0
wedgeSpan: 0.30
wedgeOpacity: 0.30
stripeOpacity: 0.07
gradient: Gradient {
GradientStop { position: 0.6; color: "transparent" }
GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.5) }
}
}
// ------------------------------------------------------------ clock
// ------------------------------------------------------------- face
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
id: face
// Sized by uiScale rather than drawn small and stretched with `scale`:
// a scale transform blurs native text, and every label here is native.
// Whole pixels, too, or the glyphs land between them.
x: Math.round(parent.width * 0.14)
y: Math.round(parent.height * 0.2)
width: Math.round(380 * surface.uiScale)
opacity: 0
spacing: 0
SplitText {
text: Qt.formatDateTime(clock.date, "HH")
pixelSize: Theme.fsMega * 1.7
split: 5
splitOpacity: 0.85
}
transform: Translate { id: drift }
SplitText {
text: Qt.formatDateTime(clock.date, "mm")
pixelSize: Theme.fsMega * 1.7
color: Theme.primary
split: 5
splitOpacity: 0.6
// The house mark, the same porthole the bar carries far left.
Porthole {
size: Math.round(26 * surface.uiScale)
}
Text {
text: Qt.formatDateTime(clock.date, "dddd, dd MMMM").toUpperCase()
topPadding: Math.round(Theme.padM * surface.uiScale)
text: Qt.formatDateTime(clock.date, "HH:mm")
color: Theme.accent
font.family: Theme.fontDisplay
font.pixelSize: Math.round(Theme.fsMega * 1.35 * surface.uiScale)
font.weight: Font.Light
font.letterSpacing: 2 * surface.uiScale
// Display size, where curves beat hinted bitmaps.
renderType: Text.CurveRendering
}
Text {
leftPadding: Math.round(4 * surface.uiScale)
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
font.family: Theme.fontDisplay
font.pixelSize: Math.round(Theme.fsBody * surface.uiScale)
font.weight: Theme.weightDisplay
font.letterSpacing: 5 * surface.uiScale
renderType: Text.NativeRendering
}
// The one line the face says to you. Nothing here is in a hurry.
Item { width: 1; height: Math.round(Theme.padL * surface.uiScale) }
GlowText {
x: Math.round(4 * surface.uiScale)
text: "Take your time."
color: Theme.subtext
glowColor: Theme.accent
pixelSize: Math.round(Theme.fsLarge * surface.uiScale)
}
// ------------------------------------------------------ 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: Math.min(300, parent.width * 0.28)
scale: surface.uiScale
transformOrigin: Item.TopRight
Item { width: 1; height: Math.round(Theme.padXL * surface.uiScale) }
RailRow {
// ------------------------------------------------------ status log
Column {
width: parent.width
leftPadding: Math.round(4 * surface.uiScale)
StatusRow {
width: parent.width - parent.leftPadding
uiScale: surface.uiScale
visible: Battery.available
glyph: Battery.icon
label: Math.round(Battery.percent) + "%"
detail: Battery.timeLabel
label: "Battery"
detail: Math.round(Battery.percent) + "%"
+ (Battery.timeLabel ? " " + 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
StatusRow {
width: parent.width - parent.leftPadding
uiScale: surface.uiScale
label: "Network"
detail: Sys.netUp ? Net.label : "no link"
tone: Sys.netUp ? Theme.accentSoft : Theme.overlay
}
RailRow {
width: parent.width
StatusRow {
width: parent.width - parent.leftPadding
uiScale: surface.uiScale
visible: Media.hasPlayer
glyph: Media.playing ? "󰝚" : "󰏤"
label: Media.title
detail: Media.artist
tone: Theme.primary
label: Media.playing ? "Playing" : "Paused"
detail: Media.artist ? Media.title + " — " + Media.artist : Media.title
tone: Media.playing ? Theme.accent : Theme.primaryDim
}
RailRow {
width: parent.width
StatusRow {
width: parent.width - parent.leftPadding
uiScale: surface.uiScale
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
label: "Notices"
detail: Notifs.count + (Notifs.count === 1 ? " message" : " messages")
tone: Theme.accentSoft
}
}
// ------------------------------------------------------- auth panel
Item { width: 1; height: Math.round(Theme.padXL * surface.uiScale) }
// ------------------------------------------------------ the field
Item {
id: authBlock
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: parent.height * 0.14
width: Math.min(460, parent.width - Theme.padXL * 2)
height: 150
scale: surface.uiScale
transformOrigin: Item.Bottom
width: parent.width
height: field.height + hint.height + hint.anchors.topMargin
transform: Translate { id: shakeShift }
transform: Translate { id: nudge }
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.alarm : Theme.text
pixelSize: Theme.fsTitle
letterSpacing: 4
horizontalAlignment: Text.AlignHCenter
split: surface.failed ? 4 : 1.6
}
// Password field: one chopped block per character.
P5Panel {
Rectangle {
id: field
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: prompt.bottom
anchors.topMargin: Theme.padL
width: Math.min(380, authBlock.width - Theme.padL * 2)
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.alarm : Theme.primary
slashWidth: 6
halftone: true
halftoneColor: surface.failed ? Theme.alarm : Theme.primary
halftoneOpacity: 0.08
width: parent.width
height: Math.round(48 * surface.uiScale)
radius: height / 2
color: Theme.alpha(Theme.hull, 0.86)
border.width: 1
border.color: surface.failed
? Theme.alarm
: (input.activeFocus && surface.entry.length > 0 ? Theme.outline : Theme.rim)
Row {
anchors.centerIn: parent
spacing: 6
visible: surface.entry.length > 0
Repeater {
model: Math.min(surface.entry.length, 22)
Skew {
required property int index
width: 10
height: 20
color: surface.failed ? Theme.alarm : Theme.primary
// 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
}
}
Behavior on border.color {
ColorAnimation { duration: Theme.durBase }
}
Icon {
id: lockGlyph
anchors.left: parent.left
anchors.leftMargin: Math.round(Theme.padL * surface.uiScale)
anchors.verticalCenter: parent.verticalCenter
text: "󰌾"
color: surface.failed ? Theme.alarm : Theme.muted
font.pixelSize: Math.round(Theme.fsLarge * surface.uiScale)
}
Text {
anchors.centerIn: parent
visible: surface.entry.length === 0 && !surface.busy
anchors.left: lockGlyph.right
anchors.leftMargin: Math.round(Theme.padM * surface.uiScale)
anchors.verticalCenter: parent.verticalCenter
visible: surface.entry.length === 0
text: "PASSWORD"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 5
font.family: Theme.fontDisplay
font.pixelSize: Math.round(Theme.fsSmall * surface.uiScale)
font.letterSpacing: Theme.tracking * 2 * surface.uiScale
renderType: Text.NativeRendering
}
// Busy sweep.
Rectangle {
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
width: 60
height: 3
color: Theme.primary
visible: surface.busy
// One dot per character, each resolving in as it is typed.
// They breathe while PAM is checking.
Row {
id: dots
anchors.left: lockGlyph.right
anchors.leftMargin: Math.round(Theme.padM * surface.uiScale)
anchors.right: enter.left
anchors.rightMargin: Math.round(Theme.padM * surface.uiScale)
anchors.verticalCenter: parent.verticalCenter
spacing: Math.round(7 * surface.uiScale)
clip: true
XAnimator on x {
Repeater {
model: Math.min(surface.entry.length, 28)
Rectangle {
id: pip
required property int index
anchors.verticalCenter: parent.verticalCenter
width: Math.round(7 * surface.uiScale)
height: width
radius: width / 2
color: surface.failed ? Theme.alarm : Theme.accent
opacity: 0
scale: 0.4
Component.onCompleted: arrive.start()
ParallelAnimation {
id: arrive
NumberAnimation {
target: pip; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutCubic
}
NumberAnimation {
target: pip; property: "scale"
to: 1; duration: Theme.durBase; easing.type: Easing.OutCubic
}
}
}
}
SequentialAnimation on opacity {
running: surface.busy
loops: Animation.Infinite
from: 20
to: field.width - 80
duration: 700
easing.type: Easing.InOutQuad
alwaysRunToEnd: true
NumberAnimation { to: 0.35; duration: 520; easing.type: Easing.InOutSine }
NumberAnimation { to: 1; duration: 520; easing.type: Easing.InOutSine }
}
}
// Submit: a disc at the field's end that lights once there is
// something to send.
Rectangle {
id: enter
readonly property bool ready: surface.entry.length > 0 && !surface.busy
anchors.right: parent.right
anchors.rightMargin: Math.round(8 * surface.uiScale)
anchors.verticalCenter: parent.verticalCenter
width: Math.round(32 * surface.uiScale)
height: width
radius: width / 2
color: enter.ready ? Theme.accent : "transparent"
border.width: enter.ready ? 0 : 1
border.color: Theme.rim
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
Icon {
anchors.centerIn: parent
text: "󰁔"
color: enter.ready ? Theme.ink : Theme.overlay
font.pixelSize: Math.round(Theme.fsLarge * surface.uiScale)
}
TapHandler {
onTapped: surface.submit()
}
}
@@ -299,7 +328,7 @@ Item {
onTextChanged: {
surface.entry = text;
if (surface.failed && text.length === 0) surface.failed = false;
if (surface.failed && text.length > 0) surface.failed = false;
}
onAccepted: surface.submit()
@@ -311,30 +340,38 @@ Item {
}
}
// Under the field: who is locked, what is happening, or what went
// wrong — one line, never more.
Row {
anchors.horizontalCenter: parent.horizontalCenter
id: hint
anchors.top: field.bottom
anchors.topMargin: Theme.padM
spacing: Theme.padS
anchors.topMargin: Math.round(Theme.padS * surface.uiScale)
anchors.left: parent.left
anchors.leftMargin: Math.round(Theme.padL * surface.uiScale)
spacing: Math.round(Theme.padS * surface.uiScale)
Icon {
Rectangle {
anchors.verticalCenter: parent.verticalCenter
text: "󰌾"
color: Theme.muted
font.pixelSize: Theme.fsBody
width: Math.round(4 * surface.uiScale)
height: width
radius: width / 2
color: surface.failed ? Theme.alarm : Theme.overlay
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: Quickshell.env("USER") || "USER"
color: Theme.muted
text: surface.failed
? (surface.status || "Access denied").toUpperCase()
: (surface.busy ? "VERIFYING" : (Quickshell.env("USER") || "user"))
color: surface.failed ? Theme.alarm : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 3
font.pixelSize: Math.round(Theme.fsMicro * surface.uiScale)
font.letterSpacing: 2 * surface.uiScale
renderType: Text.NativeRendering
}
}
}
}
// Keep focus on the field no matter what.
Timer {
@@ -369,62 +406,60 @@ Item {
surface.entry = "";
surface.failed = false;
} else {
surface.failed = true;
if (surface.status === "") surface.status = "Access denied";
input.text = "";
surface.entry = "";
shake.restart();
surface.failed = true;
if (surface.status === "") surface.status = "Access denied";
refuse.restart();
}
}
onError: err => {
surface.failed = true;
surface.status = "PAM error";
input.text = "";
surface.entry = "";
shake.restart();
surface.failed = true;
surface.status = "PAM error";
refuse.restart();
}
}
// A wrong password: the field leans away once and comes back, a head
// shaken slowly rather than a buzzer.
SequentialAnimation {
id: shake
loops: 2
id: refuse
NumberAnimation {
target: shakeShift; property: "x"
to: -16; duration: 55; easing.type: Easing.OutQuad
target: nudge; property: "x"
to: -7; duration: 90; easing.type: Easing.OutSine
}
NumberAnimation {
target: shakeShift; property: "x"
to: 16; duration: 55; easing.type: Easing.OutQuad
target: nudge; property: "x"
to: 5; duration: 140; easing.type: Easing.InOutSine
}
NumberAnimation {
target: shakeShift; property: "x"
to: 0; duration: 90; easing.type: Easing.OutBack
target: nudge; property: "x"
to: 0; duration: 220; easing.type: Easing.OutCubic
}
}
// Entry flourish when the lock comes up.
// The lock comes up the way the scene would if you opened your eyes: the
// picture first, then the type settling a few pixels into place.
Component.onCompleted: intro.start()
ParallelAnimation {
SequentialAnimation {
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
target: wall; property: "opacity"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic
}
ParallelAnimation {
NumberAnimation {
target: face; property: "opacity"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic
}
NumberAnimation {
target: timeBlock; property: "opacity"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
target: drift; property: "y"
from: 14; to: 0; duration: Theme.durLazy; easing.type: Easing.OutQuint
}
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
@@ -1,64 +0,0 @@
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
}
}
}
}
+69
View File
@@ -0,0 +1,69 @@
import QtQuick
import "root:/config"
import "root:/components"
// One line of the lock screen's status log, laid out like the dashboard's
// facts: a dot in the line's tone, the label tracked in caps, a hairline
// running out to the detail at the far end.
Item {
id: row
property string label: ""
property string detail: ""
property color tone: Theme.primaryDim
// The lock face's size factor. The row is laid out at that size rather
// than scaled, so its native text stays sharp.
property real uiScale: 1
implicitHeight: visible ? Math.round(26 * row.uiScale) : 0
Rectangle {
id: dot
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Math.round(Theme.marker * row.uiScale)
height: width
radius: width / 2
color: row.tone
}
Text {
id: name
anchors.left: dot.right
anchors.leftMargin: Math.round(Theme.padS * row.uiScale)
anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, row.width * 0.55)
text: row.label.toUpperCase()
color: Theme.text
elide: Text.ElideRight
font.family: Theme.fontDisplay
font.pixelSize: Math.round(Theme.fsSmall * row.uiScale)
font.weight: Theme.weightDisplay
font.letterSpacing: Theme.tracking * row.uiScale
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: name.right
anchors.leftMargin: Math.round(Theme.padS * row.uiScale)
anchors.right: value.left
anchors.rightMargin: Math.round(Theme.padS * row.uiScale)
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.4)
}
Text {
id: value
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, row.width * 0.4)
text: row.detail
color: Theme.muted
elide: Text.ElideRight
horizontalAlignment: Text.AlignRight
font.family: Theme.fontMono
font.pixelSize: Math.round(Theme.fsMicro * row.uiScale)
renderType: Text.NativeRendering
}
}
+39
View File
@@ -0,0 +1,39 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/services"
// The first stage of idle (services/Idle.qml): the screen dims the way a room
// does when the lights go down, slowly enough that you notice it before the
// lock does. Any input lifts it at once. Input passes straight through.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-dim"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
mask: Region {}
visible: Idle.dimmed || shade.opacity > 0
Rectangle {
id: shade
anchors.fill: parent
color: Theme.ink
opacity: Idle.dimmed ? 0.6 : 0
Behavior on opacity {
NumberAnimation {
duration: Idle.dimmed ? 8000 : Theme.durBase
easing.type: Idle.dimmed ? Easing.InOutSine : Easing.OutCubic
}
}
}
}
+55 -15
View File
@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import "root:/config"
import "root:/components"
@@ -44,6 +45,7 @@ PanelWindow {
switch (root.kind) {
case "mic": return Audio.micIcon;
case "brightness": return Brightness.icon;
case "layout": return "󰌌";
default: return Audio.icon;
}
}
@@ -52,6 +54,7 @@ PanelWindow {
switch (root.kind) {
case "mic": return Audio.micMuted ? "Mic muted" : "Microphone";
case "brightness": return "Brightness";
case "layout": return "Keyboard";
default: return Audio.muted ? "Muted" : "Volume";
}
}
@@ -71,6 +74,7 @@ PanelWindow {
// silenced speaker flashed the same colour as a live one.
case "mic": return Audio.micMuted ? Theme.treatmentOffText : Theme.accent;
case "brightness": return Theme.accent;
case "layout": return Theme.accent;
default: return Audio.muted ? Theme.treatmentOffText : Theme.accent;
}
}
@@ -100,7 +104,27 @@ PanelWindow {
function onBumped() { root.flash("brightness"); }
}
P5Panel {
// The keymap Hyprland just switched to, e.g. "English (Dvorak, intl.,
// with dead keys)", and a short name for the readout: the variant in the
// brackets if there is one, otherwise the language.
property string keymap: ""
readonly property string keymapShort: {
const m = /\(([^,)]+)/.exec(root.keymap);
return (m ? m[1] : root.keymap).trim();
}
Connections {
target: Hyprland
function onRawEvent(event) {
if (event.name !== "activelayout") return;
// "keyboard-name,Layout name" — the layout name may hold commas.
const at = event.data.indexOf(",");
root.keymap = at < 0 ? event.data : event.data.slice(at + 1);
root.flash("layout");
}
}
Panel {
id: panel
anchors.fill: parent
anchors.margins: 4
@@ -111,20 +135,22 @@ PanelWindow {
slash: true
slashColor: root.tone
slashWidth: 5
lean: 0.09
chop: 14
padding: Theme.padM
// It rises a few pixels into place instead of growing: a scale would
// stretch the native text in the panel while it moves.
opacity: root.shown ? 1 : 0
scale: root.shown ? 1 : 0.9
transform: Translate {
y: root.shown ? 0 : 8
Behavior on y {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutCubic }
}
}
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
@@ -173,20 +199,34 @@ PanelWindow {
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
// width and right-aligning the text would flatten the
// glitch flicker away. The title's left edge is anchored, so a
// widening readout only moves its own left edge.
text: Math.round(root.level * 100) + "%"
text: root.kind === "layout" ? root.keymapShort : Math.round(root.level * 100) + "%"
pixelSize: Theme.fsBody
}
}
MeterBar {
// The layout has no level; its full name stands where the meter
// would be.
Text {
visible: root.kind === "layout"
width: parent.width
height: 10
text: root.keymap
color: Theme.muted
elide: Text.ElideRight
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
MeterBar {
visible: root.kind !== "layout"
width: parent.width
height: 4
segments: 20
segmentWidth: (width - 19 * 3) / 20
spacing: 3
segmentWidth: (width - 19 * 2) / 20
spacing: 2
value: root.level * 100
activeColor: root.tone
}
+135 -136
View File
@@ -1,12 +1,16 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import "root:/config"
import "root:/components"
import "root:/services"
// Full-screen power menu. An accent wedge and stripes sweep in behind a stack of leaning
// buttons; Escape or a click on the backdrop dismisses it.
// Full-screen power menu, built like the lock screen: the desktop dims to the
// void, and one small group of type stands in it — the mark, the motto, a
// line about the session, and a NieR menu of six rows. The selected row is a
// pill of light with the text punched out of it; the three that end the
// session light in rust instead. Escape or a click on the dark dismisses it.
PanelWindow {
id: root
@@ -32,16 +36,9 @@ PanelWindow {
property int selectedAction: 0
function focusSelectedAction(): void {
const entry = buttonRepeater.itemAt(root.selectedAction);
if (entry && entry.children.length > 0)
entry.children[0].forceActiveFocus(Qt.TabFocusReason);
}
function moveSelection(delta: int): void {
root.selectedAction = (root.selectedAction + delta + root.actionModel.length)
% root.actionModel.length;
root.focusSelectedAction();
}
readonly property var actionModel: [
@@ -53,6 +50,11 @@ PanelWindow {
{ label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" }
]
FileView {
id: hostFile
path: "/etc/hostname"
}
onOpenChanged: {
if (open) {
PopupCoordinator.dismissAll();
@@ -64,134 +66,153 @@ PanelWindow {
}
// ------------------------------------------------------------- backdrop
// The desktop stays faintly there behind the dark, deepest on the left
// where the menu stands.
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.9)
opacity: 0
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: Theme.alpha(Theme.ink, 0.97) }
GradientStop { position: 0.5; color: Theme.alpha(Theme.ink, 0.93) }
GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.86) }
}
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
// ---------------------------------------------------------------- group
Column {
id: title
anchors.left: parent.left
anchors.leftMargin: root.width * 0.12
id: group
x: root.width * 0.14
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -60
spacing: -14
width: 340
opacity: 0
SplitText {
text: "TAKE"
pixelSize: Theme.fsMega
split: 4
splitOpacity: 0.9
transform: Translate { id: drift }
Porthole {
size: 26
}
SplitText {
text: "YOUR"
pixelSize: Theme.fsMega
color: Theme.accent
split: 4
splitOpacity: 0.5
}
Item { width: 1; height: Theme.padM }
SplitText {
text: "TIME."
pixelSize: Theme.fsMega
split: 4
splitOpacity: 0.9
GlowText {
text: "Take your time."
pixelSize: Theme.fsHuge
letterSpacing: 1
active: root.open
}
Text {
text: "— TAE TAKEMI"
color: Theme.primary
topPadding: Theme.padS
leftPadding: 2
text: [(Quickshell.env("USER") || "user") + "@" + hostFile.text().trim(),
Sys.uptime ? "UP " + Sys.uptime : ""].filter(t => t !== "").join(" · ")
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 5
topPadding: 24
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
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
Item { width: 1; height: Theme.padXL }
// ------------------------------------------------------------ menu
Repeater {
id: buttonRepeater
model: root.actionModel
// 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
readonly property bool selected: root.selectedAction === entry.index
readonly property color tone: entry.modelData.danger ? Theme.alarm : Theme.accent
width: group.width
height: 44
opacity: 0
transform: [
Translate { id: slide; x: 90 },
Rotation {
id: tilt
angle: -3
origin.x: entry.slabWidth / 2
origin.y: 26
}
]
P5Button {
id: actionButton
transform: Translate { id: rise; y: 6 }
// The divider after the third row: what pauses the session
// above, what ends it below.
Rectangle {
visible: entry.index === 3
anchors.top: parent.top
x: Theme.padL
width: parent.width - Theme.padL * 2
height: 1
color: Theme.alpha(Theme.outline, 0.35)
}
Rectangle {
id: slab
anchors.fill: parent
text: entry.modelData.label
icon: entry.modelData.icon
destructive: entry.modelData.danger
selected: root.selectedAction === entry.index
accent: entry.modelData.danger ? Theme.alarm : Theme.primary
lean: Theme.skew
lunge: -10
onClicked: root.invoke(entry.modelData.act)
onHoveredChanged: if (hovered) root.selectedAction = entry.index
onActiveFocusChanged: if (actionButton.activeFocus) root.selectedAction = entry.index
Keys.forwardTo: [keyController]
anchors.topMargin: 3
anchors.bottomMargin: 3
radius: height / 2
color: entry.selected ? entry.tone : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic }
}
}
// Staggered entry, one button after another.
Rectangle {
id: marker
anchors.left: parent.left
anchors.leftMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
width: Theme.marker
height: Theme.marker
radius: width / 2
color: entry.selected ? Theme.ink
: (entry.modelData.danger ? Theme.alpha(Theme.alarm, 0.6) : Theme.overlay)
}
Icon {
id: glyph
anchors.left: marker.right
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
// Fixed width so the labels line up whatever the glyph.
width: 20
horizontalAlignment: Text.AlignHCenter
text: entry.modelData.icon
color: entry.selected ? Theme.ink : Theme.muted
font.pixelSize: Theme.fsLarge
}
Text {
anchors.left: glyph.right
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
text: entry.modelData.label.toUpperCase()
color: entry.selected ? Theme.ink : Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.letterSpacing: Theme.tracking * 1.5
renderType: Text.NativeRendering
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
onHoveredChanged: if (hovered) root.selectedAction = entry.index
}
TapHandler {
onTapped: root.invoke(entry.modelData.act)
}
// Staggered entry: each row resolves a beat after the last.
Connections {
target: root
function onOpenChanged() {
@@ -202,49 +223,32 @@ PanelWindow {
SequentialAnimation {
id: entryAnim
PauseAnimation { duration: 40 + entry.index * 55 }
PauseAnimation { duration: 120 + entry.index * 40 }
ParallelAnimation {
NumberAnimation {
target: entry; property: "opacity"
to: 1; duration: Theme.durBase
to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
}
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
}
target: rise; property: "y"
from: 6; to: 0; duration: Theme.durSlow; easing.type: Easing.OutCubic
}
}
}
}
}
// Hint line.
Text {
anchors.bottom: parent.bottom
anchors.bottomMargin: 40
anchors.horizontalCenter: parent.horizontalCenter
topPadding: Theme.padXL
leftPadding: Theme.padL
text: "↑ ↓ SELECT ENTER CONFIRM ESC CANCEL"
color: Theme.muted
color: Theme.overlay
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 4
opacity: backdrop.opacity
font.letterSpacing: 2
renderType: Text.NativeRendering
}
}
function invoke(what: string) {
Actions.closePowerMenu();
@@ -286,16 +290,15 @@ PanelWindow {
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
to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
}
NumberAnimation {
target: title; property: "opacity"
to: 1; duration: Theme.durSlow
target: group; property: "opacity"
to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic
}
NumberAnimation {
target: drift; property: "y"
from: 14; to: 0; duration: Theme.durLazy; easing.type: Easing.OutQuint
}
}
@@ -303,14 +306,10 @@ PanelWindow {
id: closeAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 0; duration: Theme.durFast
to: 0; duration: Theme.durBase; easing.type: Easing.InCubic
}
NumberAnimation {
target: ground; property: "reveal"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: title; property: "opacity"
target: group; property: "opacity"
to: 0; duration: Theme.durFast
}
}
+100 -44
View File
@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
@@ -6,7 +7,14 @@ import "root:/config"
import "root:/components"
import "root:/services"
// Floating notification stack under the bar, top right. Replaces dunst.
// Notification drawer, docked in the top-right corner of the opening.
// Replaces dunst.
//
// Not a stack of floating cards: one hull surface lowered out of the bar and
// out of the rail at once, the same construction as a corner-docked popout,
// with each notification an entry in it divided by its own header hairline.
// The drawer grows and shrinks with the entries and slides back behind the
// frame when the last one goes.
PanelWindow {
id: root
@@ -15,23 +23,47 @@ PanelWindow {
// A toast window exists on every screen, but each notification lands only
// on the monitor that had focus when it arrived. Latched per toast rather
// than live, so a card does not hop screens while it is being read.
// than live, so an entry does not hop screens while it is being read.
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-toasts"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
// One pixel over the bar and the rail, so the drawer covers the rim the
// hull draws along both edges instead of sitting inside it.
anchors { top: true; right: true }
margins {
top: Theme.barHeight + Theme.barMargin * 2
right: Theme.barMargin
top: Theme.barHeight - 1
right: Theme.railWidth - 1
}
implicitWidth: 400
implicitHeight: Math.max(1, stack.implicitHeight)
readonly property real fillet: Theme.frameRadius
readonly property real corner: Theme.frameRadius
readonly property real seam: 1
readonly property real drawerWidth: 400
// Height of the entries, eased, so the drawer grows rather than jumps.
property real bodyHeight: stack.implicitHeight
Behavior on bodyHeight {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic }
}
// 0 = behind the frame, 1 = out. Follows whether anything is live.
property real reveal: root.live.length > 0 ? 1 : 0
Behavior on reveal {
NumberAnimation {
duration: Theme.durSlow
easing.type: root.live.length > 0 ? Easing.OutCubic : Easing.InCubic
}
}
implicitWidth: root.drawerWidth + root.fillet + root.seam
implicitHeight: Math.max(1, root.bodyHeight + root.fillet + root.seam)
color: "transparent"
exclusionMode: ExclusionMode.Ignore
visible: root.live.length > 0
visible: root.live.length > 0 || root.reveal > 0
mask: Region { item: stack }
// Toasts currently on screen, newest first.
property list<Notification> live: []
@@ -53,12 +85,52 @@ PanelWindow {
root.live = next;
}
Item {
anchors.fill: parent
clip: true
Item {
id: sheet
width: parent.width
height: parent.height
y: -height * (1 - root.reveal)
// Off the bar through the left fillet, round the one free corner,
// onto the rail through the bottom fillet — Popout's "topRight".
Shape {
id: silhouette
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
readonly property real w: width
readonly property real h: height
readonly property real f: root.fillet
readonly property real r: Math.min(root.corner, Math.max(0, (h - f) / 2))
readonly property string edge:
`M 0 0 A ${f} ${f} 0 0 1 ${f} ${f}`
+ ` V ${h - f - r} A ${r} ${r} 0 0 0 ${f + r} ${h - f}`
+ ` H ${w - f} A ${f} ${f} 0 0 1 ${w} ${h}`
ShapePath {
fillColor: Theme.hull
strokeColor: "transparent"
PathSvg { path: silhouette.edge + ` L ${silhouette.w} 0 Z` }
}
ShapePath {
fillColor: "transparent"
strokeColor: Theme.rim
strokeWidth: 1
PathSvg { path: silhouette.edge }
}
}
Column {
id: stack
anchors.right: parent.right
anchors.top: parent.top
width: parent.width
spacing: Theme.padS
x: root.fillet
y: root.seam
width: root.drawerWidth
bottomPadding: Theme.padS
Repeater {
model: root.live
@@ -80,29 +152,21 @@ PanelWindow {
}
}
// Countdown rule that drains along the bottom edge.
//
// It used to be a full-bleed rectangle across the whole toast,
// which meant it ran straight through the card's chopped
// bottom-left corner and out the other side — a stray bar
// hanging off the card at the one place the silhouette is
// supposed to be cut. It lives inside the cut now, and ends
// where the leaning right edge does.
readonly property real fuseSpan: Math.max(0,
width - card.chop - card.implicitHeight * card.lean)
Skew {
// Countdown: a hairline draining along the entry's
// foot, inset to the text column.
Rectangle {
id: fuse
anchors.bottom: parent.bottom
anchors.bottomMargin: 1
x: card.chop
height: 2
width: toast.fuseSpan
lean: card.lean
color: Theme.alpha(card.tone, 0.7)
x: Theme.padM + Theme.marker + Theme.padS
height: 1
width: 0
color: Theme.alpha(card.tone, 0.5)
visible: life.running
}
readonly property real fuseSpan:
width - Theme.padM * 2 - Theme.marker - Theme.padS
NumberAnimation {
id: life
target: fuse
@@ -115,9 +179,8 @@ PanelWindow {
onFinished: root.retire(toast.modelData)
}
// Slide in from the right, Persona-style.
// A new entry resolves in place: a fade, no shove.
opacity: 0
transform: Translate { id: shove; x: 60 }
Component.onCompleted: {
enter.start();
@@ -125,23 +188,14 @@ PanelWindow {
if (ms > 0) life.start();
}
ParallelAnimation {
NumberAnimation {
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
}
to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
}
// Hovering a toast pauses its fuse.
// Hovering an entry pauses its fuse.
HoverHandler {
id: toastHover
onHoveredChanged: {
if (!life.duration) return;
if (hovered) life.pause();
@@ -156,4 +210,6 @@ PanelWindow {
}
}
}
}
}
}
+366
View File
@@ -0,0 +1,366 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
// Every monitor, every workspace, every window, at once. Alt+Tab opens it on
// the focused screen with the previous window picked; more Tabs walk back
// through focus history, and letting go of Alt goes there. Hover and click
// work too, as do Enter, Escape and the workspace digits.
//
// It is a scene, like the power menu: no panel, the desktop dims, and each
// monitor is laid out as a row of small portholes — its workspaces, at its
// own aspect ratio, with live captures of the windows where they sit. The
// picked window is ringed in the light and named underneath.
PanelWindow {
id: root
required property var modelData
screen: modelData
readonly property bool open: Actions.overviewOpen
&& Actions.overviewScreen === modelData.name
visible: root.open || fade.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-overview"
WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
// ----------------------------------------------------------------- model
// Rebuilt from Hyprland's IPC snapshots on open and whenever windows move
// while open. Plain arrays rather than live models: everything here is a
// picture of one moment.
property var monitors: [] // [{ mon, ipc, workspaces: [{ ws, windows: [...] }] }]
property var mru: [] // window addresses, most recent first
property string picked: "" // address of the picked window
readonly property var pickedWindow: {
for (const m of root.monitors)
for (const w of m.workspaces)
for (const win of w.windows)
if (win.address === root.picked) return win;
return null;
}
function rebuild() {
const mons = Hyprland.monitors.values.slice()
.filter(m => m.lastIpcObject && m.lastIpcObject.width)
.sort((a, b) => a.lastIpcObject.x - b.lastIpcObject.x);
const all = [];
const out = [];
for (const mon of mons) {
const mi = mon.lastIpcObject;
const wss = Hyprland.workspaces.values
.filter(ws => ws.monitor === mon)
.filter(ws => ws.id > 0 || ws.toplevels.values.length > 0)
.sort((a, b) => (a.id < 0) - (b.id < 0) || a.id - b.id);
const rows = [];
for (const ws of wss) {
const wins = [];
for (const tl of ws.toplevels.values) {
const ipc = tl.lastIpcObject;
if (!ipc || !ipc.at || !ipc.mapped) continue;
const win = {
toplevel: tl,
address: "0x" + String(tl.address).replace(/^0x/, ""),
x: ipc.at[0] - mi.x, y: ipc.at[1] - mi.y,
w: ipc.size[0], h: ipc.size[1],
title: ipc.title || "",
appClass: ipc.class || "",
hidden: ipc.hidden === true,
floating: ipc.floating === true,
group: (ipc.grouped || []).map(a => "0x" + String(a).replace(/^0x/, "")),
history: ipc.focusHistoryID ?? 999,
};
wins.push(win);
all.push(win);
}
// Tiled first, floating on top, as Hyprland draws them.
wins.sort((a, b) => a.floating - b.floating);
rows.push({ ws: ws, windows: wins });
}
out.push({ mon: mon, ipc: mi, workspaces: rows });
}
root.monitors = out;
root.mru = all.sort((a, b) => a.history - b.history).map(w => w.address);
if (!root.touched || root.mru.indexOf(root.picked) < 0)
root.picked = root.mru.length > 1 ? root.mru[1] : (root.mru[0] ?? "");
}
function step(delta: int) {
root.touched = true;
const n = root.mru.length;
if (n === 0) return;
const at = Math.max(0, root.mru.indexOf(root.picked));
root.picked = root.mru[(at + delta + n) % n];
}
// Hyprland will not move focus while a layer holds the keyboard
// exclusively, so let go of it first and dispatch a moment later.
property string pendingDispatch: ""
function commit() {
root.pendingDispatch = root.picked !== ""
? "hl.dsp.focus({ window = \"address:" + root.picked + "\" })"
: "";
Actions.closeOverview();
if (root.pendingDispatch !== "") release.restart();
}
function goToWorkspace(id: int) {
root.pendingDispatch = "hl.dsp.focus({ workspace = " + id + " })";
Actions.closeOverview();
release.restart();
}
Timer {
id: release
interval: 40
onTriggered: Hyprland.dispatch(root.pendingDispatch)
}
// Whether the pointer has moved since the overview opened. A pointer that
// happens to rest over a card would otherwise pick that window the moment
// the cards appear under it, overriding Alt+Tab's choice.
property bool pointerLive: false
property point pointerStart: Qt.point(-1, -1)
// Whether the pick came from a person rather than from the first snapshot.
property bool touched: false
onOpenChanged: {
if (root.open) {
PopupCoordinator.dismissAll();
Hyprland.refreshMonitors();
Hyprland.refreshWorkspaces();
Hyprland.refreshToplevels();
root.picked = "";
root.touched = false;
root.pointerLive = false;
root.pointerStart = Qt.point(-1, -1);
root.rebuild();
// The IPC snapshots land a moment after the refresh request.
settle.restart();
keys.forceActiveFocus();
}
}
Timer {
id: settle
interval: 60
onTriggered: root.rebuild()
}
Connections {
target: Actions
function onOverviewStepped(delta) { if (root.open) root.step(delta); }
function onOverviewCommitted() { if (root.open) root.commit(); }
}
Connections {
target: Hyprland
enabled: root.open
function onRawEvent(event) {
const n = event.name;
if (n === "openwindow" || n === "closewindow" || n === "movewindowv2"
|| n === "changefloatingmode" || n === "fullscreen") {
Hyprland.refreshToplevels();
settle.restart();
}
}
}
// ------------------------------------------------------------- backdrop
Rectangle {
anchors.fill: parent
color: Theme.ink
opacity: root.open ? 0.92 : 0
Behavior on opacity {
NumberAnimation { id: fade; duration: Theme.durBase; easing.type: Easing.OutCubic }
}
TapHandler { onTapped: Actions.closeOverview() }
}
// ---------------------------------------------------------------- scene
Item {
id: keys
anchors.fill: parent
focus: true
HoverHandler {
onPointChanged: {
const p = point.position;
if (root.pointerStart.x < 0) { root.pointerStart = p; return; }
if (Math.abs(p.x - root.pointerStart.x) + Math.abs(p.y - root.pointerStart.y) > 6)
root.pointerLive = true;
}
}
Keys.onPressed: event => {
const k = event.key;
if (k === Qt.Key_Escape) Actions.closeOverview();
else if (k === Qt.Key_Return || k === Qt.Key_Enter || k === Qt.Key_Space) root.commit();
else if (k === Qt.Key_Tab || k === Qt.Key_Right || k === Qt.Key_Down
|| k === Qt.Key_L || k === Qt.Key_J) root.step(1);
else if (k === Qt.Key_Backtab || k === Qt.Key_Left || k === Qt.Key_Up
|| k === Qt.Key_H || k === Qt.Key_K) root.step(-1);
else if (k >= Qt.Key_1 && k <= Qt.Key_9) root.goToWorkspace(k - Qt.Key_0);
else return;
event.accepted = true;
}
// Alt+Tab is a hold: letting go of Alt is the choice.
Keys.onReleased: event => {
if (event.key === Qt.Key_Alt && Actions.overviewHold) {
event.accepted = true;
root.commit();
}
}
}
Column {
id: scene
anchors.centerIn: parent
spacing: Theme.padXL
opacity: root.open ? 1 : 0
transform: Translate {
y: root.open ? 0 : 10
Behavior on y {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutCubic }
}
}
Behavior on opacity {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic }
}
// The widest a row of cards may be, and how tall all rows together.
readonly property real maxRowWidth: root.width * 0.86
readonly property real maxRowsHeight: root.height * 0.62
// One scale for every card on every monitor, so a window is the same
// size wherever it sits: the largest that fits both the widest row and
// the summed heights.
readonly property real k: {
let k = 0.25;
let totalH = 0;
for (const m of root.monitors) {
const w = m.ipc.width / m.ipc.scale;
const h = m.ipc.height / m.ipc.scale;
const n = Math.max(1, m.workspaces.length);
k = Math.min(k, (scene.maxRowWidth - (n - 1) * Theme.padL) / (n * w));
totalH += h;
}
if (totalH > 0) k = Math.min(k, scene.maxRowsHeight / totalH);
return Math.max(0.05, k);
}
Repeater {
model: root.monitors
Column {
id: monRow
required property var modelData
readonly property real logicalW: modelData.ipc.width / modelData.ipc.scale
readonly property real logicalH: modelData.ipc.height / modelData.ipc.scale
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.padS
// The monitor's name only matters once there is more than one.
Text {
visible: root.monitors.length > 1
text: (monRow.modelData.mon.name + " · "
+ Math.round(monRow.logicalW) + "×" + Math.round(monRow.logicalH)).toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Row {
spacing: Theme.padL
Repeater {
model: monRow.modelData.workspaces
WorkspaceCard {
required property var modelData
ws: modelData.ws
windows: modelData.windows
k: scene.k
logicalW: monRow.logicalW
logicalH: monRow.logicalH
current: monRow.modelData.mon.activeWorkspace === modelData.ws
|| (modelData.ws.id < 0 && monRow.modelData.ipc.specialWorkspace?.id === modelData.ws.id)
picked: root.picked
onPick: address => {
if (!root.pointerLive) return;
root.touched = true;
root.picked = address;
}
onActivate: address => { root.picked = address; root.commit(); }
onActivateWorkspace: id => root.goToWorkspace(id)
}
}
}
}
}
// ------------------------------------------------------------ caption
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 4
width: scene.maxRowWidth * 0.7
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: root.pickedWindow ? root.pickedWindow.title : "No windows"
color: Theme.accent
elide: Text.ElideRight
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsTitle
font.weight: Font.Light
renderType: Text.NativeRendering
}
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
visible: root.pickedWindow !== null
text: root.pickedWindow
? (root.pickedWindow.appClass + " · workspace "
+ (root.pickedWindow.toplevel.workspace?.name ?? "?")).toUpperCase()
: ""
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "TAB NEXT ENTER GO 1–9 WORKSPACE ESC CANCEL"
color: Theme.overlay
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
}
}
+191
View File
@@ -0,0 +1,191 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import "root:/config"
import "root:/services"
// One workspace in the overview: a small porthole at its monitor's aspect
// ratio, the wallpaper faint behind it, and its windows as live captures where
// they sit. Under it, the workspace's number in the bar's pill when it is the
// one on screen.
Item {
id: card
required property HyprlandWorkspace ws
required property var windows
required property real k // logical px -> card px
required property real logicalW
required property real logicalH
property bool current: false
property string picked: ""
signal pick(string address)
signal activate(string address)
signal activateWorkspace(int id)
readonly property bool special: card.ws.id < 0
// The picked window, if it is one of the hidden members of a group here:
// the group's slot shows it instead of the member that is in front.
readonly property var pickedGroup: {
for (const w of card.windows)
if (w.address === card.picked && w.group.length > 1) return w.group;
return [];
}
implicitWidth: Math.round(card.logicalW * card.k)
implicitHeight: porthole.height + label.anchors.topMargin + label.height
Rectangle {
id: porthole
width: card.implicitWidth
height: Math.round(card.logicalH * card.k)
radius: Theme.radiusM
color: Theme.mantle
border.width: 1
border.color: card.current ? Theme.outline : Theme.rim
clip: true
Image {
anchors.fill: parent
anchors.margins: 1
source: Wallpaper.source
sourceSize: Qt.size(width, height)
fillMode: Image.PreserveAspectCrop
asynchronous: true
opacity: 0.35
}
// Click on bare wallpaper: go to the workspace itself.
TapHandler {
onTapped: if (!card.special) card.activateWorkspace(card.ws.id)
}
Repeater {
model: card.windows
Rectangle {
id: tile
required property var modelData
readonly property bool isPicked: tile.modelData.address === card.picked
// A group draws one member: the picked one if it is in this
// group, otherwise whichever Hyprland has in front.
visible: card.pickedGroup.length > 0 && card.pickedGroup.indexOf(tile.modelData.address) >= 0
? tile.isPicked
: !tile.modelData.hidden
x: Math.round(tile.modelData.x * card.k)
y: Math.round(tile.modelData.y * card.k)
width: Math.max(8, Math.round(tile.modelData.w * card.k))
height: Math.max(8, Math.round(tile.modelData.h * card.k))
z: tile.isPicked ? 2 : (tile.modelData.floating ? 1 : 0)
radius: Theme.radiusS
color: Theme.surface
clip: true
// The app's icon, under the capture, for the moment before the
// first frame lands or for a window that cannot be captured.
Image {
anchors.centerIn: parent
width: Math.min(32, parent.width * 0.4)
height: width
sourceSize: Qt.size(width, height)
source: {
const e = DesktopEntries.heuristicLookup(tile.modelData.appClass);
return e && e.icon ? Quickshell.iconPath(e.icon, true) : "";
}
visible: !capture.hasContent
}
ScreencopyView {
id: capture
anchors.fill: parent
captureSource: tile.modelData.toplevel.wayland
live: true
constraintSize: Qt.size(tile.width, tile.height)
}
Rectangle {
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.width: tile.isPicked ? 2 : 1
border.color: tile.isPicked ? Theme.accent : Theme.alpha(Theme.ink, 0.6)
Behavior on border.color {
ColorAnimation { duration: Theme.durFast }
}
}
// Everything else sinks back a little.
Rectangle {
anchors.fill: parent
radius: parent.radius
color: Theme.ink
opacity: tile.isPicked ? 0 : 0.28
Behavior on opacity {
NumberAnimation { duration: Theme.durFast }
}
}
// How many windows share this slot.
Rectangle {
visible: tile.modelData.group.length > 1
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 4
width: groupText.implicitWidth + 8
height: groupText.implicitHeight + 2
radius: height / 2
color: Theme.alpha(Theme.ink, 0.75)
Text {
id: groupText
anchors.centerIn: parent
text: (tile.modelData.group.indexOf(tile.modelData.address) + 1)
+ "/" + tile.modelData.group.length
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
HoverHandler {
onHoveredChanged: if (hovered) card.pick(tile.modelData.address)
// Hovered from the start, then the pointer moves: still a pick.
onPointChanged: if (hovered) card.pick(tile.modelData.address)
}
TapHandler {
onTapped: card.activate(tile.modelData.address)
}
}
}
}
// The workspace's name: the focused one wears it in the bar's light pill,
// the rest in grey.
Rectangle {
id: label
anchors.top: porthole.bottom
anchors.topMargin: Theme.padS
anchors.horizontalCenter: porthole.horizontalCenter
width: Math.max(height * 1.6, labelText.implicitWidth + 14)
height: labelText.implicitHeight + 4
radius: height / 2
color: card.current ? Theme.accent : "transparent"
Text {
id: labelText
anchors.centerIn: parent
text: card.special ? card.ws.name.replace(/^special:/, "") : String(card.ws.id)
color: card.current ? Theme.ink : Theme.muted
font.family: card.special ? Theme.fontMono : Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay
renderType: Text.NativeRendering
}
}
}
+2 -3
View File
@@ -36,7 +36,6 @@ Item {
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
}
@@ -51,7 +50,7 @@ Item {
}
}
P5Button {
ActionButton {
id: muteButton
anchors.right: parent.right
anchors.top: parent.top
@@ -60,7 +59,7 @@ Item {
onClicked: Audio.toggleMicMute()
}
P5Slider {
LevelSlider {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
+1 -3
View File
@@ -16,7 +16,6 @@ Item {
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsHuge
font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering
}
@@ -45,7 +44,6 @@ Item {
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
}
@@ -59,7 +57,7 @@ Item {
}
}
P5Slider {
LevelSlider {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
+3 -4
View File
@@ -50,7 +50,7 @@ Popout {
width: parent.width
spacing: Theme.padS
P5Button {
ActionButton {
id: outputMuteButton
width: parent.width
text: Audio.muted ? "Unmute output" : "Mute output"
@@ -58,7 +58,7 @@ Popout {
onClicked: Audio.toggleMute()
}
P5Button {
ActionButton {
width: parent.width
text: "Open audio mixer"
icon: "󰕾"
@@ -163,7 +163,6 @@ Popout {
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
}
@@ -179,7 +178,7 @@ Popout {
}
}
P5Slider {
LevelSlider {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
+4 -4
View File
@@ -99,10 +99,10 @@ Popout {
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
height: 12
height: 4
segments: 20
segmentWidth: (width - 19 * 3) / 20
spacing: 3
segmentWidth: (width - 19 * 2) / 20
spacing: 2
value: Battery.percent
activeColor: Battery.tint
}
@@ -123,7 +123,7 @@ Popout {
// 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 {
ActionButton {
id: suspendButton
anchors.top: detailText.bottom
anchors.topMargin: Theme.padL
+2 -2
View File
@@ -40,14 +40,14 @@ Popout {
anchors.left: parent.left
spacing: Theme.padS
P5Button {
ActionButton {
text: Bt.enabled ? "On" : "Off"
icon: Bt.enabled ? "󰂯" : "󰂲"
accent: Bt.enabled ? Theme.accent : Theme.muted
onClicked: Bt.toggle()
}
P5Button {
ActionButton {
text: Bt.discovering ? "Scanning" : "Scan"
icon: "󰐷"
accent: Bt.discovering ? Theme.accent : Theme.primary
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff. Show more