diff --git a/btop/btop.conf b/btop/btop.conf index b7601f5..1f5dd66 100644 --- a/btop/btop.conf +++ b/btop/btop.conf @@ -2,7 +2,7 @@ #* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. #* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" -color_theme = "/home/narl/.config/btop/themes/catppuccin_mocha.theme" +color_theme = "/home/narl/.config/btop/themes/yorha.theme" #* If the theme set background should be shown, set to False if you want terminal background transparency. theme_background = false diff --git a/btop/themes/yorha.theme b/btop/themes/yorha.theme new file mode 100644 index 0000000..5e7e30b --- /dev/null +++ b/btop/themes/yorha.theme @@ -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" diff --git a/nvim b/nvim index 4736228..19e0db6 160000 --- a/nvim +++ b/nvim @@ -1 +1 @@ -Subproject commit 4736228253fbaadc63cfce42440753cffdfb1c42 +Subproject commit 19e0db60a1f829ed28d22a733842f4f647ed0965 diff --git a/quickshell/README.md b/quickshell/README.md index 10a6b38..8f4ab78 100644 --- a/quickshell/README.md +++ b/quickshell/README.md @@ -52,9 +52,10 @@ its own panel. The shell uses three deliberate surface tiers. **Chrome** (the hull and `ChromeSurface`) is the calm, texture-free bar, rail, and tooltip layer. **Transient** (`PopoutSurface`) is reserved for expressive menus and detail panels, with one -clear composition and stronger separation from the desktop. **Cinematic** -(`P5Backdrop`) belongs only to scene-scale experiences such as power and lock; -it is never used as decoration inside ordinary chrome or popouts. +clear composition and stronger separation from the desktop. **Scene** +surfaces — the lock screen and the power menu — have no panel at all: the +desktop or the wallpaper dims, and one small group of type (the mark, a line of +thin display type, a short log, a menu or a field) stands in the dark. ``` shell.qml entrypoint — hull, bar, rail, dashboard, power menu, toasts, OSD per screen @@ -217,6 +218,12 @@ Authenticates through `/etc/pam.d/hyprlock`, which on this box is just styled in an ordinary window (`qs -p lockpreview.qml`) without locking the session. +It is the wallpaper, barely softened, with the face set in the dark of the +porthole: the mark, the time in light display type, the date, a status log +(battery, network, what is playing, unread notices), and a rounded field that +fills with dots as you type. The dots breathe while PAM checks; a wrong +password turns them and the field's rim to rust and nudges the field once. + If the shell ever dies while the session is locked, the compositor keeps the session secured by protocol and there is no unlocker left to talk to — recovery means restarting Hyprland from a TTY. @@ -270,9 +277,9 @@ Borders are structure and stay neutral. `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 tiered.** Chrome (bar, rail, tooltips) and transient panels -(popouts, OSD, toasts) are flat. The hairline grid and the dot wash live only in -`P5Backdrop`, behind the lock screen and the power menu. +**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). @@ -284,6 +291,8 @@ appear only while `glitch()` runs — NieR's brief data-corruption flicker. | `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 | 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 @@ -308,9 +317,12 @@ network and bluetooth, but keep running as the secret and pairing agents. 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** — still the `catppuccin` plugin, flavour mocha, configured in - `nvim/lua/plugins/ui.lua`. Not yet moved to the YoRHa palette. -- **GTK and Qt** — still `catppuccin-mocha-blue`. Not yet moved either. +- **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 and Qt** — still `catppuccin-mocha-blue`. Not yet moved. - **Fish** — `fish/conf.d/yorha_theme.fish`. - **The launcher** — no external theming to keep in step, because it is part of this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for diff --git a/quickshell/assets/grid.png b/quickshell/assets/grid.png deleted file mode 100644 index b3c3f7a..0000000 Binary files a/quickshell/assets/grid.png and /dev/null differ diff --git a/quickshell/components/P5Backdrop.qml b/quickshell/components/P5Backdrop.qml deleted file mode 100644 index 83332c5..0000000 --- a/quickshell/components/P5Backdrop.qml +++ /dev/null @@ -1,91 +0,0 @@ -import QtQuick -import "root:/config" -import "root:/components" - -// The shared ground for the two full-screen surfaces, in the NieR menu -// pattern: a hairline grid that sweeps in from the left, a darker column down -// the left-hand side for the menu or the clock to stand in, and a dot wash -// over the top. -// -// The lock screen and the power menu both stand on this one component, which -// 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 grid slide, the grid - // opacity and the column 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 - - // The column: a band of the void, edged by a hairline of the light. - // `wedge*` names are kept from the Persona layout so callers did not have - // to move; there is nothing wedge-shaped left. - property bool wedge: true - property real wedgeOpacity: 0.35 - property real wedgeLeft: 0 - property real wedgeSpan: 0.34 - - property bool stripes: true - property real stripeOpacity: 0.05 - - property bool halftone: true - property real halftoneOpacity: 0.05 - property color halftoneColor: Theme.primary - - // Hairline grid, sliding in from off-screen left. - Image { - anchors.fill: parent - visible: root.stripes && root.reveal > 0 - source: Theme.texGrid - 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 column. Drawn after the grid so it reads as a panel laid over it. - Item { - x: root.wedgeLeft * root.width - width: root.wedgeSpan * root.width - height: root.height - visible: root.wedge && root.reveal > 0 - opacity: root.reveal - - Rectangle { - anchors.fill: parent - color: Theme.alpha(Theme.ink, root.wedgeOpacity) - } - - // Its edge, drawn top-down as the column arrives. - Rectangle { - anchors.right: parent.right - width: 1 - height: parent.height * root.reveal - color: Theme.alpha(root.tone, 0.35) - } - } - - // Dot wash over the whole thing. - Image { - anchors.fill: parent - visible: root.halftone - source: Theme.texDots - fillMode: Image.Tile - opacity: root.halftoneOpacity - smooth: false - layer.enabled: true - layer.effect: TintEffect { tintColor: root.halftoneColor } - } -} diff --git a/quickshell/config/Theme.qml b/quickshell/config/Theme.qml index 4127685..b841775 100644 --- a/quickshell/config/Theme.qml +++ b/quickshell/config/Theme.qml @@ -240,10 +240,8 @@ Singleton { readonly property var easeIn: [0.55, 0.0, 0.9, 0.35, 1, 1] // -------------------------------------------------------------- textures - // A 1px dot every 8px, and a 32px hairline grid — the two backgrounds the - // NieR menus are printed on. + // A 1px dot every 8px, the ground NieR's menus are printed on. readonly property url texDots: "root:/assets/dots.png" - readonly property url texGrid: "root:/assets/grid.png" readonly property url texScanline: "root:/assets/scanline.png" // Last resort only. The live wallpaper comes from the Wallpaper singleton, diff --git a/quickshell/lock/LockFace.qml b/quickshell/lock/LockFace.qml index 5e46e7e..2e93c24 100644 --- a/quickshell/lock/LockFace.qml +++ b/quickshell/lock/LockFace.qml @@ -10,6 +10,13 @@ import "root:/lock" // The lock screen's whole face, kept separate from WlSessionLockSurface so it // can be instantiated in an ordinary window for development without actually // locking the session. +// +// Quiet on purpose. The wallpaper stays almost whole — the porthole, the +// planet, the girl looking out — and the face is one small group of type set +// in the dark of the porthole: the mark, the time, the date, a short status +// log, and a rounded field to type into. Nothing sweeps, splits or shakes; +// the group resolves in, and a wrong password nudges it once and turns its +// dots to rust. Item { id: surface @@ -20,12 +27,12 @@ Item { property string status: "" property bool failed: false property bool busy: pam.active - readonly property real uiScale: Math.max(0.72, Math.min(1.15, + readonly property real uiScale: Math.max(0.72, Math.min(1.3, Math.min(width / 1920, height / 1080))) Rectangle { anchors.fill: parent - color: Theme.base + color: Theme.ink } // ------------------------------------------------------- background @@ -43,290 +50,312 @@ Item { fillMode: Image.PreserveAspectCrop asynchronous: false cache: true + opacity: 0 + // Barely softened: enough that the scene steps back behind the type, + // not so much that it stops being a place. layer.enabled: true layer.effect: MultiEffect { blurEnabled: true - blur: 0.55 - blurMax: 48 - saturation: -0.15 - brightness: -0.35 + blur: 0.2 + blurMax: 32 + saturation: -0.1 + brightness: -0.18 } } - // Wash pulling the photo down toward the shell's ground. Was described as - // a teal wash back when the UI was teal; the colours it actually uses are - // the neutral surfaces, which is what it should have been all along. + // Shade deepening toward the left, where the type sits in the porthole's + // dark, and a breath of it along the foot. Rectangle { anchors.fill: parent gradient: Gradient { - GradientStop { position: 0.0; color: Theme.alpha(Theme.deep, 0.55) } - GradientStop { position: 0.6; color: Theme.alpha(Theme.base, 0.75) } - GradientStop { position: 1.0; color: Theme.alpha(Theme.base, 0.92) } + orientation: Gradient.Horizontal + GradientStop { position: 0.0; color: Theme.alpha(Theme.ink, 0.72) } + GradientStop { position: 0.45; color: Theme.alpha(Theme.ink, 0.28) } + GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.18) } } } - // The same wedge-and-stripes ground the power menu stands on. The wedge - // sits under the clock, which is where the power menu puts it under the - // title — so the two screens land the accent mass in the same place. - P5Backdrop { - id: backdrop + Rectangle { anchors.fill: parent - reveal: 0 - wedgeSpan: 0.30 - wedgeOpacity: 0.30 - stripeOpacity: 0.04 + gradient: Gradient { + GradientStop { position: 0.6; color: "transparent" } + GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.5) } + } } - // ------------------------------------------------------------ clock + // ------------------------------------------------------------- face SystemClock { id: clock precision: SystemClock.Minutes } - // The house mark, the same porthole the bar carries far left. Small, above - // the clock — the lock's hero is the time, not the branding. - Porthole { - id: mark - anchors.left: timeBlock.left - anchors.bottom: timeBlock.top - anchors.bottomMargin: 10 - size: 28 - opacity: timeBlock.opacity - } - Column { - id: timeBlock - anchors.left: parent.left - anchors.leftMargin: parent.width * 0.1 - anchors.top: parent.top - anchors.topMargin: parent.height * 0.18 - spacing: -18 + id: face + x: parent.width * 0.14 + y: parent.height * 0.2 + width: 380 + scale: surface.uiScale + transformOrigin: Item.TopLeft + opacity: 0 + spacing: 0 - SplitText { - text: Qt.formatDateTime(clock.date, "HH") - pixelSize: Theme.fsMega * 1.7 - split: 5 - } + transform: Translate { id: drift } - SplitText { - text: Qt.formatDateTime(clock.date, "mm") - pixelSize: Theme.fsMega * 1.7 - color: Theme.primary - split: 5 + // The house mark, the same porthole the bar carries far left. + Porthole { + size: 26 } Text { - text: Qt.formatDateTime(clock.date, "dddd, dd MMMM").toUpperCase() + topPadding: Theme.padM + text: Qt.formatDateTime(clock.date, "HH:mm") + color: Theme.accent + font.family: Theme.fontDisplay + font.pixelSize: Theme.fsMega * 1.35 + font.weight: Font.Light + font.letterSpacing: 2 + // Scaled with the face, so distance-field glyphs: native hinting + // at this size under a scale transform comes out stepped. + renderType: Text.QtRendering + } + + Text { + leftPadding: 4 + text: Qt.formatDateTime(clock.date, "dddd · dd MMMM").toUpperCase() color: Theme.subtext - font.family: Theme.fontMono + font.family: Theme.fontDisplay font.pixelSize: Theme.fsBody - font.letterSpacing: 6 - topPadding: 34 + font.weight: Theme.weightDisplay + font.letterSpacing: 5 renderType: Text.NativeRendering } - } - // ------------------------------------------------------ status rail - Column { - anchors.right: parent.right - anchors.rightMargin: parent.width * 0.1 - anchors.top: parent.top - anchors.topMargin: parent.height * 0.2 - spacing: Theme.padM - width: Math.min(300, parent.width * 0.28) - scale: surface.uiScale - transformOrigin: Item.TopRight + Item { width: 1; height: Theme.padXL } - RailRow { + // ------------------------------------------------------ status log + Column { width: parent.width - visible: Battery.available - glyph: Battery.icon - label: Math.round(Battery.percent) + "%" - detail: Battery.timeLabel - tone: Battery.tint + leftPadding: 4 + + StatusRow { + width: parent.width - 4 + visible: Battery.available + label: "Battery" + detail: Math.round(Battery.percent) + "%" + + (Battery.timeLabel ? " " + Battery.timeLabel : "") + tone: Battery.tint + } + + StatusRow { + width: parent.width - 4 + label: "Network" + detail: Sys.netUp ? Net.label : "no link" + tone: Sys.netUp ? Theme.accentSoft : Theme.overlay + } + + StatusRow { + width: parent.width - 4 + visible: Media.hasPlayer + label: Media.playing ? "Playing" : "Paused" + detail: Media.artist ? Media.title + " — " + Media.artist : Media.title + tone: Media.playing ? Theme.accent : Theme.primaryDim + } + + StatusRow { + width: parent.width - 4 + visible: Notifs.count > 0 + label: "Notices" + detail: Notifs.count + (Notifs.count === 1 ? " message" : " messages") + tone: Theme.accentSoft + } } - RailRow { + Item { width: 1; height: Theme.padXL } + + // ------------------------------------------------------ the field + Item { + id: authBlock width: parent.width - glyph: Net.icon - label: Net.label - detail: Sys.netUp ? Sys.netIp : "no link" - tone: Sys.netUp ? Theme.primary : Theme.muted - } + height: field.height + hint.height + Theme.padS - RailRow { - width: parent.width - visible: Media.hasPlayer - glyph: Media.playing ? "󰝚" : "󰏤" - label: Media.title - detail: Media.artist - tone: Theme.primary - } + transform: Translate { id: nudge } - RailRow { - width: parent.width - visible: Notifs.count > 0 - glyph: "󱅫" - label: Notifs.count + " notification" + (Notifs.count === 1 ? "" : "s") - detail: Notifs.history.length > 0 ? Notifs.history[0].summary : "" - tone: Theme.accent - } - } + Rectangle { + id: field + width: parent.width + height: 48 + radius: height / 2 + color: Theme.alpha(Theme.hull, 0.86) + border.width: 1 + border.color: surface.failed + ? Theme.alarm + : (input.activeFocus && surface.entry.length > 0 ? Theme.outline : Theme.rim) - // ------------------------------------------------------- auth panel - Item { - id: authBlock - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: parent.height * 0.14 - width: Math.min(460, parent.width - Theme.padXL * 2) - height: 150 - scale: surface.uiScale - transformOrigin: Item.Bottom + Behavior on border.color { + ColorAnimation { duration: Theme.durBase } + } - transform: Translate { id: shakeShift } + Icon { + id: lockGlyph + anchors.left: parent.left + anchors.leftMargin: Theme.padL + anchors.verticalCenter: parent.verticalCenter + text: "󰌾" + color: surface.failed ? Theme.alarm : Theme.muted + font.pixelSize: Theme.fsLarge + } - SplitText { - id: prompt - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - text: surface.failed - ? (surface.status || "WRONG").toUpperCase() - : (surface.busy ? "CHECKING…" : "WHO GOES THERE") - color: surface.failed ? Theme.alarm : Theme.text - pixelSize: Theme.fsTitle - letterSpacing: 4 - horizontalAlignment: Text.AlignHCenter - split: surface.failed ? 4 : 1.6 - } + Text { + anchors.left: lockGlyph.right + anchors.leftMargin: Theme.padM + anchors.verticalCenter: parent.verticalCenter + visible: surface.entry.length === 0 + text: "PASSWORD" + color: Theme.muted + font.family: Theme.fontDisplay + font.pixelSize: Theme.fsSmall + font.letterSpacing: Theme.tracking * 2 + renderType: Text.NativeRendering + } - // Password field: one chopped block per character. - P5Panel { - id: field - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: prompt.bottom - anchors.topMargin: Theme.padL - width: Math.min(380, authBlock.width - Theme.padL * 2) - height: 56 - fill: Theme.mantle - fillOpacity: 0.94 - border: surface.failed - ? Theme.accent - : (input.activeFocus ? Theme.primary : Theme.outline) - borderWidth: 2 - slash: true - slashColor: surface.failed ? Theme.alarm : Theme.primary - slashWidth: 6 - halftone: true - halftoneColor: surface.failed ? Theme.alarm : Theme.primary - halftoneOpacity: 0.08 + // One dot per character, each resolving in as it is typed. + // They breathe while PAM is checking. + Row { + id: dots + anchors.left: lockGlyph.right + anchors.leftMargin: Theme.padM + anchors.right: enter.left + anchors.rightMargin: Theme.padM + anchors.verticalCenter: parent.verticalCenter + spacing: 7 + clip: true - Row { - anchors.centerIn: parent - spacing: 6 - visible: surface.entry.length > 0 + Repeater { + model: Math.min(surface.entry.length, 28) - Repeater { - model: Math.min(surface.entry.length, 22) + Rectangle { + id: pip + required property int index + anchors.verticalCenter: parent.verticalCenter + width: 7 + height: 7 + radius: width / 2 + color: surface.failed ? Theme.alarm : Theme.accent + opacity: 0 + scale: 0.4 - Skew { - required property int index - width: 10 - height: 20 - color: surface.failed ? Theme.alarm : Theme.primary + Component.onCompleted: arrive.start() - // Each new block snaps in. - Component.onCompleted: pop.start() - NumberAnimation { - id: pop - target: parent - property: "scale" - from: 0.2; to: 1 - duration: Theme.durBase - easing.type: Easing.OutCubic + ParallelAnimation { + id: arrive + NumberAnimation { + target: pip; property: "opacity" + to: 1; duration: Theme.durBase; easing.type: Easing.OutCubic + } + NumberAnimation { + target: pip; property: "scale" + to: 1; duration: Theme.durBase; easing.type: Easing.OutCubic + } + } } } + + SequentialAnimation on opacity { + running: surface.busy + loops: Animation.Infinite + alwaysRunToEnd: true + NumberAnimation { to: 0.35; duration: 520; easing.type: Easing.InOutSine } + NumberAnimation { to: 1; duration: 520; easing.type: Easing.InOutSine } + } + } + + // Submit: a disc at the field's end that lights once there is + // something to send. + Rectangle { + id: enter + readonly property bool ready: surface.entry.length > 0 && !surface.busy + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + width: 32 + height: 32 + radius: width / 2 + color: enter.ready ? Theme.accent : "transparent" + border.width: enter.ready ? 0 : 1 + border.color: Theme.rim + + Behavior on color { + ColorAnimation { duration: Theme.durBase } + } + + Icon { + anchors.centerIn: parent + text: "󰁔" + color: enter.ready ? Theme.ink : Theme.overlay + font.pixelSize: Theme.fsLarge + } + + TapHandler { + onTapped: surface.submit() + } + } + + // The real input, invisible but focused. + TextInput { + id: input + anchors.fill: parent + opacity: 0 + focus: true + echoMode: TextInput.Password + enabled: !surface.busy + activeFocusOnTab: true + + onTextChanged: { + surface.entry = text; + if (surface.failed && text.length > 0) surface.failed = false; + } + + onAccepted: surface.submit() + + Keys.onEscapePressed: { + input.text = ""; + surface.failed = false; + } } } - Text { - anchors.centerIn: parent - visible: surface.entry.length === 0 && !surface.busy - text: "PASSWORD" - color: Theme.muted - font.family: Theme.fontMono - font.pixelSize: Theme.fsSmall - font.letterSpacing: 5 - renderType: Text.NativeRendering - } + // Under the field: who is locked, what is happening, or what went + // wrong — one line, never more. + Row { + id: hint + anchors.top: field.bottom + anchors.topMargin: Theme.padS + anchors.left: parent.left + anchors.leftMargin: Theme.padL + spacing: Theme.padS - // Busy sweep. - Rectangle { - anchors.bottom: parent.bottom - anchors.bottomMargin: 6 - width: 60 - height: 3 - color: Theme.primary - visible: surface.busy - - XAnimator on x { - running: surface.busy - loops: Animation.Infinite - from: 20 - to: field.width - 80 - duration: 700 - easing.type: Easing.InOutQuad - } - } - - // The real input, invisible but focused. - TextInput { - id: input - anchors.fill: parent - opacity: 0 - focus: true - echoMode: TextInput.Password - enabled: !surface.busy - activeFocusOnTab: true - - onTextChanged: { - surface.entry = text; - if (surface.failed && text.length === 0) surface.failed = false; + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: 4 + height: 4 + radius: 2 + color: surface.failed ? Theme.alarm : Theme.overlay } - onAccepted: surface.submit() - - Keys.onEscapePressed: { - input.text = ""; - surface.failed = false; + Text { + anchors.verticalCenter: parent.verticalCenter + text: surface.failed + ? (surface.status || "Access denied").toUpperCase() + : (surface.busy ? "VERIFYING" : (Quickshell.env("USER") || "user")) + color: surface.failed ? Theme.alarm : Theme.muted + font.family: Theme.fontMono + font.pixelSize: Theme.fsMicro + font.letterSpacing: 2 + renderType: Text.NativeRendering } } } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: field.bottom - anchors.topMargin: Theme.padM - spacing: Theme.padS - - Icon { - anchors.verticalCenter: parent.verticalCenter - text: "󰌾" - color: Theme.muted - font.pixelSize: Theme.fsBody - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: Quickshell.env("USER") || "USER" - color: Theme.muted - font.family: Theme.fontMono - font.pixelSize: Theme.fsSmall - font.letterSpacing: 3 - renderType: Text.NativeRendering - } - } } // Keep focus on the field no matter what. @@ -362,62 +391,60 @@ Item { surface.entry = ""; surface.failed = false; } else { - surface.failed = true; - if (surface.status === "") surface.status = "Access denied"; input.text = ""; surface.entry = ""; - shake.restart(); + surface.failed = true; + if (surface.status === "") surface.status = "Access denied"; + refuse.restart(); } } onError: err => { - surface.failed = true; - surface.status = "PAM error"; input.text = ""; surface.entry = ""; - shake.restart(); + surface.failed = true; + surface.status = "PAM error"; + refuse.restart(); } } + // A wrong password: the field leans away once and comes back, a head + // shaken slowly rather than a buzzer. SequentialAnimation { - id: shake - loops: 2 + id: refuse NumberAnimation { - target: shakeShift; property: "x" - to: -16; duration: 55; easing.type: Easing.OutQuad + target: nudge; property: "x" + to: -7; duration: 90; easing.type: Easing.OutSine } NumberAnimation { - target: shakeShift; property: "x" - to: 16; duration: 55; easing.type: Easing.OutQuad + target: nudge; property: "x" + to: 5; duration: 140; easing.type: Easing.InOutSine } NumberAnimation { - target: shakeShift; property: "x" - to: 0; duration: 90; easing.type: Easing.OutCubic + target: nudge; property: "x" + to: 0; duration: 220; easing.type: Easing.OutCubic } } - // Entry flourish when the lock comes up. + // The lock comes up the way the scene would if you opened your eyes: the + // picture first, then the type settling a few pixels into place. Component.onCompleted: intro.start() - ParallelAnimation { + SequentialAnimation { id: intro - // The stripes sweep in under everything, the same move the power menu - // opens with. NumberAnimation { - target: backdrop; property: "reveal" - from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo + target: wall; property: "opacity" + from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic } - NumberAnimation { - target: timeBlock; property: "opacity" - from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo - } - NumberAnimation { - target: authBlock; property: "opacity" - from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo - } - NumberAnimation { - target: shakeShift; property: "y" - from: 40; to: 0; duration: Theme.durLazy; easing.type: Easing.OutCubic + ParallelAnimation { + NumberAnimation { + target: face; property: "opacity" + from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic + } + NumberAnimation { + target: drift; property: "y" + from: 14; to: 0; duration: Theme.durLazy; easing.type: Easing.OutQuint + } } } } diff --git a/quickshell/lock/RailRow.qml b/quickshell/lock/RailRow.qml deleted file mode 100644 index 443cf99..0000000 --- a/quickshell/lock/RailRow.qml +++ /dev/null @@ -1,63 +0,0 @@ -import QtQuick -import "root:/config" -import "root:/components" - -// One line of the lock screen's status rail. -Item { - id: rail - - property string glyph: "" - property string label: "" - property string detail: "" - property color tone: Theme.primary - - implicitHeight: visible ? 38 : 0 - - Rectangle { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: 3 - height: 26 - color: rail.tone - } - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.padM - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.padS - - Icon { - anchors.verticalCenter: parent.verticalCenter - text: rail.glyph - color: rail.tone - font.pixelSize: Theme.fsLarge - } - - Column { - anchors.verticalCenter: parent.verticalCenter - spacing: -2 - - Text { - text: rail.label - color: Theme.text - elide: Text.ElideRight - font.family: Theme.fontDisplay - font.pixelSize: Theme.fsSmall - font.weight: Font.DemiBold - renderType: Text.NativeRendering - } - - Text { - visible: rail.detail !== "" - text: rail.detail - color: Theme.muted - elide: Text.ElideRight - font.family: Theme.fontMono - font.pixelSize: Theme.fsMicro - renderType: Text.NativeRendering - } - } - } -} diff --git a/quickshell/lock/StatusRow.qml b/quickshell/lock/StatusRow.qml new file mode 100644 index 0000000..c7ecd3d --- /dev/null +++ b/quickshell/lock/StatusRow.qml @@ -0,0 +1,66 @@ +import QtQuick +import "root:/config" +import "root:/components" + +// One line of the lock screen's status log, laid out like the dashboard's +// facts: a dot in the line's tone, the label tracked in caps, a hairline +// running out to the detail at the far end. +Item { + id: row + + property string label: "" + property string detail: "" + property color tone: Theme.primaryDim + + implicitHeight: visible ? 26 : 0 + + Rectangle { + id: dot + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: Theme.marker + height: Theme.marker + radius: width / 2 + color: row.tone + } + + Text { + id: name + anchors.left: dot.right + anchors.leftMargin: Theme.padS + anchors.verticalCenter: parent.verticalCenter + width: Math.min(implicitWidth, row.width * 0.55) + text: row.label.toUpperCase() + color: Theme.text + elide: Text.ElideRight + font.family: Theme.fontDisplay + font.pixelSize: Theme.fsSmall + font.weight: Theme.weightDisplay + font.letterSpacing: Theme.tracking + renderType: Text.NativeRendering + } + + Rectangle { + anchors.left: name.right + anchors.leftMargin: Theme.padS + anchors.right: value.left + anchors.rightMargin: Theme.padS + anchors.verticalCenter: parent.verticalCenter + height: 1 + color: Theme.alpha(Theme.outline, 0.4) + } + + Text { + id: value + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: Math.min(implicitWidth, row.width * 0.4) + text: row.detail + color: Theme.muted + elide: Text.ElideRight + horizontalAlignment: Text.AlignRight + font.family: Theme.fontMono + font.pixelSize: Theme.fsMicro + renderType: Text.NativeRendering + } +} diff --git a/quickshell/overlays/PowerMenu.qml b/quickshell/overlays/PowerMenu.qml index c81b049..a0153c6 100644 --- a/quickshell/overlays/PowerMenu.qml +++ b/quickshell/overlays/PowerMenu.qml @@ -1,12 +1,16 @@ import QtQuick import Quickshell import Quickshell.Wayland +import Quickshell.Io import "root:/config" import "root:/components" import "root:/services" -// Full-screen power menu. An accent wedge and stripes sweep in behind a stack of leaning -// buttons; Escape or a click on the backdrop dismisses it. +// Full-screen power menu, built like the lock screen: the desktop dims to the +// void, and one small group of type stands in it — the mark, the motto, a +// line about the session, and a NieR menu of six rows. The selected row is a +// pill of light with the text punched out of it; the three that end the +// session light in rust instead. Escape or a click on the dark dismisses it. PanelWindow { id: root @@ -32,16 +36,9 @@ PanelWindow { property int selectedAction: 0 - function focusSelectedAction(): void { - const entry = buttonRepeater.itemAt(root.selectedAction); - if (entry && entry.children.length > 0) - entry.children[0].forceActiveFocus(Qt.TabFocusReason); - } - function moveSelection(delta: int): void { root.selectedAction = (root.selectedAction + delta + root.actionModel.length) % root.actionModel.length; - root.focusSelectedAction(); } readonly property var actionModel: [ @@ -53,6 +50,11 @@ PanelWindow { { label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" } ] + FileView { + id: hostFile + path: "/etc/hostname" + } + onOpenChanged: { if (open) { PopupCoordinator.dismissAll(); @@ -64,128 +66,155 @@ PanelWindow { } // ------------------------------------------------------------- backdrop + // The desktop stays faintly there behind the dark, deepest on the left + // where the menu stands. Rectangle { id: backdrop anchors.fill: parent - color: Theme.alpha(Theme.base, 0.9) opacity: 0 + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.0; color: Theme.alpha(Theme.ink, 0.97) } + GradientStop { position: 0.5; color: Theme.alpha(Theme.ink, 0.93) } + GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.86) } + } TapHandler { onTapped: Actions.closePowerMenu() } } - // Grid and column, shared with the lock screen. - P5Backdrop { - id: ground - anchors.fill: parent - reveal: 0 - stripeOpacity: 0.05 - wedgeOpacity: 0.35 - wedgeSpan: 0.34 - // The live desktop is still visible behind this, so a dot wash would be - // laid over other people's windows rather than over a ground of our - // own. The lock owns the halftone; here it just muddies things. - halftone: false - } - - // ---------------------------------------------------------------- title + // ---------------------------------------------------------------- group Column { - id: title - anchors.left: parent.left - anchors.leftMargin: root.width * 0.12 + id: group + x: root.width * 0.14 anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: -60 - spacing: -14 + width: 340 opacity: 0 - SplitText { - text: "GLORY" - pixelSize: Theme.fsMega - split: 4 - } + transform: Translate { id: drift } - SplitText { - text: "TO" - pixelSize: Theme.fsMega - color: Theme.accent - split: 4 - } - - SplitText { - text: "MANKIND." - pixelSize: Theme.fsMega - split: 4 + Porthole { + size: 26 } Text { - text: "— YORHA" - color: Theme.primary - font.family: Theme.fontMono - font.pixelSize: Theme.fsSmall - font.letterSpacing: 5 - topPadding: 24 + topPadding: Theme.padM + text: "GLORY TO MANKIND." + color: Theme.accent + font.family: Theme.fontDisplay + font.pixelSize: Theme.fsHuge + font.weight: Font.Light + font.letterSpacing: 6 renderType: Text.NativeRendering } - } - // -------------------------------------------------------------- actions - Column { - id: actions - anchors.right: parent.right - anchors.rightMargin: root.width * 0.12 - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.padM + Text { + topPadding: Theme.padS + leftPadding: 2 + text: ["YORHA", (Quickshell.env("USER") || "user") + "@" + hostFile.text().trim(), + Sys.uptime ? "UP " + Sys.uptime : ""].filter(t => t !== "").join(" · ") + color: Theme.muted + font.family: Theme.fontMono + font.pixelSize: Theme.fsMicro + font.letterSpacing: 2 + renderType: Text.NativeRendering + } - // The widest slab in the stack. Fixed rather than derived so the ragged - // x offsets below cannot feed back into the Column's own width. - readonly property int stackWidth: 312 - width: stackWidth + Item { width: 1; height: Theme.padXL } + // ------------------------------------------------------------ menu Repeater { id: buttonRepeater model: root.actionModel - // A carrier item owns the staggered slide so the button keeps its - // own hover transform. Item { id: entry required property var modelData required property int index - readonly property int slabWidth: actions.stackWidth - - width: entry.slabWidth - height: 52 + readonly property bool selected: root.selectedAction === entry.index + readonly property color tone: entry.modelData.danger ? Theme.alarm : Theme.accent + width: group.width + height: 44 opacity: 0 - transform: [ - Translate { id: slide; x: 90 }, - Rotation { - id: tilt - angle: -3 - origin.x: entry.slabWidth / 2 - origin.y: 26 - } - ] - P5Button { - id: actionButton - anchors.fill: parent - text: entry.modelData.label - icon: entry.modelData.icon - destructive: entry.modelData.danger - selected: root.selectedAction === entry.index - accent: entry.modelData.danger ? Theme.alarm : Theme.primary - lean: Theme.skew - onClicked: root.invoke(entry.modelData.act) - onHoveredChanged: if (hovered) root.selectedAction = entry.index - onActiveFocusChanged: if (actionButton.activeFocus) root.selectedAction = entry.index - Keys.forwardTo: [keyController] + transform: Translate { id: rise; y: 6 } + + // The divider after the third row: what pauses the session + // above, what ends it below. + Rectangle { + visible: entry.index === 3 + anchors.top: parent.top + x: Theme.padL + width: parent.width - Theme.padL * 2 + height: 1 + color: Theme.alpha(Theme.outline, 0.35) } - // Staggered entry, one button after another. + Rectangle { + id: slab + anchors.fill: parent + anchors.topMargin: 3 + anchors.bottomMargin: 3 + radius: height / 2 + color: entry.selected ? entry.tone : "transparent" + + Behavior on color { + ColorAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic } + } + } + + Rectangle { + id: marker + anchors.left: parent.left + anchors.leftMargin: Theme.padL + anchors.verticalCenter: parent.verticalCenter + width: Theme.marker + height: Theme.marker + radius: width / 2 + color: entry.selected ? Theme.ink + : (entry.modelData.danger ? Theme.alpha(Theme.alarm, 0.6) : Theme.overlay) + } + + Icon { + id: glyph + anchors.left: marker.right + anchors.leftMargin: Theme.padM + anchors.verticalCenter: parent.verticalCenter + // Fixed width so the labels line up whatever the glyph. + width: 20 + horizontalAlignment: Text.AlignHCenter + text: entry.modelData.icon + color: entry.selected ? Theme.ink : Theme.muted + font.pixelSize: Theme.fsLarge + } + + Text { + anchors.left: glyph.right + anchors.leftMargin: Theme.padM + anchors.verticalCenter: parent.verticalCenter + text: entry.modelData.label.toUpperCase() + color: entry.selected ? Theme.ink : Theme.text + font.family: Theme.fontDisplay + font.pixelSize: Theme.fsBody + font.weight: Theme.weightDisplay + font.letterSpacing: Theme.tracking * 1.5 + renderType: Text.NativeRendering + } + + HoverHandler { + cursorShape: Qt.PointingHandCursor + onHoveredChanged: if (hovered) root.selectedAction = entry.index + } + + TapHandler { + onTapped: root.invoke(entry.modelData.act) + } + + // Staggered entry: each row resolves a beat after the last. Connections { target: root function onOpenChanged() { @@ -196,48 +225,31 @@ PanelWindow { SequentialAnimation { id: entryAnim - PauseAnimation { duration: 40 + entry.index * 55 } + PauseAnimation { duration: 120 + entry.index * 40 } ParallelAnimation { NumberAnimation { target: entry; property: "opacity" - to: 1; duration: Theme.durBase + to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic } NumberAnimation { - target: slide; property: "x" - from: 90; to: 0 - duration: Theme.durSlow - easing.type: Easing.OutCubic - } - // Slabs land out of true and snap square. Sliding alone - // reads like a slide deck; the tilt is what makes it - // land like a card being thrown down. Same direction - // every time — the motion is the flourish, the geometry - // stays disciplined. - NumberAnimation { - target: tilt; property: "angle" - from: -3; to: 0 - duration: Theme.durSlow - easing.type: Easing.OutCubic - easing.overshoot: 2.2 + target: rise; property: "y" + from: 6; to: 0; duration: Theme.durSlow; easing.type: Easing.OutCubic } } } } } - } - // Hint line. - Text { - anchors.bottom: parent.bottom - anchors.bottomMargin: 40 - anchors.horizontalCenter: parent.horizontalCenter - text: "↑ ↓ SELECT ENTER CONFIRM ESC CANCEL" - color: Theme.muted - font.family: Theme.fontMono - font.pixelSize: Theme.fsMicro - font.letterSpacing: 4 - opacity: backdrop.opacity - renderType: Text.NativeRendering + Text { + topPadding: Theme.padXL + leftPadding: Theme.padL + text: "↑ ↓ SELECT ENTER CONFIRM ESC CANCEL" + color: Theme.overlay + font.family: Theme.fontMono + font.pixelSize: Theme.fsMicro + font.letterSpacing: 2 + renderType: Text.NativeRendering + } } function invoke(what: string) { @@ -280,16 +292,15 @@ PanelWindow { id: openAnim NumberAnimation { target: backdrop; property: "opacity" - to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad - } - // One number now drives the slide, the stripe opacity and the wedge. - NumberAnimation { - target: ground; property: "reveal" - from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo + to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic } NumberAnimation { - target: title; property: "opacity" - to: 1; duration: Theme.durSlow + target: group; property: "opacity" + to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic + } + NumberAnimation { + target: drift; property: "y" + from: 14; to: 0; duration: Theme.durLazy; easing.type: Easing.OutQuint } } @@ -297,14 +308,10 @@ PanelWindow { id: closeAnim NumberAnimation { target: backdrop; property: "opacity" - to: 0; duration: Theme.durFast + to: 0; duration: Theme.durBase; easing.type: Easing.InCubic } NumberAnimation { - target: ground; property: "reveal" - to: 0; duration: Theme.durFast - } - NumberAnimation { - target: title; property: "opacity" + target: group; property: "opacity" to: 0; duration: Theme.durFast } }