{ "latest": "1.4.1", "releases": [ { "version": "1.4.2", "date": "2026-08-26", "kind": "patch", "unreleased": true, "summary": "ISO-only update: more room on the boot partition, plus a bootable check before finishing", "changes": [ "New installs reserve more boot partition space and get a final boot-setup check" ], "commits": [ "install: reserve ESP capacity for UKIs and snapshots", "boot: remove unused fallback image handling", "limine: remove obsolete boot entry cleanup", "fix: harden installed Limine boot chain" ] }, { "version": "1.4.1", "date": "2026-08-25", "kind": "patch", "summary": "Better GPU/laptop support, plus new touchpad controls and keyboard layouts", "changes": [ "Added a keyboard layout menu for quickly adding and switching between language layouts, including popular options like Dvorak, Colemak, AZERTY, QWERTZ, and QWERTY", "Added a toggle to enable/disable touchpad", "Improved Nvidia GPU and laptop compatibility" ], "commits": [ "00d3d0a12 gpu-config: let the modern driver answer for its own suspend", "a89fdb40d gpu-config: a driver package is not a driver", "94a11404d limine: let the menu find the other systems on the disk again", "f5af23e90 updates: wait for the repo rather than race the first boot", "2bef4fcc2 touchpad: add enable/disable toggle for touchpad", "45a61343a settings: manage keyboard layouts alongside keybinds", "fffbb8c02 localsend: read the body the sender actually sent" ], "commitNotes": { "00d3d0a12": "How a driver survives suspend is the driver's own business, and mainline changed\nits answer at 595: nvidia-utils now ships nvidia-sleep.conf asking for kernel\nsuspend notifiers, and its install scriptlet switches off the suspend, hibernate\nand resume units on the way past, because the handshake they perform is gone.\nThe legacy branches still ask to preserve video memory and still need those\nunits.\n\nBoth answers were being written to every card. The file here is called\nnvidia.conf while theirs is nvidia-sleep.conf, so it stacks rather than\nreplaces, and a Turing machine ended up holding both mechanisms at once with\nthree services switched back on underneath them. Turing and newer now get the\nmodeset options and nothing else, and say nothing about sleep at all. Maxwell\nand older are untouched.\n\nThe kms hook is left alone. It was being removed to keep the nvidia modules out\nof the initramfs, but that hook collects modules by matching paths inside the\nkernel tree, and nvidia's live beside ", "a89fdb40d": "An NVIDIA card came up with no driver at all on a machine where the driver\nwas installed. The open driver ships its module already built, under the one\nkernel directory it was built for, while asking only for \"a kernel\" and not\nthat one. When the kernel moves on its own the package is still current and\nits module is still there, just somewhere nothing will look, and the card is\nleft with nothing driving it: no device node, no outputs.\n\nThe library asked pacman whether a driver was present, which is the one\nquestion that cannot see this. It now asks whether a module exists for the\nkernel the machine is about to boot, and works out which kernel that is from\nthe module trees rather than from the running one, since inside an installer\nthe running kernel belongs to the installer.\n\nWhich tree to believe takes three answers, not one. The kernel already\nrunning wins outright, which can only happen on a live machine and is exactly\nright there. Otherwise the stock kernel, since that is what an i", "94a11404d": "Matches the ISO path. Probing was off because it offers the default EFI loader\nalong with whatever it finds, and that loader is Limine itself here, so the menu\ngained an entry that booted the menu. Off also meant systemd-boot and rEFInd\nwent unseen, and a machine sharing a disk with another Linux showed nothing of\nit. Windows is unaffected: the probe never looked for bootmgfw.efi, which is why\nit is added by name separately.", "f5af23e90": "The [mainstream] repo is put in place during the first boot, and for the moment\nthat takes there is no database behind it. Anything asking in that window gets\nnothing: the Welcome app's installs fail on packages the machine cannot see yet,\nand an update is announced that would fail the same way if the user took it. The\nwindow closes on its own in seconds, so the answer is to wait for it rather than\nreport a problem that is about to stop being true.\n\nThe repo is asked by what comes back, not by an exit status. pacman -Sl on a repo\nthat is configured but unsynced exits 0 and puts its warning on stderr, so the\nstatus says yes at exactly the moment the answer is no. An empty listing is the\nreal signal.\n\nA machine with no [mainstream] configured at all is a different thing from one\nwhose repo has not arrived yet, and is treated as such. There is no first boot to\nwait through there, so the installs let pacman speak for itself and the update\ncheck carries on as before, rather than going quiet", "2bef4fcc2": "A dropdown on the Mouse page turns the touchpad off and on. Enabled is a\nper-device option rather than one of the input.touchpad settings, so it is\napplied with hl.device() against the touchpad hyprctl reports, and the same\nstatement is persisted to the user's env.lua \u2014 what eval applies now is\nbyte-for-byte what replays next session, so the two cannot drift. The device\nname is checked before it sits inside a quoted Lua string, the same last-stop\ndiscipline the layout writer keeps, and a machine with no touchpad shows no\ncontrol for one.\n\nThe persisted block goes through the managed-block writer the keyboard\nlayouts already use, under a marker of its own.", "45a61343a": "The Keyboard settings page keeps Hyprland's layouts above the shortcut editor,\nrather than making either setting a separate destination. It reads the XKB\nlayout catalog from the system, applies each change immediately, and writes a\nsmall managed override so the selection survives a restart.\n\nThe picker searches layout codes and names, leaves already-enabled layouts out\nof its choices, and uses the same compact controls as the keybind editor. The\nlayout-cycle shortcut is shown in the page with the same keycaps as the\ncheatsheet and is bound to Ctrl+Super+K, where it moves the focused keyboard to\nits next enabled layout.\n\nThe keybind editor is a section inside the shared page instead of a nested\nscrolling page, so the two sections follow one continuous scroll area. The page\nkeeps the name every other destination carries, and its two sections live under\nit the way the bar and connectivity pages keep theirs. The override writer is a\nscript rather than a program inside a string, where it ca", "fffbb8c02": "A sender is free to stream a file without saying up front how long it is, and\nphones do exactly that. Only Content-Length was being read, so a chunked upload\nmeasured zero, the copy loop never ran once, and the empty file that had already\nbeen opened was renamed into place and answered with 200. What arrived looked\nlike a delivered file and opened as nothing. Sending was unaffected, because the\nsender here sets a length on every request.\n\nChunked bodies are now decoded, and a request that carries neither a length nor\na chunked body is refused rather than quietly stored as nothing.\n\nThe size the manifest promised is checked against what arrived. A body cut short\nleaves the partial file behind and reports the failure, instead of renaming a\nshort file over a name the user has no reason to distrust." } }, { "version": "1.4.0", "date": "2026-08-23", "kind": "feature", "summary": "Update UI Quickshell to 0.3.1", "changes": [ "Now running on Quickshell 0.3.1", "LocalSend file drops now also work on the vertical bar", "Bluetooth input devices now reconnect more reliably" ], "commits": [ "03511127a quickshell: update to 0.3.1.r1", "7fd1cf44d media: one drop target serves both bars' widgets", "04f0d354e bluetooth: start the applet the tray-icon mask was silently keeping down", "6fc1deda5 bluetooth: stop scanning the air while something is already connected", "5c421cb38 bluetooth: reconnect trusted audio devices at startup", "52158a0c9 bluetooth: pair before trusting a device" ], "commitNotes": { "03511127a": "Pin _commit to 0fed22a2c47d9568ddf13cf61586b3f2ac4378a2, the current\ntip of master and one commit past the v0.3.1 tag (1a4716c).\n\n0.3.1 is a bug-fix-only release (session lock, JsonAdapter,\nScriptModel, and pipewire crash fixes, among others). No new\ndependencies, so depends/makedepends are unchanged.", "7fd1cf44d": "The horizontal media widget opens the controls popup in transfer mode when a\nfile is dragged over it, and the vertical bar's widget is a parallel copy that\nnever received the drop target \u2014 a drag over it fell through to nothing, on\nthe bar where the widget looks the same and promises the same.\n\nThe target is one component now, under both widgets, the way the tray and the\ngroup pill are already shared between the two bars: hovering with files opens\nthe popup as a visible place to release, the popup's own drop area finalizes,\nand a send already in flight is left undisturbed. The popup already places\nitself beside whichever bar it was opened from, and the next thing a drag\nshould do lands on both bars by construction.", "04f0d354e": "The autostart override that was meant to hide blueman-applet's tray icon\ndisabled the applet instead: Hidden=true in an autostart entry removes the\nentry, it does not quiet it. That mattered more than an icon. The applet is\nwhat registers blueman's BlueZ agent \u2014 the service bluetoothd consults before\nhonoring a connection a device opens on its own \u2014 and the KDE tools that used\nto provide one left with them. Since then no agent has run at all.\n\nDaily use never noticed, because nothing in it asks: connections the user\nstarts need no authorization, and trusted devices are waved through. The gap\nis a paired device that is not trusted dialing back in by itself \u2014 a mouse\nwaking from sleep. Refused for want of anyone to ask, it retries on every\nmovement, and the user watches it connect and disconnect without end. Trusting\ndevices on connect was the working remedy; this restores the thing whose\nabsence made it necessary.\n\nThe session starts the applet itself, through a script that first writes", "6fc1deda5": "The night light panel started a Bluetooth scan when it opened and stopped one\nwhen it closed, carried over whole from the Bluetooth panel it was written from.\nNothing in it has anything to do with Bluetooth. Opening it put the adapter into\na scan, and a scan is one of the surer ways to unsettle a mouse or keyboard that\nis already connected \u2014 the link it has to hold is competing with the search for\nlinks it does not.\n\nThe list of devices found so far had the same trouble from the other direction.\nIt recorded the order they turned up in and then announced that the record had\ngrown, and the only thing reading that record is the sort inside the very list\nthe recording happens in, so the announcement asked the list to build itself\nagain. Building it recorded whatever was new, which announced again. Each new\naddress restarted it, and while a scan is running they arrive without pause.\nThe record is kept, the announcement is not: the sort reads what was written\nmoments earlier in its own run, ", "5c421cb38": "BlueZ restores the adapter at login but does not initiate a connection to\nremembered devices. The old Hyprland hook filled that gap by sleeping four\nseconds, parsing bluetoothctl's trusted-device listing, and launching one\nbackground connect process per result.\n\nMove that one-shot request into the shell's Bluetooth service. After the same\nstartup delay, it asks each trusted, disconnected device BlueZ classifies as\naudio to connect. Speakers and headsets therefore regain startup recovery without\nhard-coding a device address.\n\nInput devices are deliberately excluded. A mouse or keyboard is left to its\nnormal Bluetooth reconnection path instead of being given an unsolicited host-side\nconnection request during startup. The standalone script and its formatted-command\nparsing are no longer needed.", "52158a0c9": "The sidebar and connectivity panels labelled their unpaired-device action Pair,\nbut sent BlueZ a Connect request. Connect only asks for compatible profiles;\nPair is the request that establishes and stores the bond before discovering\nservices.\n\nBoth panels now send the pairing request and wait for the paired state before\nmarking the device trusted. A failed or cancelled pairing therefore no longer\nleaves a discovered device trusted, while a completed pairing keeps the\nprevious Always connect behavior until it is forgotten.\n\nThe startup reconnect path remains audio-only, so this does not reintroduce\nthe old host-initiated mouse connection attempts." } }, { "version": "1.3.2", "date": "2026-08-19", "kind": "patch", "summary": "Improved compatability with hybrid NVIDIA laptops and Improved Intel graphics support", "changes": [ "Allow hyprland to detect the graphics card automatically on hybrid NVIDIA laptops", "Newer Intel graphics (Arc, Meteor Lake and later) now use the newer Xe module", "Cleaned up the color picker's look and behavior", "The media controls now open beside the widget on a vertical bar too" ], "commits": [ "9fb87008a fix(gpu-config): stop managing AQ_DRM_DEVICES on hybrid NVIDIA laptops", "84127def8 gpu: name the Intel module the kernel actually bound", "cd61965e7 color field: show the color, not the alpha behind it", "617184c4e media: the controls open beside the widget on a vertical bar too", "fd401b47e widgets: a color field can be emptied where having no color means something", "cf85bd8bb widgets: a color field's swatch and box stand as tall as its button" ], "commitNotes": { "9fb87008a": "nvidia_write_aq_drm() pinned Aquamarine's AQ_DRM_DEVICES to the NVIDIA\ncard alone whenever a hybrid GPU setup was detected. On most hybrid\nlaptops the internal panel is wired to the iGPU, not the dGPU, so this\nhanded Hyprland a render device with no connector for the built-in\nscreen \u2014 session never comes up, looks like a boot failure.\n\nConfirmed on an HP Omen Max 16 (RTX 5080): env.lua carried\n hl.env(\"AQ_DRM_DEVICES\", \"/dev/dri/by-path/pci-0000:02:00.0-card\")\nwith no iGPU entry, matching this exact failure mode.\n\nA joined-list fix (NVIDIA first, plus every other connector-capable\ncard, so the panel's actual owning GPU stayed reachable) was tried\nbefore committing anything, on the theory that the missing connector\nwas the root cause. Testing on the same hardware showed it still\nfailed to boot. That points to the actual problem being NVIDIA's side\nof the multi-GPU render/blit handoff itself, not connector coverage \u2014\na known-fragile path per the Hyprland wiki's own multi-GPU/NVIDIA\nnote", "84127def8": "The initramfs was told i915 for every Intel card. Which of i915 and xe claims a\npart is the kernel's call and it moves from one release to the next, so on the\nnewest hardware \u2014 where xe takes it \u2014 the initramfs carried a module that was\nnot driving anything and early KMS quietly did not happen. Nothing broke; the\ndriver still loaded later, and the flicker-free boot it was there for simply\ngot missing on the parts most likely to want it.\n\nReading it off the card settles it. The generation tiers were no help: the one\ncovering Xe spans parts on both sides of the split, so mapping tier to module\nwould have put xe in front of an Arrow Lake, which is worse than the hardcoded\nanswer it replaced. Asking what is bound cannot disagree with the kernel, and\nfollows it for free the release a default changes. i915 when nothing answers,\nwhich is both the older parts' module and what was named here before.\n\nThe card list the compositor is given grew a seam for its lookup directory at\nthe same time, b", "cd61965e7": "The box takes a string, but a caller with a color rather than a typed one hands\nover whatever Qt makes of it \u2014 and Qt writes a color that carries alpha as nine\ndigits, the leading pair being that alpha. Nothing here expected them. The\nswatch painted the alpha too, so a color the interface draws at a tenth opacity\narrived as a near-invisible smear of itself; the box was sized for seven\ncharacters and showed a fragment of the nine; and the reader that guards typing\nrejects eight digits outright, so the value could not be edited back into shape\neither. A color nobody could see, read, or correct.\n\nThe reader now takes that form and drops the alpha, and the swatch and the box\nread through it rather than off the raw value. What a color is worth and how\nmuch of it shows through are two settings here, and only the first belongs in a\nbox that says which color it is.", "617184c4e": "The popup was taught to open under the widget that asked for it, but only along\none axis: the placement returned early on a vertical bar and the margin was\nfixed at half the screen, so wherever the media widget sat on a side bar the\ncontrols appeared at the middle. The widget there never published itself either,\nso there was nothing to place against had the placement run.\n\nIt now measures the same way on either edge and slides along whichever axis the\nbar leaves free \u2014 across under a top or bottom bar, up and down beside a left or\nright one \u2014 clamped to the screen with the same gap at both ends. The stock\nmiddle spot remains what is used until the widget has been measured, and\nwhenever it cannot be.", "fd401b47e": "A field could be given a color but never returned to not having one: an emptied\nbox normalises to nothing, and nothing was read as a typo and refused. Clearing\nit now says \"no color of my own\" wherever that is a state worth having.\n\nOff unless asked for. A gradient lane cannot draw an empty string, so refusing\nit there is still the right answer, and every field that exists today keeps the\nbehavior it had. It also takes a genuinely blank box rather than merely an\nunparseable one, so a half-typed code still snaps back instead of quietly\ndropping the color that was set.", "cf85bd8bb": "The swatch took its height from the text control's drawn outline and the button\nbeside them took its own, so a row meant to be read as one control was three\nthings of two heights. Both now take the button's, which is the tallest of them\nand the one with a hard edge to line up against.\n\nThe same component draws the border gradient colors, so those rows sit level too." } }, { "version": "1.3.1", "date": "2026-08-17", "kind": "patch", "summary": "Works with newer LocalSend devices that now expect extra security during transfer", "changes": [ "Enable sending files to receivers that now require a certificate to connect" ], "commits": [ "38d58c79a localsend: offer a certificate when a receiver asks for one" ], "commitNotes": { "38d58c79a": "Receivers began requiring the sender to present a certificate during the TLS\nhandshake. Sending never offered one, so the connection was refused before a\nbyte of the file moved, and the failure arrived as a certificate alert rather\nthan as anything about the transfer. Nothing here changed; the other end did,\nwhich is why this worked for months and then stopped.\n\nOne is generated on first use and kept, so a peer goes on seeing the same\ndevice rather than a new one per transfer. Which certificate it is does not\nmatter to the handshake \u2014 that one is offered at all does. It is presented to\nevery receiver, since one that does not ask simply ignores it, and a receiver\nthat refuses to load it is no worse off than before." } }, { "version": "1.3.0", "date": "2026-08-17", "kind": "feature", "summary": "The dock goes on any edge you like, and the bar arranges itself", "changes": [ "Put the dock on any edge of the screen: top, bottom, left or right", "The bar steps aside when you give the dock the edge it was using", "Themes now carry which edge the dock sits on, along with everything else they remember", "The flexible-space widget is retired \u2014 the bar packs its own sections now, and a layout you already saved moves across on its own", "Bar popups open centered under the widget they belong to, wherever you have put it", "A popup near the edge of the screen stays on screen instead of running off it", "The media widget keeps its size and its popup in the right place outside the center", "The timer keeps a steady width while it counts down, instead of nudging its neighbors", "Dividers stay inside the bar's rounded background", "Close the window rule editor from its own corner, and switch between rules without closing it", "Pick a window rule's app by name instead of by window class", "Drives you have mounted stay on the tab you mounted them from", "Wallpaper thumbnails show up for filenames with punctuation in them", "Media controls come back from the shortcut, and a second browser's player no longer disappears" ], "commits": [ "47a6af33c bar: the retirement scrub reads every group form the bar accepts", "a22771894 themes: a theme with no dock edge leaves the dock where it is", "350ad10da widgets: start the ripple where the pointer actually is", "ba1ba669b dock: pick an edge, and step aside for the bar", "f116b6620 media: hide a built-in player only for a browser the bridge runs in", "306812069 media: the controls IPC drives the shared open state", "6593fdc24 bar: harden the layout model and its editor", "a7cf2e745 bar: vertical side pills keep the bar's visible width", "67f4fd306 bar: resources gives way to media only when they share a pill", "45c4db442 bar: retire the spacer; the sections pack themselves", "6b245fd1e bar: hover popups stay inside the screen like the volume one", "98515b5ca bar: give a side section's spare width a single owner", "76899afe4 fix(bar): stop anchoring the timer row inside its group layout", "db4c29ee8 fix(bar): keep the timer tray a steady width while it ticks", "98cfbaab2 bar: media holds its width beside other widgets off center", "b3a3e0cd2 bar: center popups on the pill, not the window", "d274af5dc bar: the media popup opens under the widget, wherever it sits", "facf31326 bar: media keeps its size outside the center sections", "df5e4288e rules: cancel from the editor's corner, switch rules without closing", "3af94951c rules: pick from apps by name, not windows by class", "643157b80 decorations: name the blur xray option on its switch", "114e1aed9 mounter: keep mounted drives on the tab that mounted them", "9eb844837 bar: keep the line-separated dividers inside the bar", "c2d09742f picker: hash the thumbnail name the way the generator writes it" ], "name": "Everything in its place", "commitNotes": { "47a6af33c": "A layout group is not always {widgets: [...]}. It can be a bare string, or\ncarry items instead, and the bar, its editor and the seeding pass all go\nthrough one tolerant reader for exactly that reason. The scrub did not: it\nread .widgets directly, saw nothing in a group written either other way, and\nconcluded the group was empty. Empty groups are dropped. So a config holding\none spacer anywhere and one older-form group anywhere lost the second on load\nand had the loss written back, with no backup to undo it.\n\nThis is the same shape as the fault the widget seeding pass carried, which was\nfixed by handing it the tolerant reader. The scrub gets the same treatment,\nand one more guard besides: a group with nothing retired in it is now returned\nexactly as it was read, in whatever form it was written. Only a group that\nactually held a retired id is rewritten, which is the only case where changing\nits form is the point.\n\nWhile here, the spacer pivot no longer insists the spacer be alone in its\n", "a22771894": "Every theme saved before the dock could move names no edge, and applying one\nreplaces config.json wholesale, so the key simply vanishes from the file. That\nis the worst of both answers: the adapter keeps the value it already has, so the\ndock stays put and nothing looks wrong, while the file on disk no longer says\nwhere it goes. The next start reads no edge and falls back to the default, and\nthe dock moves somewhere the user never chose, one boot removed from the theme\nswitch that caused it.\n\nThe live edge is written in instead, so the screen and the file agree. A theme\nthat does name an edge still wins outright \u2014 that is the theme owning its\nlayout, which is the point \u2014 and this only fills the silence.", "350ad10da": "The ripple lives inside the button's background, which the control insets on\nboth axes, but only the vertical inset was being taken off the press\ncoordinates \u2014 and the reach was measured across the whole control rather than\nthe background. Nothing showed it until dock buttons gained a left and right\ninset, which put the ripple ten pixels off the pointer on a side dock.", "ba1ba669b": "The dock was hardwired to the bottom, so a bottom bar and the dock\nstacked on one edge and fought over the same hover strip. The dock\nnow takes a position - bottom, top, left or right - from a dropdown\nthat offers all four, renders vertically on the sides, and gives way\nto the bar when the bar is moved onto the edge it saved. Asking for\nthe bar's own edge from the dock's setting is the one case the dock\ndoes not give way: the bar crosses to the far side of its axis and\nthe dock takes what was asked for, because refusing the choice or\nquietly answering with a different edge are both worse than moving\nthe thing that was not being talked about. One edge value drives the window anchors, the reveal\nslide, the layout flow, the magnify origin, the running-window dots\nand every popup's direction; the anchor sets swap through states\nbecause a live flip through conditional bindings passes an illegal\nthree-anchor moment that wedges the layout. The bar and dock edges\nresolve in one shared place th", "f116b6620": "The built-in was hidden whenever any per-window bridge of the same\nbrowser make existed, so with a second browser running unbridged its\nonly player vanished from the popup while the bar kept its title. No\nname-level linkage ties a bridge to its browser - the bridge embeds\nits own host pid while the browser names its player after its D-Bus\nconnection or its pid depending on version - but the bridge host is\nalways a child of the browser that owns the built-in's connection, so\na helper resolves both connections to processes and matches ancestry.", "306812069": "The IPC handlers wrote the loader's active property directly, and an\nimperative write severs its binding to the shared open flag - after\none IPC call the widget click, Esc, the global shortcut and the\ntransfer auto-close all stopped reaching the popup. Nothing consumes\nthe loader directly: the keybind already goes through the shared\nflag, and the bridge extension never talks to the shell.", "6593fdc24": "The editor offers a bar-managed widget back when a layout arrives\nwithout one, floats such entries to the front of their group so drags\ncannot reorder around something it does not show, and reads groups\nthrough the shared ObjectUtils helper instead of a diverged copy.\nChromeless detection ignores entries the bar cannot render, a missing\nenabled key counts as enabled everywhere, the space-taking widget\nlist lives in one predicate, media stops yielding when the window\ntitle already stretches the pill, pilled groups no longer build a\nhidden duplicate of every widget, and the timer tray can no longer be\nsquashed while its reveal animates.", "a7cf2e745": "The side sections span the whole window, but on the floating style\nthe bar's visible body is inset by the gap, so their pills ran wider\nthan the bar they sat on while the center pills came out at the\nbar's own width. Both side columns now inset by the same gap, and\nevery pill reads as one shape.", "67f4fd306": "The meters beyond memory hid whenever anything was playing, wherever\nthe widget sat - the collapse exists to hand the shared pill's room\nto media, and a resources widget in its own pill was shrinking for a\nplayer it was not even next to. Standing alone it now keeps the full\nreadout; beside media it still hands the room back while a track\nplays.", "45c4db442": "With each side row carrying its own stretch filler, the left cluster\npacks left and the right cluster packs right on their own, and the\nspacer was a movable way of asking for what is now the fixed rule.\nThe widget is gone from the bar, the default layout and the customize\nmenu.\n\nA layout saved before this still names the spacer, and the bar draws\nnothing for an id it no longer knows \u2014 so the room the spacer held\ndisappears and every widget beside it slides over, on a bar the user\nnever asked to rearrange. Leaving that to the editor was not enough:\nit only rewrites a section once someone opens it, so the bar stays\nrearranged until they do. The config drops retired ids as it loads\ninstead, once, taking a group left holding nothing with them. The\neditor reads the same list, so the two cannot come to disagree, and a\nfresh install names no retired id and so is never written to at\nstartup \u2014 the one thing this file must not do while a first login is\nseeding it from outside.\n\nDropping the widg", "6b245fd1e": "The volume and media popups clamp their position to the screen, but\nthe shared hover popup placed itself purely by centring on its widget,\nso weather, resources or the update indicator near a screen edge\npushed their popups past it. The same clamp now applies to every\npopup that centers on a bar widget, on both bar orientations.", "98515b5ca": "Fill meant two different things: share-the-set-width inside a center\npill, and take-the-section outside it. Splitting those claims, only\nthe widgets whose job is genuinely width still take a section \u2014 the\nspacer, which exists to eat room, and the window title, which needs\nroom to shrink to an ellipsis. Everything else keeps its fill inside\ncenter pills and its set width everywhere else, instead of clock and\nmedia ballooning in turn.\n\nThe media-and-resources pairing is the one exception: media asks for\nits minimum and takes back the room resources leaves, an exchange\nthat only means something inside a pill with a set width. That pill\nkeeps its width floor and media keeps its fill in every section, so\nthe pair reads the same wherever it sits.\n\nThat alone still left the surplus unowned: a row wider than its\ncontent does not push items to its edge \u2014 columns with no stretch\nshare the leftover in proportion to their size, so free-standing\npills drifted apart across the section. The stock arr", "76899afe4": "The tray is a BarGroup, whose default property places children in a\nGridLayout, and anchoring an item a layout manages is undefined\nbehavior that also logged a warning on every relayout. Centring\nthrough the layout's own alignment puts the row in the same place\nwithout fighting it.", "db4c29ee8": "The time readouts render in the UI font, whose digits each take their\nown width, so the pill grew and shrank a few pixels every second as the\ndigits changed. Tabular figures give every digit the same advance; the\npill now only resizes when a readout gains a digit or a section appears,\nwhich the existing width animation already covers.", "98cfbaab2": "Media fills inside its pill so a center pill's spare room goes to the\ntrack title \u2014 the pill there has a set width, so the fill is bounded.\nOff center the pill is as wide as its group-mates make it: put media\nbeside the window title or clock and the pill legitimately fills the\nsection for them, and media's fill then split that surplus too, growing\nit past its set width. The fill now applies only in center pills, so\noff-center media takes its preferred width and the surplus stays with\nthe widget the section fill exists for. Shortened resources rides the\nsame scope, being the other half of the stock pairing.", "b3a3e0cd2": "Every anchored bar popup centerd itself using the visible pill's width\nbut positioned the window, and the pill sits a shadow margin inside the\nwindow's anchored edge \u2014 so clock, weather, volume and the rest all\nrendered that margin off center toward the right, or downward on the\nvertical bar. The offset now subtracts the pill's inset, written as the\nsame expression the background uses for it, so the two cannot drift\napart again.", "d274af5dc": "The popup's position was arithmetic from the stock layout \u2014 half the\nscreen minus half the popup minus the widget width \u2014 which pinned it to\nthe center-left spot the widget used to occupy, however the bar is\narranged now.\n\nThe widget hands itself to the popup through a shared Item reference,\nset on every path that opens it: click, receive view, and a file\ndragged onto it. The popup measures that widget against its bar window\nonce per open \u2014 the loader rebuilds the window each time, so the\nmeasurement is current without binding against the layout \u2014 centers\nitself under it, and clamps to the screen. Anything short of a clean\nmeasurement leaves the stock position in place, and the measurement can\nnever take the click down with it: publishing is one assignment, and\nthe placement runs guarded inside the popup. The vertical bar keeps its\nown placement.", "facf31326": "A group holding a fill-width widget fills its section when placed off\ncenter, which exists so a window title can shrink to an ellipsis rather\nthan run past its section. Media is on the fill-width list for a\ndifferent reason \u2014 leftover room inside its own pill goes to the track\ntitle \u2014 and promoting that to section level meant a media group moved\nto the left or right side stretched across the whole section. The pill\nnow fills only for widgets that genuinely want section width; media and\nthe shortened resources keep their fill inside the pill.", "df5e4288e": "Editing a different rule meant cancelling, scrolling back, and opening\nagain: every card's Edit button went dead while the editor was open,\nand the only way out was the Cancel at the very bottom of a long\neditor. A close button now sits in the editor's top corner, and Edit\nstays live on every rule but the one being edited.\n\nSwitching does not repoint the open editor \u2014 a used ConfigSwitch has\nhad its checked: binding broken by the click, so the editor is torn\ndown and rebuilt for the new rule, the same teardown closing has\nalways relied on. Unsaved changes are dropped on a switch, exactly as\nthey are on Cancel. Reorder and delete stay locked while the editor is\nopen, since moving a rule under an editor keyed to its index would\nsave over the wrong one.", "3af94951c": "The picker listed one row per open window under its raw class and\ntitle, so three browser windows made three entries and a person hunting\nfor their file manager had to know it answers to org.gnome.Nautilus.\nIt now lists each open app once, under the name its desktop entry\ngives it; a class with no entry is tidied instead \u2014 reverse-DNS prefix\ndropped, dashes opened into spaces, first letters raised. Selecting one\nwrites the same anchored class pattern as before.", "643157b80": "The switch carries the option's real name, xray, so what it writes can\nbe looked up against Hyprland's own docs. The tooltip already says what\nit does in a sentence.", "114e1aed9": "A drive picked on the Local tab vanished from it the moment Mount was\npressed, reappearing only under a separate Mounted tab \u2014 which read as\nthe drive disappearing. Mounted entries now stay put: a Mounted category\non the Local tab for block devices and a Mounted shares category on the\nNetwork tab for network filesystems, each with the same one-click\nunmount row the old tab had, and the tab itself is gone. The row moves\nto a shared component so both categories draw the entries one way.", "9eb844837": "The separator component sizes itself with Layout margins, which only\nexist inside a layout. The two dividers flanking the center groups are\nanchored children instead, so those margins did nothing and the\ntop/bottom anchors stretched each line across the bar's full height \u2014\npast the rounded background, which the Float corner style insets. They\nnow center at the same short height the layout margins produce, and the\ncomponent carries that height itself so any anchored use gets it.", "c2d09742f": "The freedesktop thumbnail name is an md5 of the file URI, and the two\nhalves disagreed on what that URI looks like: the bulk generator gets\nits URI from GIO, which leaves @ & = , : $ + raw in a path, while the\npicker encoded them through encodeURIComponent. Any wallpaper with one\nof those characters in its name hashed to a thumbnail that was never\nwritten and drew as an empty tile. The picker now puts GLib's raw set\nback after encoding, and the magick fallback's own urlencode is held to\nthe same rule so both generators write one name. Semicolons stay\nescaped on every side \u2014 GLib escapes them too." } }, { "version": "1.2.0", "date": "2026-08-12", "kind": "feature", "summary": "Windows the shape you want them, and rules for the ones that are different", "changes": [ "Shape your windows from Settings: corner radius, border thickness, and the gaps between them and around the screen", "Set how see-through a window is and tune the blur behind it", "Dim the windows you are not using, by as much as you like", "Give your borders a gradient of your own colors, or leave them following the wallpaper", "Shape the shadow under a window: its size, how sharply it fades, how dark it is, and which way it falls", "Pick how windows animate \u2014 Expressive, Smooth, or Minimal \u2014 or drop in an animation file of your own", "A window rule editor: teach one app where to open, how to look, and whether it floats", "Themes now carry shape, transparency, blur, dim, borders, shadow, animations and window rules as well as your colors and interface changes", "Set a folder as your wallpaper and let it rotate on a timer", "Qt apps now take their colors from your wallpaper along with everything else", "Your session is captured while it runs, so a crash no longer loses your window layout" ], "commits": [ "4d7a7486b readme: bring the front page up to 1.2.0", "193d32686 update: curate the 1.2.0 release notes and let a release carry a name", "813f2c88d hyprbars: build the title bars from our fork", "8f56791f6 system: a way to pull an upstream plugin fix onto built machines", "3ec465332 welcome: route the Resolve update button through the session", "7846abb72 gaming: stop naming the legacy lib32 driver pacman cannot resolve", "8f9b5ae48 settings: build the editing menu when it is asked for", "ffdd7dac5 changelog: credit the to-do list fix, and say desktop rather than shell", "593941ebd scrolloverview: stop the remembered commit outranking the branch", "99d6832fc dock: stop drawing a desktop action's icon name as text", "98db9073e hyprland: frost behind app menus so see-through ones stay readable", "1b67db16b overview: give each wallpaper its own scaled copy", "ec875adb7 settings: stop doing work whose result is thrown away", "ca1bf64ef themes: read the borders a previous release turned off", "9b204117a settings: finish what the last two changes started", "6b3d1a71a hyprland: keep a bad rule from taking the good ones with it", "deb7b7cf1 themes: stop the decoration writer from breaking general.lua", "38a38e133 settings: keep what a rule holds and what a switch shows", "42f28c42b themes: hold the whole border toggle when an old theme restores", "7ba676f27 themes: keep a theme's own values from reaching the compositor as code", "0ba6d1ff2 themes: restore decorations whole, not just the keys a snapshot names", "c03548021 themes: carry window rules with a theme", "fdd069756 decorations: add a window rules editor", "7f4c5df90 hyprland: give the settings shell its own window rules file", "01ad9606c decorations: one press back to stock for the window sections", "cc0da30a3 decorations: the first section names the windows it governs", "e7dee1b99 decorations: animation styles that travel with a theme", "caeb8653f decorations: gradient border from the palette or two colors of your own", "70c25ee6e decorations: blur noise, saturation and see-through", "f5a09782d decorations: shadow sliders", "f250d71b5 decorations: corner radius, border thickness and window gaps", "c6787734f decorations: expose blur depth and window transparency", "53c3b59e1 decorations: one reader and writer for the decoration settings", "bcea466c7 settings: text fields carry the shell's own context menu", "34d249890 display: spell color the way the rest of the shell does", "0b783abbf settings: save a slider only when the user moves it", "d4d80ac53 hyprland: let a see-through window frost what is behind it", "95862df83 hyprland: let the override file hold a key with a dot, and a table", "8a1dccb6e themes: bound what waits, keep what was chosen, cap what grows", "3205817aa themes: stop the colour pipeline corrupting the files it writes" ], "name": "Let there be RICE", "commitsTotal": 55, "commitNotes": { "4d7a7486b": "Names what the release added, and corrects how the shell's provenance is\ndescribed: the dock is no longer offered as an example of what came from\nupstream now that four fifths of it is new, and a theme is described by\nwhat it carries rather than as saved colors \u2014 colors being the one thing\nupstream is already known for.", "193d32686": "The generated notes were commit subjects, which read as a work log rather\nthan a description of what changed for someone using it. A release can\nalso carry a name now \u2014 1.2.0 is Let there be RICE \u2014 carried through the\nmanifest as an optional field so unnamed releases show no empty slot.", "813f2c88d": "The fork carries a fix confirmed on the affected hardware: title bars\nrender wrong on NVIDIA whenever blur is on, transparent on one driver\nbranch and a solid block on another, because the rounded-corner mask\nclears and writes the stencil buffer without opening the stencil mask\nfirst. Closed, the clear and the write are silently dropped and the\ntest that follows reads whatever was left in the buffer.\n\nThe fork's branch is based on the commit that builds against the\nHyprland we ship rather than on current upstream, which has already\nmoved to headers 0.56 does not carry. Merging upstream forward is a\n0.57 job, kept on mainstream-next until then.", "8f56791f6": "The rebuild scripts try their cached last-good ref first and stop at the\nfirst ref that builds, so a machine that already builds the plugin never\nlooks at the branch again. That is what keeps a routine Hyprland patch\nbump down to seconds, but it also means an upstream fix stays invisible\nuntil Hyprland changes enough to break the cached commit \u2014 arriving by\naccident, months later, or not at all. Nothing in Repair Install cleared\nit either.\n\nDropping the cached ref makes the next run fall through to the branch.\nA serial recorded per machine keeps that to once, so ordinary upgrades\nstill hit the cache. Machines that predate the marker only record where\nthey stand, since there is no way to tell whether their ref is behind\nand refreshing the fleet on arrival would rebuild everyone for nothing;\na fresh install skips the rebuild too, its baked plugin being current.\n\nBump _PLUGIN_REFRESH_SERIAL when a fix lands upstream that machines need.", "3ec465332": "The notification's Install button runs in the weekly check's detached\nwaiter, which lives in the user manager's cgroup rather than the login\nsession's scope \u2014 polkit maps that to no session at all, so the\npasswordless rule (active local wheel) cannot match and there is no\nagent to prompt, and the update was refused every time. The same pkexec\nfrom the Welcome app works because the app is a child of the session.\n\nThe button now asks the compositor to spawn the pkexec, making it a\nsession child with exactly the standing the rule already trusts. The\ndirect call stays as the fallback for a terminal run, and the refusal\nnotification stays for whatever remains.", "7846abb72": "The legacy NVIDIA editions install the branch-matched 32-bit userspace\n(lib32-nvidia-580xx-utils and kin) with the driver itself, and those\npackages exist in no online repo \u2014 the first-boot keyring switch removes\nthe baked file repo that carried them. pacman -S resolves every named\ntarget against the sync databases before --needed can skip it, so the\nGaming Mode installer naming the already-installed package aborted the\nentire transaction with 'target not found', taking Steam and the rest of\nthe stack down with it.\n\nThe lib32 driver is now added only when it is absent and resolvable:\nalready installed means nothing to do, resolvable (mainline\nlib32-nvidia-utils from multilib) means install it, neither means warn\nand carry on so the rest of the gaming stack still lands. A nouveau\nsystem no longer picks up the mainline proprietary 32-bit userspace by\ndefault either \u2014 lib32-mesa already covers it.", "8f9b5ae48": "Every text field built the menu along with itself, and the menu asks the\nfield whether there is anything to paste. That question goes to the\nclipboard, which is quick to answer when something owns the selection and\nhas no answer at all when nothing does \u2014 the installer on live media being\nexactly that case, where a password box is often the first field to exist.\n\nNothing needs the menu until a right click, so nothing builds it until then.", "ffdd7dac5": "The curated bullet called it the shell, which is what we call it among\nourselves and not what anyone reading a changelog calls the thing their\nfonts changed on.", "593941ebd": "The last commit that built was tried before everything else, so once a\nmachine had one that still compiled it kept it forever and nothing merged\nafterwards ever reached it. It is now the way back rather than the first\nchoice, tried only if the branch itself will not build.\n\nNothing recorded which sources a plugin came from, only which Hyprland, so\na run could not tell that it had already built these very sources \u2014 the\ndaily timer recompiled them every night to arrive back where it started.\nThe stamp carries the commit as a second line, and a stamp written without\none reads as unknown, which is what carries existing installs across.", "99d6832fc": "The menu names its icons as Material Symbols, and a desktop action carries\nan icon theme name instead. The symbol font drew that name as its own\nletters, so Code's actions read COM.VISUALSTUDIO.<>.OSS \u2014 <> being the\nfont's own ligature for the word code.", "98db9073e": "A menu belonging to a transparent window was drawn straight onto the\nwallpaper, which left the text sitting on whatever happened to be behind\nit. Nothing frosts a popup unless the compositor is told to.", "1b67db16b": "Every theme keeps its wallpaper as wallpaper.jpg, and the scaled copy the\noverview plugin is handed was named after that basename alone. All themes\ntherefore shared one cache entry, and the freshness check compares only\ntimestamps, so whichever theme wrote it last was shown behind every other\none. Key the entry by the path it came from and the size it was scaled to.", "ec875adb7": "The border gradient rewrote its override file and reloaded the whole\nconfig on every palette change, which is every wallpaper change and so\nevery step of a slideshow \u2014 with both lanes off that was two resets of\nkeys already absent. It remembers what it last wrote and says nothing\nwhen nothing differs.\n\nThe window list was fetched when the page loaded, but it is only read\nwhile the editor is open and opening it fetches a fresh one, so that\nfirst list was always discarded. It also carried two fields no caller\nreads.\n\nThe theme snapshot asked the schema which profile names ship; it asks\nthe script that owns the schema instead, as the apply already does.", "ca1bf64ef": "That release recorded the setting by commenting its line out, so an\nupgraded install has -- border_size = 4 where the field should be. That\nread as nothing set, which showed the switch on while the compositor had\nno borders, and a theme saved from there turned them back on for good.\nA commented field now reads as the off value the old toggle meant by it.\n\nWhich profile names ship is also answered here now, rather than by each\ncaller reading the schema and coming to its own conclusion.", "9b204117a": "Editing a rule kept the keys the editor has no widget for, but not the\nvalues it can only half-show: a workspace named rather than numbered\nbecame 1, a rounding became nothing, a size on a tiled window was\ndropped. Those are set aside with the rest and written back as they\nwere.\n\nA second save while the first was still running killed it, and the\nkilled run's exit read as a failure, which reloaded the file over the\nnewer list \u2014 deleted rules came back. Saves queue instead. A theme\nlanding under an open editor now closes it, so a save cannot land at an\nindex into a list that has been replaced.\n\nA custom effect's fields were changed inside the array, which notifies\nnothing, so the Save button never noticed the rule had become saveable.\n\nThe reset re-armed seven switches; the two border-gradient lanes and\nTitle Bars were not among them and kept showing the old state.", "6b3d1a71a": "A store that could not be understood read as an empty list, so the page\nshowed no rules and the next save wrote that emptiness over the file \u2014\nevery rule gone, with nothing said. The reason travels with the result\nnow; a regenerate refuses rather than publishing nothing, and the page\nholds back every write until the file makes sense again.\n\nEffect values were never checked for shape, so a size that was a number\ninstead of a pair either crashed the writer, leaving the rules\nunwritable, or was quietly coerced into something else. And a newline in\na pattern ended the Lua string it sat in, silently killing every rule\nafter it while the run still reported OK.\n\nThe two files are published under one lock, so a reader cannot catch the\nstore from one write and the generated rules from another. The apply\npasses --no-reload, which is the second of the two reloads a theme was\nstill paying for.", "deb7b7cf1": "Two ways it could write a file Hyprland cannot parse, and hyprland.lua\nrequires general.lua without a pcall, so a broken one takes env, rules,\ncolors and keybinds down with it.\n\nA value was matched up to the first comma, so a colour written the way\nHyprland also accepts it \u2014 rgba(20, 20, 20, 0.5) \u2014 was read as rgba(20\nand the replacement landed in front of the rest of the line. Quoted\nstrings are now matched whole.\n\nAdding a setting anchored on the last line that began at the top level\nof its block, which is the opening line of a nested table when the block\nends with one; the new line went inside that table and left a stray\ncomma after its brace. The anchor is now the last line that ends back at\nthe top level.\n\nThe file is also read, changed and written back under a lock, with a\ntemp name of its own: the settings page is live while a theme applies,\nand both edit this file. A shared temp name meant one writer could\nrename a file the other was still filling.", "38a38e133": "Editing a rule rebuilt it from the editor's own fields, so a rule made by\nhand or carried by a theme lost anything this editor has no widget for.\nThose keys ride along untouched now.\n\nThe preview compiles the pattern with the browser's regex, but Hyprland\nmatches with RE2 \u2014 a pattern one accepts and the other doesn't was\ntreated as invalid and blocked the save. It only means there is nothing\nto preview, and says so.\n\nA colour field stopped following its value after the first typed edit,\nand the switches on the decorations page kept their old positions\nthrough a reset: both had lost their bindings to an assignment, and both\nare re-armed. Summaries build their text through placeholders so a\ntranslation can move the number, and the write helper nothing called is\ngone.", "42f28c42b": "A snapshot from before the border and corner keys were split carries the\nold bools, and borders off meant no border, no gaps and no resize edge \u2014\nnot just a zero border size. The schema already spells out what each old\nbool drove, so the restore reads that table instead of the two mappings\nit knew by name.\n\nA failed snapshot left an empty object behind, which now reads as reset\neverything to stock; it leaves no file at all, which an apply skips. The\nstock animation profile is loaded through pcall like the chosen one, so\na broken profile can no longer take the input and misc config down with\nit, and animationProfile joins the motion group its sibling is in.", "7ba676f27": "Both files render values from a theme into Lua the compositor evaluates,\nand a theme can arrive by import from anywhere. The window-rule custom\nvalue accepted any brace-wrapped text, so a table constructor could hold\na call instead of a list; it now takes a comma-separated run of literals\nand nothing else. Decoration strings went in unescaped, so a value\ncarrying a quote could close it and continue as an expression; the quote\nand the backslash are now escaped on both paths that reach Hyprland, and\nthe vector pair is coerced to numbers rather than pasted.", "0ba6d1ff2": "A theme saved before a setting existed says nothing about it, and the\nrestore used to leave such keys alone \u2014 so applying an older theme kept\nwhatever the previous theme had put there, and the older theme stopped\nlooking like its save. The new restore verb fills unnamed keys from the\nschema defaults instead: stock is what the machine showed when the theme\nwas saved. Snapshots from before two keys were renamed still say\nborders / roundCorners; a false there now restores as its modern\nspelling's zero. The completed set feeds both the file write and the\nlive keyword push, so the compositor hears the same values the config\ngot.", "c03548021": "A saved theme snapshots the rules store, empty list included \u2014 no\nrules at save time is part of the look. Applying restores through\nthe owner script's write verb rather than a copy, so an imported\ntheme's rules pass the same validation the settings page gets.\nThemes saved before rules existed carry no file and leave the live\nrules alone.", "fdd069756": "Per-app exceptions to the sections above it: pick an open window,\nor type a pattern and watch which of the windows open right now it\ncatches, then choose from curated effects \u2014 placement, transparency,\nfrost, chrome, tearing, idle inhibit \u2014 with a custom escape hatch\nfor the rest. Cards reorder because later rules win.\n\nLast section on the page, so the list grows without pushing other\nsettings around. The editor is built fresh on every open: a clicked\nswitch keeps its own state over a binding, and a persistent editor\nstops following the draft reset. Saving or cancelling scrolls the\npage back to the section, where a new rule's card is.", "7f4c5df90": "Rules the shell manages live as JSON and compile whole into\nuserrules.lua, loaded after rules.lua so a rule made in settings\noutranks a shipped one for the same window. One direction only:\nnothing parses Lua back, and rules written by hand elsewhere are\nnever read, shown or touched. The write verb holds every effect to\na schema \u2014 a custom effect is a field name plus a literal that must\nlook like one, so the generated file cannot carry anything else\ninto the compositor's config.", "01ad9606c": "Every window section returns to what a fresh install has, from the same\ntable the track marks are read from \u2014 the reset and the dots on the\nsliders cannot disagree about what stock is. Title bars go through their\nown service, which owns the flag and the plugin's reload; the border\ngradient resets through config.json, where its service is watching.\nSystem look and below are left alone: the cursor and the fonts belong to\nthe whole desktop, not to its windows.\n\nThe reader's assignments moved into a function the reset shares, so what\na key drives is written once.", "cc0da30a3": "Every section in the window half of the page says so in its title; the\ntoggle section at the top is part of that half and now says so too.", "e7dee1b99": "The animation set moves out of the main config into named profile files \u2014\nexpressive is the stock set verbatim, joined by snappy, smooth, bouncy and\nminimal. A profile is the whole set at once: curves, speeds and styles are\ntuned against each other, so they travel together rather than as fourteen\nseparate knobs. The name in animations/active picks the file at config\nload; a missing or unreadable name falls back to the stock profile, and\nthe file is read with dofile so a reload always sees what is on disk.\n\nThe Decorations page offers the profiles in an Animations section of its\nown, greyed with the animations toggle. The picker lists whatever the\nprofile directory holds: shipped profiles keep their translated names and\ntheir order, everything else is named by its filename \u2014 so a profile\nsomeone wrote themselves shows up without the page having to know it\nexists.\n\nThe choice rides the decoration schema as a namefile key: it reads and\nwrites the one-word file rather than a Lua field, rea", "caeb8653f": "Two modes. The default draws from the palette's primary and tertiary at an\nangle and is re-derived whenever the palette moves, so it keeps up with the\nwallpaper the way everything else does \u2014 a pair of literal colors would be\nright once and then wrong after the next wallpaper. The role pair itself is\nnot offered as a choice: every other combination sits too close together to\nread as a gradient at all, so that setting was four ways of picking the same\nflat border and one that worked.\n\nThe second mode holds the two colors literally and stops tracking the\npalette, which is the point of asking for them. They are entered with a\nswatch, a hex field and an eyedropper. The eyedropper is the one that\nmatters: a color worth putting in a border is usually already on the screen\nsomewhere, and hyprpicker returns whatever is under the cursor.\n\nEither way it is written to the shell's override file, which is required\nafter the generated palette, so matugen regenerating its own border color\nleaves this", "70c25ee6e": "The last three the blur block holds. Noise puts grain over the blur, which\nis what hides the banding a heavy blur leaves on a gradient. Saturation\ndecides how much colour it keeps from what is behind. See-through blurs\nthe desktop rather than the windows stacked underneath.\n\nThe first two are sliders; the third is a switch, being a switch.", "f5a09782d": "Shadow size, falloff and darkness get a section of their own, greyed\nwhile shadows are off, the same way blur's rows behave.\n\nShadow falloff is the setting that could not be reached before. It sits\nafter the offset pair inside the shadow block, and a scan that stopped at\nthe first closing brace never got that far. It runs 1 to 4, the range the\ncompositor honors \u2014 it clamps anything higher to 4, so a longer track was\nselling distinctions that all rendered the same shadow. The shipped\nconfig said 10 and drew 4; it says 4 now.\n\nSize and falloff only shape the shadow; whether any of it shows is the\nalpha of its color, and that had no control. A darkness slider carries it.\nThe hue is kept from whatever the color already is, so a theme that tints\nits shadow keeps the tint while the darkness moves. The color travels\nthrough the schema as the whole rgba string, which gives the reader and\nwriter their first string-typed key: parsing strips the quotes, rendering\nrestores them and escapes what it", "f250d71b5": "Four more settings the compositor has always had, in a Shape section\nbeside the toggles that switch them on: corner radius, border thickness,\nand the gaps between windows and around the screen edge. Each is a row\nagainst the schema.\n\nA slider row can ask for a shorter track. The label had 120px whatever it\nsaid, and anything longer ran underneath the track \u2014 \"Fullscreen windows\"\nsat on top of its own slider. Long labels are cut short rather than\nallowed to overlap, and the rows that need the room take a 340px track\ninstead of the rest of the line.\n\nThe Borders switch no longer resets the gaps. It set them to 4 and 5 on\nits way through, from when there was nowhere else to put them; with rows\nof their own that would undo whatever was chosen there. A theme saved\nbefore those rows existed still zeroes them, so an old theme looks the way\nit did.\n\nThe marker each track carries sits at the start of its own range rather\nthan at 1, which on a track that stops at 0.8 was drawn past the end.", "c6787734f": "Five settings the compositor has always had and the page never offered:\nblur radius and passes, the focused and unfocused window\nopacities, and how far an unfocused window is dimmed. Each is one row\nagainst the schema \u2014 no new parsing, no new restore path, and a theme\nsaved now carries all of them.\n\nA slider emits on every pixel of a drag, and each write is a process and a\nround trip to the compositor, so the changes are collected and sent once\nthe drag settles. Several sliders moved together arrive as one change.\n\nBlur radius and passes are greyed rather than hidden when blur is off, and\nthe dim amount likewise when dimming is off: the setting stays where it\nwas found instead of the page rearranging itself as things are toggled.\n\nThe two opacities are not written in the shipped config \u2014 Hyprland\ndefaults them \u2014 so their lines appear the first time one is moved.\n\nBoth sections go above System look rather than below it. System look picks\nthe app style, the icon set and the mouse cursor,", "53c3b59e1": "The settings page, the snapshot a saved theme takes, and the restore an\napply performs each parsed and rewrote general.lua themselves, in three\nlanguages \u2014 regexes in the page's JavaScript, in the snapshot's Python, and\nin apply-theme's. Six settings across three implementations was tolerable;\nit is the shape that stops being tolerable, because nothing keeps the three\nin agreement and a disagreement shows up as a setting that saves but does\nnot come back.\n\nThey share one now. decorations.py knows where each setting lives and\nnothing else does; a schema file lists the settings and every site iterates\nit. Adding one is a JSON object rather than three regexes.\n\nWhere a setting lives is derived from its hyprctl keyword rather than\nwritten down twice: decoration:blur:size is the field `size`, inside\n`blur`, inside `decoration`.\n\nBlocks are found by matching braces. The old scan ran to the next `}`,\nwhich cannot reach a field sitting after a nested block has closed \u2014\nrender_power is behind s", "bcea466c7": "A right click on a text field opened the toolkit's editing menu, drawn in\na style nothing else on the desktop uses. The fields draw the same menu\nevery other right click gets \u2014 the shadowed surface card with ripple rows\n\u2014 offering cut, copy, paste and select all, each row live only when its\naction can do something. A field showing dots instead of characters keeps\nits content out of the clipboard: cut and copy stay disabled however much\nis selected. The toolkit's own menu is switched off, since leaving it in\nplace would open both.", "34d249890": "Two strings on the Display page kept the British spelling while every\nother string says color. The translation keys follow the source strings,\nso each file carries the new key in the old one's place.", "0b783abbf": "A slider's value is bound to the setting it edits and animates towards it, so\na setting changed anywhere else \u2014 a theme apply, another window, an edit to\nconfig.json \u2014 walks the handle across a run of intermediate values. Saving on\nvalueChanged wrote one of those back, and once written the binding settled\nthere: the clock font sliders, with no equality guard at all, came to rest a\nfraction below whatever they were aimed at.\n\nmoved() is emitted for a drag or a key press and never for a change that came\nin through the binding, which is the distinction that was missing. The track\nstill follows the bound value, so a setting changed elsewhere moves the handle\nwithout being written back.", "d4d80ac53": "A window rule blanked blur on every window, so the blur settings reached\nonly the shell's own surfaces, and a window made translucent showed what\nwas behind it untouched. The compositor already skips opaque windows \u2014\nblur is computed only where alpha falls below one \u2014 so windows at full\nopacity look and cost exactly what they did, and the transparency\nsettings are what bring the frost in.\n\nThe xwayland menu rule stays: those buffers carry their own shadows, and\nblurring them draws a halo around every context menu.\n\nThe blur brightness line is gone because one is the value at which both\nof its shader branches leave the image untouched.", "95862df83": "Two limits in the writer behind HyprlandConfig, both reached by the border\ncolour and neither by anything using it today.\n\nA key whose last part contains a dot came out as\n{general={col.active_border=...}}, which Lua will not parse \u2014 a bare name\ncannot hold a dot. It is written as a bracket string now. Every caller so\nfar separates with colons only, so nothing was producing this yet.\n\nValues could only be scalars. A gradient in Lua is a table:\n{colors={\"rgba(..)\",\"rgba(..)\"},angle=45}. The single-string form hyprlang\ntakes is rejected outright \u2014 \"invalid color\" \u2014 so the shell had no way to\nexpress one. --set-lua writes a value through untouched for that case.\n\nThe file this writes is required after both the generated palette and the\ncustom overrides, so what it holds survives a theme apply: with a border\nset here, matugen regenerating its own value leaves the override standing,\nand resetting the key puts the palette back.\n\nAn edit applies itself: the file is replaced atomically, which ", "8a1dccb6e": "Locks and fallbacks.\n\nThe derivatives lock had no timeout, and the section it guards re-encodes\nwith ImageMagick and can reach pkexec for the login background. Bounded at\nthirty seconds; a run that gives up still does its work, so a wallpaper\nchange costs a derivative that may land out of order rather than a\nsubshell parked forever.\n\nThe Papirus base is checked before anything is published. Only */places is\noverridden and the rest is inherited, so with no base on disk the theme\nresolves to bare hicolor \u2014 every icon on the desktop. Nothing to layer on\nnow means nothing published and the icon theme left as it was. The\nreadback also samples an alias beside folder.svg, since the links are laid\ndown one at a time and a directory holding the newer folder.svg over\naliases from the previous colour otherwise reads as finished.\n\nA restage that is skipped leaves a marker. Waiting is what this script may\nnever do, but forgetting is not required: a burst now lands the last\npalette rather than the f", "3205817aa": "config.json. The three places switchwall edits it \u2014 the wallpaper path, the\nthumbnail path, the accent colour \u2014 each read it, filtered it through jq\ninto a temp named after the file itself, and renamed that over the top,\nwith no lock. The temp name being fixed is what turns a lost update into a\nbroken file: two writers open the same path, one renames a document the\nother is still writing, and what lands is truncated JSON. An unreadable\nconfig is an empty wallpaper path, which is the invisible bar. They share\none helper now, with a unique temp beside the target and a short bounded\nlock; failing to take the lock still writes, since a rewrite that raced is\nbetter than one that never happened.\n\ngeneral.lua. Both writers replaced it in a way that empties it first \u2014 one\ncopying in from /tmp across filesystems, the other opening it for writing\nand refilling on close \u2014 while switchwall leaves a hyprctl reload running\nin the background that can parse it at any moment. A reload landing inside\nth" } }, { "version": "1.1.0", "date": "2026-07-31", "kind": "feature", "summary": "A bar you arrange, themes you can share, and a look you choose", "changes": [ "Export a theme to a single file and import it on another computer", "A saved theme now remembers your app style, icons and pointer", "Applying a theme or wallpaper finishes in about a third of the time", "Switching themes no longer flashes the new layout with the old colors", "A theme that fails to apply now says so instead of doing nothing", "Show, hide, reorder and group any widget on your bar", "Drag two bar widgets together to combine them into one pill", "Choose your app style, icon theme and mouse cursor in Settings", "Pick fonts from a searchable list that shows each one in its typeface", "Most apps now use the font you pick for the desktop", "Pick how a dock icon animates when it launches an app: Bounce, Pulse, Pop or Wobble (thanks @sanaruca, end-4/dots-hyprland#3553)", "The last item on a to-do list is no longer tucked behind the add button (thanks @ar-Raqmi)", "Video wallpapers just play, with no package to install yourself", "Pick a cursor size from the sizes your theme can actually draw", "Turn on Shake to Locate to magnify or enlarge a lost cursor", "Mirror one display onto another from the monitor settings", "A bar icon and a notification tell you when a new release is out", "The script install now lands on the newest release, with --edge to follow the branch instead" ], "commits": [ "fc489658c gaming: fix gaming-mode-install conflicts on legacy nvidia installs", "6b89a4367 gpu-config: detect any installed NVIDIA driver, not all of them", "880a9650c system: re-install Limine BIOS stages after a limine upgrade", "b88763b93 Update release-notes.json", "dce475292 resolve: add an Install button to the update notification", "42584c974 welcome: fix duplicate Component.onCompleted breaking launch", "af918239c shell: surface Resolve update state in Welcome", "3651449d9 resolve: add mainstream-resolve-update-check notifier", "9abb6e5a7 previews: store them small instead of holding them decoded", "7e0dc3973 bar: stop the retry timer from replacing the icon lookup", "298b4c480 dock: stop the retry timer from replacing the icon lookup", "fa22e9d0d keybinds: shell to desktop", "e59933c12 Update desktop.webp", "59652912d Update: comment out the version notice for now", "279b3f030 update: put the version in a notice of its own", "7294116fa update: build the changelog from tags, with curated notes on top", "51b31d0fa translations: register the strings added since 1.0.5", "2bbacaaf1 settings: draw the display arrangement in logical coordinates", "003aabe9e sddm: migrate an existing greeter config to Lua on update", "75e85438a sddm: move the greeter's Hyprland config to Lua", "45d340364 plugins: arm the ABI guard on fresh installs instead of passing them open", "dd6b20bf6 plugins: stamp the ABI guard with the package release, not just the version", "8dfc1fdd6 themes: keep the saved-theme previews decoded between visits", "b94b321c5 dock: stop rebuilding every app entry when a window opens", "049964f57 dock: animate the icon when an app launches", "251af81da bar: stop seeding a widget into a saved layout", "cee970e26 gpu-config: classify pre-GCN fusion APUs as old AMD", "963589125 gpu-drivers: drop the Vulkan driver on pre-GCN AMD", "d9c41c47f gpu-config: select Intel packages by GPU generation", "4a80440ca resolve: carry the fixes worth taking from the AMD and NVIDIA installers", "2967d5ff3 dock: quiesce a window preview before closing it", "8af023054 overview: hand the tile back when a close is refused", "31fc57c07 settings: show which release the system is on", "85e4637cf extras: stop claiming the app store handles system packages", "5753697d4 wallpapers: add the primary lockup at 8K", "a464a7151 install.sh: default to the newest release, add --edge for the branch", "a445163e4 bar: describe the stock layout once", "7ceb2fc70 manifest: stop describing the desktop, and drop the version nobody reads", "21c254948 bar popups: leave the hover ones out of the dismissal plumbing", "903e9315d updates: keep the release cache out of the tree updatems resets" ], "commitsTotal": 112, "commitNotes": { "fc489658c": "The \"Steam Big Picture\" step of mainstream-welcome-install always adds lib32-nvidia-utils for any NVIDIA GPU (PCI vendor 10de), regardless of which driver branch is actually installed. On cards that need a legacy branch \u2014 e.g. a GTX 1050 (Pascal) running nvidia-580xx-utils \u2014 this pulls in mainline nvidia-utils and conflicts with the already-installed legacy driver on nvidia-libgl. Since the transaction runs --noconfirm, pacman can't prompt to resolve it, and the whole step fails.", "6b89a4367": "_gpu_nvidia_has_driver passed all seven driver package names to a single\n`pacman -Qq`, which exits 1 unless every one is installed. Since the branches\nconflict with each other that can never hold, so the probe was always false and\nsilently skipped the entire NVIDIA path in both orchestrators: no\nnvidia_drm.modeset=1 on the kernel cmdline, no /etc/modprobe.d/nvidia.conf, no\ndeferred KMS, no suspend/hibernate/resume units, and none of the GBM_BACKEND /\nLIBVA_DRIVER_NAME / NVD_BACKEND / hypridle tweaks.\n\nQuery one package at a time and return on the first hit. The test harness stubbed\nthe probe out, so the semantics were never exercised; fixture it at the new\npacman seam instead and assert any-of behaviour.", "880a9650c": "Limine boots a BIOS machine from stages 1-2 in the MBR and stage 3\n(limine-bios.sys) on the boot filesystem, all from one package build. Upgrading\nlimine replaces stage 3 and cannot touch the MBR, and Arch ships no hook to\nreconcile them \u2014 so the boot after a routine `pacman -Syu` drops straight into\nfirmware setup with no bootloader and no error.\n\nShip limine-bios-sync and 61-limine-bios.hook from mainstream-system, which the\nISO already installs. Package-owned means it reaches machines already in the\nfield on their next upgrade, not just fresh installs. No-op on UEFI.", "dce475292": "The weekly check told the user a new DaVinci Resolve was out and left them to\nrun the installer themselves. Put the action on the notification instead.\n\nPressing Install now hands off to `pkexec mainstream-welcome-install\nresolve-update` \u2014 the same privileged path the Welcome app's install button\nalready uses, and already covered by 49-mainstream-welcome.rules, so this adds\nno new privileged code, no new polkit rule, and no password prompt on a local\nsession. Nothing runs unless the button is pressed; --no-action removes it.", "42584c974": "The Resolve version check was added as a second Component.onCompleted on the\nroot ApplicationWindow, which already had one for reapplyTheme(). QML allows one\nassignment per property per object, so the whole config failed to load:\n\n Property value set multiple times\n\nMove refreshResolve() into the existing handler. Same behaviour, one handler.", "af918239c": "The Updates service shells out to checkupdates, which by construction cannot\nsee a local package \u2014 so the one piece of software Mainstream installs that way\nwas the one the update UI could say nothing about.\n\nUpdates.qml gains the Resolve state, read from `install-davinci-resolve --check\n--json` so the version logic stays in one place. refreshResolve() is on demand\nonly, deliberately not wired to the existing 120-minute checkInterval: one\nrequest to Blackmagic every two hours from every install is the stampede the\ntimer's RandomizedDelaySec exists to avoid. Every failure path leaves the\nproperties untouched, so the UI degrades to exactly what it showed before.\n\nwelcome-tutorial.qml relabels the two Resolve rows \u2014 Install / Installed /\nUpdate to X.Y.Z \u2014 and locks a row that is already current so the button cannot\nkick off a no-op. Package-to-row matching is prefix-based, so a beta claims its\nrow without implying a version comparison.", "3651449d9": "A thin wrapper around `install-davinci-resolve --check --json` \u2014 it owns no\nversion logic of its own, so the two can never disagree about what \"out of\ndate\" means.\n\nIt notifies and nothing else: no pkexec, no sudo, no call into\nmainstream-welcome-install. The notification names the new version and the\nexact command to run; running it is the user's decision.\n\nEach version is announced once, tracked in\n$XDG_STATE_HOME/mainstream-resolve/last-notified, since the timer fires weekly\nand Blackmagic can go months between releases. --force re-notifies, --quiet\nreports through the exit status only.\n\nExit codes mirror --check (0 / 10 / 1), with one deliberate exception: a failed\ncheck exits 0 in silence. This runs on a timer, and a laptop that wakes on a\ntrain with no network must not produce an error notification.\n\nresolve: fix silent exit when no OpenCL ICD is installed\n\nverify_opencl counted the installed ICDs with `ls /etc/OpenCL/vendors/*.icd |\nwc -l`. With no ICD present the glob goes unma", "9abb6e5a7": "8dfc1fd went to some trouble to avoid re-decoding the saved-theme previews: a\nsingleton held a reference on every one so Qt's sweep couldn't drop them, and\nthe size and fill mode had to match across the pins and the cards or the pixmap\nkeys diverged silently. All of it was in service of a preview.png that grim\nwrote at monitor resolution \u2014 several megabytes and a few hundred milliseconds\neach, for an image nothing draws larger than 400x225.\n\nCapture caps it at previewMaxDimension on the way out now. What is left is cheap\nenough to decode that holding it was more machinery than the problem justified,\nso the pins and the Instantiator are gone and ThemeLibrary is back to being the\nindex, the last-applied slug and their watchers. `>` on the resize only ever\nshrinks, and a missing magick leaves the full-size shot rather than the save\nlosing its preview.\n\nThe two views of the live wallpaper had the same problem and no fix at all: both\nread the wallpaper itself with cache: false, and Quick Se", "7e0dc3973": "Same defect as the dock, one directory over: the retry timer assigned to\ndesktopEntry, which is a binding, so the first retry froze the property at that\nattempt's result.\n\nTask buttons run off the same TaskbarApps.apps model and stopped being rebuilt\non every window open at the same time the dock's did, so they lost the same\naccidental repair. The lookup reads the entry list to pick up entries that\narrive late, and the timer bumps a counter instead of assigning.", "298b4c480": "Chromium and mpv would sometimes fall back to a generic icon and stay on it\nuntil a reload.\n\ndesktopEntry is a binding, and the timer that retries the lookup assigned\nstraight to it. Assigning to a bound property throws the binding away, so from\nthe first retry onward the property was frozen at whatever that one attempt\nreturned \u2014 null included, which is how the icon ended up on the guessIcon\nfallback and stayed there.\n\nIt never showed before because every window that opened handed the model a\nfresh object for every app, so the list view dropped and rebuilt each delegate\nand the binding came back with it. Keeping entries between runs keeps the\ndelegates too, so a binding thrown away while the database was still filling in\nnow stays thrown away for the rest of the session. That is also why it only\never hit some apps: the ones whose id resolves past byId into the heuristic and\nfuzzy paths are the ones still coming back null when the timer first fires.\n\nThe lookup is readonly now and read", "279b3f030": "Sitting beside the System Update heading, the version read as part of the\nheading rather than as something being reported. It moves into the same kind of\nnotice the page already uses for its other two remarks, so it is separated from\nthe title and sits with the rest of what the page has to say.\n\nThe icon carries the state \u2014 a check when there is nothing to move to, the\nupdate glyph when there is \u2014 which is what the colour was doing before. Inside\na notice the old colour would have been read against the wrong background.\n\nThe arrow still only appears when there is a release to point at.", "7294116fa": "The manifest derived everything from commit subjects, so a release could only\never say what one commit said. The first release had no predecessor tag to diff\nagainst and derived nothing at all.\n\nrelease-notes.json now overrides summary and changes for named versions only.\nEverything else stays derived, so the default cannot drift and every deviation\nis visible in one file. Kind stays tag-driven \u2014 the tag is the right place to\nsay how important a release is. A curated list is not capped either: the cap\nexists because a derived list is however long the commit range happened to be,\nwhile a curated one is exactly what someone chose to say.\n\nAlongside that, every release carries its unedited range: commits as\n\" \", commitNotes for the commits that wrote a body, merges\nwhen the first-parent walk stepped over any, and commitsTotal when the cap\ntruncates the list. These are derived and deliberately not overridable, because\ntheir whole value is that anyone can reproduce them ", "51b31d0fa": "Sixty-five interface strings landed with the bar layout editor, theme import\nand export, the Decorations page, the release indicator and its popup, the\ncursor and shake-to-locate settings, and the font picker. None of them existed\nin any language file, so none could be translated at all.\n\nAll fourteen files gain the same sixty-five keys, each holding the English\ntext. That is what the extraction tool writes for a new key, and it is what the\nrest of these files already look like: between half and three quarters of every\nlanguage is still the English placeholder waiting on a translator.\n\nDeliberately not machine-translated. A placeholder announces itself \u2014 a\ntranslator can find every outstanding string by looking for a value equal to\nits key \u2014 while a plausible but wrong translation reads as finished work and\nsurvives review. Getting them right is for people who speak the language.", "2bbacaaf1": "The monitor-arrangement canvas sized each rectangle from the mode's\npixel width/height while positioning it from x/y, which hyprctl reports\nin logical coordinates. On a scaled display the rectangle came out too\nbig and its neighbour was pushed too far away \u2014 a 4K at 1.666667 was\ndrawn 3840 wide and shoved the next monitor 3840 across instead of 2304.\nRotation was ignored entirely, so picking Portrait left the preview\nrectangle landscape.\n\nAdd a logicalSize() helper that swaps the axes for transforms 1/3/5/7\nand divides by the scale, matching what Hyprland does, and use it for\nthe sizes in canvasLayout, resolveEffectivePos and the canvas delegate.\nThe scale comes from monitors.lua rather than hyprctl's JSON, which\nprints it to two decimals and would turn 1.666667 into 1.67 \u2014 a 5px\nerror per monitor that accumulates across the row.\n\nPreview only: the applied configuration was already correct, because\nPosition writes an auto-center-* keyword and Hyprland does the real\narithmetic. The prev", "003aabe9e": "The switch to a Lua greeter config only reached fresh installs. exp-update,\nwhich is what updatems runs, never re-runs the setups step, so a machine\ninstalled before the switch kept its .conf and kept showing the deprecation\nnotice at every login with nothing to clear it.\n\nupdatems-system already refreshes the greeter's neighbours \u2014 the background\nhelper and both polkit files \u2014 on every update, and runs as root, so the\nmigration goes there. It only acts when the greeter directory already exists,\nso it will not set one up on a machine that never asked for one.\n\nHyprland looks for hyprland.lua before hyprland.conf, so writing the new file\nis what actually resolves it. The old one is moved to hyprland.conf.old rather\nthan deleted, and only once the write has succeeded: if a greeter somehow will\nnot start, renaming one file puts it back. That is the same thing the installer\nalready does with a user's own pre-Lua config.\n\nThe config itself moves out of the heredoc into sdata/sddm/hyprland.l", "75e85438a": "The ISO writes this config from post-install-login; ./setup writes the same one\nfrom here, so both had to move together or a scripted install would still show\nthe .conf deprecation notice on the login screen.\n\nByte-identical to the archiso copy. The old .conf is removed first, so a machine\ninstalled before this stops carrying both.", "45d340364": "The guard reads two stamps: /var/lib/hyprland-plugins/hyprland-version for the\nHyprland this system has, and .builtfor for the one each .so was built\nagainst. It returned true whenever either was missing, and nothing but the\nrebuild scripts ever wrote them. Those run from the pacman hook on a hyprland\nupgrade, or from a timer five minutes after boot -- neither has happened yet the\nfirst time a new install reaches the greeter. So every fresh install loaded both\nplugins with no version check at all, which is the one moment a mismatched .so\nis most likely, and a segfault at dlopen there is a login loop.\n\nThe system version now gates the check. Absent, this is not an install whose\nplugins we manage and the check still passes open. Present, a .so with no stamp\nis refused rather than trusted -- the same conclusion quarantine_stale_targets\nalready reaches when it moves a stampless plugin aside.\n\n./setup now writes both halves: the system version before it builds, so a failed\nbuild sti", "dd6b20bf6": "The guard records which Hyprland a plugin was built against, and it read that\nfrom pkg-config, which reports 0.56.1 and knows nothing about the package\nrelease. A rebuild that only moves the release changes the ABI: 0.56.1-1 to\n0.56.1-2 landed today, rebuilding Hyprland against aquamarine 0.14.0, which\nflips _aq_0.13 to _aq_0.14 inside the hash a plugin checks at load. The stamp\neither side of that read 0.56.1, so the guard would have compared them equal and\nhanded back a plugin built against the older ABI.\n\nIt asks pacman first, which reports 0.56.1-2, and keeps pkg-config as the\nfallback so a rebuild against a hand-built Hyprland still works.\n\nAll four scripts move together. The version they write is read back by\nquarantine_stale_targets, by each .so's .builtfor file, and by the check in\ncustom/general.lua that runs at every login \u2014 change one and the comparison\nfails closed, silently disabling both plugins.", "8dfc1fdd6": "Settings swaps one Loader's source to navigate, so the Themes page is torn\ndown and rebuilt every time it is opened \u2014 and again on every apply, since\na theme change bumps Appearance.themeRevision and forces a page reload. Each\nrebuild re-read and re-decoded every preview.png, which are full-monitor grim\nscreenshots: 24 MB and about 450ms of decode across five saved themes, scaling\nlinearly with the number of themes.\n\nThree things had to change together, because any one alone leaves the reload\nvisible:\n\nThe index, last-applied slug and their loader processes move to a new\nThemeLibrary singleton, so a rebuilt page has its grid populated on the first\nframe instead of after two subprocesses return. That removes the empty-grid\nphase and the \"\u2014 None \u2014\" flash in the Day/Night dropdowns.\n\nThe previews drop cache: false. The ?v= token already in their URLs\nis the invalidation mechanism and is restamped at millisecond resolution\neverywhere preview.png is written, so a replaced screensho", "b94b321c5": "Opening an app made the whole dock slide sideways for a moment and settle back.\nIt was never the click: the app list is a binding over the open windows, so a\nwindow appearing re-ran it and handed the model a brand new object for every\napp, including all the ones that had not changed. The model treated each as a\nnew item, the dock dropped and re-added every icon, its appearance transition\nran, and the animated width moved the centred row.\n\nThat also explains why it only happened on a launch. Dragging re-runs the same\nbinding but suppresses the transition while reordering, and clicking an app that\nis already open or right-clicking it never touches the window list at all.\n\nEntries are now kept between runs and updated in place, so unchanged apps keep\ntheir identity and their delegates. An app that genuinely appears or goes away\nstill gets a new or destroyed entry and still animates.\n\nWorth having on its own: every open window used to rebind every dock delegate.", "049964f57": "Cherry-picked from upstream, with the launch animation defaulting to Bounce.\n\nThree places needed a decision rather than a straight merge:\n\nUpstream played the animation on every click. Here a click also opens an app\nfolder or toggles a window preview, and neither is a launch \u2014 animating those\nwould say something happened that did not. It now plays only in the branch that\nactually starts the app.\n\nThe icon loader keeps its exclusion of folders, which have their own loader and\nnever launch, and gains the animation transform.\n\nThe tint overlay had to repeat that transform. It is anchored to fill the icon,\nbut anchors follow geometry and scale/rotation do not touch geometry \u2014 so with\nTint app icons on, the tint would have sat still while the icon moved under it.\n\nThe picker sits under Tint app icons in Interface settings, where the rest of\nthe dock's appearance options are.", "251af81da": "Slotting the release indicator into layouts saved before it existed meant\nrecording which ids had been offered, and recording that meant writing the\nconfig file from the load handler. At 1.0.5 that handler was one line and the\nshell wrote this file exactly once, when it created it \u2014 nothing else touched it\nat startup.\n\nThat is a property worth having back. A first login sets the wallpaper path from\noutside the shell a moment after login, and it works only because nothing else\nwrites the file afterwards. Adding a startup write puts a second writer into\nthat window for the sake of one widget appearing on its own.\n\nSo the seeding goes, and with it the record it needed. Someone upgrading will\nnot find the release indicator in their bar; it is in the layout editor with\neverything else. A widget you have to add yourself is a smaller cost than a\nfirst boot that comes up unthemed.\n\nThe legacy-switch migration stays: it only fires for a file written before\nbar.layout existed, which no fresh ins", "cee970e26": "Llano/Sumo, Wrestler and Trinity/Richland sit well above the pre-GCN id\nceiling and advertise HD 7xxx/8xxx names, so neither the id test nor the\nname regex saw them. They were handed the amdgpu-only autoconfig arm\ndespite needing the radeon module, and a RADV package set that has no\nsupport below GCN1.\n\nMatch their four id windows exactly, so the GCN APUs sharing those pages\nstay modern: Kabini/Temash, Mullins, Carrizo/Stoney and Kaveri.", "963589125": "RADV is GCN1+ only, so vulkan-radeon enumerates zero devices on pre-GCN\nparts, and because it provides 'vulkan-driver' it also satisfied the\ncatch-all and suppressed the lavapipe fallback, leaving no working Vulkan\nat all. Install mesa alone there and let the catch-all supply swrast.\n\nVA-API is part of mesa itself now, so the retired libva-mesa-driver\ntarget goes with it. Aligns ./setup and the gpu-drivers tool with the\nISO install.", "d9c41c47f": "Intel package selection carried no generation signal, so every Intel GPU\nreceived one flat set: the iHD VA-API driver alongside the legacy i965\ndriver, and never an OpenCL runtime.\n\nAdd INTEL_GEN (xe|modern|legacy|none) to gpu_detect. Intel device ids are\nnot monotonic, so the tiers match known id prefixes newest-first with a\nname fallback for Arc/Iris Xe; anything unrecognised falls to 'modern',\nwhich is safe on any Gen8+ part.\n\nThe tiers track the two boundaries that actually change packages:\n xe Gen12+/Arc \u2014 the only tier upstream's compute runtime and the VPL\n runtime support; gets intel-compute-runtime + ocl-icd + vpl-gpu-rt\n modern Gen8-Gen11 \u2014 iHD VA-API, no OpenCL. Upstream moved Gen8-11 compute\n to legacy1 packages that Arch does not ship, so the runtime would\n be ~346 MB that cannot enumerate a device\n legacy Gen4-Gen7.5 \u2014 i965 VA-API, the driver that actually covers this\n hardware; predates iHD entirely\n\nRoute 2.setups.sh and scr", "4a80440ca": "Four changes, drawn from two community installers and checked against this\nmachine rather than taken on trust.\n\nA launcher wrapper replaces the bare symlink, and the desktop entry now routes\nthrough it, so a menu launch gets the same treatment as a terminal one. It\nunsets QT_QPA_PLATFORMTHEME and QT_STYLE_OVERRIDE, which the session sets to\nqt6ct while Resolve's bundled Qt 5.15 ships no platformthemes plugins at all,\nand on a hybrid laptop it exports the PRIME offload variables. Without those,\nthe GL context comes from the iGPU while OpenCL runs on the NVIDIA card, and\nclCreateContext fails across the split \u2014 Resolve hangs on the Color page. The\ndetection runs at launch, not at install, so a MUX flip or a dock is picked up,\nand every gate is required: opencl-nvidia depends only on zlib, so an NVIDIA\nbox can have no GLX vendor library, where exporting the vendor name turns a\nworking launch into a GLX failure. Two lspci calls, because lspci honours only\nthe last -d and one call would hav", "2967d5ff3": "The dock's preview tiles bound a ScreencopyView straight at the toplevel\nwith no gate, so both close paths tore the window down underneath a live\ncapture. hidePreview() only starts a fade \u2014 the Loader stays active and\nthe capture stays attached \u2014 so the toplevel died while the tile was\nstill capturing it, and Wayland answers that with a fatal EINVAL\nprotocol error that takes the whole shell with it.\n\nLatch the capture off before dispatching the close on both the\nmiddle-click and the close-button path. The latch is one-way for the\nlife of the tile: nothing here tells a client refusing to close apart\nfrom a close still in flight, and the next preview builds the tile\nfresh. Same guard the overview carries.", "8af023054": "The closing latch that quiesces a window preview before dispatching a\nclose never cleared, so a client that refuses to quit \u2014 an unsaved-changes\nprompt is the usual reason \u2014 left its tile inert for the rest of the\noverview session: no hover, no click, no tooltip, and a stuck hover tint.\n\nSplit the latch. `closing` now drives only the input gates and clears\nitself after a one-second grace period, while a new monotonic\n`captureSuppressed` drives captureSource. Nothing here can tell a refused\nclose apart from one still in flight, so the capture stays down until the\nnext overview open rebuilds the tile; re-attaching to a toplevel that then\ndies is the fatal Wayland error this guards against.", "31fc57c07": "About names the release next to the brand, and the System Update header\ncarries the transition the button is about to perform, so the version is\nvisible both where you look up what you are running and where you decide\nwhether to update. The arrow only appears while a release is actually\nwaiting; otherwise the header shows the installed version alone.\n\nContentSection gains titleExtra alongside headerExtra. The version belongs\nagainst the title it qualifies, and headerExtra sits the other side of the\nspacer that pushes buttons to the far end of the row, so it could only ever\nrender against the Copy button.\n\nReading these values instantiates ReleaseUpdates in the settings process as\nwell as the shell, so announcements are now gated behind _announceEnabled,\nwhich only the shell sets. Without that, opening Settings could raise a\nsecond notification for a release already announced. This mirrors how\nThemeManager keeps its Day/Night scheduler to the main shell.", "85e4637cf": "Arch builds gnome-software without the PackageKit plugin \u2014 its shipped plugin\nset has flatpak, fwupd, appstream, even dpkg, and no packagekit at all, and\npackagekit is not among its optional dependencies. So the store has never\nhandled native packages here and cannot be configured into doing so; the\nplugin is a compile-time option, not a setting.\n\nThe description said otherwise, and every person installing Mainstream read it\nin the Included Extras list. The PKGBUILD's optdepends is generated from this\nsame file, so it corrects itself on the next build.", "a464a7151": "The bootstrap cloned the mainstream branch and left the clone on its tip,\nso a script install landed ahead of every numbered release with no record\nof which release it descended from. Two things followed from that missing\nmarker: the desktop's release indicator read the install as unversioned\nand stayed silent, and the next updatems run reset the clone to the newest\ntag rather than seeing it was already past it.\n\nResolve the newest release tag with the same pattern updatems matches,\ncheck the branch out at it, and write .updatems-applied-tag once setup\nsucceeds. --edge keeps the previous behaviour and marks itself with the\nrelease it descends from, so an edge install still reports a version and\nupdatems leaves it alone until a release reaches it.\n\nBoth channels land on a branch rather than a detached HEAD, since setup's\nbranch detection tries to bounce a detached HEAD back to main or master\nand neither exists in this clone.", "a445163e4": "The arrangement the bar ships with was written out twice \u2014 once as the\nconfig defaults a first run gets, once inside the settings page for its\n\"Reset to defaults\" button. Nothing held the two together, so adding a\nwidget meant remembering to edit both, and forgetting would have given\nReset a different bar than a fresh install.\n\nDeclare it on Config and have the adapter and the settings page read the\nsame thing.", "7ceb2fc70": "The generator explained itself in terms of what the desktop does with the\nresult \u2014 which dot colour a kind produces, and a per-kind notification\nfilter that no longer exists. Say what the field is instead and leave the\npresentation to the thing presenting it.\n\nThe schema number went out with every manifest and nothing has ever read\nit. A version nobody checks is worse than none: it suggests a bump would be\nhandled somewhere.", "21c254948": "A popup that only shows while the pointer is over its widget closes when\nthe pointer leaves, and has nothing to say to the focus grab. It was still\nlistening for a dismissal it could never act on, and unregistering itself\non teardown from a list it was never added to \u2014 so every bar widget you\nhovered over built a connection to the grab and emitted a signal into\nnothing. Only the click-triggered popups take part now.", "903e9315d": "The manifest copy and the record of which release has already been\nannounced were written into the dotfiles clone, next to the tag file\nupdatems owns. They are untracked there, and updatems stashes untracked\nfiles and hard-resets that tree on every run \u2014 and re-clones it outright\nwhen the branch has moved on. So the record of what the user had already\nbeen told was thrown away by the very thing that made a new release worth\ntelling them about, and the notification came again for a release they had\nalready seen.\n\nPut the cache under the cache directory and the record under state, where\nthe rest of the shell's own files live. Only the applied tag stays in the\nclone, because updatems writes that one." } }, { "version": "1.0.5", "date": "2026-07-22", "kind": "patch", "summary": "A one-click Repair Install, plus graphics driver and screen sharing fixes", "changes": [ "Repair Install on the Recovery page re-runs setup and rebuilds plugins", "Adding the desktop to existing Arch now fits the matching NVIDIA driver", "Without hardware Vulkan, Gaming Mode explains itself instead of bouncing you to login", "Screen sharing and recording keep working after the desktop restarts" ], "commits": [ "e057265e2 setups: fix GPU vendor detection + load gpu-config before it's used", "5f7415a10 mainstream-system: own the plugin machinery + configs, add Repair Install", "c4691cfad gaming: refuse to switch sessions without hardware Vulkan", "633e23d96 hyprland: re-sync the screencast portal on compositor (re)start" ], "commitNotes": { "e057265e2": "detect_gpu_vendors matched the name 'ati', a substring of 'VGA compatible\ncontroller' \u2014 so every machine falsely detected an AMD GPU and installed the\nmesa/vulkan-radeon stack (seen on a GTX 1050). Match the PCI vendor IDs from\nlspci -nn instead: [10de] nvidia, [1002] amd, [8086] intel.\n\nAlso move the sdata/lib/gpu-config.sh source to the top of the file. It was\nsourced ~450 lines after setup_gpu_drivers runs, so gpu_detect /\ngpu_classify_nvidia_driver were 'command not found' and the NVIDIA driver\nclassification silently failed (no proprietary driver selected on a fresh\n./setup install).", "5f7415a10": "Extracts the plugin-rebuild machinery (hyprbars/scrolloverview rebuild scripts,\nhooks, timers, status-notify, limine-restore-auto, preset) out of mainstream-extras\ninto a new mainstream-system package that also owns canonical /usr/share templates\nfor the two plugin configs. Its install scriptlet seeds /etc from those templates\nand comments out any stale version pin (HYPRBARS_REF/SCROLLOVERVIEW_REF) \u2014 the fix\nfor the unowned /etc/hyprbars.conf that froze hyprbars at v0.55.0 and blocked\nrebuilds on Hyprland 0.56. mainstream-extras becomes an apps metapackage depending\non mainstream-system; install-deps builds it first.\n\nAdds a Recovery-page 'Repair Install' button: pkexec mainstream-repair pins the\ndotfiles clone to the latest release tag, runs ./setup install-setups\nnon-interactively, re-asserts mainstream-system, un-pins the plugin configs, and\nrebuilds the plugins \u2014 streamed into the log pane, no terminal. updatems and\ninstall-setups also ensure mainstream-system." } }, { "version": "1.0.4", "date": "2026-07-21", "kind": "patch", "summary": "The next update picks up the config and packages an earlier one missed", "changes": [ "An update no longer applies just part of a release", "A failed package sync is retried once before the update stops", "Closing a window from the overview no longer risks crashing the shell" ], "commits": [ "cc5bafd6d update: apply the full last-applied-to-tag range and retry pacman once", "e4ab2ca87 overview: quiesce a window preview before closing it" ] }, { "version": "1.0.3", "date": "2026-07-20", "kind": "patch", "summary": "Safer update handling, fresher plugin rebuilds, and a quick-toggle fix", "changes": [ "The desktop update is held back when the system package update fails", "Title bar and overview plugins rebuild from the newest published source", "Game mode shows its real state in the Classic quick toggles" ], "commits": [ "51bbb7300 update: defer dotfiles when the package step fails", "4a790d84b plugins: check out branch refs from origin so cached clones track pushes", "52827ac9b extras: start the rebuild timers on install, not just next boot", "6588a1b9e extras: own the plugin rebuild system and recovery wrapper", "002606654 shell: dispatch through hl.dsp instead of legacy names", "101e6127c quicktoggles: read bool-typed animations option on Hyprland 0.56" ] }, { "version": "1.0.2", "date": "2026-07-20", "kind": "patch", "summary": "A Hyprland update can no longer strand you at the login screen", "changes": [ "Title bars and the overview stay off until they rebuild for a new Hyprland", "Restore Snapshot in Settings now makes the snapshot you booted from permanent", "Super plus scroll moves workspaces the way you scroll again" ], "commits": [ "c202752aa keybinds: track Hyprland 0.56 scroll axis naming fix", "ff8320423 hyprbars: build from upstream hyprland-plugins", "d368b3524 recover: drive the limine-snapper-sync 1.31 restore prompts", "85da839bf plugins: cap rebuild commit walk-back at 15", "7f5b29e88 plugins: survive Hyprland updates that outrun plugin ABI" ], "commitNotes": { "7f5b29e88": "A rebuild that fails for a new Hyprland version now quarantines the\nversion-mismatched .so instead of leaving it for dlopen to segfault on \u2014\nthe user logs into a plugin-less session with the existing status\nnotification instead of looping at SDDM, and the 24h retry timer restores\nthe plugin once upstream ships a compatible commit. Successful builds\nstamp each .so with the Hyprland version they were built for, and the\nconfig refuses to load a stamped plugin that does not match the installed\nHyprland \u2014 which also covers booting an older system snapshot against a\nnewer plugin in the shared home subvolume." } }, { "version": "1.0.1", "date": "2026-07-19", "kind": "patch", "summary": "Two small fixes for virtual machine guests and the installer's welcome screen", "changes": [ "Copy and paste between your host and a QEMU/KVM virtual machine", "The installer's welcome screen notes when you're already connected" ], "commits": [ "f57c08e2d execs: start the SPICE agent inside virtual machines", "7a73e9921 installer: note when the network is already connected" ] }, { "version": "1.0.0", "date": "2026-07-18", "kind": "feature", "summary": "The first Mainstream OS release: a Hyprland desktop and an Arch-based installer ISO", "changes": [ "Configure the whole desktop from one Settings app instead of a config file", "Enter a gamescope Steam Big Picture session with Super+G", "Retheme the shell, apps, terminal and lock screen from your wallpaper", "Boot back into an earlier Btrfs snapshot from the Limine menu", "Preinstall GE-Proton and set it as Steam's default compatibility tool", "Arrange monitors and set scale, HDR and ICC profiles in Settings", "Give each workspace its own layout: dwindle, master, scrolling, monocle or float", "Remap any keyboard shortcut from Settings instead of editing config files", "Turn window title bars on or off without logging out", "Save wallpaper, palette and decorations as themes that switch day and night", "Start with a set of everyday apps, each one optional at install", "Install more apps from a store with pictures instead of package names", "Zoom out to a scrolling map of every workspace with Super+O", "Send files to nearby devices by dropping them on the bar", "Reopen your windows on their original workspaces after a reboot", "Install from a graphical installer with a Get Started setup picker", "Encrypt the whole system with LUKS2 and a boot passphrase", "Install alongside Windows and add it to the boot menu", "Detect the graphics card during install and fit matching drivers", "Meet the desktop with a first-boot tour and one-click extra installs", "Update packages, Flatpaks and the desktop from one Settings button", "Install prebuilt signed packages instead of building from the AUR" ], "commits": [] } ] }