aboutsummaryrefslogtreecommitdiffstats
AgeCommit message (Collapse)AuthorFilesLines
29 hoursfix(desktop): honest alwaysActive docs and working Escape in confirmDanilo M.7-41/+51
29 hoursdocs: correct drawer exit behaviour and dead module commandsDanilo M.4-26/+33
The README claimed a click outside returns to the grid; it closes the drawer, and only Escape-with-a-page or the back arrow return to the grid. The three moved module READMEs' run and IPC commands still named the deleted volume-osd, mail-overview and vm-manager paths, so point them at qs -p desktop and ipc call drawer open sound|mail|vm.
29 hoursdocs: the desktop shell and what the merge changedDanilo M.6-20/+180
Five components become three, and the Theme symlink count with them. Two new notes: a pragma Singleton in a subdirectory resolves through a plain directory import with no qmldir, and Virsh.sampling gates only the stats poll, not the lifecycle stream that keeps the tile correct while the page is closed.
29 hoursfeat(desktop): move vm-manager in as the vm moduleDanilo M.11-544/+457
Virsh.qml is unchanged; VmPanel's body becomes the page and the stat row becomes a Flow, because four stats with 150px bars do not fit a 600px drawer as one row. The page starts and stops Virsh.sampling, so the 2s poll runs only while something is looking at it; the lifecycle event stream still runs always, which is what keeps the tile's dots current.
29 hoursfix(desktop): start the mail Accounts singleton at shell launchDanilo M.1-0/+7
The singleton was referenced only inside lazy Components, so its qtmaildir.conf watcher and unread-count refresh did not start until the drawer first opened, contradicting alwaysActive and the old MailPanel, which shell.qml instantiated directly. Naming Accounts at the Module top level creates it at shell load; the page keeps its own refresh on open.
29 hoursfeat(desktop): move mail-overview in as the mail moduleDanilo M.12-331/+274
Accounts.qml is unchanged and MailPanel's body becomes the page. The three scripts move with it, which changes the absolute paths in autostart and in the waybar module; both are outside this repo and listed in the plan's final task. Button.qml, byte-identical here and in vm-manager, lands at the shell root as the single copy.
29 hoursfeat(desktop): move volume-osd in as the sound moduleDanilo M.10-126/+346
VolumeOsd.qml did three jobs in one file: PipeWire tracking, the OSD surface and the player transport. They become Service, Osd and the page. The OSD keeps its namespace so the existing Hyprland blur rule still matches, and the service stays always-active because the OSD has to answer a keypress with no drawer open.
29 hoursfeat(desktop): the appearance tileDanilo M.2-1/+39
A tile with no page: appearance stays its own process because a wallpaper picker needs more room than a 600px drawer, so the tile only fires its existing IPC. This is the case the contract's activate() exists for.
29 hoursdocs: correct the plan's no-arg ipc open and the head pipeDanilo M.1-6/+20
Quickshell 0.3.1 IPC requires every declared argument, so the plan's `ipc call drawer open` with no page fails; the page-less grid entry is the zero-argument `toggle`. Also records that `... | head` races timeout and kills the shell early, so verification uses a file redirect.
29 hoursfix(desktop): slide the page with a transform, not anchored xDanilo M.1-3/+7
The page Loader is anchored to its parent, so the x binding on it was overridden and the slide never rendered. Drive the motion with a Translate transform instead, which the anchors do not touch. Opacity and the back-arrow wiring are unchanged.
29 hoursfeat(desktop): the drawer, with the top reservedDanilo M.2-4/+210
Left of DP-1 because conky holds the right; ExclusionMode.Normal so the bar the drawer is reached from stays visible and clickable. The top is an empty Item claiming the space the notification engine will fill, so the grid already sits where it will sit once that lands.
29 hoursfeat(desktop): tile and page chromeDanilo M.2-0/+147
The page body scrolls because mail with several accounts and vm with several VMs both exceed the drawer height; the tile grid deliberately does not, being fixed at a 3x3 ceiling.
29 hoursfeat(desktop): the module contractDanilo M.1-0/+49
A module provides a tile, a page, both or neither, plus alwaysActive, which governs the background service rather than the page: the drawer is closed most of the time and three of four modules have background work.
31 hoursdocs: a subdirectory singleton needs no qmldir, drop it from the planDanilo M.1-71/+54
The plan claimed a pragma Singleton outside the config root is invisible until a qmldir names it, and had Task 1 create one plus Tasks 6, 7 and 8 append a line each. That premise was wrong. Tested: a pragma Singleton in modules/sub/, reached by a plain import "modules/sub", resolves with no qmldir anywhere. The control was a reference to a genuinely undefined type, which warns "ReferenceError: <name> is not defined"; the singleton case produced no such warning and the binding evaluated. This matches what AGENTS.md already documented about Theme.qml, which the code review pointed out. Four registration steps removed and the remaining steps renumbered. The AGENTS.md note Task 9 adds now records the tested behaviour rather than the invented one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
31 hoursfeat(desktop): skeleton shell with the keepalive windowDanilo M.2-0/+30
The window draws nothing and catches nothing; it exists because a quickshell config with no visible window exits silently, and this shell's drawer is closed most of the time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
31 hoursdocs: warn that the config task's tilde paths must be typed absoluteDanilo M.1-0/+9
The paths are written with a tilde because this file is committed and the repo forbids home paths in committed files. But neither Hyprland's exec_cmd nor waybar's exec goes through a shell, so a tilde there never expands and fails silently, which reads as a broken keybind rather than a bad path. The two rules genuinely conflict here, so the plan now says which form goes where instead of shipping lines that would not run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
31 hoursdocs: implementation plan for the desktop shellDanilo M.1-0/+2817
Ten tasks: skeleton, contract, chrome, drawer, then the four modules in increasing order of risk, then documentation, then the six config edits outside this repo. Appearance goes first among the modules despite being last in the grid, because it is nine lines of substance and puts something in the grid before the three migrations start. Sound, mail and vm follow in that order: sound splits one 302-line file into three, mail is the gentlest move and carries the only automated oracle in the project, and vm is the largest and the one whose polling behaviour actually changes. Each task leaves the repo working, because several delete a component and a half-finished deletion is painful to unpick. Paths in the config task are written with a tilde: the gitleaks hook rejected the absolute form, correctly, since this file is committed and the live Hyprland config it describes is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
31 hoursdocs: settle the vm tile's cold state on drawer openDanilo M.1-3/+4
The vm module's service is lazy, so the tile had no state to show before its page had been visited. One `virsh list` when the drawer opens fixes that for the cost of a single call; the per-VM polling for guest-agent stats and managed-save detection still waits for the page, which is where the expense actually is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
31 hoursdocs: design for a unified desktop shell with a drawer and modulesDanilo M.1-0/+271
Five components mean five processes, five autostart lines, five blur rules, and functionality scattered across surfaces with nothing in common. Nothing is discoverable without remembering it exists. The design is one left-side drawer on DP-1, reached from a launcher at the left end of waybar, hosting modules as tiles that may open full-height pages. Modules declare a tile, a page, both or neither, plus whether their background service runs while the drawer is closed: sound and mail must (the OSD reacts to keypresses, the unread count outlives the drawer), vm must not, since polling virsh only paints a page nobody is looking at. Three components merge in (volume-osd, mail-overview, vm-manager), two stay separate because a 600px drawer is the wrong shape for them: window-switcher is a fullscreen ALT+TAB grid, appearance needs room for a wallpaper picker. Appearance still earns a tile that fires its existing IPC, which is the case that proves a tile needs no page of its own. The top of the drawer is reserved and empty. The notification engine is a later project, but the space has to be claimed now: build the grid anywhere else and it moves the day notifications land, having been tuned against a position that does not survive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
47 hoursdocs(mail-overview): correct the lastmod range in the READMEDanilo M.1-1/+2
The query starts one revision after the stored one, so the lower bound is exclusive.
47 hoursfix(mail-overview): don't re-notify the prior batch, flatten newlinesDanilo M.4-11/+22
The lastmod range is inclusive at both ends and the stored revision was reused as the next start, so the batch just notified matched again. Start one revision later. A subject can contain a newline, which broke the bullet rows and the +N more count; flatten CR/LF to a space before escaping. Also reconciles the state-write comment and spec with the actual skip-and-advance failure behavior, and corrects two doc wordings.
47 hoursdocs(mail-overview): document the arrival notifierDanilo M.2-20/+82
Why a revision counter rather than a count delta or a date watermark, why the state file carries a UUID, and why an unseeded run stays quiet. Also records the final notification presentation: the split heading, the absolute-path icon, and the show_indicators setting that keeps the action indicator off.
48 hoursfix(mail-overview): pass the mail icon by absolute pathDanilo M.1-2/+9
dunst here resolves a themed icon name only through icon_path, which holds no mail icon, so the name rendered nothing and dunst stored an empty icon_path. An absolute path resolves, which is what this machine's other notifiers already do.
48 hoursfeat(mail-overview): split notification heading and use theme iconDanilo M.1-4/+6
The user's dunst format renders %a bold on the top line and %s italic below it, so the app name now carries "New Mail" and the summary carries "<account> (<count>)". mail-unread is not in the active icon theme; mail-unread-multiple is.
48 hoursfeat(mail-overview): title, icon and spaced bullet bodyDanilo M.2-18/+23
A live test showed the old title ("<label> · N new") read poorly and the body packed the rows into one block. Title is now "New Mail - <label> (N)" with the mail-unread icon, and each message is a bullet on its own line with a blank line between. +N more counts rows, not physical lines, so the blank lines do not skew it.
2 daysfeat(mail-overview): notify per account when mail arrivesDanilo M.1-1/+128
Asks notmuch what changed since the revision last seen, then sends one notification per account with mail in that window. Counts are validated as integers because a rejected notmuch query prints nothing and exits 1, and an empty string must not read as zero. dunstify is backgrounded because -b blocks until the notification is dismissed, which would otherwise stall the loop for the full timeout on every account. The watch is on the xapian directory rather than a file inside it, because a commit replaces files and a watch held on a filename dies with it.
2 daystest(mail-overview): exercise the integer guard in read_prev_revDanilo M.1-1/+1
2 daysfeat(mail-overview): track the notified revisionDanilo M.2-0/+62
Stores the database UUID beside the revision, because notmuch revisions only compare within one database and a rebuild restarts the counter. Missing, corrupt or mismatched state reports nothing, which the caller treats as seed-silently: with no floor, lastmod:0.. matches every unread message and startup becomes a wall of popups.
2 daysfeat(mail-overview): build notification bodiesDanilo M.2-0/+63
Three rows of author and subject, then +N more, which bounds the height so a mailing list burst is not a wall. Markup characters are escaped because dunst renders body markup and a subject is untrusted text off the internet.
2 daysfeat(mail-overview): parse accounts for the notifierDanilo M.2-0/+82
Same source the drawer parses, so adding an account in qtmaildir notifies with no edit here. Keys run to the closing bracket because real ones contain dots, and parsing walks lines because a folder named [Gmail]/Bozze ends a section body early.
2 daysfeat(mail-overview): skeleton for the arrival notifierDanilo M.2-0/+89
A library guard so the test can source the script for its pure functions without starting a watch loop, and the check itself, which is one bash script rather than a framework.
2 daysdocs: implementation plan for mail arrival notificationsDanilo M.1-0/+924
Six tasks, TDD against one bash check rather than a framework: the script is sourced as a library so the pure functions, the ones that only move text around, are asserted without notmuch, dunstify or inotify in the picture. The two parsing traps that already cost this component a debugging session each get a fixture apiece: a key containing dots, and a folder value containing a bracket. Verification does not wait for mail. Backdating the stored revision by 2000 replays a real window on demand, and the seed run before it proves the anti-storm rule with 101 unread messages on disk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ZwWrCEbdKzmisfg9bb1nS
2 daysdocs: design for mail arrival notificationsDanilo M.1-0/+183
One notification per account per arriving batch, from a new mail-notify.sh sibling to waybar-mail.sh. Arrival is detected with notmuch's lastmod revision counter rather than a count delta or a date watermark: a commit is not the same as new mail, since reading and tagging also commit, and date: is the message's own Date header, so backdated mail would never notify and future-dated mail would notify forever. A separate process rather than extending waybar-mail.sh, which already has the arrival edge: waybar owns that process, so a bar restart would stop notifications with nothing reporting it. State carries the database UUID beside the revision, because notmuch revisions are only comparable within one database. Missing, corrupt or mismatched state seeds silently: with no floor, lastmod:0.. matches all 101 unread messages here and startup becomes a wall of popups. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ZwWrCEbdKzmisfg9bb1nS
2 daysfeat(mail-overview): show mail-watcher health in the drawerDanilo M.3-31/+107
The watcher already writes ~/.local/state/mail-watcher.heartbeat every 60s, so the drawer reads it and shows a dot: green when idling, yellow when a folder gave up, red when the heartbeat is stale or missing. Backoff stays green because it is normal recovery and the watcher's own health check treats it that way. The staleness rule is reimplemented in a few lines rather than shelling out to --status; the heartbeat file is the documented cheap-to-poll surface. Read once per open rather than watched: the heartbeat is written by atomic replace, so an inotify watch held on the old inode dies with it. "Sync now" is removed with its Process. The watcher triggers syncs, the cron tick is the backstop, and waybar's right-click still runs mailsync.sh.
2 daysfeat(vm-manager): offer to discard a saved state that will not restoreDanilo M.4-5/+75
A VM saved rather than shut down restores its memory image on the next start. When that image cannot be restored the start fails every time with "unable to execute QEMU command 'migrate-incoming'", and the panel showed an ordinary "shut off" with a Start button that could never work. Detection is virsh dominfo grepped for "Managed save: yes", polled per VM on every list refresh the way the agent rows are. domstats does not report it: the shut-off reason reads "failed", from the failed start, not from the save. virsh list --all --managed-save does print "saved" in the state column, but not together with --name, which is the form the panel lists with. The button appears only when a saved image exists, since managedsave-remove fails without one and would be noise on every other VM. It takes a confirmation click but not a typed name: it deletes the memory image and leaves the disk alone, so the cost of a misclick is a cold boot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lo1FG4qTr1inavmhqhfobe
3 daysdocs(window-switcher): write down what the build cost to learnDanilo M.3-10/+191
The component is finished and on the user's ALT+TAB, so what is left is the part that would otherwise have to be rediscovered. Most of the time this build took went to three things that report success and then do nothing: Hyprland 0.56.2 evaluating dispatch arguments as Lua, so the documented-looking `focuswindow address:0x...` is a silent syntax error; a focus dispatched while the overlay still holds an exclusive keyboard grab being accepted and ignored; and a sort on a `focusHistoryID` property that does not exist, comparing NaN and leaving an arbitrary order that looks plausible. None of them logs anything. Those six, plus the missing `0x` prefix, the empty `toplevels` before `refreshToplevels()` and `ScreencopyView` reporting (-1, -1) rather than (0, 0), generalise past this component, so they go in AGENTS.md next to the other notes that exist because a commit message explained itself once. The card geometry and the blank icon square are specific to the grid and stay in the component README. Also corrects the counts that four components made true and five do not: the component lists, "the other three use ExclusionMode.Ignore", and the Theme symlink note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysfix(window-switcher): namespace it like the other fourDanilo M.1-1/+1
The layer surface announced itself as window-switcher, while the other components all use a quickshell- prefix and the blur rules in decorations.lua match on it. A rule for the odd one out would have been the only one in that file matching a bare name, so the component matches the convention instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysfix(window-switcher): focus after the overlay is really goneDanilo M.1-4/+25
Picking a window focused nothing, and focus returned to whatever had it before. The overlay holds keyboard focus exclusively, and the compositor will not move window focus out from under that grab: the dispatch is accepted, reports ok, and is then ignored. Nothing in the log says so, which is why this looked like a dispatch that had worked. Closing first in the same turn is not enough either, because close() only clears a property and the layer surface survives until the frame after, so the dispatch still lands inside the grab. The target is now remembered and focused once the surface is actually gone. Tested in both directions, since the case that exposed it was cross-monitor: DP-1 to a window on DP-3, and back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysfix(window-switcher): clamp the card size by height tooDanilo M.1-1/+12
Only the width was clamped, so rows were free to run off the top and bottom of the screen. A centred Grid has no way to scroll to a card that lands there, so the cards were unreachable rather than merely cramped: at 19 windows on a 1080-tall screen the fourth row was clipped, and it grew worse from there. Cards now shrink to fit the available height as well, which leaves every count up to 18 exactly as it was, since the width term still wins there. Thirty windows fit in 995px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysfeat(window-switcher): the grid, the selection, and an empty stateDanilo M.1-8/+100
Columns are the window count capped at six and the grid centres, so one card sits dead centre and two straddle the middle. Cards grow to fill their row up to 640px: without a ceiling a lone preview becomes a full-screen mirror of the window it stands for. The close button leaves the overlay open, because closing is a side errand and a dismissal would mean reopening to close a second window. That makes an empty overlay reachable by closing the last window, so there is an empty state rather than a full-screen dim with nothing in it, which reads as a hang. The selection needs no reset. The LazyLoader destroys the overlay on close and constructs a new one on the next opening, verified with a build counter: two openings, two constructions. selectedIndex is therefore 0 on every showing by initialisation, and an explicit assignment in Component.onCompleted would be a no-op. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysfeat(window-switcher): one card, with the preview fittedDanilo M.1-0/+136
The card box is a fixed 16:10 and the preview is fitted inside it rather than the card taking its preview's shape. DP-1 windows are near 21:9 and DP-3 reports transform=1, so its windows are portrait 9:16: a 4.4x span of aspect ratio in one grid, and ragged rows would break the alignment of the three text lines that make it scannable. Both corner overlays carry a scrim. They are drawn over an arbitrary window, so a themed icon can otherwise land on a same-coloured region and disappear. An uncaptured ScreencopyView reports sourceSize (-1, -1) rather than (0, 0), so the aspect ratio guard tests for a positive height. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysdocs(window-switcher): focusHistoryID is not on HyprlandToplevelDanilo M.1-2/+12
Implementing the model found it. The property reads undefined, so the planned sort compared NaN and silently did nothing, leaving the list in arbitrary tracker order while looking entirely plausible. It is on lastIpcObject instead. Carried into the plan's later tasks so the grid does not reintroduce the bare property, and into the notes that Task 6 puts in AGENTS.md, since a sort that quietly does nothing is the kind of wrong answer this repo's notes exist to prevent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysfeat(window-switcher): the window model, filtered and sortedDanilo M.1-0/+67
One place for everything that touches Hyprland, so the UI never issues a dispatch itself. That matters more than usual here because dispatch arguments are Lua on 0.56.2 and the documented-looking form is a syntax error that fails silently, and because HyprlandToplevel.address omits the 0x prefix that a dispatch needs. focusHistoryID is not a HyprlandToplevel property, only a field of the raw hyprctl object, so reading it directly returned undefined and left the sort a no-op. It is read through lastIpcObject, with a missing one sorting last rather than as most recent. Only special: workspaces are filtered, matching the rofi script this replaces. Windows elsewhere are kept whatever their visibility, since jumping to another desktop is the point of a switcher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysfix(window-switcher): pick the screen the way the other components doDanilo M.2-2/+8
The screen was found by a name written inline, falling back to null. The other three panels all take a monitor property and fall back to screens[0], so this now does too: a machine without a DP-1 gets its first monitor rather than a null screen, and the name is in one place if it ever needs changing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysfeat(window-switcher): the overlay skeleton, and a proven EscapeDanilo M.3-0/+109
A fifth component, so the same keepalive window as the other four: a config with no visible window exits without reporting anything, and this overlay is hidden almost always. Escape is handled on a focused Item inside the PanelWindow rather than on the window, because key events reach a focused item and Keys.onEscapePressed on a PanelWindow never fires. Proven before the grid is built on top of it, since keyboard navigation depends on the same grab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysdocs(window-switcher): the implementation planDanilo M.1-0/+849
Six tasks, each ending in a commit: the skeleton and the keyboard grab, the window model, one card, the grid and empty state, the Hyprland wiring, and the docs. Two things were checked against the running system while writing it rather than left for the implementation to discover. The keyboard grab works: Keys.onEscapePressed on a focused Item inside the layer surface fires, so the AGENTS.md workaround holds and keyboard navigation is safe to build on. And the dispatcher for sending a key is send_shortcut with the underscore; sendshortcut does not exist, and a test written against that name would have failed in a way that looks exactly like the key never arriving. No qmldir is added. There is none anywhere in this repo and Theme resolves without one, since quickshell scans the config directory itself; the AGENTS.md note about needing one is about singletons outside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysdocs(window-switcher): an empty state, rather than opening on nothingDanilo M.1-0/+19
ALT + TAB with no windows would otherwise give a full-screen dim with nothing in it, which reads as a hang. It is reachable from a fresh session, and from closing the last window in the switcher itself, which stays open by design. It now says so instead. The empty state has no card to click, so the backdrop closes the overlay on click as well as on Escape. That applies generally, not only when there is nothing to show. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysdocs(window-switcher): dispatch args are Lua, and focus raises in monocleDanilo M.1-6/+31
Two findings from testing against the running compositor, both of which would have cost time during implementation. Hyprland 0.56.2 evaluates dispatch arguments as Lua, so the form this design previously specified, dispatch focuswindow address:0x..., is a syntax error rather than a command. It fails silently unless stderr is read: the window does not focus and nothing is reported. hypr-windows.sh was written with hl.dsp.focus({ window = "..." }) for that reason, and this design had misread that as legacy oddity. The fullscreen batch sequence from that script is not carried over. Workspaces 1 and 8 are monocle and 4 and 5 are scrolling, so a stacked window is the normal case here rather than an edge case, and the worry was that focus would land without the window becoming visible. Tested with two stacked windows on workspace 1: focus alone raised the buried one, confirmed on screen in both directions, so no alterzorder and no batch sequence are needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysdocs(window-switcher): cards fit both orientations, and grow to a ceilingDanilo M.1-7/+32
The card cannot take its shape from its preview. DP-3 reports transform=1, so its windows are 1076x1916 portrait against 2556x1034 on DP-1, a 4.4x span of aspect ratio in one grid, and a fullscreen app on that monitor is portrait too. So the card is a fixed 16:10 box with the preview fitted inside it, which keeps rows aligned; sizing each card to its own preview would give ragged rows and break the alignment of the three text lines that makes the grid scannable. Cards now grow to fill their row up to 640px rather than sitting at a fixed size, so a lone window is shown large without a preview becoming a full-screen mirror of the window it stands for. Column count is the window count capped at six, centred, so one card sits dead centre and two straddle the middle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
3 daysdocs: design for the window-switcher componentDanilo M.1-0/+159
A full-screen grid of live window previews to replace the rofi list on ALT + TAB. The script it replaces is not broken, so this is an upgrade from a textual list to something that shows what is in each window. Settled during design: toggle-style rather than hold-and-release, since a missed ALT release would leave a stuck full-screen overlay; cards wrap into a grid rather than shrinking, so a high window count degrades by growing downwards; only special: workspaces are filtered, because jumping to another desktop is the point of a switcher. Feasibility was checked against quickshell 0.3.1 rather than assumed. ScreencopyView is compiled in and captured all four open windows, the scratchpad on special:special included, so hidden windows need no icon-only fallback. Hyprland.toplevels reads 0 until refreshToplevels() is called, and its address field omits the 0x that dispatches need. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S