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
nvrl 1f7f5aef8a fixed some quickshell stuff + hypr 2026-09-28 23:27:00 +02:00
nvrl 32b43bdbb3 quickshell fixes 2026-09-24 13:35:20 +02:00
nvrlandClaude Opus 5 d2975650fa theme: move the alarm colour from pink to mauve
Mocha's pink is a pastel. Filled at slab size against the near-black
Persona surfaces it went chalky rather than sharp, which is what made
hovered destructive buttons read as a washed-out red. Mauve is saturated
enough to bite against that ground and still cannot be confused with the
blue accent.

Applies across quickshell, Hyprland, dunst, fish and nvim. The GTK
overrides are dropped entirely — stock Catppuccin Mocha Blue already
paints those surfaces, and a hand-written destructive colour there was
one more thing to keep in step for no gain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 02:12:53 +02:00
nvrlandClaude Opus 5 e221445a51 theme: Catppuccin Mocha blue/pink variant of the Persona rice
Keeps the Takemi geometry — the lean, the chamfer, the slabs — and swaps
the palette wholesale. Mocha carries the ground; blue is the accent for
anything live (focus, selection, borders, hover) and pink is the alarm
for anything destructive or failed, so one hue no longer has to mean
both 'active' and 'wrong'.

Destructive controls rest as a pink keyline and fill solid only when
hovered or selected: Mocha's pink is a pastel, and a resting slab of it
reads as washed-out red rather than as a warning. The load ramp climbs
text -> blue -> pink for the same reason, instead of fading straight
through a muddy mid-tone.

Surfaces covered: quickshell, Hyprland borders, alacritty, fish, dunst,
and nvim (via the catppuccin plugin, submodule branch catppuccin-mocha).
GTK 3/4, Qt and xsettingsd move to catppuccin-mocha-blue and are now
tracked, since nothing else makes non-shell apps match the rice; the
previous Takemi copies are in theme-repair-backups/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 02:11:02 +02:00
nvrlandClaude Opus 5 3fb89c4370 fix(qs): show power menu only on the focused screen
A PowerMenu window exists per screen and every one of them lit up on
open. Latch the focused monitor in Actions at open time — same shape as
launcherScreen — and gate each window's open state on it, so exactly one
copy draws and takes keyboard focus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 01:53:56 +02:00
nvrlandClaude Opus 5 1cf5a4b68d chore: add enqs-headroom user service, set nvim as text/plain handler
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 01:52:50 +02:00
156 changed files with 6330 additions and 4503 deletions

No files matched your search

+19
View File
@@ -28,3 +28,22 @@
fish/fish_variables fish/fish_variables
waybar/fluxo-rs waybar/fluxo-rs
!fluxo/ !fluxo/
# Desktop theming. These live outside the per-app configs above, but they are
# what makes an app that is neither GTK-aware nor shell-aware match the rice,
# so the branch carries them.
!gtk-3.0/
gtk-3.0/*
!gtk-3.0/settings.ini
!gtk-3.0/gtk.css
!gtk-4.0/
gtk-4.0/*
!gtk-4.0/settings.ini
!gtk-4.0/gtk.css
!xsettingsd/
!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"] [submodule "hypr/catppuccin-hyprland"]
path = hypr/catppuccin-hyprland path = hypr/catppuccin-hyprland
url = https://github.com/catppuccin/hyprland.git url = https://github.com/catppuccin/hyprland.git
[submodule "hypr/catppuccin-hyprlock"] [submodule "hypr/catppuccin-hyprlock"]
path = hypr/catppuccin-hyprlock path = hypr/catppuccin-hyprlock
url = https://github.com/catppuccin/hyprlock.git 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"] [submodule "nvim/pack/nvim/start/mini.nvim"]
path = nvim/pack/nvim/start/mini.nvim path = nvim/pack/nvim/start/mini.nvim
url = https://github.com/echasnovski/mini.nvim url = https://github.com/echasnovski/mini.nvim
[submodule "nvim/pack/nvim/start/nvim-lspconfig"] [submodule "nvim/pack/nvim/start/nvim-lspconfig"]
path = nvim/pack/nvim/start/nvim-lspconfig path = nvim/pack/nvim/start/nvim-lspconfig
url = https://github.com/neovim/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"] [submodule "nvim"]
path = nvim path = nvim
url = https://git.narl.io/nvrl/nvim-config.git url = https://git.narl.io/nvrl/nvim-config.git
+3 -3
View File
@@ -13,7 +13,7 @@ position = "None"
dynamic_padding = true dynamic_padding = true
decorations = "None" 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. ## glass and translucency read as a different design language entirely.
opacity = 1.0 opacity = 1.0
blur = false blur = false
@@ -28,7 +28,7 @@ columns = 82
lines = 24 lines = 24
## Blank space added around the window in pixels. Roomier than the old 3/0 — ## 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] [window.padding]
x = 12 x = 12
y = 8 y = 8
@@ -107,7 +107,7 @@ ipc_socket = true
## GENERAL -------------------------------------------------------- ## GENERAL --------------------------------------------------------
## Import additional configuration files. ## Import additional configuration files.
import = ["~/.config/alacritty/takemi.toml", "~/.config/alacritty/fonts.toml"] import = ["~/.config/alacritty/yorha.toml", "~/.config/alacritty/fonts.toml"]
[[keyboard.bindings]] [[keyboard.bindings]]
key = "Return" key = "Return"
+4 -2
View File
@@ -4,9 +4,11 @@ foreground = "#cdd6f4"
dim_foreground = "#7f849c" dim_foreground = "#7f849c"
bright_foreground = "#cdd6f4" bright_foreground = "#cdd6f4"
# Cursor in the shell accent (Mocha blue) rather than stock rosewater, so the
# terminal caret matches the focus colour everywhere else on the desktop.
[colors.cursor] [colors.cursor]
text = "#1e1e2e" text = "#11111b"
cursor = "#f5e0dc" cursor = "#89b4fa"
[colors.vi_mode_cursor] [colors.vi_mode_cursor]
text = "#1e1e2e" text = "#1e1e2e"
+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. #* 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" #* 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. #* If the theme set background should be shown, set to False if you want terminal background transparency.
theme_background = false 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] [global]
corner_radius = 4 corner_radius = 0
transparency = 90 transparency = 90
frame_color = "#f38ba8" frame_color = "#545c52"
frame_width = 1 frame_width = 1
separator_color= frame separator_color= frame
highlight = "#89dceb" highlight = "#eef1ea"
[urgency_low] [urgency_low]
background = "#1e1e2e" background = "#0f110e"
foreground = "#cdd6f4" foreground = "#ccd2cb"
timeout = 4 timeout = 4
[urgency_normal] [urgency_normal]
background = "#1e1e2e" background = "#0f110e"
foreground = "#cdd6f4" foreground = "#ccd2cb"
timeout = 6 timeout = 6
[urgency_critical] [urgency_critical]
background = "#1e1e2e" background = "#0f110e"
foreground = "#cdd6f4" foreground = "#ccd2cb"
frame_color = "#f38ba8" frame_color = "#c8684f"
+100
View File
@@ -0,0 +1,100 @@
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand.
function __fish_claude_account_global_optspecs
string join \n h/help V/version
end
function __fish_claude_account_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_claude_account_global_optspecs) -- $cmd 2>/dev/null
or return
if set -q argv[1]
# Also print the command, so this can be used to figure out what it is.
echo $argv[1]
return 1
end
return 0
end
function __fish_claude_account_using_subcommand
set -l cmd (__fish_claude_account_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end
complete -c claude-account -n "__fish_claude_account_needs_command" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_needs_command" -s V -l version -d 'Print version'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "status" -d 'Show every account, every proxy, every name and everything that is wrong'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "list" -d 'List the accounts, one per line'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "switch" -d 'Make an account the one `claude` runs as'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "add" -d 'Register another account, with a profile and a proxy of its own'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "remove" -d 'Forget an account. Its profile directory is left where it is'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "current" -d 'Print the name of the active account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "dir" -d 'Print the profile directory of an account, for `CLAUDE_CONFIG_DIR`'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "env" -d 'Print the shell lines that put this shell into an account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "url" -d 'Print the address a name resolves to'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "setup" -d 'Install everything at once: the wrapper, a proxy per account, the router'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "adopt" -d 'Take over the proxy services that were written by hand'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "doctor" -d 'Report every problem and exit non-zero when there is one'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "shim" -d 'The `claude` wrapper in ~/.local/bin'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "headroom" -d 'The proxy of one account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "router" -d 'The router that resolves <name>.headroom.localhost'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "completions" -d 'Tab completion, with the account names in it'
complete -c claude-account -n "__fish_claude_account_using_subcommand status" -s v -l verbose -d 'Add the last lines each proxy logged'
complete -c claude-account -n "__fish_claude_account_using_subcommand status" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand list" -l names -d 'Print the names alone, for scripts and for tab completion'
complete -c claude-account -n "__fish_claude_account_using_subcommand list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand switch" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l dir -d 'A profile directory elsewhere' -r -F
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l port -d 'A port for its proxy; the next free one by default' -r
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l switch -d 'Make it the account `claude` runs as straight away'
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand remove" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand current" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand dir" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand env" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand url" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand setup" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand adopt" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand doctor" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -f -a "install" -d 'Write ~/.local/bin/claude'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -f -a "uninstall" -d 'Remove it again'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from install" -l force -d 'Overwrite a `claude` that this tool did not write'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "install" -d 'Write the service for an account and start it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "start" -d 'Start the proxy of an account'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "stop" -d 'Stop it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "restart" -d 'Stop and start it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "uninstall" -d 'Stop it and remove its service'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "port" -d 'Give an account another port, and write it everywhere it is named'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "logs" -d 'Print what one proxy logged'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from start" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from stop" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from restart" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from port" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from logs" -s n -l lines -d 'How many lines' -r
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from logs" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "serve" -d 'Serve the names until stopped'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "install" -d 'Install the router as a user service'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "uninstall" -d 'Stop it and remove its service'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "port" -d 'Move the router to another port'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "list" -d 'List the names that resolve'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from serve" -l port -r
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from serve" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from port" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand completions" -l install -d 'Write the file where the shell reads it, instead of printing it'
complete -c claude-account -n "__fish_claude_account_using_subcommand completions" -s h -l help -d 'Print help'
# The accounts as they are right now, asked for when Tab is pressed.
complete -c claude-account -n '__fish_seen_subcommand_from switch dir env url remove start stop restart logs install uninstall port' -f -a '(claude-account list --names)'
-50
View File
@@ -1,50 +0,0 @@
# Takemi — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/takemi.toml and ~/.config/nvim/lua/takemi.lua.
#
# Replaces the catppuccin fish_frozen_theme.fish that fish 4.3 generated on
# upgrade. Same content as themes/Takemi.theme — that copy exists only so
# `fish_config` can preview it; this one is what actually loads.
#
# Note that `fish_config` writing a theme from the web UI will recreate
# fish_frozen_theme.fish, which sorts before this file and so would lose.
set --global fish_color_normal ffffff
set --global fish_color_command ffffff --bold
set --global fish_color_keyword ff2d40
set --global fish_color_param c8c8ce
set --global fish_color_option 8d9bb8
set --global fish_color_quote 7f9b6a
set --global fish_color_escape 9db8bd
set --global fish_color_operator c8c8ce
set --global fish_color_redirection ff6b78
set --global fish_color_end 6b6b70
set --global fish_color_comment 6b6b70 --italics
set --global fish_color_error ff2d40 --bold
set --global fish_color_cancel ff2d40
set --global fish_color_gray 6b6b70
set --global fish_color_autosuggestion 4a4a52
set --global fish_color_selection 000000 --background=ff2d40
set --global fish_color_search_match 000000 --background=c8c8ce
set --global fish_color_match 000000 --background=ff2d40
set --global fish_color_history_current --bold
set --global fish_color_valid_path --underline
set --global fish_color_cwd ffffff --bold
set --global fish_color_cwd_root ff2d40 --bold
set --global fish_color_user c8c8ce
set --global fish_color_host ffffff --bold
set --global fish_color_host_remote ff6b78
set --global fish_color_status ff2d40
set --global fish_pager_color_progress 6b6b70 --background=141418
set --global fish_pager_color_background
set --global fish_pager_color_prefix ffffff --bold
set --global fish_pager_color_completion c8c8ce
set --global fish_pager_color_description 6b6b70
set --global fish_pager_color_secondary_background --background=0d0d10
set --global fish_pager_color_secondary_completion c8c8ce
set --global fish_pager_color_secondary_description 6b6b70
set --global fish_pager_color_secondary_prefix ffffff --bold
set --global fish_pager_color_selected_background --background=ff2d40
set --global fish_pager_color_selected_completion 000000
set --global fish_pager_color_selected_description 000000
set --global fish_pager_color_selected_prefix 000000 --bold
+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
-3
View File
@@ -38,6 +38,3 @@ if status is-login && test -z "$DISPLAY" -a -z "$WAYLAND_DISPLAY"
end end
end end
# Claude environments
alias claude="CLAUDE_CONFIG_DIR=~/.claude command claude"
alias claude-work="CLAUDE_CONFIG_DIR=~/.claude-work command claude"
+57
View File
@@ -0,0 +1,57 @@
/* 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);
+7
View File
@@ -0,0 +1,7 @@
[Settings]
gtk-theme-name=adw-gtk3-dark
gtk-icon-theme-name=Papirus-Dark
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
View File
@@ -0,0 +1,95 @@
/* 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);
+7
View File
@@ -0,0 +1,7 @@
[Settings]
gtk-theme-name=adw-gtk3-dark
gtk-icon-theme-name=Papirus-Dark
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 { wallpaper {
monitor = monitor =
path = ~/.wallpapers/takemi_3.jpg path = ~/.wallpapers/space-1.jpg
fit_mode = cover 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/ -- 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 -- The screen is a porthole: the hull (quickshell/bar/Frame.qml) sits on the
-- scale, and a borderangle looping forever so the focused border was in -- top layer, so anything that moves past the edge of the opening passes
-- constant motion. The bounce is dialled back to a hint, the popin barely -- behind the frame instead of over the bar.
-- scales, and the border angle does not animate at all. --
-- 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({ hl.config({
animations = { animations = {
@@ -14,34 +28,45 @@ hl.config({
}) })
-- ------------------------------------------------------------------- curves -- ------------------------------------------------------------------- curves
hl.curve("snap", { type = "bezier", points = { { 0.16, 1.00 }, { 0.30, 1.00 } } }) -- outQuint: fast off the mark, long quiet settle.
hl.curve("overshoot", { type = "bezier", points = { { 0.34, 1.12 }, { 0.64, 1.00 } } }) hl.curve("glide", { type = "bezier", points = { { 0.22, 1.00 }, { 0.36, 1.00 } } })
hl.curve("hard", { type = "bezier", points = { { 0.55, 0.00 }, { 0.90, 0.35 } } }) -- 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 } } }) 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 -- --------------------------------------------------------------------- tree
-- speed is in ds (1ds = 100ms). Unset leaves inherit their parent. -- speed is in ds (1ds = 100ms). Unset leaves inherit their parent.
hl.animation({ leaf = "global", enabled = true, speed = 4, bezier = "snap" }) hl.animation({ leaf = "global", enabled = true, speed = 4, bezier = "glide" })
hl.animation({ leaf = "border", enabled = true, speed = 5, bezier = "snap" }) 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 = "windows", enabled = true, speed = 4, bezier = "glide" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4, bezier = "overshoot", style = "popin 96%" }) hl.animation({ leaf = "windowsIn", enabled = true, speed = 4, bezier = "glide", style = "popin 94%" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 2.6, bezier = "hard", style = "popin 97%" }) hl.animation({ leaf = "windowsOut", enabled = true, speed = 2.4, bezier = "exit", style = "popin 96%" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 3.6, bezier = "snap" }) hl.animation({ leaf = "windowsMove", enabled = true, speed = 4, bezier = "standard" })
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "snap" }) hl.animation({ leaf = "fade", enabled = true, speed = 3.4, bezier = "glide" })
hl.animation({ leaf = "fadeIn", enabled = true, speed = 2.4, bezier = "snap" }) hl.animation({ leaf = "fadeIn", enabled = true, speed = 3.4, bezier = "glide" })
hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.8, bezier = "hard" }) hl.animation({ leaf = "fadeOut", enabled = true, speed = 2.2, bezier = "exit" })
hl.animation({ leaf = "fadeSwitch", enabled = true, speed = 2, bezier = "snap" }) hl.animation({ leaf = "fadeSwitch", enabled = true, speed = 3, bezier = "glide" })
hl.animation({ leaf = "fadeShadow", enabled = true, speed = 3, bezier = "snap" }) hl.animation({ leaf = "fadeShadow", enabled = true, speed = 3.4, bezier = "glide" })
hl.animation({ leaf = "fadeDim", enabled = true, speed = 2.4, bezier = "snap" }) hl.animation({ leaf = "fadeDim", enabled = true, speed = 3, bezier = "glide" })
hl.animation({ leaf = "layers", enabled = true, speed = 3, bezier = "snap" }) -- Quickshell animates its own surfaces; the compositor only fades them, so the
hl.animation({ leaf = "layersIn", enabled = true, speed = 3, bezier = "snap", style = "slide" }) -- two motions never stack.
hl.animation({ leaf = "layersOut", enabled = true, speed = 2, bezier = "hard", style = "slide" }) 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 = "workspaces", enabled = true, speed = 5.5, bezier = "drift", style = "slidefadevert 18%" })
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 3, bezier = "snap", style = "slidevert" }) hl.animation({ leaf = "workspacesIn", enabled = true, speed = 5.5, bezier = "drift", style = "slidefadevert 18%" })
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 2.4, bezier = "hard", style = "slidevert" }) 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" })
+1 -1
View File
@@ -17,7 +17,7 @@ return {
-- results appear as you type; a dmenu could only filter a list it was -- results appear as you type; a dmenu could only filter a list it was
-- handed once, which made searching a two-step. It spawns its own terminal -- handed once, which made searching a two-step. It spawns its own terminal
-- (class pdfs-prompt, floated in lua/rules/windows.lua). -- (class pdfs-prompt, floated in lua/rules/windows.lua).
pdfs = "pdfs-prompt --fzf", pdfs = "pdfs-prompt --gtk",
-- bluetooth menu -- bluetooth menu
bluetooth = "fluxo bt menu", bluetooth = "fluxo bt menu",
+7 -1
View File
@@ -10,12 +10,18 @@ hl.on("hyprland.start", function()
hl.exec_cmd("uwsm app -- qs") hl.exec_cmd("uwsm app -- qs")
hl.exec_cmd("uwsm app -- nm-applet --indicator") 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 -- clipboard history, text and images
hl.exec_cmd("wl-paste --type text --watch cliphist store") hl.exec_cmd("wl-paste --type text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --watch cliphist store") hl.exec_cmd("wl-paste --type image --watch cliphist store")
-- 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("uwsm app -- pdfs-prompt --preload")
-- hl.exec_cmd("uwsm app -- nextcloud --background") -- hl.exec_cmd("uwsm app -- nextcloud --background")
-- hl.exec_cmd("uwsm app -- rclone mount google_drive: ~/gdrive") -- hl.exec_cmd("uwsm app -- rclone mount google_drive: ~/gdrive")
-- hl.exec_cmd("uwsm app -- protonvpn-app") -- hl.exec_cmd("uwsm app -- protonvpn-app")
+7 -4
View File
@@ -5,10 +5,13 @@ local scripts = os.getenv("HOME") .. "/.config/hypr/scripts/"
-- ------------------------------------------------------------- screenshots -- ------------------------------------------------------------- screenshots
-- PRINT selects a region, CTRL grabs the focused window, SHIFT saves to disk -- PRINT selects a region, CTRL grabs the focused window, SHIFT saves to disk
-- instead of the clipboard. -- instead of the clipboard.
hl.bind("PRINT", hl.dsp.exec_cmd('grim -g "$(slurp)" - | wl-copy')) -- All four go through scripts/shot.sh, which also posts a notification with
hl.bind("SHIFT + PRINT", hl.dsp.exec_cmd('grim -g "$(slurp)" ~/Pictures/Screenshots/$(date +\'%Y-%m-%d_%H-%M-%S\').png')) -- the capture as its thumbnail (Open, Edit with satty, Save).
hl.bind("CTRL + PRINT", hl.dsp.exec_cmd(scripts .. "screenshot_window.sh copy")) local shot = scripts .. "shot.sh "
hl.bind("CTRL + SHIFT + PRINT", hl.dsp.exec_cmd(scripts .. "screenshot_window.sh save")) 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 -- ------------------------------------------------------- replay and camera
-- On release, so the shortcut is not caught mid-chord. -- On release, so the shortcut is not caught mid-chord.
+15
View File
@@ -8,6 +8,21 @@ end
shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" }) shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" })
shell("power", { key = mod .. " + P", description = "power menu" }) 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("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("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" }) shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
-- Not a shell call, but it lives with the other session toggles.
local mirror = require("lua/mirror")
hl.bind(mod .. " + SHIFT + P", mirror.toggle, { description = "mirror laptop to all screens" })
+4 -2
View File
@@ -18,8 +18,10 @@ end
-- ------------------------------------------------------------------- groups -- ------------------------------------------------------------------- groups
hl.bind(mod .. " + n", hl.dsp.group.toggle()) hl.bind(mod .. " + n", hl.dsp.group.toggle())
hl.bind("ALT + Tab", hl.dsp.group.next()) -- Alt+Tab belongs to the overview (lua/binds/shell.lua), so tabs in a group
-- hl.bind("ALT + Tab", hl.dsp.group.prev()) -- 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 -- -------------------------------------------------------------------- mouse
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) 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" local mod = "SUPER"
@@ -13,10 +13,6 @@ end
hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" })) hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
hl.bind(mod .. " + mouse_up", 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. -- Scratchpad: S holds anything virtual. Chat opens wherever it is launched.
local special = { S = "virtual", D = "discord" } hl.bind(mod .. " + S", hl.dsp.workspace.toggle_special("virtual"))
hl.bind(mod .. " + SHIFT + S", hl.dsp.window.move({ workspace = "special:virtual" }))
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
+7
View File
@@ -29,3 +29,10 @@ hl.env("EDITOR", "nvim")
-- hl.env("INTEL_DEBUG", "noccs") -- hl.env("INTEL_DEBUG", "noccs")
-- hl.env("WLR_DRM_NO_ATOMIC", "1") -- hl.env("WLR_DRM_NO_ATOMIC", "1")
-- hl.env("debug:full_cm_proto", "true") -- hl.env("debug:full_cm_proto", "true")
-- headroom
hl.env("HEADROOM_ROLLOUT_CHANNEL", "beta")
hl.env("HEADROOM_OUTPUT_SHAPER", "1")
+1 -8
View File
@@ -79,9 +79,6 @@ local function reconcile(ignore)
return return
end 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 panel_off = want_off
if want_off then if want_off then
@@ -91,11 +88,7 @@ local function reconcile(ignore)
end end
monitors.apply_external() monitors.apply_external()
-- Quickshell follows monitor hotplug by itself; only the wallpaper daemon -- Quickshell follows monitor hotplug by itself, wallpaper included.
-- needs a kick.
if not settled then
hl.exec_cmd("systemctl restart --user hyprpaper")
end
end end
-- ------------------------------------------------------------------ triggers -- ------------------------------------------------------------------ triggers
+50 -28
View File
@@ -1,4 +1,5 @@
-- Takemi — Persona 5 flavoured theme, 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 -- Everything static about how the desktop is painted. Motion lives next door
-- in lua/animations.lua. -- in lua/animations.lua.
@@ -8,14 +9,20 @@ local c = require("lua/palette")
hl.config({ hl.config({
-- https://wiki.hypr.land/Configuring/Basics/Variables/ -- https://wiki.hypr.land/Configuring/Basics/Variables/
general = { 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_in = 5,
gaps_out = { top = 6, right = 8, bottom = 8, left = 8 }, gaps_out = 10,
border_size = 2, border_size = 1,
col = { col = {
active_border = c.activeBorder, active_border = c.activeBorder,
inactive_border = c.outline, -- 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.overlay,
}, },
resize_on_border = false, resize_on_border = false,
@@ -28,26 +35,32 @@ hl.config({
}, },
decoration = { decoration = {
-- Persona 5 has no rounded corners. Neither do we. -- Concentric with the porthole: the opening's corners are 18px and
rounding = 0, -- 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. -- Fully opaque, both states. Flat ink on flat ground.
active_opacity = 1.0, active_opacity = 1.0,
inactive_opacity = 1.0, inactive_opacity = 1.0,
-- 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 = { shadow = {
enabled = true, enabled = true,
range = 18, sharp = false,
range = 24,
render_power = 3, render_power = 3,
offset = { 0, 4 }, offset = { 0, 6 },
color = "rgba(000000ee)", color = "rgba(000000aa)",
color_inactive = "rgba(00000088)", color_inactive = "rgba(00000066)",
}, },
-- Off. Blur is the signature of the glass-and-frost look, which is the -- Off. Every surface here is opaque, so there is nothing to blur, and
-- opposite of what this theme is doing — every surface here is opaque, -- blur would cost three render passes for no visible result.
-- flat and hard-edged. With nothing translucent left to blur it also
-- costs three render passes for no visible result.
blur = { blur = {
enabled = false, enabled = false,
}, },
@@ -58,35 +71,44 @@ hl.config({
disable_hyprland_logo = true, disable_hyprland_logo = true,
-- 0 = off, 1 = always, 2 = fullscreen only (G7 is a 240Hz VRR panel) -- 0 = off, 1 = always, 2 = fullscreen only (G7 is a 240Hz VRR panel)
vrr = 0, vrr = 0,
font_family = "FiraCode Nerd Font", font_family = "JetBrainsMono Nerd Font",
background_color = c.base, background_color = c.base,
focus_on_activate = true, 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 = { group = {
col = { col = {
border_active = c.activeBorder, border_active = c.activeBorder,
border_inactive = c.outline, 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 = { groupbar = {
font_family = "JetBrainsMono Nerd Font", render_titles = false,
font_size = 11,
height = 24,
gradients = 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 = { col = {
active = c.accent, 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,
}, },
}, },
+106
View File
@@ -0,0 +1,106 @@
-- Presentation mode: every external output shows the laptop panel.
--
-- Toggled from lua/binds/shell.lua. Mirroring is just a `mirror` field on the
-- monitor rule, so switching it off clears that field and re-applies the specs
-- from lua/monitors.lua to put the Samsung back where it belongs.
--
-- Anything plugged in while mirroring is on gets mirrored too, which is the
-- usual order at a talk: press the key, then find the HDMI cable.
local monitors = require("lua/monitors")
local M = {}
local function notify(text)
hl.notification.create({ text = text, timeout = 3000 })
end
-- The panel is off while docked with the lid shut, see lua/lid.lua. Mirroring
-- a disabled output would leave every screen black.
local function builtin()
for _, mon in ipairs(hl.get_monitors()) do
if mon.name == monitors.laptop then
return mon
end
end
return nil
end
-- A mirroring output drops out of hl.get_monitors(), so the only place left
-- to find it is the panel's own `mirrors` list. That list is also the state:
-- a Lua flag would reset on every config reload while the outputs stay
-- mirrored.
local function mirroring(panel)
local names = {}
for _, mon in ipairs(panel and panel.mirrors or {}) do
table.insert(names, mon.name)
end
return names
end
-- Outputs keep their own mode while mirroring, and the panel image gets
-- rescaled to fit it every frame. The Dells at 1920x1200 flicker with that,
-- so match the panel's resolution; Hyprland falls back to the preferred mode
-- if the output cannot do it.
local function mirror_all(panel)
local mode = panel.width .. "x" .. panel.height
for _, mon in ipairs(monitors.externals_present()) do
hl.monitor({ output = mon.name, mirror = monitors.laptop, mode = mode, position = "auto" })
end
end
-- 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("sh -c 'qs kill; exec uwsm app -- qs'")
end
-- hl.monitor() only takes effect after the calling handler returns, so
-- anything that has to see the new layout goes on a timer.
local function later(fn)
hl.timer(fn, { timeout = 500, type = "oneshot" })
end
-- Clearing `mirror` alone brings the image back, but Hyprland never
-- re-announces the output to clients: hyprpaper and Quickshell keep seeing
-- only the panel, however often they restart. Cycling the output through
-- disabled makes it a fresh wl_output. Undeclared outputs get their auto
-- defaults back, apply_external() then overrides what lua/monitors.lua knows
-- better.
local function unmirror_all(names)
for _, name in ipairs(names) do
hl.monitor({ output = name, mirror = "", disabled = true })
end
later(function()
for _, name in ipairs(names) do
hl.monitor({ output = name, disabled = false, mode = "preferred", position = "auto" })
end
monitors.apply_external()
later(restart_clients)
end)
end
function M.toggle()
local panel = builtin()
local mirrored = mirroring(panel)
if #mirrored > 0 then
unmirror_all(mirrored)
notify("Mirroring off")
elseif panel then
mirror_all(panel)
later(restart_clients)
notify("Mirroring " .. monitors.laptop .. " to all screens")
else
notify("Laptop panel is off, open the lid first")
end
end
hl.on("monitor.added", function()
local panel = builtin()
if #mirroring(panel) > 0 then
mirror_all(panel)
end
end)
return M
+30 -27
View File
@@ -1,6 +1,7 @@
-- Takemi palette — kept in step with ~/.config/quickshell/config/Theme.qml. -- YoRHa dark palette — kept in step with ~/.config/quickshell/config/Theme.qml.
-- --
-- Crimson, black, white. 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 -- Returned as a table, so every module that paints something does
-- local c = require("lua/palette") -- local c = require("lua/palette")
@@ -8,44 +9,46 @@
local c = {} local c = {}
-- ----------------------------------------------------------------- THE KNOB -- ---------------------------------------------------------------- THE KNOBS
-- The one colour in this setup. Change this line and the window borders, -- Two colours in this setup: the planet light for anything live (window
-- group indicators and focus states all recolour with it. -- borders, group indicators, focus), the hull red for anything wrong. Change these lines and every
-- painted edge recolours with them.
-- --
-- Keep it in step with `readonly property color accent` in -- Keep them in step with `accent` / `alarm` in
-- ~/.config/quickshell/config/Theme.qml, which is what colours the shell. -- ~/.config/quickshell/config/Theme.qml, which is what colours the shell.
-- Those are the only two places the colour is written. -- Those are the only places the colours are written.
c.accent = "rgb(ff2d40)" c.accent = "rgb(eef1ea)"
c.alarm = "rgb(c8684f)"
c.accentDim = "rgb(8f1826)" c.accentDim = "rgb(8e978d)"
c.accentSoft = "rgb(ff6b78)" c.accentSoft = "rgb(bcc1bf)"
-- ------------------------------------------------------------------ neutrals -- ------------------------------------------------------------------ neutrals
c.ink = "rgb(000000)" c.ink = "rgb(090a08)"
c.base = "rgb(0a0a0c)" c.base = "rgb(0f110e)"
c.mantle = "rgb(0d0d10)" c.mantle = "rgb(0c0d0b)"
c.surface = "rgb(141418)" c.surface = "rgb(161915)"
c.surfaceAlt = "rgb(1c1c22)" c.surfaceAlt = "rgb(20241f)"
c.overlay = "rgb(26262e)" c.overlay = "rgb(383e35)"
c.outline = "rgb(3a3a44)" c.outline = "rgb(545c52)"
c.text = "rgb(ffffff)" c.text = "rgb(ccd2cb)"
c.subtext = "rgb(c8c8ce)" c.subtext = "rgb(9fa7a2)"
c.muted = "rgb(6b6b70)" c.muted = "rgb(79847a)"
-- Retired hues, kept as aliases so existing references resolve. The shell has -- Semantic aliases, so existing references resolve without inventing hues.
-- no teal any more — the wallpaper is the colour in the composition.
c.primary = c.text c.primary = c.text
c.primaryDim = "rgb(9a9aa2)" c.primaryDim = "rgb(8e978d)"
c.glow = c.accent c.glow = c.accent
c.blue = c.primaryDim c.blue = c.accent
c.deep = c.surfaceAlt c.deep = c.surfaceAlt
c.warn = c.accentSoft c.warn = c.alarm
c.ok = c.text c.ok = c.text
-- Crimson into its own shadow, on the diagonal. The accent is the only hue -- The focused window is lit by the planet: full light at the top-left, the
-- here — a gradient into a second colour is what put blue in the border. -- 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 } c.activeBorder = { colors = { c.accent, c.accentDim }, angle = 135 }
return c return c
+10
View File
@@ -11,3 +11,13 @@ for _, namespace in ipairs({ "bottom", "top", "takemi-launcher" }) do
ignore_alpha = 0.5, ignore_alpha = 0.5,
}) })
end 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 -- Special workspaces
hl.window_rule({ match = { class = "^(Spotify|spotify)$" }, workspace = "special:virtual" }) hl.window_rule({ match = { class = "^(Spotify|spotify)$" }, workspace = "special:virtual" })
hl.window_rule({ match = { class = "^(discord|vesktop)$" }, workspace = "special:discord" })
-- ========================================== -- ==========================================
-- STEAM & GAMING -- 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 &
+2
View File
@@ -22,6 +22,7 @@ x-scheme-handler/msteams=teams-for-linux.desktop
x-scheme-handler/slack=slack.desktop x-scheme-handler/slack=slack.desktop
audio/flac=mpv.desktop audio/flac=mpv.desktop
x-scheme-handler/codex=chatgpt.desktop x-scheme-handler/codex=chatgpt.desktop
x-scheme-handler/proton-inbox=proton-mail.desktop
[Added Associations] [Added Associations]
application/pdf=org.pwmt.zathura-pdf-poppler.desktop; application/pdf=org.pwmt.zathura-pdf-poppler.desktop;
@@ -40,3 +41,4 @@ application/xhtml+xml=zen.desktop;
application/x-extension-xhtml=zen.desktop; application/x-extension-xhtml=zen.desktop;
application/x-extension-xht=zen.desktop; application/x-extension-xht=zen.desktop;
audio/flac=mpv.desktop; audio/flac=mpv.desktop;
text/plain=org.neovim.nvim.desktop;
+1 -1
Submodule nvim updated: 2cbfe6946a...19e0db60a1.
+2 -1
View File
@@ -3,6 +3,7 @@ aalib
abseil-cpp abseil-cpp
acl acl
acpi acpi
adw-gtk-theme
adwaita-cursors adwaita-cursors
adwaita-fonts adwaita-fonts
adwaita-icon-theme adwaita-icon-theme
@@ -87,7 +88,6 @@ calibre
cargo-audit cargo-audit
catppuccin-gtk-theme-mocha catppuccin-gtk-theme-mocha
catppuccin-qt5ct-git catppuccin-qt5ct-git
catppuccinifier-cli-git
cbindgen cbindgen
cblas cblas
cdparanoia cdparanoia
@@ -111,6 +111,7 @@ cups-pk-helper
curl curl
cxxbridge cxxbridge
cython cython
darkly-bin
dav1d dav1d
db5.3 db5.3
dbus 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
+32
View File
@@ -0,0 +1,32 @@
[Appearance]
color_scheme_path=/home/narl/.config/qt6ct/colors/yorha.conf
custom_palette=true
icon_theme=Papirus-Dark
standard_dialogs=default
style=Darkly
[Fonts]
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
buttonbox_layout=0
cursor_flash_time=1000
dialog_buttons_have_icons=1
double_click_interval=400
gui_effects=@Invalid()
keyboard_scheme=2
menus_have_icons=true
show_shortcuts_in_context_menus=true
stylesheets=@Invalid()
toolbutton_style=4
underline_shortcut=1
wheel_scroll_lines=3
[SettingsWindow]
geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\a\x80\0\0\0\0\0\0\xe\xef\0\0\x4l\0\0\a\x80\0\0\0\0\0\0\xe\xef\0\0\x4l\0\0\0\x1\x2\0\0\0\a\x80\0\0\a\x80\0\0\0\0\0\0\xe\xef\0\0\x4l)
[Troubleshooting]
force_raster_widgets=1
ignored_applications=@Invalid()
+325 -103
View File
@@ -1,20 +1,23 @@
# Takemi # Takemi — YoRHa dark
A Persona 5 flavoured Quickshell desktop for Hyprland. Crimson, black and white, A NieR:Automata flavoured Quickshell desktop for Hyprland, kept dark and lit
with no gradients anywhere. Everything leans by the same 14°, corners are like its wallpaper: a lone figure at a porthole, the void outside, one planet's
chopped rather than rounded, and every surface is one continuous slab divided by light. The planet white for anything live, the hull red for anything wrong,
hairlines rather than a cluster of floating cards. 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. Replaces Waybar, dunst and hyprlock.
## Running ## Running
```sh ```sh
qs # starts the shell (Hyprland does this via autostart.conf) qs # starts the shell (Hyprland does this via autostart.lua)
qs -p lockpreview.qml # the lock screen in an ordinary window, for styling qs -p lockpreview.qml # the lock screen in an ordinary window, for styling
``` ```
Hyprland starts it from `~/.config/hypr/modules/autostart.conf`. Hyprland starts it from `~/.config/hypr/lua/autostart.lua`.
## Layout ## Layout
@@ -24,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. 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 █ ◉ │ 1 2 3 │ window title ♪ TRACK │ CPU │ 20:04 █ bar — 40px, part of the hull
├────────────────────────────────────────────┤ █╭──────────────────────────────────────────╮ ▐▌█
│ ▐█▌│ rail — 48px exclusive █│ │ ▐▌█ rail — 48px, part of the hull
│ windows ▐ ▌│ █│ windows │ ▐▌█
│ ▐ ▌│ █│ │ ▐▌█
│ ▐ ▌│ █╰──────────────────────────────────────────╯ █ frame — 8px left and bottom
└────────────────────────────────────────────┘ ████████████████████████████████████████████████
``` ```
- **bar** — one slab: identity and workspaces on the left; the focused window, - **hull** — `bar/Frame.qml`: one surface round the whole screen, with the
the player, the CPU/MEM/GPU readout and the clock on the right. desktop as a rounded opening. The bar and the rail sit on it.
- **rail** — one slab: network, bluetooth, audio, mic, backlight, battery, tray, - **bar** — identity and workspaces on the left (a ring after them while the
notifications, power. Popouts open leftward, into the screen. `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 Modules are divided by hairlines. `BarPill` is chromeless by default — a module
divided by sheared hairlines. `BarPill` is chromeless by default — a module is is *content inside* the hull, not a surface of its own — and marks hover with
*content inside* a surface, not a surface of its own — and marks hover with a an accent underline. Pass `standalone: true` for the rare thing that really is
crimson underline. Pass `standalone: true` for the rare thing that really is its its own panel.
own panel.
The shell uses three deliberate surface tiers. **Chrome** (`ChromeSurface`) is The shell uses three deliberate surface tiers. **Chrome** (the hull and
the calm, texture-free bar, rail, and tooltip layer. **Transient** `ChromeSurface`) is the calm, texture-free bar, rail, and tooltip layer. **Transient**
(`PopoutSurface`) is reserved for expressive menus and detail panels, with one (`PopoutSurface`) is reserved for expressive menus and detail panels, with one
clear composition and stronger separation from the desktop. **Cinematic** clear composition and stronger separation from the desktop. **Scene**
(`P5Backdrop`) belongs only to scene-scale experiences such as power and lock; surfaces — the lock screen and the power menu — have no panel at all: the
it is never used as decoration inside ordinary chrome or popouts. 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 config/ palette, type scale, geometry, motion curves
components/ the P5 primitives: panels, gauges, sliders, split-ink text components/ the primitives: panels, sliders, list rows, display text
services/ data sources, all singletons services/ data sources, all singletons
bar/ the two surfaces and their modules bar/ the two surfaces and their modules
popouts/ hover panels hanging off rail and bar 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 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 + callers set implicit sizes from their own layout (`row.implicitWidth +
contentPad * 2`), which keeps the shape maths out of binding loops. 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 ## Modules
| Module | Where | Left click | Right click | Middle | Scroll | Hover | | Module | Where | Left click | Right click | Middle | Scroll | Hover |
|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|
| Sigil | bar | app menu | reload shell | | | | | Sigil | bar | app menu | reload shell | | | |
| Workspaces | bar | switch | | | | | | Workspaces | bar | switch | | | | |
| Clock | bar | pin calendar | | | | calendar | | Clock | bar | dashboard | | | | calendar |
| Network | rail | pin details | | | | state + networks | | Network | rail | pin details | | | | state + networks |
| Bluetooth | rail | pin details | | | | state + devices | | Bluetooth | rail | pin details | | | | state + devices |
| Volume | rail | pin audio | | | volume | state + scroll hint + audio | | Volume | rail | pin audio | | | volume | state + scroll hint + audio |
@@ -106,12 +157,17 @@ dismisses the screen.
## Keybinds ## Keybinds
Set in `~/.config/hypr/modules/keybinds.conf`: Set in `~/.config/hypr/lua/binds/shell.lua`:
- `Super+Ctrl+L` — lock - `Super+Ctrl+L` — lock
- `Super+P` — power menu - `Super+P` — power menu
- `Super+Shift+N` — toggle do not disturb - `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+Ctrl+Shift+N` — clear notifications
- `Super+A` — ask panel
- `Super+Shift+R` — reload the shell - `Super+Shift+R` — reload the shell
All of these go through Quickshell's IPC, so they work from any script: All of these go through Quickshell's IPC, so they work from any script:
@@ -120,10 +176,54 @@ All of these go through Quickshell's IPC, so they work from any script:
qs ipc call shell lock qs ipc call shell lock
qs ipc call shell power qs ipc call shell power
qs ipc call shell toggleDnd 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 qs ipc call shell clearNotifications
qs ipc call shell reloadConfig qs ipc call shell reloadConfig
``` ```
## Ask
`Super+A` drops a panel off the top edge and hands whatever you type to
`claude -p`, streamed token by token. It is the subscription that answers —
the CLI already holds the credentials, so nothing here has an API key and
nothing here has to be given one.
The panel is a chat, not a coding session. Settings sources, MCP servers and
project files are switched off by default, which keeps a question asked from
the desktop from arriving with the last project's hooks, `CLAUDE.md` and tone
attached. The thread survives closing the panel; `Ctrl+L` starts a new one.
| Key | Does |
|---|---|
| `Enter` | ask |
| `Shift+Enter` | newline |
| `Ctrl+M` | next model, written back to `ask.json` |
| `Ctrl+Y` | copy the last answer |
| `Ctrl+L` | new conversation |
| `Ctrl+S` | stop the answer |
| `Escape` | hide, keeping the thread |
Configured in `ask.json`, watched and reloaded live:
```json
{
"model": "sonnet",
"systemPrompt": "…",
"keepContext": true,
"settingSources": "",
"allowedTools": ["WebSearch", "WebFetch"],
"disallowedTools": ["Bash", "Edit", "Write"],
"workingDirectory": "/home/narl",
"maxTurns": 6
}
```
`settingSources` is the escape hatch: set it to `"user,project,local"` and the
panel becomes your ordinary Claude Code, hooks and all.
## Data sources ## Data sources
Hardware telemetry comes from `fluxo`, whose hardware modules are configured to Hardware telemetry comes from `fluxo`, whose hardware modules are configured to
@@ -136,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 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. 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 ## Lock screen
Authenticates through `/etc/pam.d/hyprlock`, which on this box is just Authenticates through `/etc/pam.d/hyprlock`, which on this box is just
@@ -143,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 styled in an ordinary window (`qs -p lockpreview.qml`) without locking the
session. 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 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 session secured by protocol and there is no unlocker left to talk to — recovery
means restarting Hyprland from a TTY. means restarting Hyprland from a TTY.
@@ -150,6 +307,9 @@ means restarting Hyprland from a TTY.
## What was retired ## What was retired
- `waybar.service` — disabled. Its config is still in `~/.config/waybar`. - `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 - `dunst.service` — masked, so it cannot be D-Bus activated and steal
`org.freedesktop.Notifications`. Undo with `org.freedesktop.Notifications`. Undo with
`systemctl --user unmask dunst.service`. `systemctl --user unmask dunst.service`.
@@ -160,89 +320,151 @@ means restarting Hyprland from a TTY.
`power-profiles-daemon` is not installed (TLP is), so the power-profile `power-profiles-daemon` is not installed (TLP is), so the power-profile
switching in `services/Battery.qml` stays dormant. Nothing else depends on it. switching in `services/Battery.qml` stays dormant. Nothing else depends on it.
The display face is Archivo Black, set italic almost everywhere. Change The display face is Noto Sans, upright and widely tracked — the nearest free
`Theme.fontDisplay` and everything follows. face to NieR's FOT-Rodin that is already installed. Change `Theme.fontDisplay`
and everything follows.
## Palette discipline ## Palette discipline
Crimson, black, white. That is the whole palette. 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.
**Changing the accent.** The colour is written in exactly three places, one per - **Light (`accent`, `#eef1ea`)** means live: the focused workspace, the
program: 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.
| File | Line | Covers | **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
| `quickshell/config/Theme.qml` | `readonly property color accent: "#ff2d40"` | the shell | brings the marker up to full light, but never inverts. Destructive-active is
| `hypr/modules/takemi.conf` | `$accent = rgb(ff2d40)` | window borders, groups | the same slab in red. The `treatment*` tokens in `Theme.qml` encode all of
| `alacritty/takemi.toml` | `red`, cursor, selection | the terminal | this; use them rather than picking colours per widget.
Everything else derives. `accentDim` and `accentSoft` come off `accent` via **Dots, not slashes.** The current row, a button under the pointer, the end of
`Qt.darker`/`Qt.lighter`, `glow` (the hover highlight) *is* `accent`, and every a header rule and a notification's urgency carry a small dot (`Theme.marker`).
Hyprland border, group indicator and focus state references `$accent`. Change Borders are structure and stay neutral.
those lines and the whole setup recolours — swap in Persona 4's yellow or
Persona 3's blue and nothing else needs touching. **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).
**Changing the palette.** The colours are written once per program:
| File | Covers |
|---|---|
| `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. `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 ## The rest of the desktop
- **Alacritty** — `alacritty/takemi.toml`, selected by the `import` in - **Hyprland** — `hypr/lua/look.lua`. Windows round at 8px, concentric with
`alacritty.toml`. The Catppuccin files are left in place, just unreferenced. the opening's 18px corners at a 10px gap (`Theme.frameRadius - gaps_out`);
Near-black ground, white text, crimson cursor and selection. The sixteen ANSI change one and the others follow. No blur, both opacities at `1.0`, 1px
slots keep their hues because syntax highlighters need to tell them apart, but borders running from the light into the suit's grey. The shadow is soft and
every one is desaturated so nothing competes with the accent. wide, so windows float in the opening. Group tabs are thin rounded lines with
- **Neovim** — `nvim/lua/takemi.lua`, loaded through `nvim/colors/takemi.lua` no titles, the current one in the light; the bar writes the title once and
so `:colorscheme takemi` and `require("takemi").lualine()` both work. Same adds the window's place in the group (`2/3`). Motion
palette, same rule: crimson is spent only on keywords, types and errors, (`hypr/lua/animations.lua`) glides and settles with no overshoot: windows
functions are plain white-bold, and `terminal_color_0..15` mirror resolve in from a hair smaller, workspaces drift a short way vertically and
`takemi.toml` slot for slot so `:terminal` matches the shell around it. fade (sliding behind the hull), the `SUPER+S` scratchpad lowers out from
- **Fish** — `fish/conf.d/takemi_theme.fish`, with a preview copy in under the bar like the dashboard, and layers only fade, since quickshell
`fish/themes/Takemi.theme` for `fish_config`. Replaced the Catppuccin animates its own surfaces.
`fish_frozen_theme.fish` that fish 4.3 generated on upgrade. The prompt arrow - **Alacritty** — `alacritty/yorha.toml`, selected by the `import` in
is white and turns crimson on a non-zero exit — the shell's one "wrong" `alacritty.toml`. Muted earth hues on the void; selection inverts.
signal, and so the one place that earns the accent. 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 - **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 this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for
`fuzzel --dmenu` where a script needs a picker. `fuzzel --dmenu` where a script needs a picker.
- **No glass anywhere.** Alacritty is `opacity = 1.0, blur = false`, and - **No glass anywhere.** Alacritty is `opacity = 1.0, blur = false`, and
Hyprland's `decoration:blur` is disabled with both opacities at `1.0`. Blur is Hyprland's `decoration:blur` is disabled.
the signature of the frosted-glass look, which is the opposite of flat ink on
flat ground — and with nothing translucent left it was costing three render
passes for no visible result.
White is the resting voice — text, icons, idle indicators. Crimson is the only There are **no gradients in the chrome**; every fill is flat. `Theme.sheen`
accent and always means one thing: active, or wrong. Black is structure, used as still exists as a no-op property so old call sites need not be chased down.
a hard keyline rather than as a soft shadow. **There are no gradients in this
shell**; every fill is flat. `Theme.sheen` still exists as a no-op property
because a teal-to-transparent wash on every panel was the single biggest source
of the murky blue cast the design used to have.
`primary`, `glow`, `blue`, `ok` and `warn` survive in `Theme.qml` only as
aliases onto white, crimson and grey, so the existing call sites keep working —
don't introduce new uses. `warn` used to alias `accentSoft`, which put a
washed-out pink-red wherever something was merely notable; there is no
half-crimson in this palette, so it aliases the real `accent` now and anything
merely informational (brightness, most readouts) is white instead. `Theme.heat()` is a continuous white→crimson ramp, so
a CPU figure that has gone red means the same thing as a battery that has gone
red. Tray icons are force-flattened to a single colour for the same reason: app
brand colours wreck the palette faster than anything else in the shell.
The wallpaper is teal. The UI used to be teal too, and the two fought each other
into a blue-grey mush. Going monochrome-plus-red is what lets the wallpaper be
the colour in the composition.
## Geometry discipline ## Geometry discipline
**One angle, one chamfer, everywhere.** `Theme.skew` is `tan(14°)` and every **The hull.** Borrowed from caelestia's screen border: the bar, the rail and a
edge in the shell leans by exactly that, in the same direction; every corner is thin band down the left and along the bottom (`Theme.frame`) are one surface,
chopped by exactly `Theme.cut`. A vertical panel (the rail) leans its horizontal drawn by `bar/Frame.qml`, and the desktop is a rounded opening cut out of it
edges by a fraction of its *width*, which works out to the same 14°. (`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 **Soft, everywhere else.** The porthole is the shape the desktop is built
module a different lean, chamfer and height from per-index variance tables, on around, and every smaller surface is a gentler echo of it. `Theme.skew` and
the theory that Persona geometry is "chaotic". It isn't — Persona UI is `Theme.cut` are `0`; `Panel` and `Skew` draw `Geom.roundedRect` with
meticulously clean, and the chaos lives in composition and motion, not in each `Theme.radiusS` (pills, chips, meter tracks), `radiusM` (rows, buttons) or
element being a different shape. Randomised geometry just reads as noise. If you `radiusL`. `radiusL` is `frameRadius - padS`: anything inset by `padS` inside
find yourself adding a variance table, don't. 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 **Motion glides.** No overshoot anywhere: things ease in on `OutCubic` or
hairline rules rather than by gaps. A row of separate floating cards reads as `OutExpo` and settle. Nothing scales up or nudges sideways on hover either;
clutter; that is what the dividers exist to avoid. 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.
+11
View File
@@ -0,0 +1,11 @@
{
"model": "sonnet",
"fallbackModel": "",
"systemPrompt": "You are a desktop answer panel. Answer the question directly and compactly, in markdown, with no preamble. Prefer a few sentences over a list, and a list over an essay. Say plainly when you do not know.",
"keepContext": true,
"settingSources": "",
"allowedTools": ["WebSearch", "WebFetch"],
"disallowedTools": ["Bash", "Edit", "Write", "NotebookEdit", "Agent", "Task"],
"workingDirectory": "/home/narl",
"maxTurns": 6
}
+416
View File
@@ -0,0 +1,416 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/popouts"
import "root:/services"
// The ask panel. It comes down off the top edge, takes the keyboard, and hands
// a question to Claude.
//
// Deliberately not the launcher: the launcher is a list you pick from in one
// keystroke, this is a conversation you read. The shape says so — full width of
// the question, answer below it, and the thing drops from the top rather than
// rising into the middle so the two are never mistaken for each other mid-type.
PanelWindow {
id: root
required property var modelData
screen: modelData
// One screen at a time, latched at open — same reason as the launcher: the
// window takes focus, and a prompt should not hop monitors mid-question.
readonly property bool open: Actions.askOpen
&& Actions.askScreen === modelData.name
visible: root.open || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-ask"
WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
readonly property int panelWidth: Math.min(860, root.width - Theme.padXL * 2)
readonly property int maxTranscript: Math.round(root.height * 0.55)
onOpenChanged: {
if (root.open) {
PopupCoordinator.dismissAll();
input.text = Actions.askSeed;
openAnim.restart();
input.forceActiveFocus();
input.cursorPosition = input.text.length;
if (Actions.askSeed !== "") root.send();
} else {
closeAnim.restart();
}
}
function send(): void {
const q = input.text.trim();
if (q === "" || Ask.busy) return;
Ask.ask(q);
input.text = "";
}
function copyLast(): void {
for (let i = Ask.turns.length - 1; i >= 0; i--) {
if (Ask.turns[i].role === "assistant" && Ask.turns[i].text !== "") {
Actions.copyText(Ask.turns[i].text);
return;
}
}
}
// -------------------------------------------------------------- backdrop
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.6)
opacity: 0
TapHandler { onTapped: Actions.closeAsk() }
}
// ----------------------------------------------------------------- panel
Item {
id: shell
width: root.panelWidth
height: header.height + divider.height + body.height + hint.height
anchors.horizontalCenter: parent.horizontalCenter
y: 0
opacity: 0
// The drop. The offset is animated, never bound: binding it to the
// panel's own height means every height change — every streamed line —
// snaps the panel back off the top of the screen.
transform: Translate { id: drop; y: 0 }
PopoutSurface {
anchors.fill: parent
tone: Ask.busy ? Theme.accent : Theme.primary
padding: 0
slashWidth: Ask.busy ? 8 : 5
// -------------------------------------------------------- header
Item {
id: header
width: parent.width
height: 62
anchors.top: parent.top
Row {
anchors.fill: parent
anchors.leftMargin: Theme.padL
anchors.rightMargin: Theme.padL
spacing: Theme.padM
// Pulses rather than spins. Native text rendering is
// subpixel-antialiased, so a rotated glyph smears into
// colour fringes; scale keeps it on the pixel grid.
Icon {
anchors.verticalCenter: parent.verticalCenter
text: "󰭹"
color: Ask.busy ? Theme.accent : Theme.primary
font.pixelSize: Theme.fsTitle
pulsing: Ask.busy
pulseScale: 1.12
}
TextInput {
id: input
anchors.verticalCenter: parent.verticalCenter
width: parent.width - x - modelChip.width - Theme.padM
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsLarge
selectionColor: Theme.accent
selectedTextColor: Theme.ink
clip: true
focus: true
renderType: Text.NativeRendering
Keys.priority: Keys.BeforeItem
Keys.onPressed: event => root.handleKey(event)
Text {
anchors.verticalCenter: parent.verticalCenter
visible: input.text === ""
text: Ask.busy ? "thinking…"
: Ask.turns.length > 0 ? "follow up…" : "ask anything"
color: Theme.muted
font: input.font
renderType: Text.NativeRendering
}
}
// The model, which is also the control for it: Ctrl+M walks
// the ring and writes the choice back to ask.json, so the
// panel and the file never disagree.
Item {
id: modelChip
anchors.verticalCenter: parent.verticalCenter
width: modelText.width + Theme.padM * 2
height: 24
Skew {
anchors.fill: parent
color: Theme.surface
}
Text {
id: modelText
anchors.centerIn: parent
text: Ask.model.toUpperCase()
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 2
renderType: Text.NativeRendering
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Ask.cycleModel()
}
}
}
}
Skew {
id: divider
anchors.top: header.bottom
width: parent.width
height: Theme.stroke
color: Theme.alpha(Theme.outline, 0.8)
lean: 0
}
// --------------------------------------------------- transcript
Item {
id: body
anchors.top: divider.bottom
width: parent.width
height: Math.min(transcript.contentHeight + Theme.padM * 2,
root.maxTranscript)
visible: Ask.turns.length > 0 || Ask.error !== ""
clip: true
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
ListView {
id: transcript
anchors.fill: parent
anchors.margins: Theme.padM
anchors.leftMargin: Theme.padL
anchors.rightMargin: Theme.padL
model: Ask.turns
spacing: Theme.padM
boundsBehavior: Flickable.StopAtBounds
// Stay at the bottom while an answer streams in, but leave
// the view alone the moment it is scrolled by hand.
property bool pinned: true
onMovementEnded: pinned = atYEnd
onContentHeightChanged: if (pinned) positionViewAtEnd()
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
delegate: Column {
required property var modelData
width: transcript.width
spacing: Theme.padS
Text {
visible: modelData.role === "user"
text: "YOU"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 3
renderType: Text.NativeRendering
}
Text {
width: parent.width
text: modelData.text
textFormat: modelData.role === "assistant"
? Text.MarkdownText : Text.PlainText
color: modelData.role === "user" ? Theme.subtext : Theme.text
linkColor: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsBody
wrapMode: Text.Wrap
renderType: Text.NativeRendering
onLinkActivated: link => Actions.run(["xdg-open", link])
}
}
footer: Text {
width: transcript.width
visible: Ask.error !== ""
topPadding: visible ? Theme.padM : 0
text: Ask.error
color: Theme.alarm
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
}
}
// -------------------------------------------------------- hints
Item {
id: hint
anchors.top: body.visible ? body.bottom : divider.bottom
width: parent.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 3
renderType: Text.NativeRendering
text: Ask.busy
? "⏎ QUEUE CTRL+S STOP ESC HIDE"
: "⏎ ASK CTRL+M MODEL CTRL+Y COPY CTRL+L NEW ESC HIDE"
}
Text {
anchors.right: parent.right
anchors.rightMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
visible: Ask.turns.length > 0
text: Math.floor(Ask.turns.length / 2) + " TURN"
+ (Ask.turns.length > 2 ? "S" : "")
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
}
}
}
// ------------------------------------------------------------------ keys
function handleKey(event: var): void {
if (!root.open) return;
const ctrl = (event.modifiers & Qt.ControlModifier) !== 0;
if (event.key === Qt.Key_Escape || (ctrl && event.key === Qt.Key_C)) {
// Hide, do not clear. The thread outlives the panel so that closing
// it to read something and reopening to ask about that is one
// conversation, not two.
Actions.closeAsk();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
// Shift+Enter is a newline; a multi-line question is still one
// question. Plain Enter sends.
if ((event.modifiers & Qt.ShiftModifier) === 0) {
root.send();
event.accepted = true;
}
return;
}
if (ctrl && event.key === Qt.Key_L) {
Ask.clear();
input.text = "";
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_S) {
Ask.stop();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_M) {
Ask.cycleModel();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_Y) {
root.copyLast();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_U) {
input.text = "";
event.accepted = true;
return;
}
// The transcript scrolls on the arrows, because the input is one line
// and has nowhere for them to go.
if (event.key === Qt.Key_PageDown || (ctrl && event.key === Qt.Key_J)) {
transcript.flick(0, -900);
event.accepted = true;
return;
}
if (event.key === Qt.Key_PageUp || (ctrl && event.key === Qt.Key_K)) {
transcript.flick(0, 900);
event.accepted = true;
return;
}
}
// ------------------------------------------------------------- animation
ParallelAnimation {
id: openAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad
}
NumberAnimation {
target: shell; property: "opacity"
to: 1; duration: Theme.durFast
}
NumberAnimation {
target: drop; property: "y"
from: -shell.height; to: 0
duration: Theme.durSlow; easing.type: Easing.OutCubic
}
}
ParallelAnimation {
id: closeAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: shell; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: drop; property: "y"
to: -shell.height; duration: Theme.durFast; easing.type: Easing.InQuad
}
}
}
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
}
+45 -8
View File
@@ -20,6 +20,31 @@ Item {
|| toplevel.monitor.id === root.monitor.id) || toplevel.monitor.id === root.monitor.id)
readonly property string incomingTitle: mine && toplevel.title ? toplevel.title : "—" 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: "—" property string displayedTitle: "—"
// Which toplevel the landing transition last played for. The landing is a // 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. // The bar shrinks this when the three clusters would otherwise collide.
property real maxWidth: 240 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 implicitHeight: 20
Behavior on implicitWidth { Behavior on implicitWidth {
@@ -45,24 +71,23 @@ Item {
Text { Text {
id: label id: label
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width width: parent.width - (groupTag.visible ? groupTag.width + Theme.padS : 0)
text: root.displayedTitle text: root.displayedTitle
color: root.mine ? Theme.subtext : Theme.muted color: root.mine ? Theme.subtext : Theme.muted
elide: Text.ElideRight elide: Text.ElideRight
font.pixelSize: Theme.fsSmall font.pixelSize: Theme.fsSmall
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.italic: true
font.weight: Font.DemiBold font.weight: Font.DemiBold
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
// Crimson duplicate: visible only during the landing transition. The // 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. // without leaving permanent motion in the bar.
Text { Text {
id: ghost id: ghost
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width width: label.width
text: root.displayedTitle text: root.displayedTitle
color: Theme.accent color: Theme.accent
opacity: 0 opacity: 0
@@ -70,6 +95,18 @@ Item {
font: label.font font: label.font
renderType: Text.NativeRendering 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. // Adopt the settled caption, and stamp it only when focus actually moved.
@@ -119,7 +156,7 @@ Item {
landedToplevel = toplevel; landedToplevel = toplevel;
} }
// A quick crimson wipe under the title on focus change. // A quick accent wipe under the title on focus change.
Rectangle { Rectangle {
id: wipe id: wipe
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -144,7 +181,7 @@ Item {
NumberAnimation { NumberAnimation {
target: label; property: "x" target: label; property: "x"
from: -10; to: 0 from: -10; to: 0
duration: Theme.durBase; easing.type: Easing.OutBack duration: Theme.durBase; easing.type: Easing.OutCubic
} }
NumberAnimation { NumberAnimation {
target: label; property: "opacity" target: label; property: "opacity"
+13 -33
View File
@@ -27,29 +27,16 @@ PanelWindow {
right: true right: true
} }
margins { // Flush with the screen edge and drawn on nothing: the hull behind it is
top: Theme.barMargin // painted by Frame, which is what joins the bar, the rail and the border
left: Theme.barMargin // into one surface. Exclusive zones are Frame's job too, so every piece
right: Theme.barMargin // 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 implicitHeight: Theme.barHeight
exclusiveZone: Theme.barHeight + Theme.barMargin
color: "transparent" color: "transparent"
// Calm permanent chrome: one opaque slab with no texture or offset copy. readonly property real edgeInset: Theme.padM
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
// ------------------------------------------------------------------ left // ------------------------------------------------------------------ left
Row { Row {
@@ -108,25 +95,18 @@ PanelWindow {
} }
} }
// A hairline rule between modules, sheared to the house angle. This is what // A hairline rule between modules. This is what divides the bar — gaps
// divides the bar now — gaps between cards are what made it look scattered. // between cards are what made it look scattered.
component Divider: Item { component Divider: Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
implicitWidth: Theme.padM * 2 implicitWidth: Theme.padM * 2
implicitHeight: Theme.barHeight implicitHeight: Theme.barHeight
// A leaning hairline needs a box wide enough to hold the lean: at 20px Rectangle {
// 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 {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.ruleHeight * Theme.skew + parent.ruleWeight width: 1
height: parent.ruleHeight height: 16
color: Theme.alpha(Theme.outline, 0.9) color: Theme.alpha(Theme.outline, 0.6)
} }
} }
} }
+4 -4
View File
@@ -5,7 +5,7 @@ import "root:/components"
// Shared chrome for every bar module. // Shared chrome for every bar module.
// //
// The bar is one continuous slab, so by default a module draws no panel of its // The bar is one continuous slab, so by default a module draws no panel of its
// own — it is content inside the bar's surface, and marks hover with a crimson // own — it is content inside the bar's surface, and marks hover with an accent
// underline rather than by lighting up a card. That is what stops the bar from // underline rather than by lighting up a card. That is what stops the bar from
// reading as a row of floating islands. // reading as a row of floating islands.
// //
@@ -34,7 +34,7 @@ Item {
// //
// The rule used to be drawn into the same box as the content, so anything // The rule used to be drawn into the same box as the content, so anything
// taller than about 32px — the media pill's art, the metric meters — had a // taller than about 32px — the media pill's art, the metric meters — had a
// crimson line struck through its bottom edge on hover. Reserving the band // accent line struck through its bottom edge on hover. Reserving the band
// and centring content in what is left is what keeps the rule under the // and centring content in what is left is what keeps the rule under the
// module instead of across it. // module instead of across it.
// The band reserved the full rule plus its inset — 7px off a 40px module, // The band reserved the full rule plus its inset — 7px off a 40px module,
@@ -73,7 +73,7 @@ Item {
anchors.fill: parent anchors.fill: parent
active: root.standalone active: root.standalone
sourceComponent: P5Panel { sourceComponent: Panel {
lean: root.lean lean: root.lean
leanLeft: root.leanLeft leanLeft: root.leanLeft
chop: root.chop chop: root.chop
@@ -102,7 +102,7 @@ Item {
anchors.bottomMargin: root.ruleBand anchors.bottomMargin: root.ruleBand
} }
// Hover mark for in-bar modules: a crimson rule under the content, wiping // Hover mark for in-bar modules: an accent rule under the content, wiping
// out from the leading edge. Sheared to match the one angle everything uses. // out from the leading edge. Sheared to match the one angle everything uses.
Item { Item {
visible: !root.standalone && root.interactive visible: !root.standalone && root.interactive
+14 -13
View File
@@ -3,8 +3,10 @@ import Quickshell
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
import "root:/popouts" 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 { BarPill {
id: root id: root
@@ -20,7 +22,7 @@ BarPill {
precision: SystemClock.Minutes precision: SystemClock.Minutes
} }
onClicked: calendar.pinned = !calendar.pinned onClicked: Actions.toggleDashboard()
Row { Row {
id: layout id: layout
@@ -36,7 +38,8 @@ BarPill {
color: Theme.accent color: Theme.accent
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.letterSpacing: 2.5 font.weight: Font.Bold
font.letterSpacing: 3
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -50,16 +53,15 @@ BarPill {
} }
} }
// Declared with room for its own lean, so the slash sits inside the box // A hairline between the date and the time.
// the Row gave it instead of hanging several px into the date column. Rectangle {
Skew {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 22 * Theme.skew + 3 width: 1
height: 22 height: 18
color: Theme.alpha(Theme.accent, 0.6) 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 // 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 // several px narrower than 08:48 — and shoved the rest of the bar
// around. Reserve the widest possible reading and centre inside it. // around. Reserve the widest possible reading and centre inside it.
@@ -69,7 +71,6 @@ BarPill {
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsTitle font.pixelSize: Theme.fsTitle
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5 font.letterSpacing: 0.5
} }
@@ -98,7 +99,6 @@ BarPill {
text: Qt.formatDateTime(clock.date, "HH:mm") text: Qt.formatDateTime(clock.date, "HH:mm")
pixelSize: Theme.fsTitle pixelSize: Theme.fsTitle
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
split: 1.4
} }
} }
@@ -117,7 +117,8 @@ BarPill {
CalendarPopout { CalendarPopout {
id: calendar id: calendar
anchorItem: root anchorItem: root
triggerHovered: root.hovered // Not while the dashboard is down: it already shows the month.
triggerHovered: root.hovered && !Actions.dashboardOpen
clockDate: clock.date 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.Shapes
import QtQuick.Window import QtQuick.Window
import Quickshell import Quickshell
import Quickshell.Widgets
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
import "root:/services" import "root:/services"
@@ -41,40 +42,28 @@ BarPill {
anchors.left: parent.left anchors.left: parent.left
spacing: Theme.padS spacing: Theme.padS
// Album art, chopped into the house shape. // Album art, rounded, in a hairline frame.
Item { ClippingRectangle {
id: artFrame id: artFrame
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 24 width: 24
height: 24 height: 24
radius: Theme.radiusS
Shape { color: Theme.surface
id: artMask border.width: 1
anchors.fill: parent border.color: Theme.overlay
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])
}
}
}
Image { Image {
id: art id: art
anchors.fill: parent anchors.fill: parent
anchors.margins: 1
source: Media.artUrl source: Media.artUrl
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
cache: true cache: true
asynchronous: true asynchronous: true
visible: status === Image.Ready visible: status === Image.Ready
layer.enabled: true sourceSize.width: 48
layer.smooth: true sourceSize.height: 48
layer.effect: MaskedArt { maskItem: artMask }
} }
// Fallback: the source app's glyph. // Fallback: the source app's glyph.
@@ -109,7 +98,6 @@ BarPill {
font.pixelSize: Theme.fsSmall font.pixelSize: Theme.fsSmall
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.weight: Font.DemiBold font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -125,8 +113,10 @@ BarPill {
} }
} }
// Compact play-state equaliser. It moves only while audio is playing // Signal meter: five thin rounded bars, NieR's audio readout rather than a
// and freezes into three quiet bars when paused. // 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 // Sampled from a timer rather than tweened by an infinite
// NumberAnimation. DP-1 runs at 240 Hz, and a frame-synced animation // NumberAnimation. DP-1 runs at 240 Hz, and a frame-synced animation
@@ -145,7 +135,7 @@ BarPill {
Row { Row {
id: eq id: eq
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 2 spacing: 3
// Sampling interval — the one knob trading smoothness against cost. // Sampling interval — the one knob trading smoothness against cost.
// Frames are the whole expense: a full-bar repaint runs about 0.3% of // 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 // sine would, is three times the motion and reads far busier than
// this meter is supposed to. // this meter is supposed to.
function level(i: int): real { function level(i: int): real {
const lo = 4 + (i * 5) % 13; // old first NumberAnimation `to` const lo = 2 + (i * 3) % 5;
const hi = 14 - (i * 4) % 9; // old second NumberAnimation `to` const hi = 14 - (i * 4) % 7;
const d1 = 320 + i * 90; // ...and their durations const d1 = 320 + i * 90; // ...and their durations
const d2 = 280 + i * 70; const d2 = 280 + i * 70;
@@ -189,12 +179,13 @@ BarPill {
} }
Repeater { Repeater {
model: 3 model: 5
Rectangle { Rectangle {
required property int index required property int index
width: 3 width: 2
height: Media.playing ? eq.level(index) : 6 radius: 1
height: Media.playing ? eq.level(index) : 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: Media.playing ? Theme.primary : Theme.muted color: Media.playing ? Theme.primary : Theme.muted
@@ -213,11 +204,16 @@ BarPill {
function onTrackChanged() { titleGlitch.restart(); } function onTrackChanged() { titleGlitch.restart(); }
} }
// A new track resolves in, the way NieR text does: a brief dip and back.
SequentialAnimation { SequentialAnimation {
id: titleGlitch id: titleGlitch
NumberAnimation { NumberAnimation {
target: artFrame; property: "rotation" target: meta; property: "opacity"
from: -8; to: 0; duration: Theme.durSlow; easing.type: Easing.OutBack to: 0.25; duration: 60
}
NumberAnimation {
target: meta; property: "opacity"
to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
} }
} }
-72
View File
@@ -1,72 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Notification bell with an unread badge. Click opens the history, right click
// toggles do-not-disturb.
BarPill {
id: root
required property var screenRef
accent: Notifs.dnd ? Theme.muted : Theme.primary
padding: Theme.padS
implicitWidth: Theme.barHeight - 4 + contentPad
onClicked: {
popout.pinned = !popout.pinned;
Notifs.markRead();
}
onRightClicked: Notifs.toggleDnd()
Icon {
id: bell
anchors.centerIn: parent
text: Notifs.icon
font.pixelSize: Theme.fsLarge
color: {
if (Notifs.dnd) return Theme.muted;
if (Notifs.hasUnread) return Theme.accent;
return root.hovered ? Theme.glow : Theme.primary;
}
}
// Unread badge, chopped like everything else.
Skew {
visible: Notifs.unread > 0 && !Notifs.dnd
anchors.right: parent.right
anchors.rightMargin: 2
anchors.top: parent.top
anchors.topMargin: 4
width: Math.max(12, badge.implicitWidth + 5)
height: 12
color: Theme.accent
// The badge count stays upright. It used to inherit the shear from the
// transform on the block below, which at 8px left the digits smeared.
Text {
id: badge
anchors.centerIn: parent
text: Notifs.unread > 9 ? "9+" : Notifs.unread
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: 8
font.weight: Font.Bold
renderType: Text.NativeRendering
}
}
Connections {
target: Notifs
function onPopupRequested() { bell.bump(); }
}
NotifPopout {
id: popout
anchorItem: root
triggerHovered: root.hovered
}
}
-42
View File
@@ -1,42 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Far right. Opens the full-screen power menu.
BarPill {
id: root
required property var screenRef
accent: Theme.accent
slash: true
padding: Theme.padS
active: Actions.powerMenuOpen
implicitWidth: Theme.barHeight - 4 + contentPad
onClicked: Actions.togglePowerMenu()
Icon {
id: glyph
anchors.centerIn: parent
text: "󰐥"
font.pixelSize: Theme.fsLarge
color: root.hovered || Actions.powerMenuOpen ? Theme.accent : Theme.subtext
}
// A crimson halo that swells under the pointer.
Rectangle {
anchors.centerIn: glyph
z: -1
width: root.hovered ? 26 : 0
height: width
radius: width / 2
color: Theme.alpha(Theme.accent, 0.18)
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
}
+41 -40
View File
@@ -30,30 +30,14 @@ PanelWindow {
right: true right: true
} }
margins { // Hangs from the underside of the bar, flush with the screen edge. Like
top: Theme.barHeight + Theme.barMargin * 2 // the bar it draws no surface of its own; see Frame.
bottom: Theme.barMargin margins.top: Theme.barHeight
right: Theme.barMargin exclusionMode: ExclusionMode.Ignore
}
implicitWidth: Theme.railWidth implicitWidth: Theme.railWidth
exclusiveZone: Theme.railWidth + Theme.barMargin
color: "transparent" color: "transparent"
// The same calm permanent-chrome preset used by the top bar. readonly property real edgeInset: Theme.padM
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
// ------------------------------------------------------------------- top // ------------------------------------------------------------------- top
Column { Column {
@@ -63,6 +47,19 @@ PanelWindow {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: 0 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 { RailIcon {
id: netIcon id: netIcon
icon: Net.icon icon: Net.icon
@@ -94,7 +91,7 @@ PanelWindow {
statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName
hint: "Scroll to adjust" hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive suppressTooltip: audioPopout.detailActive
alert: Audio.muted dim: Audio.muted
onClicked: audioPopout.togglePinned() onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05) onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
} }
@@ -106,7 +103,7 @@ PanelWindow {
statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName
hint: "Scroll to adjust" hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive suppressTooltip: audioPopout.detailActive
alert: Audio.micMuted dim: Audio.micMuted
onClicked: audioPopout.togglePinned() onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05)) onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
} }
@@ -132,7 +129,7 @@ PanelWindow {
label: "Battery" label: "Battery"
statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel
suppressTooltip: battPopout.detailActive suppressTooltip: battPopout.detailActive
alert: Battery.critical alarm: Battery.critical
pulsing: Battery.critical pulsing: Battery.critical
onClicked: battPopout.togglePinned() onClicked: battPopout.togglePinned()
} }
@@ -153,6 +150,19 @@ PanelWindow {
Divider { visible: tray.visible } 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 { RailIcon {
id: bellIcon id: bellIcon
icon: Notifs.icon icon: Notifs.icon
@@ -165,7 +175,7 @@ PanelWindow {
: "" : ""
reserveReadout: true reserveReadout: true
dim: Notifs.dnd dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd live: Notifs.hasUnread && !Notifs.dnd
suppressTooltip: notifPopout.detailActive suppressTooltip: notifPopout.detailActive
onClicked: { onClicked: {
notifPopout.togglePinned(); notifPopout.togglePinned();
@@ -178,30 +188,21 @@ PanelWindow {
icon: "󰐥" icon: "󰐥"
label: "Power" label: "Power"
statusText: "Session actions" statusText: "Session actions"
alert: Actions.powerMenuOpen live: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu() onClicked: Actions.togglePowerMenu()
} }
} }
// The rail's counterpart to the bar's divider: a horizontal hairline, // The rail's counterpart to the bar's divider: a horizontal hairline.
// sheared to the same angle the rail's own edges lean at.
component Divider: Item { component Divider: Item {
implicitWidth: Theme.railWidth implicitWidth: Theme.railWidth
implicitHeight: Theme.padM + 4 implicitHeight: Theme.padM + 4
// Same correction as the bar's divider, on the other axis: a 22px rule Rectangle {
// 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 {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.ruleWidth width: 16
height: parent.ruleWidth * Theme.skew + parent.ruleWeight height: 1
vertical: true color: Theme.alpha(Theme.outline, 0.6)
color: Theme.alpha(Theme.outline, 0.9)
} }
} }
+32 -23
View File
@@ -3,10 +3,10 @@ import "root:/config"
import "root:/components" import "root:/components"
// One slot on the right-hand rail: a glyph with an optional micro readout // One slot on the right-hand rail: a glyph with an optional micro readout
// stacked beneath it, and a crimson bar that wipes down the leading edge on // stacked beneath it, and an accent bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so unlike StatusIcon the readout sits // hover. The rail is only 48px wide, so the readout sits under the glyph
// under the glyph rather than beside it, and anything longer than a percentage // rather than beside it, and anything longer than a percentage belongs in the
// belongs in the popout instead. // popout instead.
Item { Item {
id: root id: root
@@ -17,7 +17,15 @@ Item {
property string hint: "" property string hint: ""
property bool suppressTooltip: false property bool suppressTooltip: false
property color tint: Theme.primary property color tint: Theme.primary
property bool alert: false
// Three states, three meanings. `live` wants attention but nothing is wrong
// (unread notifications, an open power menu): accent. `alarm` is wrong or
// critical (a dying battery): alarm. `dim` is off by choice (muted, radio
// off, do-not-disturb): grey. These used to be one `alert` flag painted in
// the accent, which made a muted speaker look like the most active thing
// on the rail.
property bool live: false
property bool alarm: false
property bool dim: false property bool dim: false
property bool pulsing: false property bool pulsing: false
@@ -63,7 +71,7 @@ Item {
id: glyphMetrics id: glyphMetrics
text: root.icon text: root.icon
font.family: Theme.fontIcon font.family: Theme.fontIcon
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsRailIcon
} }
Item { Item {
@@ -76,9 +84,10 @@ Item {
y: (root.glyphBand - height) / 2 y: (root.glyphBand - height) / 2
text: root.icon text: root.icon
pulsing: root.pulsing pulsing: root.pulsing
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsRailIcon
color: { color: {
if (root.alert) return Theme.accent; if (root.alarm) return Theme.alarm;
if (root.live) return Theme.accent;
if (root.dim) return Theme.muted; if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow; if (root.hovered) return Theme.glow;
return root.tint; return root.tint;
@@ -96,7 +105,11 @@ Item {
anchors.topMargin: root.glyphBand + 1 anchors.topMargin: root.glyphBand + 1
visible: root.readout !== "" visible: root.readout !== ""
text: root.readout text: root.readout
color: root.dim ? Theme.muted : Theme.subtext color: {
if (root.alarm) return Theme.alarm;
if (root.dim) return Theme.muted;
return Theme.subtext;
}
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.DemiBold font.weight: Font.DemiBold
@@ -109,22 +122,21 @@ Item {
} }
// Leading-edge wipe. On the rail this runs vertically down the left side, // 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 // 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 // at x = 0 this landed exactly on the slab's own border.
// scale then pushed it out past the silhouette onto the desktop. The inset
// has to survive that scale, which is why it is not simply 1 or 2.
// //
// It is crimson rather than `tint` because it only ever appears on hover, // It is the accent rather than `tint` because it only ever appears on
// and hover is crimson everywhere else in the shell (Theme.glow). Painting // hover, and hover is the accent everywhere else in the shell (Theme.glow).
// it white put a white bar on the rail's edge for no stated reason. // Painting it white put a white bar on the rail's edge for no stated reason.
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: root.wipeInset anchors.leftMargin: root.wipeInset
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 2 width: 2
height: root.hovered ? root.height - 4 : 0 radius: 1
height: root.hovered ? Theme.marker * 2 : 0
color: Theme.glow color: Theme.glow
Behavior on height { Behavior on height {
@@ -132,12 +144,9 @@ Item {
} }
} }
scale: root.hovered ? 1.08 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump() onAlarmChanged: if (alarm) glyph.bump()
onLiveChanged: if (live) glyph.bump()
HoverHandler { HoverHandler {
id: hover id: hover
@@ -153,7 +162,7 @@ Item {
onWheel: event => root.scrolled(event.angleDelta.y) onWheel: event => root.scrolled(event.angleDelta.y)
} }
P5Tooltip { Tooltip {
anchorItem: root anchorItem: root
triggerHovered: root.hovered triggerHovered: root.hovered
title: root.label title: root.label
+2 -3
View File
@@ -63,7 +63,7 @@ BarPill {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.height * Theme.skew + 1 width: parent.height * Theme.skew + 1
height: parent.height height: parent.height
color: Theme.alpha(Theme.outline, 0.8) color: Theme.alpha(Theme.outline, 0.5)
} }
} }
@@ -106,7 +106,7 @@ BarPill {
id: labelText id: labelText
anchors.baseline: gauge.baseline anchors.baseline: gauge.baseline
text: metric.label text: metric.label
color: Theme.muted color: Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.Bold font.weight: Font.Bold
@@ -121,7 +121,6 @@ BarPill {
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5 font.letterSpacing: 0.5
renderType: Text.NativeRendering renderType: Text.NativeRendering
+3 -21
View File
@@ -12,33 +12,15 @@ BarPill {
required property var screenRef required property var screenRef
accent: Theme.accent accent: Theme.accent
slash: true
padding: Theme.padS padding: Theme.padS
lean: Theme.skew
implicitWidth: Theme.barHeight + contentPad implicitWidth: Theme.barHeight + contentPad
onClicked: Actions.toggleLauncher() onClicked: Actions.toggleLauncher()
onRightClicked: Actions.reloadShell() onRightClicked: Actions.reloadShell()
// The wordmark. Was the Death arcana skull (nf-md-skull, U+F068C) — a Porthole {
// pictogram in a shell that otherwise says everything in heavy italic
// type. SplitText puts it in the display face with the crimson-and-black
// offset every other heading carries, so the house mark is built out of
// the same parts as the rest of the bar.
SplitText {
id: mark
anchors.centerIn: parent anchors.centerIn: parent
text: "P5" size: 18
pixelSize: Theme.fsLarge lit: root.hovered
letterSpacing: 0
color: root.hovered ? Theme.accent : Theme.primary
}
// Kick the ghosts out and let them snap back. The skull could spin a full
// turn and still read as a skull; a wordmark upside down just reads as
// broken, so the click gets the glitch instead.
Connections {
target: root
function onClicked() { mark.glitch(); }
} }
} }
-117
View File
@@ -1,117 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Network, bluetooth, audio, mic, backlight and battery, in one slab. Each
// glyph carries its own popout.
BarPill {
id: root
required property var screenRef
accent: Theme.primary
padding: Theme.padS
interactive: false
implicitWidth: layout.implicitWidth + contentPad * 2
Row {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
StatusIcon {
id: netIcon
icon: Net.icon
label: Sys.netUp ? Sys.rate(Sys.netRx + Sys.netTx) : ""
labelWidth: 62
tint: Theme.primary
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
}
StatusIcon {
id: btIcon
icon: Bt.icon
tint: Bt.connected.length > 0 ? Theme.blue : Theme.primary
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
StatusIcon {
id: volIcon
icon: Audio.icon
label: Audio.muted ? "" : Audio.volumePercent + "%"
labelWidth: Audio.muted ? 0 : 34
alert: Audio.muted
onClicked: Audio.toggleMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onMiddleClicked: Actions.openAudioSettings()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
StatusIcon {
id: micIcon
icon: Audio.micIcon
alert: Audio.micMuted
onClicked: Audio.toggleMicMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
StatusIcon {
id: brightIcon
visible: Brightness.available
icon: Brightness.icon
label: Math.round(Brightness.percent) + "%"
labelWidth: 34
// Brightness is never an alarm — it stays white and lets the
// battery be the only thing in the cluster that can go crimson.
tint: Theme.text
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
StatusIcon {
id: battIcon
visible: Battery.available
icon: Battery.icon
label: Math.round(Battery.percent) + "%"
labelWidth: 34
tint: Battery.tint
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
NetPopout {
id: netPopout
anchorItem: netIcon
triggerHovered: netIcon.hovered
}
BtPopout {
id: btPopout
anchorItem: btIcon
triggerHovered: btIcon.hovered
}
AudioPopout {
id: audioPopout
anchorItem: volIcon
triggerHovered: volIcon.hovered || micIcon.hovered
}
BatteryPopout {
id: battPopout
anchorItem: battIcon
triggerHovered: battIcon.hovered
visible: Battery.available && shown
}
}
-108
View File
@@ -1,108 +0,0 @@
import QtQuick
import "root:/config"
import "root:/components"
// One indicator inside the status cluster: a glyph, an optional readout, and a
// crimson underline that wipes in on hover.
Item {
id: root
property string icon: ""
property string label: ""
property color tint: Theme.primary
property bool alert: false
property bool dim: false
property bool pulsing: false
// Fixed label width. Readouts that change width constantly (throughput,
// percentages) would otherwise shove the whole cluster around.
property real labelWidth: 0
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitWidth: layout.implicitWidth
implicitHeight: Theme.barHeight - Theme.padS * 2
Row {
id: layout
anchors.centerIn: parent
spacing: 5
Icon {
id: glyph
anchors.verticalCenter: parent.verticalCenter
text: root.icon
pulsing: root.pulsing
color: {
if (root.alert) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
visible: root.label !== ""
text: root.label
width: root.labelWidth > 0 ? root.labelWidth : implicitWidth
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
color: root.dim ? Theme.muted : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
height: 2
width: root.hovered ? layout.implicitWidth : 0
color: root.alert ? Theme.accent : root.tint
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
scale: root.hovered ? 1.06 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y)
}
}
+35 -21
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Widgets import Quickshell.Widgets
@@ -15,10 +14,36 @@ Column {
id: root id: root
spacing: Theme.padS + 2 spacing: Theme.padS + 2
visible: SystemTray.items.values.length > 0 visible: root.shownItems.length > 0
// Applets whose whole job the rail already does. blueman keeps running as
// the pairing agent, but its icon was a second bluetooth glyph a few slots
// below the rail's own.
readonly property var hiddenIds: ["blueman"]
readonly property var shownItems: SystemTray.items.values
.filter(item => root.hiddenIds.indexOf(item.id) === -1)
// Electron and Chromium apps send only a full-colour pixmap, and flattening
// that loses everything inside the outline: Brave's lion became a plain
// shield. The icon theme often ships a panel glyph drawn for exactly this
// job, so look one up by the app's name and use the pixmap only when none
// exists. The id is no help here ("chrome_status_icon_1" for Brave and
// Proton Pass alike), so the tooltip title is the name we go by.
function themedIcon(item: SystemTrayItem): string {
if (item.icon.startsWith("image://icon/")) return item.icon;
const name = (item.tooltipTitle || item.title || item.id)
.toLowerCase().replace(/[^a-z0-9]+/g, "-");
for (const suffix of ["-tray", "-panel", "-symbolic"]) {
const path = Quickshell.iconPath(name + suffix, true);
if (path !== "") return path;
}
return item.icon;
}
Repeater { Repeater {
model: SystemTray.items model: root.shownItems
Item { Item {
id: entry id: entry
@@ -31,38 +56,27 @@ Column {
// App icons arrive in their own brand colours, which wrecks the // App icons arrive in their own brand colours, which wrecks the
// two-hue palette faster than anything else in the shell. Flatten // two-hue palette faster than anything else in the shell. Flatten
// every one of them to a single-colour silhouette: white at rest, // every one of them to a single-colour silhouette: subtext at rest,
// crimson under the pointer. Detail inside the glyph is lost on // a notch under the rail's own glyphs, and accent under the pointer. Detail inside the glyph is lost on
// purpose — that flattening *is* the screen-printed look. // purpose — that flattening *is* the screen-printed look.
IconImage { IconImage {
id: img id: img
anchors.fill: parent anchors.fill: parent
source: entry.modelData.icon source: root.themedIcon(entry.modelData)
asynchronous: true asynchronous: true
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: TintEffect {
colorization: 1.0 tintColor: itemHover.hovered
colorizationColor: itemHover.hovered
? Theme.accent ? Theme.accent
: Theme.text : Theme.subtext
Behavior on colorizationColor { Behavior on tintColor {
ColorAnimation { duration: Theme.durFast } ColorAnimation { duration: Theme.durFast }
} }
} }
opacity: itemHover.hovered ? 1.0 : 0.82
Behavior on opacity {
NumberAnimation { duration: Theme.durFast }
}
} }
scale: itemHover.hovered ? 1.18 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
HoverHandler { HoverHandler {
id: itemHover id: itemHover
+84 -18
View File
@@ -4,8 +4,8 @@ import Quickshell.Hyprland
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// Workspace indicator: leaning ticks that stretch open when focused. Occupied // Workspace indicator: soft pills that stretch open when focused. Occupied
// workspaces glow teal, urgent ones snap to crimson. // workspaces fill in, urgent ones snap to the alarm colour.
Item { Item {
id: root id: root
@@ -65,38 +65,51 @@ Item {
// Every tick is the same height and the same shear. Only the // Every tick is the same height and the same shear. Only the
// width changes, and only to say which workspace is focused. // width changes, and only to say which workspace is focused.
width: focused ? 32 : (occupied ? 16 : 9) //
height: 18 // Kept well short of the bar's 20px divider rules. At 18px an
// empty outline tick was the same height, lean and weight as a
// divider, and the workspace strip read as a row of rules.
width: focused ? 30 : (occupied ? 16 : 8)
height: 14
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Theme.durSlow duration: Theme.durSlow
easing.type: Easing.OutBack easing.type: Easing.OutCubic
easing.overshoot: 1.6
} }
} }
Skew { Skew {
id: tick id: tick
anchors.fill: parent anchors.fill: parent
radius: height / 2
// Focused is a solid crimson block. Occupied is a white // Focused is a solid accent block. Urgent is a solid alarm
// block at half weight. Empty is an outline. Three states, // block — it used to be the accent too, which made an
// two colours, no ambiguity. // urgent workspace indistinguishable from the focused one.
// Occupied is a subtext block. Empty is a flat recessed
// slot, filled rather than outlined so it cannot pass for a
// divider.
color: { color: {
if (pip.focused || pip.urgent) return Theme.accent; if (pip.focused) return Theme.accent;
if (pip.urgent) return Theme.alarm;
if (pip.hovered) return Theme.text; if (pip.hovered) return Theme.text;
if (pip.occupied) return Theme.alpha(Theme.text, 0.5); if (pip.occupied) return Theme.subtext;
return "transparent"; return Theme.surfaceAlt;
} }
borderColor: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
borderWidth: 1
Behavior on color { Behavior on color {
ColorAnimation { duration: Theme.durBase } ColorAnimation { duration: Theme.durBase }
} }
// Urgent is the one pip that has to be seen peripherally.
SequentialAnimation on opacity {
running: pip.urgent && !pip.focused
loops: Animation.Infinite
alwaysRunToEnd: true
NumberAnimation { to: 0.4; duration: 520; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 520; easing.type: Easing.InOutQuad }
}
} }
// The focused workspace wears its number. // The focused workspace wears its number.
@@ -107,9 +120,8 @@ Item {
text: pip.wsId text: pip.wsId
color: Theme.ink color: Theme.ink
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering renderType: Text.NativeRendering
Behavior on opacity { Behavior on opacity {
@@ -121,6 +133,7 @@ Item {
Rectangle { Rectangle {
visible: pip.info !== undefined && pip.info.fullscreen && !pip.focused visible: pip.info !== undefined && pip.info.fullscreen && !pip.focused
width: 4; height: 4 width: 4; height: 4
radius: 2
color: Theme.accent color: Theme.accent
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.bottom anchors.top: parent.bottom
@@ -142,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 QtQuick
import "root:/config" 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 { FocusScope {
id: root id: root
@@ -11,7 +12,9 @@ FocusScope {
property bool destructive: false property bool destructive: false
property bool selected: false property bool selected: false
property real lean: Theme.skew 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 property bool wide: false
signal clicked() signal clicked()
@@ -21,6 +24,12 @@ FocusScope {
readonly property bool focusedOrSelected: root.activeFocus || root.selected readonly property bool focusedOrSelected: root.activeFocus || root.selected
readonly property bool highlighted: root.hovered || root.focusedOrSelected readonly property bool highlighted: root.hovered || root.focusedOrSelected
// 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 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 // Pointer activation must not turn a transient action into the black/white
// keyboard-focus treatment. Tab still reaches it, and programmatic focus // keyboard-focus treatment. Tab still reaches it, and programmatic focus
// (power-menu roving selection / pinned popouts) still works. // (power-menu roving selection / pinned popouts) still works.
@@ -39,30 +48,31 @@ FocusScope {
implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2) implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2)
implicitHeight: 44 implicitHeight: 44
P5Panel { Panel {
id: panel id: panel
anchors.fill: parent anchors.fill: parent
lean: root.lean lean: root.lean
padding: Theme.padS padding: Theme.padS
fill: { fill: {
if (!root.enabled) return Theme.treatmentDisabledFill; if (!root.enabled) return Theme.treatmentDisabledFill;
if (root.destructive) return Theme.treatmentDestructiveFill; if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveFill
: Theme.treatmentDestructiveFill;
if (root.inverted) return Theme.treatmentFocusFill;
if (root.highlighted) return Theme.treatmentHoverFill; if (root.highlighted) return Theme.treatmentHoverFill;
return Theme.treatmentRestFill; return Theme.treatmentRestFill;
} }
fillOpacity: root.enabled ? 1.0 : 0.72 fillOpacity: root.enabled ? 1.0 : 0.72
border: { border: {
if (!root.enabled) return Theme.treatmentDisabledMark; if (!root.enabled) return Theme.treatmentDisabledMark;
if (root.destructive) return Theme.treatmentFocusMark; if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveMark
: Theme.treatmentDestructiveMark;
if (root.inverted) return Theme.treatmentFocusMark;
if (root.highlighted) return Theme.treatmentHoverMark; if (root.highlighted) return Theme.treatmentHoverMark;
return Theme.alpha(root.accent, 0.55); return Theme.alpha(root.accent, 0.55);
} }
borderWidth: Theme.stroke borderWidth: Theme.stroke
slash: !root.highlighted
slashColor: root.destructive ? Theme.treatmentDestructiveMark : root.accent
halftone: root.enabled && root.highlighted
halftoneColor: Theme.ink
halftoneOpacity: 0.14
Behavior on fillOpacity { Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast } NumberAnimation { duration: Theme.durFast }
@@ -74,6 +84,21 @@ FocusScope {
anchors.left: parent.left anchors.left: parent.left
spacing: Theme.padM 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 { Icon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.icon !== "" visible: root.icon !== ""
@@ -81,26 +106,32 @@ FocusScope {
font.pixelSize: Theme.fsTitle font.pixelSize: Theme.fsTitle
color: { color: {
if (!root.enabled) return Theme.treatmentDisabledText; if (!root.enabled) return Theme.treatmentDisabledText;
if (root.destructive) return Theme.treatmentDestructiveText; if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveText
: Theme.treatmentDestructiveText;
if (root.inverted) return Theme.treatmentFocusText;
if (root.highlighted) return Theme.treatmentHoverText; if (root.highlighted) return Theme.treatmentHoverText;
return root.accent; return root.accent;
} }
} }
Text { Text {
id: label
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root.text text: root.text
color: { color: {
if (!root.enabled) return Theme.treatmentDisabledText; if (!root.enabled) return Theme.treatmentDisabledText;
if (root.destructive) return Theme.treatmentDestructiveText; if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveText
: Theme.treatmentDestructiveText;
if (root.inverted) return Theme.treatmentFocusText;
if (root.highlighted) return Theme.treatmentHoverText; if (root.highlighted) return Theme.treatmentHoverText;
return Theme.treatmentRestText; return Theme.treatmentRestText;
} }
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
font.italic: true font.letterSpacing: Theme.tracking
font.letterSpacing: 1.5
font.capitalization: Font.AllUppercase font.capitalization: Font.AllUppercase
renderType: Text.NativeRendering renderType: Text.NativeRendering
@@ -111,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 { transform: Translate {
x: root.pressed ? 0 : (root.highlighted ? root.lunge : 0) x: root.pressed ? 0 : (root.highlighted ? root.lunge : 0)
y: root.pressed ? 1 : 0
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
duration: Theme.durBase 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 { HoverHandler {
id: hover id: hover
+4 -9
View File
@@ -3,8 +3,8 @@ import "root:/config"
import "root:/components" import "root:/components"
// Calm, permanent-chrome preset. It is deliberately flat and opaque: one // Calm, permanent-chrome preset. It is deliberately flat and opaque: one
// neutral keyline, no texture, no offset copy, and no accent unless active. // neutral hairline, no texture, no ticks, and no accent unless active.
P5Panel { Panel {
property bool active: false property bool active: false
property color tone: Theme.treatmentActiveMark property color tone: Theme.treatmentActiveMark
@@ -12,16 +12,11 @@ P5Panel {
fillOpacity: 1.0 fillOpacity: 1.0
border: Theme.treatmentRestMark border: Theme.treatmentRestMark
borderWidth: 1 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 halftone: false
sheen: false sheen: false
slash: active slash: active
slashColor: tone slashColor: tone
slashWidth: 4 slashWidth: 2
lean: 0.06
chop: Theme.cut
padding: Theme.padS padding: Theme.padS
radius: Theme.radiusM
} }
-120
View File
@@ -1,120 +0,0 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
// Radial gauge used across the resource popout. Sweeps a 270° arc with a
// tick-marked rail behind it and the reading set in the middle.
Item {
id: root
property real value: 0 // 0..100
property string label: ""
property string readout: Math.round(value) + "%"
property string sub: ""
property color arcColor: Theme.heat(value)
property real thickness: 7
property real startAngle: 135
property real sweep: 270
property int ticks: 24
implicitWidth: 96
implicitHeight: 96
readonly property real _r: Math.min(width, height) / 2 - thickness / 2 - 4
readonly property real _cx: width / 2
readonly property real _cy: height / 2
// Rail
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: Theme.alpha(Theme.outline, 0.5)
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep
}
}
}
// Tick marks around the rail.
Repeater {
model: root.ticks
Rectangle {
required property int index
readonly property real a: (root.startAngle + root.sweep * (index / (root.ticks - 1))) * Math.PI / 180
readonly property real rr: root._r + root.thickness / 2 + 3
width: 2
height: index % 4 === 0 ? 6 : 3
color: Theme.alpha(Theme.primary, index % 4 === 0 ? 0.55 : 0.25)
x: root._cx + Math.cos(a) * rr - width / 2
y: root._cy + Math.sin(a) * rr - height / 2
rotation: (root.startAngle + root.sweep * (index / (root.ticks - 1))) + 90
}
}
// Value arc
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: root.arcColor
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
id: valueArc
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep * Math.max(0, Math.min(100, root.value)) / 100
Behavior on sweepAngle {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
}
}
}
Column {
anchors.centerIn: parent
spacing: -2
SplitText {
anchors.horizontalCenter: parent.horizontalCenter
text: root.readout
pixelSize: Theme.fsLarge
color: Theme.text
split: 1.2
splitOpacity: 0.6
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.label
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: root.sub !== ""
text: root.sub
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
+24 -1
View File
@@ -3,7 +3,11 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell 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. // get chopped rather than rounded.
Singleton { Singleton {
id: root id: root
@@ -86,6 +90,25 @@ Singleton {
return root.closed(out); 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 { function closed(pts: var): var {
if (pts.length === 0) return pts; if (pts.length === 0) return pts;
const out = pts.slice(); 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 color: Theme.primary
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter 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 { Behavior on color {
ColorAnimation { duration: Theme.durBase } ColorAnimation { duration: Theme.durBase }
@@ -35,11 +39,11 @@ Text {
id: bumpAnim id: bumpAnim
NumberAnimation { NumberAnimation {
target: root; property: "scale" 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 { NumberAnimation {
target: root; property: "scale" 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:/config"
import "root:/components" 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 { Item {
id: root id: root
@@ -30,11 +30,14 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right 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.
color: root.activeFocus color: root.activeFocus
? Theme.treatmentFocusFill ? Theme.surfaceAlt
: Theme.alpha(root.enabled ? Theme.outline : Theme.treatmentDisabledMark, 0.7) : Theme.alpha(root.enabled ? Theme.outline : Theme.treatmentDisabledMark, 0.7)
borderColor: root.activeFocus ? Theme.treatmentFocusMark : "transparent" borderColor: root.activeFocus ? Theme.accent : "transparent"
borderWidth: root.activeFocus ? Theme.stroke : 0 borderWidth: root.activeFocus ? Theme.stroke : 0
Behavior on height { Behavior on height {
@@ -47,7 +50,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
width: Math.max(0, Math.min(1, root.value)) * root.width 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 color: root.enabled ? root.accent : Theme.treatmentDisabledText
Behavior on width { Behavior on width {
@@ -63,14 +66,15 @@ Item {
// Handle // Handle
Skew { Skew {
id: handle id: handle
width: 6 width: height
height: root.activeFocus || hover.hovered || drag.active ? 18 : 14 radius: height / 2
height: root.activeFocus || hover.hovered || drag.active ? 12 : 10
color: root.enabled ? Theme.text : Theme.treatmentDisabledText color: root.enabled ? Theme.text : Theme.treatmentDisabledText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(1, root.value)) * root.width - width / 2 x: Math.max(0, Math.min(1, root.value)) * root.width - width / 2
Behavior on height { Behavior on height {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack } NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutCubic }
} }
Behavior on x { Behavior on x {
enabled: !drag.active enabled: !drag.active
+120
View File
@@ -0,0 +1,120 @@
import QtQuick
import "root:/config"
// One row of a popout list — a Wi-Fi network, a bluetooth device, an audio
// output. The pickers used to draw their own rows, each with its own height and
// its own translucent white wash for hover, which on Mocha's blue-grey ground
// 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 a dot at its leading edge and
// an accent glyph, and its label goes to full weight.
Item {
id: root
property string icon: ""
property string label: ""
// Optional mono line under the label: a device state, an address.
property string detail: ""
property bool current: false
// Right-aligned trailing content — a lock, a battery level, a tag.
default property alias trailing: trailingRow.data
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
implicitHeight: root.detail !== "" ? 36 : 30
Rectangle {
anchors.fill: parent
radius: Theme.radiusM
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 2
width: root.current ? Theme.marker : 0
height: width
radius: width / 2
color: Theme.accent
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic }
}
}
Icon {
id: glyph
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
width: 18
visible: root.icon !== ""
text: root.icon
color: root.current ? Theme.accent : (root.hovered ? Theme.text : Theme.subtext)
font.pixelSize: Theme.fsLarge
}
Column {
anchors.left: glyph.visible ? glyph.right : parent.left
anchors.leftMargin: glyph.visible ? Theme.padS : Theme.padM
anchors.right: trailingRow.left
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: -1
Text {
width: parent.width
text: root.label
elide: Text.ElideRight
color: root.current || root.hovered ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: root.current ? Font.DemiBold : Font.Medium
renderType: Text.NativeRendering
}
Text {
width: parent.width
visible: text !== ""
text: root.detail
elide: Text.ElideRight
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
Row {
id: trailingRow
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: 4
}
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
}
+24 -29
View File
@@ -2,8 +2,12 @@ import QtQuick
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// A segmented load meter — slanted ticks that light up left to right. Reads as // A load meter: a soft rounded track with the level filling it from the left.
// a gauge at a glance and keeps the panel language consistent. // 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 { Item {
id: root id: root
@@ -12,42 +16,33 @@ Item {
property real segmentWidth: 4 property real segmentWidth: 4
property real spacing: 3 property real spacing: 3
property color activeColor: Theme.heat(value) 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 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 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 anchors.fill: parent
spacing: root.spacing radius: height / 2
color: root.idleColor
}
Repeater { Rectangle {
model: root.segments height: parent.height
// Never narrower than its own height, so a near-zero level is a dot
Item { // rather than a sliver.
required property int index width: root.fraction > 0 ? Math.max(height, parent.width * root.fraction) : 0
width: root.segmentWidth radius: height / 2
height: root.height color: root.activeColor
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
Behavior on width {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutCubic }
}
Behavior on color { Behavior on color {
ColorAnimation { duration: Theme.durBase } ColorAnimation { duration: Theme.durBase }
} }
} }
} }
}
}
}
+3 -5
View File
@@ -2,8 +2,8 @@ import QtQuick
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// One metric in a popout: a label and its reading on a line, with a wide // One metric in a popout: a label and its reading on a line, with a thin
// sheared segmented meter underneath. // segmented meter underneath.
// //
// This replaces the radial Gauge. A 270° dial was the only curved thing in a // 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 // shell whose whole geometry is sheared slabs and hard chamfers, so it read as
@@ -70,8 +70,6 @@ Item {
text: root.readout text: root.readout
pixelSize: Theme.fsBody pixelSize: Theme.fsBody
color: Theme.text color: Theme.text
split: 1
splitOpacity: 0.5
} }
} }
@@ -80,7 +78,7 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
height: 12 height: 4
segments: root.segments segments: root.segments
spacing: root.segmentSpacing spacing: root.segmentSpacing
// Fill the row exactly rather than leaving a ragged tail on the right. // Fill the row exactly rather than leaving a ragged tail on the right.
+165 -71
View File
@@ -1,93 +1,167 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
import "root:/services" 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 { Item {
id: root id: root
required property var notif required property var notif
property bool showActions: true 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 readonly property color tone: root.notif
? Notifs.urgencyColor(root.notif.urgency) ? Notifs.urgencyColor(root.notif.urgency)
: Theme.primary : 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() signal dismissed()
// The card's own geometry, published so anything drawn along its edges — implicitHeight: layout.implicitHeight + Theme.padM * 2
// 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: 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 { Behavior on color {
id: panel ColorAnimation { duration: Theme.durFast }
width: parent.width }
height: layout.implicitHeight + Theme.padM * 2 }
lean: root.lean
chop: root.chop
fill: Theme.surface
fillOpacity: 0.97
border: Theme.alpha(root.tone, hover.hovered ? 0.95 : 0.5)
slash: true
slashColor: root.tone
slashWidth: 4
halftone: root.notif?.urgency === NotificationUrgency.Critical
halftoneColor: Theme.accent
halftoneOpacity: 0.12
padding: Theme.padM
Column { Column {
id: layout id: layout
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.top: parent.top
spacing: 3 anchors.margins: Theme.padM
spacing: 4
// App line // Sender: marker, glyph, name, then the hairline to the edge.
Row { Item {
spacing: Theme.padS 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 { Icon {
id: glyph
visible: root.showSender && text !== ""
anchors.left: marker.right
anchors.leftMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: Notifs.appIconFor(root.notif) text: Notifs.appIconFor(root.notif)
color: root.tone color: root.critical ? Theme.alarm : Theme.muted
font.pixelSize: Theme.fsBody font.pixelSize: Theme.fsBody
} }
Text { Text {
id: sender
anchors.left: glyph.visible ? glyph.right : marker.right
anchors.leftMargin: root.showSender ? Theme.padS : 0
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: root.showSender ? implicitWidth : 0
visible: root.showSender
text: (root.notif?.appName || "System").toUpperCase() text: (root.notif?.appName || "System").toUpperCase()
color: root.tone color: root.critical ? Theme.alarm : Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 2 font.letterSpacing: 2
renderType: Text.NativeRendering 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 { Text {
width: parent.width width: parent.width
leftPadding: Theme.marker + Theme.padS
text: root.notif?.summary || "" text: root.notif?.summary || ""
color: Theme.text color: root.hovered ? Theme.accent : Theme.text
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 2 maximumLineCount: 2
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody font.pixelSize: Theme.fsBody
font.weight: Font.Black font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
} }
Text { Text {
width: parent.width width: parent.width
leftPadding: Theme.marker + Theme.padS
visible: text !== "" visible: text !== ""
text: root.notif?.body || "" text: root.notif?.body || ""
color: Theme.subtext color: Theme.subtext
@@ -97,53 +171,93 @@ Item {
textFormat: Text.StyledText textFormat: Text.StyledText
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall font.pixelSize: Theme.fsSmall
lineHeight: 1.1
renderType: Text.NativeRendering 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 { Image {
visible: source != "" && status === Image.Ready id: inline
source: root.notif?.image || "" 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 sourceSize.height: 90
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
asynchronous: true asynchronous: true
} }
}
Row { Row {
x: Theme.marker + Theme.padS
visible: root.showActions && (root.notif?.actions?.length ?? 0) > 0 visible: root.showActions && (root.notif?.actions?.length ?? 0) > 0
spacing: Theme.padS spacing: Theme.padS
topPadding: Theme.padS topPadding: 4
Repeater { Repeater {
model: root.notif?.actions ?? [] model: root.notif?.actions ?? []
// A hairline pill. Hover lifts and lights it; it never inverts.
Item { Item {
id: actionChip id: actionChip
required property var modelData required property var modelData
implicitWidth: actionLabel.implicitWidth + Theme.padM implicitWidth: chipRow.implicitWidth + Theme.padM * 2
implicitHeight: 22 implicitHeight: 24
Skew { Rectangle {
anchors.fill: parent anchors.fill: parent
color: actionHover.hovered radius: height / 2
? Theme.alpha(root.tone, 0.3) color: actionHover.hovered ? Theme.treatmentHoverFill : "transparent"
: Theme.alpha(root.tone, 0.1) border.width: 1
borderWidth: 1 border.color: actionHover.hovered ? Theme.treatmentHoverMark : Theme.overlay
borderColor: Theme.alpha(root.tone, 0.6)
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 { Text {
id: actionLabel id: actionLabel
anchors.centerIn: parent anchors.verticalCenter: parent.verticalCenter
text: actionChip.modelData.text text: actionChip.modelData.text.toUpperCase()
color: Theme.text color: actionHover.hovered ? Theme.accent : Theme.text
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.italic: true font.letterSpacing: Theme.tracking
font.weight: Font.DemiBold
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
}
HoverHandler { HoverHandler {
id: actionHover id: actionHover
@@ -161,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 { HoverHandler {
id: hover id: hover
} }
-103
View File
@@ -1,103 +0,0 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
import "root:/components"
// The shared ground for the two full-screen surfaces: a crimson wedge leaning
// at the house angle, a tiled stripe field that sweeps in from the left, and a
// halftone dot wash over the top.
//
// The lock screen and the power menu each used to draw their own version of
// this. They disagreed — the power menu had a wedge and a full stripe field,
// the lock had a 42px stripe band and no wedge, and the wedge leaned at 0.32
// where every panel in the shell leans at Theme.skew. Building both on one
// component is what makes them read as the same object seen twice rather than
// as two guesses at the same brief.
Item {
id: root
// 0 = withdrawn, 1 = fully present. Drives the stripe slide, the stripe
// opacity and the wedge together, so a caller animates one number instead
// of keeping three in sync. The power menu runs it both ways on open and
// close; the lock runs it once on entry.
property real reveal: 1
property color tone: Theme.accent
property bool wedge: true
property real wedgeOpacity: 0.35
// Bottom-left corner of the wedge and its span, as fractions of width. The
// wedge leans right going up, like every other edge in the shell, so its
// top edge sits `leanOffset` further right than its bottom.
property real wedgeLeft: 0
property real wedgeSpan: 0.34
property bool stripes: true
property real stripeOpacity: 0.10
property bool halftone: true
property real halftoneOpacity: 0.05
property color halftoneColor: Theme.primary
// ONE angle, everywhere — the same rule the panels follow.
readonly property real leanOffset: root.height * Theme.skew
// Diagonal stripe field, sliding in from off-screen left.
Image {
anchors.fill: parent
visible: root.stripes && root.reveal > 0
source: Theme.texStripes
fillMode: Image.Tile
opacity: root.stripeOpacity * root.reveal
smooth: false
layer.enabled: true
layer.effect: TintEffect {
tintColor: root.tone
}
transform: Translate {
x: -root.width * (1 - root.reveal)
}
}
// The wedge. Drawn after the stripes so it reads as a slab laid over them.
Shape {
anchors.fill: parent
visible: root.wedge && root.reveal > 0
opacity: root.reveal
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.alpha(Theme.accentDim, root.wedgeOpacity)
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: {
const x = root.wedgeLeft * root.width;
const span = root.wedgeSpan * root.width;
const off = root.leanOffset;
return Geom.closed([
Qt.point(x + off, 0),
Qt.point(x + off + span, 0),
Qt.point(x + span, root.height),
Qt.point(x, root.height)
]);
}
}
}
}
// Screen-printed dot wash over the whole thing.
Image {
anchors.fill: parent
visible: root.halftone
source: Theme.texHalftone
fillMode: Image.Tile
opacity: root.halftoneOpacity
smooth: false
layer.enabled: true
layer.effect: TintEffect { tintColor: root.halftoneColor }
}
}
@@ -4,8 +4,12 @@ import QtQuick.Effects
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// The base surface for everything in this shell: a leaning, corner-chopped slab // The base surface for everything in this shell. Under the YoRHa theme it is a
// with an optional halftone wash and a crimson slash along its leading edge. // 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 { Item {
id: root id: root
@@ -29,12 +33,16 @@ Item {
property real chop: Theme.cut property real chop: Theme.cut
property var chopCorners: [1, 3] property var chopCorners: [1, 3]
// Crimson 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 bool slash: false
property color slashColor: Theme.accent property color slashColor: Theme.accent
property real slashWidth: 4 property real slashWidth: 4
// Screen-printed dot wash. // Dot-grid wash.
property bool halftone: false property bool halftone: false
property color halftoneColor: Theme.primary property color halftoneColor: Theme.primary
property real halftoneOpacity: 0.07 property real halftoneOpacity: 0.07
@@ -45,19 +53,28 @@ Item {
// call sites that still set it don't have to be chased down. // call sites that still set it don't have to be chased down.
property bool sheen: false property bool sheen: false
// The screen-print stack. A pure-black copy of the silhouette sits behind // Optional print stack: an ink copy of the silhouette offset behind the
// the fill, offset down-and-right, and the fill carries a black keyline // fill, and an ink keyline outside the border. Both off by default — on a
// outside its coloured border. Together these are what stop the panel from // dark ground they only thicken the edge.
// reading as a flat clip-path. property bool drop: false
property bool drop: true
property real dropOffset: Theme.dropOffset property real dropOffset: Theme.dropOffset
property color dropColor: Theme.ink property color dropColor: Theme.ink
property bool keyline: true property bool keyline: false
property real keylineWidth: Theme.keylineWidth 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: readonly property real leanInset:
(vertical ? width : height) * Math.abs(lean) (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 readonly property var polygon: vertical
? (leanLeft ? Geom.parallelogramVL(width, height, lean) ? (leanLeft ? Geom.parallelogramVL(width, height, lean)
: Geom.parallelogramV(width, height, lean)) : Geom.parallelogramV(width, height, lean))
@@ -90,8 +107,8 @@ Item {
strokeColor: "transparent" strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin joinStyle: ShapePath.MiterJoin
PathPolyline { PathSvg {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners) path: root.outline
} }
} }
} }
@@ -114,8 +131,8 @@ Item {
strokeColor: "transparent" strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin joinStyle: ShapePath.MiterJoin
PathPolyline { PathSvg {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners) path: root.outline
} }
} }
} }
@@ -139,8 +156,8 @@ Item {
joinStyle: ShapePath.MiterJoin joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap capStyle: ShapePath.FlatCap
PathPolyline { PathSvg {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners) path: root.outline
} }
} }
@@ -152,8 +169,8 @@ Item {
joinStyle: ShapePath.MiterJoin joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap capStyle: ShapePath.FlatCap
PathPolyline { PathSvg {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners) path: root.outline
} }
} }
} }
@@ -165,7 +182,7 @@ Item {
asynchronous: true asynchronous: true
sourceComponent: Image { sourceComponent: Image {
anchors.fill: parent anchors.fill: parent
source: Theme.texHalftone source: Theme.texDots
fillMode: Image.Tile fillMode: Image.Tile
opacity: root.halftoneOpacity opacity: root.halftoneOpacity
smooth: false smooth: false
@@ -183,7 +200,7 @@ Item {
// //
// Its polygon is built from the panel's raw corners, so on a panel with a // Its polygon is built from the panel's raw corners, so on a panel with a
// chopped corner the slash used to run straight through the cut and out the // chopped corner the slash used to run straight through the cut and out the
// far side — a crimson wedge sitting a few pixels outside the silhouette, // far side — an accent wedge sitting a few pixels outside the silhouette,
// at an angle that matched nothing. It is masked to the outline now, so the // at an angle that matched nothing. It is masked to the outline now, so the
// slash ends exactly where the chamfer does. // slash ends exactly where the chamfer does.
Loader { Loader {
@@ -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 { Item {
id: contentHolder id: contentHolder
anchors.fill: parent anchors.fill: parent
+237 -76
View File
@@ -1,10 +1,22 @@
import QtQuick import QtQuick
import QtQuick.Shapes
import Quickshell import Quickshell
import "root:/config" 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 // 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 { PopupWindow {
id: root id: root
@@ -19,9 +31,14 @@ PopupWindow {
property Item initialFocusItem: null property Item initialFocusItem: null
property bool remappingPinned: false property bool remappingPinned: false
// Extra surface around the content so the entry overshoot and glow are not // Fillet radius, and the radius of the panel's own free corners — the same
// clipped by the window edge. // as the opening's, so a popout meets the frame the way the opening does.
readonly property int bleed: 24 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 default property alias body: contentHolder.data
@@ -31,28 +48,122 @@ PopupWindow {
signal dismissedFully() signal dismissedFully()
// Panels hanging off the vertical rail open sideways, so the entry slide has // Panels hanging off the vertical rail open sideways, out of its inner
// to follow whichever axis the popout actually travels along, and start on // edge; everything else drops from the underside of the bar.
// the side nearest its trigger.
readonly property bool horizontal: readonly property bool horizontal:
fromEdge === Edges.Left || fromEdge === Edges.Right 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 // Which edges of the frame the panel is attached to. A panel that would
anchor.edges: fromEdge // land within reach of the opening's corner docks into the corner instead,
anchor.gravity: fromEdge // attached to the bar and the rail at once — otherwise it leaves a thin
anchor.adjustment: horizontal // sliver of opening between itself and the rail, two walls a few pixels
? PopupAdjustment.SlideY // apart.
: PopupAdjustment.SlideX //
anchor.margins.top: Theme.popoutGap // "top" hangs from the bar fillets left and right
anchor.margins.bottom: Theme.popoutGap // "right" juts from the rail fillets above and below
anchor.margins.left: Theme.popoutGap // "topRight" fills the top-right corner fillets left and below
anchor.margins.right: Theme.popoutGap // "bottomRight" fills the bottom-right one fillets above and left
property string dock: horizontal ? "right" : "top"
implicitWidth: contentWidth + bleed * 2 // How close to a corner a panel may come before it docks into it.
implicitHeight: contentHeight + bleed * 2 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" color: "transparent"
visible: shown visible: shown
grabFocus: pinned && shown grabFocus: pinned && shown
@@ -97,6 +208,7 @@ PopupWindow {
function open() { function open() {
PopupCoordinator.claim(root); PopupCoordinator.claim(root);
root.place();
exitAnim.stop(); exitAnim.stop();
shown = true; shown = true;
enterAnim.restart(); enterAnim.restart();
@@ -152,10 +264,7 @@ PopupWindow {
remappingPinned = false; remappingPinned = false;
pinned = false; pinned = false;
shown = false; shown = false;
contentHolder.opacity = 0; root.reveal = 0;
contentHolder.scale = 0.94;
contentHolder.x = root.bleed;
contentHolder.y = root.bleed;
PopupCoordinator.release(root); PopupCoordinator.release(root);
root.dismissedFully(); 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 { Item {
id: contentHolder id: contentHolder
focus: root.pinned focus: root.pinned
x: root.bleed x: root.contentX
y: root.bleed y: root.contentY
width: root.contentWidth width: root.contentWidth
height: root.contentHeight height: root.contentHeight
opacity: 0 // Read by PopoutSurface, which leaves the fill and the frame to
transformOrigin: { // the hull when it finds itself in here.
switch (root.fromEdge) { readonly property bool frameAttached: true
case Edges.Left: return Item.Right;
case Edges.Right: return Item.Left;
case Edges.Top: return Item.Bottom;
default: return Item.Top;
}
}
scale: 0.94
// The handler has to live on the content item, not the window: a // The handler has to live on the content item, not the window:
// HoverHandler parented to the PopupWindow itself latches on and the // a HoverHandler parented to the PopupWindow itself latches on
// panel never learns the pointer left. // and the panel never learns the pointer left.
// //
// Disabled while the popout is down, which clears `hovered`. An unmapped // Disabled while the popout is down, which clears `hovered`. An
// surface gets no leave event, so a handler that was hovered when the // unmapped surface gets no leave event, so a handler that was
// window went away stayed hovered — `wantsOpen` never went false again // hovered when the window went away stayed hovered — `wantsOpen`
// and the panel came back up and refused to close. // never went false again and the panel came back up and refused
// to close.
HoverHandler { HoverHandler {
id: panelHover id: panelHover
enabled: root.shown enabled: root.shown
@@ -217,44 +400,22 @@ PopupWindow {
} }
} }
} }
}
}
ParallelAnimation { NumberAnimation {
id: enterAnim id: enterAnim
NumberAnimation { target: root; property: "reveal"
target: contentHolder; property: "opacity" to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
to: 1.0; duration: Theme.durBase; easing.type: Easing.OutExpo
}
NumberAnimation {
target: contentHolder; property: "scale"
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
}
NumberAnimation {
target: contentHolder; property: root.slideProperty
from: root.slideOrigin; to: root.bleed
duration: Theme.durSlow; easing.type: Easing.OutBack
}
} }
ParallelAnimation { NumberAnimation {
id: exitAnim id: exitAnim
NumberAnimation { target: root; property: "reveal"
target: contentHolder; property: "opacity" to: 0; duration: Theme.durBase; easing.type: Easing.InCubic
to: 0.0; duration: Theme.durFast; easing.type: Easing.InQuad
}
NumberAnimation {
target: contentHolder; property: "scale"
to: 0.96; duration: Theme.durFast; easing.type: Easing.InQuad
}
NumberAnimation {
target: contentHolder; property: root.slideProperty
to: root.bleed + (root.slideOrigin - root.bleed) * 0.6
duration: Theme.durFast; easing.type: Easing.InQuad
}
onFinished: { onFinished: {
root.shown = false; root.shown = false;
root.pinned = false; root.pinned = false;
contentHolder.x = root.bleed;
contentHolder.y = root.bleed;
PopupCoordinator.release(root); PopupCoordinator.release(root);
root.dismissedFully(); 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 }
}
}
}
+35
View File
@@ -0,0 +1,35 @@
import QtQuick
import "root:/config"
// Section caption for popouts: a spaced mono label running into a hairline.
// Every panel used to label its lists differently — a bare caption here, a
// caption and rule there — which is part of why no two popouts looked related.
Item {
id: root
property string text: ""
implicitHeight: visible ? 18 : 0
Text {
id: label
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.text.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: label.right
anchors.leftMargin: Theme.padS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.5)
}
}
+14 -6
View File
@@ -3,7 +3,8 @@ import QtQuick.Shapes
import "root:/config" import "root:/config"
import "root:/components" 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 // 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 // Theme.skew. Shearing a plain Rectangle with a Matrix4x4 does that, but Qt
@@ -22,15 +23,20 @@ Shape {
property color borderColor: "transparent" property color borderColor: "transparent"
property real borderWidth: 0 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 real lean: Theme.skew
property bool leanLeft: false property bool leanLeft: false
// Wide, short pieces — the rail's hairline rules — slant their horizontal // 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. // rail slab.
property bool vertical: false 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 // 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 // `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 // what is left: a Skew declared 1px wide and 20px tall does not draw a 1px
@@ -55,13 +61,15 @@ Shape {
joinStyle: ShapePath.MiterJoin joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap capStyle: ShapePath.FlatCap
PathPolyline { PathSvg {
path: { path: {
const w = root.width, h = root.height, l = root.lean; const w = root.width, h = root.height, l = root.lean;
if (l === 0)
return Geom.roundedRect(w, h, root.radius);
if (root.vertical) 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)); : 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)); : Geom.parallelogram(w, h, l));
} }
} }
+56 -46
View File
@@ -1,8 +1,13 @@
import QtQuick import QtQuick
import "root:/config" import "root:/config"
// Display text with the wallpaper's RGB split baked in: a crimson and a cyan // Display text: upright, widely tracked, sitting on a soft ink copy a pixel
// ghost sitting a pixel off the white body. Drives most headings in the shell. // down-right — the faint drop NieR puts under every line of its interface.
// Drives most headings in the shell.
//
// 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 { Item {
id: root id: root
@@ -10,25 +15,36 @@ Item {
property int pixelSize: Theme.fsLarge property int pixelSize: Theme.fsLarge
property int weight: Theme.weightDisplay property int weight: Theme.weightDisplay
property string family: Theme.fontDisplay property string family: Theme.fontDisplay
property bool italic: true property bool italic: false
property color color: Theme.text property color color: Theme.text
property real split: 1.6
property real splitOpacity: 0.85
property int elide: Text.ElideNone property int elide: Text.ElideNone
property int horizontalAlignment: Text.AlignLeft property int horizontalAlignment: Text.AlignLeft
property real letterSpacing: 0.5 property real letterSpacing: Theme.tracking
// 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 / 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
// `splitOpacity` is asking for the old permanent split and still gets it.
property real split: 1.6
property real splitOpacity: 0
// 0..1, driven only by `glitch()`. Kept apart from `split` and
// `splitOpacity` so the animation never has to remember a caller's values.
property real kick: 0
readonly property real ghostOffset: root.split + root.kick * 4
readonly property real ghostOpacity: Math.max(root.splitOpacity, root.kick * 0.85)
implicitWidth: body.implicitWidth implicitWidth: body.implicitWidth
implicitHeight: body.implicitHeight implicitHeight: body.implicitHeight
Text { component Layer: Text {
id: ghostRed
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: -root.split
anchors.topMargin: root.split * 0.4
text: root.text text: root.text
color: Theme.splitRed
opacity: root.splitOpacity
elide: root.elide elide: root.elide
horizontalAlignment: root.horizontalAlignment horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@@ -40,39 +56,33 @@ Item {
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Text { Layer {
id: ghostCyan anchors.leftMargin: root.shadowOffset
anchors.fill: parent anchors.rightMargin: -root.shadowOffset
anchors.leftMargin: root.split anchors.topMargin: root.shadowOffset
anchors.topMargin: -root.split * 0.4 anchors.bottomMargin: -root.shadowOffset
text: root.text color: root.shadowColor
color: Theme.splitCyan
opacity: root.splitOpacity
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
} }
Text { Layer {
visible: root.ghostOpacity > 0.01
anchors.leftMargin: -root.ghostOffset
anchors.topMargin: root.ghostOffset * 0.4
color: Theme.accent
opacity: root.ghostOpacity
}
Layer {
visible: root.ghostOpacity > 0.01
anchors.leftMargin: root.ghostOffset
anchors.topMargin: -root.ghostOffset * 0.4
color: Theme.alarm
opacity: root.ghostOpacity
}
Layer {
id: body id: body
anchors.fill: parent
text: root.text
color: root.color color: root.color
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
} }
// Kick the ghosts outward briefly — used on track changes, mode flips, etc. // Kick the ghosts outward briefly — used on track changes, mode flips, etc.
@@ -83,14 +93,14 @@ Item {
SequentialAnimation { SequentialAnimation {
id: glitchAnim id: glitchAnim
NumberAnimation { NumberAnimation {
target: root; property: "split" target: root; property: "kick"
to: 5.5; duration: Theme.durFast to: 1; duration: Theme.durFast
easing.type: Easing.OutExpo easing.type: Easing.OutExpo
} }
NumberAnimation { NumberAnimation {
target: root; property: "split" target: root; property: "kick"
to: 1.6; duration: Theme.durSlow to: 0; duration: Theme.durSlow
easing.type: Easing.OutBack easing.type: Easing.OutCubic
} }
} }
} }
+8 -2
View File
@@ -1,11 +1,17 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
// Layer effect that recolours whatever it is applied to — used to tint the // Layer effect that recolours whatever it is applied to into a flat silhouette
// white texture tiles without shipping a copy per colour. // of `tintColor` — used for the white texture tiles and for tray icons.
//
// Colorization alone keeps the source's lightness, so a dark icon came out as a
// dark shade of the tint and all but vanished against the rail. Brightness runs
// first and lifts every pixel to white, which leaves colorization nothing to
// preserve but the alpha.
MultiEffect { MultiEffect {
property color tintColor: "white" property color tintColor: "white"
brightness: 1.0
colorization: 1.0 colorization: 1.0
colorizationColor: tintColor colorizationColor: tintColor
} }
@@ -69,7 +69,6 @@ PopupWindow {
ChromeSurface { ChromeSurface {
anchors.fill: parent anchors.fill: parent
padding: root.pad padding: root.pad
lean: 0.06
Column { Column {
id: textColumn id: textColumn
@@ -84,7 +83,6 @@ PopupWindow {
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
+152 -100
View File
@@ -3,112 +3,135 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
// Takemi palette: crimson, black, white. Nothing else. // YoRHa dark: the NieR:Automata interface language, lit the way
// ~/.wallpapers/space-1.jpg is lit.
// //
// White does the work: text, icons, idle indicators. Crimson is the only accent // NieR's menus are sand paper and graphite — monochrome, hairline rules,
// and it always means the same thing — active, or wrong. Black is structure, // letter-spaced capitals, a small square marking where you are, and selection
// used as a hard keyline rather than as a shadow. There are no gradients in // shown by inverting the row rather than by colouring it. That language is
// this shell; every fill is flat. // 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.
// //
// The wallpaper is teal. The UI used to be teal too, and the two fought each // There is still no third colour. The only warm thing in the wallpaper is the
// other into a murky blue-grey. Letting the UI go monochrome-plus-red is what // tiny red of the hull's indicator lights, and that is what `alarm` is.
// lets the wallpaper read as the colour in the composition.
Singleton { Singleton {
id: root id: root
// ---------------------------------------------------------------- colors // ---------------------------------------------------------------- colors
readonly property color ink: "#000000" // 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: "#0a0a0c" // The void. Green-black rather than neutral so the shell and the
readonly property color mantle: "#0d0d10" // wallpaper read as one dark, not two.
readonly property color surface: "#141418" readonly property color base: "#0f110e"
readonly property color surfaceAlt: "#1c1c22" readonly property color mantle: "#0c0d0b"
readonly property color overlay: "#26262e" readonly property color surface: "#161915"
readonly property color outline: "#3a3a44" readonly property color surfaceAlt: "#20241f"
readonly property color overlay: "#383e35"
readonly property color text: "#ffffff" // The hull the desktop is framed by. Darker than anything inside the
readonly property color subtext: "#c8c8ce" // opening, so the frame reads as the inside of the ship and the windows as
readonly property color muted: "#6b6b70" // what you are looking out at.
readonly property color hull: root.mantle
readonly property color rim: "#2a2f28"
readonly property color outline: "#545c52"
// ------------------------------------------------------------ THE KNOB readonly property color text: "#ccd2cb"
// The one colour in this shell. Change this line and the entire accent readonly property color subtext: "#9fa7a2"
// recolours — everything below is derived from it, and nothing else in the readonly property color muted: "#79847a"
// palette carries hue at all.
// ------------------------------------------------------------ THE KNOBS
// Two colours, two meanings, and nothing else in the shell carries hue.
// //
// Keep it in step with `$accent` at the top of // `accent` is the planet light: anything live, focused or selected. It is
// ~/.config/hypr/modules/takemi.conf, which is what colours the Hyprland // barely a colour at all, which is the point — NieR marks attention with
// window borders. Those are the only two places the colour is written. // brightness and inversion, not with hue. `alarm` is the hull's red:
readonly property color accent: "#ff2d40" // anything destructive, failed or critical. Things that are merely off —
// 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: "#eef1ea"
readonly property color alarm: "#c8684f"
readonly property color accentDim: Qt.darker(root.accent, 1.9) readonly property color accentDim: "#8e978d"
readonly property color accentSoft: Qt.lighter(root.accent, 1.35) readonly property color accentSoft: "#bcc1bf"
// `primary` is the resting voice of the UI, which is now simply white. // `primary` is the resting voice of the UI — the sage text grey. Kept under
// Kept under the old name so the existing call sites don't all have to move. // the old name so the existing call sites don't all have to move.
readonly property color primary: root.text readonly property color primary: root.text
readonly property color primaryDim: "#9a9aa2" readonly property color primaryDim: "#8e978d"
readonly property color deep: root.surfaceAlt readonly property color deep: root.surfaceAlt
// `glow` is the hover highlight. It used to be a brighter teal, which now // `glow` is the hover highlight: pointing at a thing brightens it toward
// that everything rests at white would have made hover invisible — so it // the planet light, the same signal as focus at a lower volume.
// resolves to crimson. Pointing at a thing lights it up red.
readonly property color glow: root.accent readonly property color glow: root.accent
// ------------------------------------------------ semantic treatments // ------------------------------------------------ semantic treatments
// These tokens describe interaction meaning, not new palette entries. Each // These tokens describe interaction meaning, not new palette entries. Each
// state is composed only from Takemi's black, white, crimson, and neutral // state is composed only from ink, the grey ramp, the light and the red, so
// surfaces so callers do not invent one-off state colors. // callers do not invent one-off state colours.
readonly property color treatmentRestFill: root.surface 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 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 treatmentHoverMark: root.accent
readonly property color treatmentHoverText: root.text readonly property color treatmentHoverText: root.accent
readonly property color treatmentFocusFill: root.text // 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 treatmentFocusMark: root.ink
readonly property color treatmentFocusText: root.ink readonly property color treatmentFocusText: root.ink
readonly property color treatmentSelectedFill: root.treatmentFocusFill readonly property color treatmentSelectedFill: root.treatmentFocusFill
readonly property color treatmentSelectedMark: root.treatmentFocusMark readonly property color treatmentSelectedMark: root.treatmentFocusMark
readonly property color treatmentSelectedText: root.treatmentFocusText 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 treatmentActiveMark: root.accent
readonly property color treatmentActiveText: root.text readonly property color treatmentActiveText: root.accent
// Crimson carries destructive/error meaning. Text and keylines stay black: // Red carries destructive/error meaning — never the light, so a dangerous
// black-on-crimson is 5.70:1, while small white-on-crimson is only 3.68:1. // button can never be mistaken for a merely focused one.
readonly property color treatmentDestructiveFill: root.accent //
readonly property color treatmentDestructiveMark: root.ink // At rest it is a hairline and a label, not a fill. The solid red is held
readonly property color treatmentDestructiveText: root.ink // back for the moment the pointer or the selection is actually on the
readonly property color treatmentErrorFill: root.treatmentDestructiveFill // thing, with ink punched out of it.
readonly property color treatmentErrorMark: root.treatmentDestructiveMark readonly property color treatmentDestructiveFill: root.surface
readonly property color treatmentErrorText: root.treatmentDestructiveText readonly property color treatmentDestructiveMark: root.alarm
readonly property color treatmentDestructiveText: root.alarm
readonly property color treatmentDestructiveActiveFill: root.alarm
readonly property color treatmentDestructiveActiveMark: root.ink
readonly property color treatmentDestructiveActiveText: root.ink
readonly property color treatmentErrorFill: root.treatmentDestructiveActiveFill
readonly property color treatmentErrorMark: root.treatmentDestructiveActiveMark
readonly property color treatmentErrorText: root.treatmentDestructiveActiveText
readonly property color treatmentUrgentFill: root.surface readonly property color treatmentUrgentFill: root.surface
readonly property color treatmentUrgentMark: root.accent readonly property color treatmentUrgentMark: root.alarm
readonly property color treatmentUrgentText: root.text readonly property color treatmentUrgentText: root.text
readonly property color treatmentDisabledFill: root.surface readonly property color treatmentDisabledFill: root.surface
readonly property color treatmentDisabledMark: root.outline readonly property color treatmentDisabledMark: root.overlay
readonly property color treatmentDisabledText: root.muted readonly property color treatmentDisabledText: root.muted
// Retired hues. Aliases only — do not introduce new uses. // Off is a state the user chose — muted, do-not-disturb, radio switched
// // off. It is neither live nor wrong, so it sinks into the grey ramp.
// `warn` used to be accentSoft, which put a washed-out pink-red on screen readonly property color treatmentOffFill: root.surface
// wherever something was merely notable. That reads as a faded accent, not readonly property color treatmentOffMark: root.overlay
// as a decision. There is no half-crimson in this palette: a thing is wrong readonly property color treatmentOffText: root.muted
// (accent) or it is not (text). `warn` now resolves to the real accent, so
// low battery shouts properly and everything merely informational is white.
readonly property color blue: root.primaryDim
readonly property color ok: root.text
readonly property color warn: root.accent
// Display text carries a crimson and a black offset rather than an RGB
// split. Same trick, correct palette.
readonly property color splitRed: root.accent
readonly property color splitCyan: root.ink
function alpha(c: color, a: real): color { function alpha(c: color, a: real): color {
return Qt.rgba(c.r, c.g, c.b, a); return Qt.rgba(c.r, c.g, c.b, a);
@@ -122,22 +145,31 @@ Singleton {
a.a + (b.a - a.a) * k); a.a + (b.a - a.a) * k);
} }
// Load readouts sit white until the number actually matters, then bleed to // Load readouts sit at text until the number actually matters, brighten
// crimson. Not a hue cycle — red always means the same thing. // 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 { function heat(pct: real): color {
if (pct <= 55) return root.text; if (pct <= 55) return root.text;
return root.mix(root.text, root.accent, (pct - 55) / 45); if (pct <= 80) return root.mix(root.text, root.accent, (pct - 55) / 25);
return root.mix(root.accent, root.alarm, (pct - 80) / 20);
} }
// ----------------------------------------------------------------- fonts // ----------------------------------------------------------------- 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 fontMono: "JetBrainsMono Nerd Font"
readonly property string fontIcon: "JetBrainsMono Nerd Font" readonly property string fontIcon: "JetBrainsMono Nerd Font"
readonly property int weightDisplay: Font.Black readonly property int weightDisplay: Font.Medium
readonly property int weightBody: Font.Medium readonly property int weightBody: Font.Normal
readonly property int fsMicro: 9 // Letter-spacing for uppercase labels and headings.
readonly property real tracking: 2.5
readonly property int fsMicro: 10
readonly property int fsSmall: 11 readonly property int fsSmall: 11
readonly property int fsBody: 12 readonly property int fsBody: 12
readonly property int fsLarge: 15 readonly property int fsLarge: 15
@@ -145,28 +177,47 @@ Singleton {
readonly property int fsHuge: 34 readonly property int fsHuge: 34
readonly property int fsMega: 92 readonly property int fsMega: 92
readonly property int fsRailIcon: 17
// -------------------------------------------------------------- geometry // -------------------------------------------------------------- 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 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 railWidth: 48
readonly property int frame: 8
readonly property int frameRadius: 18
// ONE angle, everywhere. tan(14deg). Persona geometry is disciplined, not // Nothing leans and nothing is chopped. Both knobs are kept so the shape
// random: every edge in the shell leans by exactly this much in exactly the // code still has one place to ask.
// same direction, and every corner is chopped by exactly `cut`. Varying readonly property real skew: 0
// these per module reads as noise, not as style. readonly property int cut: 0
readonly property real skew: 0.249 readonly property int stroke: 1
readonly property int cut: 10
readonly property int stroke: 2
// Pure-black keyline and offset copy behind panels. This is the one piece of // No printed drop and no ink contour: on a dark ground they only thicken
// layering the style needs; it is not a soft shadow and never blurs. // the edge. The shadow under display text is the one offset left.
readonly property real dropOffset: 3 readonly property real dropOffset: 0
readonly property real keylineWidth: 2 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 padS: 6
readonly property int padM: 12 readonly property int padM: 12
@@ -177,24 +228,25 @@ Singleton {
readonly property int popoutGap: 10 readonly property int popoutGap: 10
// ------------------------------------------------------------ animation // ------------------------------------------------------------ animation
// P5 motion is snappy and slightly overshooting — never soft. // NieR motion is calm and mechanical: things glide in and settle, they
readonly property int durFast: 110 // never bounce.
readonly property int durBase: 190 readonly property int durFast: 120
readonly property int durSlow: 340 readonly property int durBase: 220
readonly property int durLazy: 620 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 easeSnap: [0.22, 1.0, 0.36, 1.0, 1, 1] // outQuint
readonly property var easeBack: [0.34, 1.56, 0.64, 1.0, 1, 1] // overshoot 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] readonly property var easeIn: [0.55, 0.0, 0.9, 0.35, 1, 1]
// -------------------------------------------------------------- textures // -------------------------------------------------------------- textures
readonly property url texHalftone: "root:/assets/halftone.png" // A 1px dot every 8px, the ground NieR's menus are printed on.
readonly property url texStripes: "root:/assets/stripes.png" readonly property url texDots: "root:/assets/dots.png"
readonly property url texScanline: "root:/assets/scanline.png" readonly property url texScanline: "root:/assets/scanline.png"
// Last resort only. The live wallpaper comes from the Wallpaper singleton, // Last resort only. The live wallpaper comes from the Wallpaper singleton,
// which asks hyprpaper what is actually on screen — a path written down // which asks hyprpaper what is actually on screen — a path written down
// here goes stale the moment the wallpaper is switched, which is exactly // here goes stale the moment the wallpaper is switched, which is exactly
// what happened to the lock screen. // what happened to the lock screen.
readonly property url fallbackWallpaper: "file:///home/narl/.wallpapers/takemi_1.jpg" 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 { NumberAnimation {
target: rise; property: "y" 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
} }
} }
+3 -2
View File
@@ -28,6 +28,7 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: -Theme.padS anchors.leftMargin: -Theme.padS
anchors.rightMargin: -Theme.padS anchors.rightMargin: -Theme.padS
radius: Theme.radiusM
color: root.selected ? Theme.treatmentSelectedFill : "transparent" color: root.selected ? Theme.treatmentSelectedFill : "transparent"
opacity: root.selected ? 1 : 0 opacity: root.selected ? 1 : 0
@@ -36,7 +37,7 @@ Item {
} }
} }
// Hover is a crimson tick in the gutter rather than a second slab, so // Hover is an accent tick in the gutter rather than a second slab, so
// pointing at a row cannot be mistaken for selecting it. // pointing at a row cannot be mistaken for selecting it.
Skew { Skew {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -46,7 +47,7 @@ Item {
color: Theme.accent color: Theme.accent
Behavior on height { Behavior on height {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack } NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutCubic }
} }
} }
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff. Show more