aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore10
-rw-r--r--LICENSE338
-rw-r--r--README.md135
-rw-r--r--SPEC.md208
-rwxr-xr-xbin/privacy_dots.sh199
-rwxr-xr-xbin/vms_dots.sh89
-rwxr-xr-xbin/wb-icon171
-rwxr-xr-xbin/wb-idle45
-rwxr-xr-xbin/wb-lang46
-rw-r--r--config.jsonc49
-rw-r--r--icons/flag-gb.svg7
-rw-r--r--icons/flag-it.svg7
-rw-r--r--icons/slackware.svg5
-rwxr-xr-xinstall.sh130
-rw-r--r--modules/clock.jsonc35
-rw-r--r--modules/custom/launcher.jsonc13
-rw-r--r--modules/custom/privacy_dots.jsonc12
-rw-r--r--modules/custom/vmdot.jsonc10
-rw-r--r--modules/extras/taskbar.jsonc18
-rw-r--r--modules/extras/tray.jsonc8
-rw-r--r--modules/hyprland/workspaces.jsonc26
-rw-r--r--modules/image/idle_inhibitor.jsonc15
-rw-r--r--modules/image/language.jsonc18
-rw-r--r--modules/image/volume.jsonc44
-rw-r--r--styles/fonts.css19
-rw-r--r--styles/global.css31
-rw-r--r--styles/modules.css180
-rw-r--r--styles/pills.css102
-rw-r--r--styles/style.css19
29 files changed, 1989 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7ee526b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+HANDOFF.md
+
+# Generated by udt-palette from the selected scheme. Tracking it would turn
+# every scheme switch into a diff.
+/styles/theme.css
+
+# Generated by install.sh: the accent is substituted into it.
+/icons/slackware-accent.svg
+idea1.png
+idea2.png
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9efa6fb
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,338 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Moe Ghoul>, 1 April 1989
+ Moe Ghoul, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0a37bd5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,135 @@
+# waybar-theme-udt
+
+A waybar bar for DP-3: pill clusters, icons from the desktop's own icon theme
+instead of font glyphs, and colours driven by
+[unified-desktop-theme](../unified-desktop-theme).
+
+ [◆] [📅 Wed 15/09] [🕑 14:32] [1..8] [●●] [ open apps ] [🔊] [tray] [⏻] [🇮🇹]
+
+Each bracket is a pill. Pills group into clusters with gaps between them, so
+the bar reads as a few objects rather than one strip. Hover cross-fades
+background and text over 0.3s; taskbar buttons underline in the accent.
+
+## Install
+
+ ./install.sh
+
+Installs to `~/.config/waybar-udt/` and prints the command to run it. **The
+existing bar at `~/.config/waybar/` is left alone**, so DP-1 keeps working
+while this one is tried on DP-3.
+
+Needs `unified-desktop-theme` checked out next to this repo: the palette comes
+from there, and `install.sh` fails clearly if it is missing.
+
+## No glyphs
+
+Every symbol on this bar is a real icon from `Material-Black-Plum-Suru`, the
+theme GTK already uses. Icons keep their own colours, matching every other
+application on the desktop.
+
+waybar offers three ways to draw one, and this bar uses all three because no
+single one covers everything:
+
+| Mechanism | Used by | Why |
+| --- | --- | --- |
+| `icon-theme` config key | taskbar, tray | Native support, nothing else needed. |
+| `image` module | volume, microphone, presentation mode, language | Runs a script that resolves a state to an icon path. |
+| CSS `background-image` | workspaces, clock, launcher | `format-icons` takes text, not paths. |
+
+The consequence worth knowing: **the workspace icons live in
+`styles/modules.css`, not in the module config.** Changing one means editing
+CSS. The alternative was eight `custom` modules faking a workspace strip,
+which would have cost click-to-switch and the active and urgent states.
+
+The language flag went the other way. `hyprland/language` styles as
+`#language` and nothing else, with no per-language class, so the flag could
+not be picked by a selector; it is an `image` module driven by `wb-lang`
+instead.
+
+`wb-icon --selftest` checks every icon name the bar asks for, CSS ones
+included, and `install.sh` runs it. A missing icon renders as an empty pill
+with nothing to say why, so it fails while someone is watching instead.
+
+### Why the icons do not follow the accent
+
+2466 of the 5169 panel icons in `Material-Black-Plum-Suru` carry a hardcoded
+gradient and are not recolourable. That is intended here: icons match the rest
+of the desktop rather than the bar.
+
+The two icons this repo ships are the exception, because the icon theme has
+neither. `icons/slackware.svg` (Simple Icons, CC0) is single-path monochrome
+and takes the udt accent at install time. The two flags keep their national
+colours.
+
+## Layout
+
+| Cluster | Modules |
+| --- | --- |
+| left | launcher, date, time, workspaces, VM dots |
+| center | open applications |
+| right | privacy dots, volume drawer, tray, presentation mode, language |
+
+No battery, temperature, CPU, memory or backlight: this is a desktop.
+
+The clock is the only text on the bar. An icon cannot say 14:32, so the icon
+sits before the text, which is what makes the pair match the icon-only pills
+around it.
+
+### Workspaces
+
+| WS | Icon | |
+| --- | --- | --- |
+| 1 | `web-browser` | 5 | `document-edit` |
+| 2 | `utilities-terminal` | 6 | `applications-graphics` |
+| 3 | `text-editor` | 7 | `internet-chat` |
+| 4 | `network-server` | 8 | `input-gaming` |
+
+Workspace 4 uses the same icon as the quickshell drawer's VM tile. Names are
+generic, so the bar does not lie when the application on a workspace changes.
+
+## Colours
+
+This repo holds the bar; udt holds the palette. `styles/theme.css` is
+generated by `udt-palette` and gitignored, like every other generated file in
+that project.
+
+The dot scripts are bash and cannot read `@define-color`, so `install.sh`
+writes `~/.config/waybar-udt/dots.colors` for them to source. Both fall back to
+Macchiato values when it is missing, so they still run standalone.
+
+## Layout of this repo
+
+ config.jsonc entry point; `include`s every module file
+ modules/ one file per module, by source
+ styles/ style.css imports the rest in order
+ icons/ the two icons the icon theme lacks
+ bin/ wb-icon, wb-idle, and the two dot scripts
+
+Wildcard `include` has been broken since waybar v0.14.0, so `config.jsonc`
+lists every module file explicitly.
+
+`bin/vms_dots.sh` and `bin/privacy_dots.sh` used to live only in `~/bin`, in no
+repository. They are versioned here now and symlinked back; `install.sh` keeps
+any real file it finds there as `.pre-udt` first.
+
+## Reload
+
+ killall -SIGUSR2 waybar
+
+`reload_style_on_change` is on, so CSS edits apply by themselves.
+
+Visual changes are measured, not reasoned about:
+
+ grim -o DP-3 /tmp/shot.png
+
+## License
+
+GPLv2 only. See `LICENSE`.
+
+## Development Approach
+
+This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback.
+
+All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions.
+
+The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability.
diff --git a/SPEC.md b/SPEC.md
new file mode 100644
index 0000000..a4d2341
--- /dev/null
+++ b/SPEC.md
@@ -0,0 +1,208 @@
+# waybar-theme-udt
+
+A waybar bar for DP-3, themed by [unified-desktop-theme][udt] and drawn with
+icons from the desktop's own icon theme rather than font glyphs.
+
+[udt]: ../unified-desktop-theme
+
+## Goal
+
+Three things at once:
+
+- **No font glyphs.** Every module that shows a symbol shows a real icon from
+ `Material-Black-Plum-Suru`, the theme GTK already uses. Icons keep their own
+ colours, because that is what every other application on this desktop shows.
+- **Colours from udt.** Pill backgrounds, text and states resolve from
+ `palette/roles-<scheme>.conf`, so switching scheme recolours this bar with
+ everything else.
+- **Pill clusters with transitions.** The grouped-capsule layout of `idea1.png`
+ with the hover and state transitions of `idea2.png`.
+
+The current bar on DP-1 is untouched. This one installs alongside it and is
+pointed at DP-3.
+
+## Layout
+
+ ┌ left ──────────────────────────────┐ ┌ center ┐ ┌ right ─────────────────────┐
+ [◆] [📅 Wed 15/09] [🕑 14:32] [1..8] [●●] [ open apps ] [🔊 62%] [tray] [⏻] [🇮🇹]
+ launcher date time workspaces vm taskbar volume tray present lang
+
+Each bracket is one pill. Neighbouring pills in a cluster share a background
+and weld together with asymmetric corner radii; clusters are separated by
+transparent gaps, so the bar reads as groups rather than a continuous strip.
+
+### Modules
+
+| Position | Module | Icon source | Notes |
+| --- | --- | --- | --- |
+| left | `custom/launcher` | `slackware.svg`, shipped here | Accent background, the one always-coloured pill. Opens the quickshell drawer. |
+| left | `clock#date` | `x-office-calendar` | Icon then text. Calendar tooltip kept. |
+| left | `clock#time` | `clock` | Icon then text. |
+| left | `hyprland/workspaces` | eight themed icons, via CSS | See below. |
+| left | `custom/vmdot` | coloured dots | Dots keep their shape; colours move to roles. |
+| center | `wlr/taskbar` | `icon-theme` config key | Per-window application icons. |
+| right | `group/wireplumber` | `image` module | Drawer group kept as it is today. |
+| right | `tray` | application-provided | |
+| right | `idle_inhibitor` | `image` module | Presentation mode. |
+| right | `image#lang` | `flag-it.svg` / `flag-gb.svg`, shipped here | An `image` module: `hyprland/language` styles as `#language` only, with no per-language class, so a flag cannot be selected in CSS. |
+
+Dropped from the current bar: `custom/breaktimer`, `custom/mail`,
+`custom/privacy_dots` stays only if you want it on this bar too (it is
+currently listed; say if it should follow the others out). No battery, no
+temperature, no cpu, no memory, no backlight: this is a desktop.
+
+Clock is the one module that stays text. An icon cannot say 14:32; the icon
+sits before the text, which is what makes it match the rest.
+
+### Workspace icons
+
+| WS | Icon | Was |
+| --- | --- | --- |
+| 1 | `web-browser` | browser glyph |
+| 2 | `utilities-terminal` | terminal glyph |
+| 3 | `text-editor` | code glyph |
+| 4 | `network-server` | matches the quickshell drawer's VM tile (`U+F233`, Font Awesome `server`) |
+| 5 | `document-edit` | pen on a document: the alternate text editor lives here |
+| 6 | `applications-graphics` | graphics glyph |
+| 7 | `internet-chat` | chat glyph |
+| 8 | `input-gaming` | joypad: games and media players |
+
+All eight resolve in `Material-Black-Plum-Suru` today.
+
+## How icons reach the bar
+
+Three mechanisms, because waybar offers three and no single one covers
+everything. This is the part worth reading before changing anything.
+
+**1. Native `icon-theme` (taskbar, tray).** `wlr/taskbar` takes `icon-theme`
+and `icon-size` outright. Today it asks for `Papirus`, **which is not
+installed**, so GTK has been silently falling back. Pointing it at
+`Material-Black-Plum-Suru` is a bug fix, not a preference.
+
+**2. The `image` module (volume, presentation mode).** `image` runs `exec` and
+expects `$path\n$tooltip` on stdout, so a small script maps state to an icon
+name, resolves it through the GTK icon theme, and prints the path. One `image`
+module is one icon, so a five-state volume indicator is one module plus one
+script, not five modules.
+
+**3. CSS `background-image` (workspaces).** Workspace buttons come from a
+single `hyprland/workspaces` module and `format-icons` takes text, not paths.
+Faking the strip with eight `custom` modules would lose click-to-switch and
+the active/urgent states, so instead the real module keeps its behaviour, its
+labels are emptied, and each button gets an icon through
+`#workspaces button:nth-child(n) { background-image: url(...) }`.
+
+The consequence, stated plainly: **the eight workspace icon paths live in the
+stylesheet, not in the config.** Changing a workspace icon means editing CSS.
+
+### Why the icons cannot follow the accent
+
+2466 of the 5169 panel icons in `Material-Black-Plum-Suru` carry a hardcoded
+`oomox` gradient (`#a08fbf` to `#5a33a3`) and seven further named gradients
+each. They are not monochrome and not recolourable by CSS `color`. So icons
+show their own colours and do not track the scheme, which is the intended
+behaviour here: they match every other application on the desktop.
+
+The two icons this repo ships are the exception. `slackware.svg` (from
+Simple Icons) and the two flags are single-path monochrome, so `slackware.svg`
+is tinted with the udt accent by a one-line `fill` substitution at install
+time. The flags keep their national colours.
+
+## Repository layout
+
+Modular, one file per module, mirroring the current setup:
+
+ config.jsonc entry point, `include`s every module file
+ modules/
+ clock.jsonc clock#date and clock#time
+ hyprland/
+ workspaces.jsonc
+ language.jsonc
+ custom/
+ launcher.jsonc
+ vmdot.jsonc
+ extras/
+ taskbar.jsonc
+ tray.jsonc
+ wireplumber.jsonc
+ image/
+ idle_inhibitor.jsonc presentation mode, via `image`
+ styles/
+ style.css entry point, imports the rest in order
+ theme.css GENERATED by udt-palette, gitignored
+ fonts.css
+ global.css #waybar, tooltip, button base rules
+ pills.css the cluster geometry and transitions
+ modules.css per-module rules, including workspace icons
+ icons/
+ slackware.svg carries @ACCENT@, substituted on install
+ flag-it.svg
+ flag-gb.svg
+ bin/
+ wb-icon state to icon path, for the `image` modules
+ vms_dots.sh moved here from ~/bin, symlinked back
+ privacy_dots.sh moved here from ~/bin, symlinked back
+ install.sh
+ LICENSE
+ README.md
+
+Wildcard `include` is broken in waybar 0.14, so `config.jsonc` lists every
+module file explicitly. That is a waybar limitation, not a choice.
+
+## Palette integration
+
+This repo is a **consumer** of udt: it holds the bar, udt holds the colours.
+
+`styles/theme.css` is generated by `udt-palette` and gitignored, exactly like
+every other generated file in that repo. Generating it needs two changes on
+the udt side:
+
+**A new `[waybar]` role block.** The current one names `temperature`,
+`memory`, `cpu`, `backlight` and `battery`, none of which exist on this bar.
+The new roles are `launcher`, `workspaces`, `vmdots`, `taskbar`, `volume`,
+`tray`, `present`, `lang`, `date`, `time`, plus the existing `main_*`,
+`hover_*` and `outline`.
+
+**Added to all nine `roles-*.conf`.** `udt-palette --selftest` enforces that
+every scheme defines the same role set, so a role added to one file and not
+the rest fails the check. That is the check doing its job.
+
+Dot colours come from the existing `[state]` roles: `success` for a running
+VM, `warning` for paused, `critical` for crashed, `info` for saved, and
+`fg_faint` for shut off. Both dot scripts currently hardcode `#30D158`,
+`#FFD60A`, `#FF9F0A`, `#9B32FA` and `#6E6E73`; they will read a generated
+colour block instead.
+
+## Install
+
+`install.sh` follows udt's conventions: symlink what this repo owns
+canonically, copy what another application owns, guard every reload.
+
+- Module and style files are **symlinked** into `~/.config/waybar-udt/`, so the
+ repo cannot drift from what is installed.
+- `icons/slackware.svg` is **generated** into place with `@ACCENT@` substituted,
+ the same way udt generates `launcher.rasi`.
+- `bin/vms_dots.sh` and `bin/privacy_dots.sh` are symlinked to `~/bin/`,
+ replacing the copies that live there now. **The originals are backed up
+ first**: they are the only copy and are not currently in any repository.
+- The bar is **not** started and `~/.config/waybar` is **not** touched. Running
+ it is one command, printed at the end, so DP-1's bar keeps working until you
+ choose otherwise.
+
+Reload is `killall -SIGUSR2 waybar`, and `reload_style_on_change` is already on.
+
+## Verification
+
+Per udt's own rule, visual claims are measured, not reasoned about:
+
+ grim -o DP-3 /tmp/shot.png
+
+`wb-icon` gets one runnable self-check: every icon name the bar asks for must
+resolve in the icon theme, so a missing icon fails loudly at install time
+rather than rendering as a blank pill.
+
+## Open questions
+
+1. `custom/privacy_dots` is in the keep list but was not in the ten modules you
+ named. On this bar or not?
+2. Licence: GPLv2-only unless you say otherwise.
diff --git a/bin/privacy_dots.sh b/bin/privacy_dots.sh
new file mode 100755
index 0000000..2074631
--- /dev/null
+++ b/bin/privacy_dots.sh
@@ -0,0 +1,199 @@
+#!/usr/bin/env bash
+# A dot per active capture: microphone, camera, location, screen sharing.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# dependencies: pipewire (pw-dump), jq, psmisc (fuser)
+#
+# Shows nothing when everything is off, which is the point: the pill only
+# appears when something is actually capturing.
+#
+# The colours come from the palette, not from here: install.sh writes
+# ~/.config/waybar-udt/dots.colors from the selected udt scheme, and this
+# sources it. The defaults below are only what a missing file falls back to.
+set -euo pipefail
+
+JQ_BIN="${JQ:-jq}"
+PW_DUMP_CMD="${PW_DUMP:-pw-dump}"
+
+# Palette. Sourced, with fallbacks, so this script works standalone.
+DOT_SUCCESS="#a6da95"
+DOT_WARNING="#eed49f"
+DOT_CRITICAL="#ed8796"
+DOT_INFO="#8bd5ca"
+DOT_ACCENT="#b7bdf8"
+DOT_FAINT="#6e738d"
+colors="$HOME/.config/waybar-udt/dots.colors"
+# shellcheck source=/dev/null
+[[ -r "$colors" ]] && . "$colors"
+
+mic=0
+cam=0
+loc=0
+scr=0
+
+mic_app=""
+cam_app=""
+loc_app=""
+scr_app=""
+
+# mic & camera
+if command -v "$PW_DUMP_CMD" >/dev/null 2>&1 && command -v "$JQ_BIN" >/dev/null 2>&1; then
+ dump="$($PW_DUMP_CMD 2>/dev/null || true)"
+
+ mic="$(
+ printf '%s' "$dump" \
+ | $JQ_BIN -r '
+ [ .[]
+ | select(.type=="PipeWire:Interface:Node")
+ | select((.info.props."media.class"=="Audio/Source" or .info.props."media.class"=="Audio/Source/Virtual"))
+ | select((.info.state=="running") or (.state=="running"))
+ ] | (if length>0 then 1 else 0 end)
+ ' 2>/dev/null || echo 0
+ )"
+
+ if [[ "$mic" -eq 1 ]]; then
+ mic_app="$(
+ printf '%s' "$dump" \
+ | $JQ_BIN -r '
+ [ .[]
+ | select(.type=="PipeWire:Interface:Node")
+ | select((.info.props."media.class"=="Stream/Input/Audio"))
+ | select((.info.state=="running") or (.state=="running"))
+ | .info.props["node.name"]
+ ] | unique | join(", ")
+ ' 2>/dev/null || echo ""
+ )"
+ fi
+
+ if command -v fuser >/dev/null 2>&1; then
+ cam=0
+ for dev in /dev/video*; do
+ if [ -e "$dev" ] && fuser "$dev" >/dev/null 2>&1; then
+ cam=1
+ break
+ fi
+ done
+ else
+ cam=0
+ fi
+
+ if command -v fuser >/dev/null 2>&1; then
+ for dev in /dev/video*; do
+ if [ -e "$dev" ] && fuser "$dev" >/dev/null 2>&1; then
+ pids=$(fuser "$dev" 2>/dev/null)
+ for pid in $pids; do
+ pname=$(ps -p "$pid" -o comm=)
+ if [[ -n "$pname" ]]; then
+ cam_app+="$pname, "
+ fi
+ done
+ fi
+ done
+ cam_app="${cam_app%, }"
+ fi
+
+fi
+
+# location
+if command -v gdbus >/dev/null 2>&1; then
+ if pids=$(pgrep -x geoclue); then
+ loc=1
+ for pid in $pids; do
+ pname=$(ps -p "$pid" -o comm=)
+ [[ -n "$pname" ]] && loc_app+="$pname, "
+ done
+ loc_app="${loc_app%, }"
+ else
+ loc=0
+ fi
+fi
+
+# screen sharing
+if command -v "$PW_DUMP_CMD" >/dev/null 2>&1 && command -v "$JQ_BIN" >/dev/null 2>&1; then
+ if [[ -z "${dump:-}" ]]; then
+ dump="$($PW_DUMP_CMD 2>/dev/null || true)"
+ fi
+
+ scr="$(
+ printf '%s' "$dump" \
+ | $JQ_BIN -e '
+ [ .[]
+ | select(.info?.props?)
+ | select(
+ (.info.props["media.name"]? // "")
+ | test("^(xdph-streaming|gsr-default)")
+ )
+ ]
+ | (if length > 0 then true else false end)
+ ' >/dev/null && echo 1 || echo 0
+ )"
+fi
+
+if [[ "$scr" -eq 1 ]]; then
+ scr_app="$(
+ printf '%s' "$dump" \
+ | $JQ_BIN -r '
+ [ .[]
+ | select(.type=="PipeWire:Interface:Node")
+ | select((.info.props."media.class"=="Stream/Input/Video") or (.info.props."media.name"=="gsr-default_output"))
+ | select((.info.state=="running") or (.state=="running"))
+ | .info.props["media.name"]
+ ] | unique | join(", ")
+ ' 2>/dev/null || echo ""
+ )"
+fi
+
+# output. Capture is a state worth noticing, so these are the alert roles:
+# the microphone is the quiet one, the camera and the screen are not.
+dot() {
+ local on="$1" color="$2"
+ if [[ "$on" -eq 1 ]]; then
+ printf '<span foreground="%s">●</span>' "$color"
+ else
+ printf ''
+ fi
+}
+
+dots=()
+mic_dot="$(dot "$mic" "$DOT_SUCCESS")"; [[ -n "$mic_dot" ]] && dots+=("$mic_dot")
+cam_dot="$(dot "$cam" "$DOT_WARNING")"; [[ -n "$cam_dot" ]] && dots+=("$cam_dot")
+loc_dot="$(dot "$loc" "$DOT_INFO")"; [[ -n "$loc_dot" ]] && dots+=("$loc_dot")
+scr_dot="$(dot "$scr" "$DOT_CRITICAL")"; [[ -n "$scr_dot" ]] && dots+=("$scr_dot")
+
+text="${dots[*]}"
+
+if [[ -n "$mic_app" ]]; then
+ mic_status="Mic: $mic_app"
+else
+ mic_status="Mic: off"
+fi
+
+if [[ -n "$cam_app" ]]; then
+ cam_status="Cam: $cam_app"
+else
+ cam_status="Cam: off"
+fi
+
+if [[ -n "$loc_app" ]]; then
+ loc_status="Location: $loc_app"
+else
+ loc_status="Location: off"
+fi
+
+if [[ -n "$scr_app" ]]; then
+ scr_status="Screen sharing: $scr_app"
+else
+ scr_status="Screen sharing: off"
+fi
+
+tooltip="$mic_status | $cam_status | $loc_status | $scr_status"
+
+classes="privacydot"
+[[ $mic -eq 1 ]] && classes="$classes mic-on" || classes="$classes mic-off"
+[[ $cam -eq 1 ]] && classes="$classes cam-on" || classes="$classes cam-off"
+[[ $loc -eq 1 ]] && classes="$classes loc-on" || classes="$classes loc-off"
+[[ $scr -eq 1 ]] && classes="$classes scr-on" || classes="$classes scr-off"
+
+$JQ_BIN -c -n --arg text "$text" --arg tooltip "$tooltip" --arg class "$classes" \
+ '{text:$text, tooltip:$tooltip, class:$class}'
diff --git a/bin/vms_dots.sh b/bin/vms_dots.sh
new file mode 100755
index 0000000..bf95070
--- /dev/null
+++ b/bin/vms_dots.sh
@@ -0,0 +1,89 @@
+#! /bin/bash
+# One dot per defined VM, coloured by state.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# dependencies: libvirt (virsh), jq
+#
+# The colours come from the palette, not from here: install.sh writes
+# ~/.config/waybar-udt/dots.colors from the selected udt scheme, and this
+# sources it. The defaults below are only what a missing file falls back to,
+# so the bar still draws if the theme has not been installed yet.
+set -euo pipefail
+
+JQ_BIN="${JQ:-jq}"
+
+# Palette. Sourced, with fallbacks, so this script works standalone.
+DOT_SUCCESS="#a6da95"
+DOT_WARNING="#eed49f"
+DOT_CRITICAL="#ed8796"
+DOT_INFO="#8bd5ca"
+DOT_ACCENT="#b7bdf8"
+DOT_FAINT="#6e738d"
+colors="$HOME/.config/waybar-udt/dots.colors"
+# shellcheck source=/dev/null
+[[ -r "$colors" ]] && . "$colors"
+
+# Require virsh
+if ! command -v virsh >/dev/null 2>&1; then
+ $JQ_BIN -cn '{text: "", tooltip: "virsh not found", class: "vmdot"}'
+ exit 0
+fi
+
+# Collect all defined VMs and their states.
+# virsh list --all columns: Id Name State (state may be multi-word, e.g. "shut off")
+declare -A vm_states
+while IFS= read -r line; do
+ # Skip header lines and separator lines
+ [[ "$line" =~ ^[[:space:]]*Id[[:space:]] ]] && continue
+ [[ "$line" =~ ^[-[:space:]]+$ ]] && continue
+ [[ -z "${line// }" ]] && continue
+
+ # Fields: id (may be "-"), name, state (rest of line)
+ read -r _id name state_rest <<< "$line"
+ vm_states["$name"]="$state_rest"
+done < <(virsh list --all 2>/dev/null)
+
+if [[ ${#vm_states[@]} -eq 0 ]]; then
+ $JQ_BIN -cn '{text: "", tooltip: "No VMs defined", class: "vmdot"}'
+ exit 0
+fi
+
+# libvirt state → palette role.
+declare -A state_color=(
+ ["running"]="$DOT_SUCCESS"
+ ["idle"]="$DOT_WARNING" # running but idle
+ ["paused"]="$DOT_WARNING"
+ ["pmsuspended"]="$DOT_ACCENT" # PM sleep / ACPI S3
+ ["saved"]="$DOT_INFO" # managed save
+ ["crashed"]="$DOT_CRITICAL"
+ ["shut off"]="$DOT_FAINT"
+)
+fallback_color="$DOT_FAINT" # any unknown future state
+
+dots=()
+tooltip_parts=()
+classes="vmdot"
+
+# Sort VM names for stable ordering
+mapfile -t sorted_names < <(printf '%s\n' "${!vm_states[@]}" | sort)
+
+for name in "${sorted_names[@]}"; do
+ state="${vm_states[$name]}"
+ color="${state_color[$state]:-$fallback_color}"
+ # Derive a CSS-safe class token (spaces → hyphens)
+ state_class="${state// /-}"
+
+ dots+=("<span foreground=\"${color}\">●</span>")
+ classes="$classes vm-${state_class}"
+ tooltip_parts+=("${name}: ${state}")
+done
+
+text="${dots[*]}"
+tooltip="$(IFS=' | '; echo "${tooltip_parts[*]}")"
+
+$JQ_BIN -cn \
+ --arg text "$text" \
+ --arg tooltip "$tooltip" \
+ --arg class "$classes" \
+ '{text: $text, tooltip: $tooltip, class: $class}'
diff --git a/bin/wb-icon b/bin/wb-icon
new file mode 100755
index 0000000..9c29c60
--- /dev/null
+++ b/bin/wb-icon
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+# Resolve a module state to an icon path from the desktop's icon theme.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# waybar's `image` module runs a command and reads "$path\n$tooltip" from its
+# stdout. That is the only way a themed icon reaches a module that is not the
+# taskbar or the tray, so volume, microphone and presentation mode all come
+# through here.
+#
+# wb-icon volume # reads wpctl, prints the matching icon
+# wb-icon mic
+# wb-icon idle <activated|deactivated>
+# wb-icon --selftest # every name this bar asks for must resolve
+#
+# Icons keep their own colours: this desktop's icon theme draws them with
+# baked-in gradients, so they are not recolourable and are not meant to be.
+# Only the two icons this repo ships are monochrome.
+
+import subprocess
+import sys
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk # noqa: E402 (must follow require_version)
+
+# The icon theme to draw from. Kept in one place because changing it is a
+# single edit, and because --selftest needs the same value the modules use.
+THEME = "Material-Black-Plum-Suru"
+
+SIZE = 22
+
+# State to icon name. Every name here is checked by --selftest, so a theme that
+# drops one fails at install time rather than rendering an empty pill.
+ICONS = {
+ "volume": {
+ "muted": "audio-volume-muted",
+ "low": "audio-volume-low",
+ "medium": "audio-volume-medium",
+ "high": "audio-volume-high",
+ },
+ "mic": {
+ "muted": "microphone-sensitivity-muted",
+ "on": "audio-input-microphone",
+ },
+ "idle": {
+ "activated": "x-office-presentation",
+ "deactivated": "preferences-desktop-screensaver",
+ },
+}
+
+
+def lookup(name):
+ """Absolute path of an icon in THEME, or None."""
+ theme = Gtk.IconTheme.new()
+ theme.set_custom_theme(THEME)
+ info = theme.lookup_icon(name, SIZE, 0)
+ return info.get_filename() if info else None
+
+
+def wpctl(node):
+ """(volume percent, muted) for a wireplumber node, or (None, False).
+
+ wpctl prints e.g. "Volume: 0.62" or "Volume: 0.62 [MUTED]".
+ """
+ try:
+ out = subprocess.run(
+ ["wpctl", "get-volume", node],
+ capture_output=True, text=True, timeout=2,
+ ).stdout
+ except (OSError, subprocess.SubprocessError):
+ return None, False
+ if "Volume:" not in out:
+ return None, False
+ muted = "MUTED" in out
+ try:
+ return round(float(out.split("Volume:")[1].split()[0]) * 100), muted
+ except (IndexError, ValueError):
+ return None, muted
+
+
+def volume_state(pct, muted):
+ if muted or pct == 0:
+ return "muted"
+ if pct < 34:
+ return "low"
+ if pct < 67:
+ return "medium"
+ return "high"
+
+
+def emit(name, tooltip):
+ """Print what the image module expects: a path, then a tooltip."""
+ path = lookup(name)
+ if not path:
+ # An unresolved icon is a broken bar, not a warning: say so on stderr
+ # and print nothing, so the module stays empty rather than showing a
+ # stale icon.
+ print(f"wb-icon: no icon named {name!r} in {THEME}", file=sys.stderr)
+ return 1
+ print(path)
+ print(tooltip)
+ return 0
+
+
+def selftest():
+ """Every icon this bar asks for, including the ones set in CSS."""
+ # The workspace icons live in styles/modules.css rather than here, because
+ # waybar's format-icons takes text and not paths. They are still this
+ # bar's icons, so they are checked here too.
+ workspace_icons = [
+ "web-browser", "utilities-terminal", "text-editor", "network-server",
+ "document-edit", "applications-graphics", "internet-chat",
+ "input-gaming",
+ ]
+ clock_icons = ["x-office-calendar", "clock"]
+
+ names = sorted(
+ {n for group in ICONS.values() for n in group.values()}
+ | set(workspace_icons) | set(clock_icons)
+ )
+ missing = [n for n in names if not lookup(n)]
+ for n in names:
+ print(f" {'ok ' if n not in missing else 'MISS'} {n}")
+ if missing:
+ print(f"\n{len(missing)} icon(s) missing from {THEME}", file=sys.stderr)
+ return 1
+ print(f"\nall {len(names)} icons resolve in {THEME}")
+ return 0
+
+
+def main(argv):
+ if len(argv) < 2 or argv[1] in ("-h", "--help"):
+ print(__doc__ or "usage: wb-icon <volume|mic|idle|--selftest> [state]")
+ return 0
+
+ what = argv[1]
+
+ if what == "--selftest":
+ return selftest()
+
+ if what == "volume":
+ pct, muted = wpctl("@DEFAULT_AUDIO_SINK@")
+ if pct is None:
+ return emit(ICONS["volume"]["muted"], "Volume: unavailable")
+ state = volume_state(pct, muted)
+ label = "muted" if state == "muted" else f"{pct}%"
+ return emit(ICONS["volume"][state], f"Volume: {label}")
+
+ if what == "mic":
+ pct, muted = wpctl("@DEFAULT_AUDIO_SOURCE@")
+ if pct is None:
+ return emit(ICONS["mic"]["muted"], "Microphone: unavailable")
+ state = "muted" if muted or pct == 0 else "on"
+ label = "muted" if state == "muted" else f"{pct}%"
+ return emit(ICONS["mic"][state], f"Microphone: {label}")
+
+ if what == "idle":
+ state = argv[2] if len(argv) > 2 else "deactivated"
+ state = state if state in ICONS["idle"] else "deactivated"
+ on = state == "activated"
+ return emit(ICONS["idle"][state],
+ f"Presentation mode: {'on' if on else 'off'}")
+
+ print(f"wb-icon: unknown subject {what!r}", file=sys.stderr)
+ return 2
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/bin/wb-idle b/bin/wb-idle
new file mode 100755
index 0000000..05c4a8c
--- /dev/null
+++ b/bin/wb-idle
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# Presentation mode: keep the screen awake, and say so with a themed icon.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# waybar's own idle_inhibitor module draws its state with format-icons, which
+# is text. This drives the same idea through hypridle and reports the state as
+# an icon path for an `image` module instead.
+#
+# wb-idle status # print "$path\n$tooltip" for the image module
+# wb-idle toggle # flip the inhibitor, then refresh the module
+#
+# The inhibitor is hypridle's own pause, so nothing else has to be running.
+set -euo pipefail
+
+state_file="${XDG_RUNTIME_DIR:-/tmp}/wb-idle.state"
+
+active() { [[ -f "$state_file" ]]; }
+
+case "${1:-status}" in
+toggle)
+ if active; then
+ rm -f "$state_file"
+ # Resume idling. Guarded: hypridle may not be running, and that is
+ # not an error worth failing a click over.
+ pkill -USR2 -x hypridle 2>/dev/null || true
+ else
+ touch "$state_file"
+ pkill -USR1 -x hypridle 2>/dev/null || true
+ fi
+ # Refresh the module now rather than waiting for its interval.
+ pkill -RTMIN+9 -x waybar 2>/dev/null || true
+ ;;
+status)
+ if active; then
+ wb-icon idle activated
+ else
+ wb-icon idle deactivated
+ fi
+ ;;
+*)
+ echo "usage: wb-idle <status|toggle>" >&2
+ exit 2
+ ;;
+esac
diff --git a/bin/wb-lang b/bin/wb-lang
new file mode 100755
index 0000000..5a0cd5d
--- /dev/null
+++ b/bin/wb-lang
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+# Keyboard layout, as a flag.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# waybar's hyprland/language module styles as #language and nothing else: it
+# has no per-language CSS class, so a flag cannot be selected by a selector.
+# format-<lang> exists but takes text, not a path. So the flag arrives the
+# same way every other icon on this bar does, through an `image` module.
+#
+# wb-lang # print "$path\n$tooltip" for the image module
+#
+# The flags live next to this bar's config because the icon theme ships 13 and
+# neither Italian nor British is among them.
+set -euo pipefail
+
+flags="$HOME/.config/waybar-udt"
+
+# hyprctl names the layout in full ("Italian", "English (US)"), which is what
+# the keymap reports; match on that rather than on the short code, because the
+# short code is not in this output.
+layout="$(hyprctl devices -j 2>/dev/null \
+ | python3 -c 'import json,sys
+try:
+ d = json.load(sys.stdin)
+except Exception:
+ sys.exit(0)
+for k in d.get("keyboards", []):
+ if k.get("name") == "2.4g-dongle":
+ print(k.get("active_keymap", ""))
+ break' 2>/dev/null || true)"
+
+case "$layout" in
+Italian*) flag="flag-it.svg"; name="Italian" ;;
+English*) flag="flag-gb.svg"; name="English" ;;
+"") flag="flag-it.svg"; name="unknown" ;;
+*) flag="flag-gb.svg"; name="$layout" ;;
+esac
+
+if [[ ! -r "$flags/$flag" ]]; then
+ echo "wb-lang: missing $flags/$flag" >&2
+ exit 1
+fi
+
+echo "$flags/$flag"
+echo "Layout: $name"
diff --git a/config.jsonc b/config.jsonc
new file mode 100644
index 0000000..9d6afae
--- /dev/null
+++ b/config.jsonc
@@ -0,0 +1,49 @@
+// waybar-theme-udt: the bar on DP-3.
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+// Licensed under the GNU General Public License v2 only.
+//
+// One file per module, included explicitly: wildcard `include` has been broken
+// since waybar v0.14.0, so every module file has to be listed by name.
+{
+ "include": [
+ "$HOME/.config/waybar-udt/modules/custom/launcher.jsonc",
+ "$HOME/.config/waybar-udt/modules/clock.jsonc",
+ "$HOME/.config/waybar-udt/modules/hyprland/workspaces.jsonc",
+ "$HOME/.config/waybar-udt/modules/custom/vmdot.jsonc",
+ "$HOME/.config/waybar-udt/modules/extras/taskbar.jsonc",
+ "$HOME/.config/waybar-udt/modules/custom/privacy_dots.jsonc",
+ "$HOME/.config/waybar-udt/modules/image/volume.jsonc",
+ "$HOME/.config/waybar-udt/modules/extras/tray.jsonc",
+ "$HOME/.config/waybar-udt/modules/image/idle_inhibitor.jsonc",
+ "$HOME/.config/waybar-udt/modules/image/language.jsonc",
+ ],
+
+ // Clusters, not a continuous strip: the stylesheet welds neighbours
+ // together and leaves gaps between groups.
+ "modules-left": [
+ "custom/launcher",
+ "clock#date",
+ "clock#time",
+ "hyprland/workspaces",
+ "custom/vmdot"
+ ],
+ "modules-center": [
+ "wlr/taskbar"
+ ],
+ "modules-right": [
+ "custom/privacy_dots",
+ "group/volume",
+ "tray",
+ "image#idle",
+ "image#lang"
+ ],
+
+ "layer": "top",
+ "output": "DP-3",
+ "height": 0,
+ "width": 0,
+ "margin": 0,
+ "spacing": 0,
+ "mode": "dock",
+ "reload_style_on_change": true
+}
diff --git a/icons/flag-gb.svg b/icons/flag-gb.svg
new file mode 100644
index 0000000..7991383
--- /dev/null
+++ b/icons/flag-gb.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-gb" viewBox="0 0 640 480">
+ <path fill="#012169" d="M0 0h640v480H0z"/>
+ <path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0z"/>
+ <path fill="#C8102E" d="m424 281 216 159v40L369 281zm-184 20 6 35L54 480H0zM640 0v3L391 191l2-44L590 0zM0 0l239 176h-60L0 42z"/>
+ <path fill="#FFF" d="M241 0v480h160V0zM0 160v160h640V160z"/>
+ <path fill="#C8102E" d="M0 193v96h640v-96zM273 0v480h96V0z"/>
+</svg>
diff --git a/icons/flag-it.svg b/icons/flag-it.svg
new file mode 100644
index 0000000..20a8bfd
--- /dev/null
+++ b/icons/flag-it.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-it" viewBox="0 0 640 480">
+ <g fill-rule="evenodd" stroke-width="1pt">
+ <path fill="#fff" d="M0 0h640v480H0z"/>
+ <path fill="#009246" d="M0 0h213.3v480H0z"/>
+ <path fill="#ce2b37" d="M426.7 0H640v480H426.7z"/>
+ </g>
+</svg>
diff --git a/icons/slackware.svg b/icons/slackware.svg
new file mode 100644
index 0000000..c0f42cf
--- /dev/null
+++ b/icons/slackware.svg
@@ -0,0 +1,5 @@
+<!-- Slackware logo, from Simple Icons (CC0 1.0 Universal).
+ @ACCENT@ is substituted by install.sh with the udt accent colour. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
+ <path fill="@ACCENT@" d="M12.924 1.228c-.584-.01-1.251 0-1.485.027-2.46.282-4.138 1.3-4.753 2.891-.218.552-.274 1.002-.243 1.772.048 1.21.419 2.004 1.262 2.742 1.225 1.06 2.98 1.508 5.998 1.508 2.737 0 3.71.413 3.916 1.675.313 1.867-1.57 3.07-4.414 2.827-1.878-.16-3.496-.912-4.223-1.967a7.772 7.772 0 01-.355-.62c-.382-.76-.64-.978-1.176-.978-.43.005-.732.165-.918.494l-.133.24v4.03l.137.296c.165.344.4.546.744.63.35.09.794-.036 1.42-.402l.5-.29.826.185c1.82.403 2.75.523 4.065.523 1.103.005 1.548-.046 2.455-.285 1.124-.297 1.974-.785 2.717-1.57.8-.844 1.15-1.853 1.097-3.147-.069-1.628-.695-2.698-2-3.414-.96-.525-2.292-.79-4.377-.88-2.042-.086-2.794-.155-3.515-.32-.51-.12-.785-.25-1.076-.515-.653-.589-.59-1.755.136-2.482.642-.637 1.511-.928 2.774-.928 1.432.005 2.393.27 3.412.955.185.127.721.62 1.193 1.092.886.902 1.135 1.082 1.506 1.082.244 0 .59-.163.732-.344.26-.329.303-.63.303-2.2 0-1.66-.043-1.91-.377-2.282-.387-.425-.848-.42-1.75.031l-.59.297-.63-.17c-1.496-.392-2.038-.477-3.178-.504zM0 13.775v9h24v-1H1v-8z"/>
+</svg>
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..2561bcc
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,130 @@
+#!/bin/bash
+# Install waybar-theme-udt by symlink, so the repo stays canonical.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# This installs a SECOND bar, at ~/.config/waybar-udt/. The existing bar at
+# ~/.config/waybar/ is left alone, so DP-1 keeps working while this one is
+# tried on DP-3. Nothing is started: the command to run it is printed at the
+# end.
+set -euo pipefail
+
+repo="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+target="$HOME/.config/waybar-udt"
+udt="$(cd "$repo/../unified-desktop-theme" 2>/dev/null && pwd || true)"
+icons="$HOME/.local/share/icons/Material-Black-Plum-Suru"
+
+mkdir -p "$target/modules/hyprland" "$target/modules/custom" \
+ "$target/modules/extras" "$target/modules/image" \
+ "$target/styles" "$HOME/bin"
+
+# ---------------------------------------------------------------- colours --
+# The palette is udt's, not this repo's. Generating it needs the sibling
+# checkout, so a missing one is a clear failure rather than a bar with no
+# colours: every stylesheet below names @main-bg and friends.
+if [ -z "$udt" ]; then
+ echo "error: unified-desktop-theme not found next to this repo" >&2
+ echo " expected at $repo/../unified-desktop-theme" >&2
+ exit 1
+fi
+"$udt/bin/udt-palette"
+install -Dm644 "$udt/templates/waybar/theme.css" "$target/styles/theme.css"
+
+# The dot scripts are bash, not CSS, so they cannot read @define-color. Write
+# the handful of colours they need as a shell fragment they source.
+#
+# The values are read back out of the stylesheet udt-palette just generated,
+# because that is the only place they exist in a form this script can see:
+# udt-palette renders files, it does not answer queries about single roles.
+#
+# Pango wants a colour it can parse, and the generated file mixes #rrggbb with
+# rgb()/rgba() spellings, so each is normalised to hex on the way out.
+scheme="$(sed -n 's/^scheme *= *//p' "$HOME/.config/udt/roles.conf")"
+
+css_color() {
+ # Last definition wins, matching how GTK resolves a redefined colour.
+ sed -n "s/^@define-color $1 *\(.*\);\$/\1/p" "$target/styles/theme.css" | tail -1
+}
+
+to_hex() {
+ python3 -c '
+import re, sys
+v = sys.argv[1].strip()
+m = re.match(r"rgba?\(([^)]*)\)", v)
+if m:
+ p = [x.strip() for x in m.group(1).split(",")]
+ print("#%02x%02x%02x" % tuple(int(float(c)) for c in p[:3]))
+else:
+ print(v)
+' "$2"
+}
+
+{
+ echo "# Generated by waybar-theme-udt/install.sh from the $scheme scheme."
+ echo "# Sourced by vms_dots.sh and privacy_dots.sh. Do not edit."
+ # The generated stylesheet carries no success/info roles, so the dots use
+ # the palette names it does define. green/teal/mauve are role-resolved by
+ # udt-palette, so they still follow the scheme.
+ for pair in "SUCCESS:green" "WARNING:warning" "CRITICAL:critical" \
+ "INFO:teal" "ACCENT:mauve" "FAINT:overlay0"; do
+ var="${pair%%:*}"
+ raw="$(css_color "${pair##*:}")"
+ [ -n "$raw" ] && printf 'DOT_%s="%s"\n' "$var" "$(to_hex "$var" "$raw")"
+ done
+} > "$target/dots.colors"
+
+# ------------------------------------------------------------------ files --
+# Config and modules are symlinked: this repo owns them outright, so linking
+# means the installed bar cannot drift from what is committed.
+ln -sfn "$repo/config.jsonc" "$target/config.jsonc"
+for f in modules/clock.jsonc \
+ modules/hyprland/workspaces.jsonc \
+ modules/custom/launcher.jsonc modules/custom/vmdot.jsonc \
+ modules/custom/privacy_dots.jsonc \
+ modules/extras/taskbar.jsonc modules/extras/tray.jsonc \
+ modules/image/volume.jsonc modules/image/idle_inhibitor.jsonc \
+ modules/image/language.jsonc; do
+ ln -sfn "$repo/$f" "$target/$f"
+done
+
+for f in style.css fonts.css global.css pills.css; do
+ ln -sfn "$repo/styles/$f" "$target/styles/$f"
+done
+
+# modules.css is generated rather than linked: it carries the icon theme's
+# absolute path, which cannot be committed, and the accent-tinted launcher.
+sed -e "s|@ICONS@|$icons|g" -e "s|@CONFIG@|$target|g" \
+ "$repo/styles/modules.css" > "$target/styles/modules.css"
+
+# The flags ship as they are; the Slackware mark is monochrome, so it takes
+# the accent. Read from the generated palette rather than restated here.
+accent="$(sed -n 's/^@define-color accent *\(.*\);$/\1/p' "$target/styles/theme.css" | head -1)"
+accent="${accent:-#b7bdf8}"
+sed "s|@ACCENT@|$accent|" "$repo/icons/slackware.svg" > "$target/slackware.svg"
+install -Dm644 "$repo/icons/flag-it.svg" "$target/flag-it.svg"
+install -Dm644 "$repo/icons/flag-gb.svg" "$target/flag-gb.svg"
+
+# ---------------------------------------------------------------- scripts --
+# The two dot scripts used to live only in ~/bin, in no repository at all.
+# They are this repo's now, and ~/bin gets links. Any real file found there is
+# kept: it is the only copy of whatever was running before.
+for s in vms_dots.sh privacy_dots.sh; do
+ if [ -f "$HOME/bin/$s" ] && [ ! -L "$HOME/bin/$s" ]; then
+ mv "$HOME/bin/$s" "$HOME/bin/$s.pre-udt"
+ echo "kept your $s as ~/bin/$s.pre-udt"
+ fi
+ ln -sfn "$repo/bin/$s" "$HOME/bin/$s"
+done
+ln -sfn "$repo/bin/wb-icon" "$HOME/bin/wb-icon"
+ln -sfn "$repo/bin/wb-idle" "$HOME/bin/wb-idle"
+ln -sfn "$repo/bin/wb-lang" "$HOME/bin/wb-lang"
+
+# ----------------------------------------------------------------- checks --
+# A missing icon renders as an empty pill and nothing says why, so the names
+# are checked here, while someone is watching.
+"$repo/bin/wb-icon" --selftest
+
+echo
+echo "installed to $target"
+echo "run it with: waybar -c $target/config.jsonc -s $target/styles/style.css"
+echo "your existing bar on DP-1 is untouched."
diff --git a/modules/clock.jsonc b/modules/clock.jsonc
new file mode 100644
index 0000000..1c99b9a
--- /dev/null
+++ b/modules/clock.jsonc
@@ -0,0 +1,35 @@
+// Date and time. The one pair that stays text: an icon cannot say 14:32.
+// The icon sits before the text as a CSS background, which is what makes
+// these two match the icon-only pills around them.
+{
+ "clock#date": {
+ "format": "{:%a %d/%m}",
+ "min-length": 9,
+ "tooltip-format": "<tt>{calendar}</tt>",
+ "calendar": {
+ "mode": "year",
+ "mode-mon-col": 3,
+ "weeks-pos": "right",
+ "on-scroll": 1,
+ // Colours here are Pango markup inside a tooltip, which CSS
+ // cannot reach, so they are the only colours in this repo that
+ // do not come from the palette.
+ "format": {
+ "months": "<span color='#7dc4e4'><b>{}</b></span>",
+ "days": "<span color='#8aadf4'><b>{}</b></span>",
+ "weeks": "<span color='#99ffdd'><b>W{}</b></span>",
+ "weekdays": "<span color='#b7bdf8'><b>{}</b></span>",
+ "today": "<span color='#f0c6c6'><b><u>{}</u></b></span>"
+ }
+ },
+ "actions": {
+ "on-click": "mode"
+ }
+ },
+
+ "clock#time": {
+ "format": "{:%H:%M}",
+ "min-length": 5,
+ "tooltip-format": "Standard Time: {:%I:%M %p}"
+ }
+}
diff --git a/modules/custom/launcher.jsonc b/modules/custom/launcher.jsonc
new file mode 100644
index 0000000..56f61fb
--- /dev/null
+++ b/modules/custom/launcher.jsonc
@@ -0,0 +1,13 @@
+// Opens the quickshell desktop drawer. A static button: no "exec", so it
+// cannot show whether the drawer is open, which would need the shell to feed
+// waybar.
+//
+// The Slackware mark is a CSS background image, tinted with the udt accent at
+// install time. It is the one always-coloured pill on the bar.
+{
+ "custom/launcher": {
+ "format": "",
+ "tooltip": false,
+ "on-click": "qs -p $HOME/Programming/GIT/quickshell/desktop ipc call drawer toggle"
+ }
+}
diff --git a/modules/custom/privacy_dots.jsonc b/modules/custom/privacy_dots.jsonc
new file mode 100644
index 0000000..30d86b3
--- /dev/null
+++ b/modules/custom/privacy_dots.jsonc
@@ -0,0 +1,12 @@
+// A dot per active capture: microphone, camera, location, screen sharing.
+// Shows nothing when everything is off, which is the point. The script is in
+// this repo and symlinked into ~/bin, so the colours follow the palette.
+{
+ "custom/privacy_dots": {
+ "exec": "$HOME/bin/privacy_dots.sh",
+ "interval": 3,
+ "return-type": "json",
+ "format": "{}",
+ "tooltip": true
+ }
+}
diff --git a/modules/custom/vmdot.jsonc b/modules/custom/vmdot.jsonc
new file mode 100644
index 0000000..1955a6e
--- /dev/null
+++ b/modules/custom/vmdot.jsonc
@@ -0,0 +1,10 @@
+// One dot per defined VM, coloured by state. The script is in this repo and
+// symlinked into ~/bin, so the colours follow the palette.
+{
+ "custom/vmdot": {
+ "exec": "$HOME/bin/vms_dots.sh",
+ "interval": 2,
+ "return-type": "json",
+ "tooltip": true
+ }
+}
diff --git a/modules/extras/taskbar.jsonc b/modules/extras/taskbar.jsonc
new file mode 100644
index 0000000..8c9afde
--- /dev/null
+++ b/modules/extras/taskbar.jsonc
@@ -0,0 +1,18 @@
+// Open applications, drawn with each application's own icon.
+//
+// icon-theme used to name Papirus, which is not installed on this machine, so
+// GTK fell back silently. It now names the theme the rest of the desktop uses.
+{
+ "wlr/taskbar": {
+ "all-outputs": false,
+ "format": "{icon}",
+ "icon-theme": "Material-Black-Plum-Suru",
+ "icon-size": 20,
+ "tooltip": true,
+ "tooltip-format": "{title}",
+ "on-click": "activate",
+ "on-click-middle": "close",
+ "ignore-list": ["kitty-scratchpad"],
+ "cursor": true
+ }
+}
diff --git a/modules/extras/tray.jsonc b/modules/extras/tray.jsonc
new file mode 100644
index 0000000..32ec0bf
--- /dev/null
+++ b/modules/extras/tray.jsonc
@@ -0,0 +1,8 @@
+// System tray. Icons come from the applications themselves.
+{
+ "tray": {
+ "icon-size": 16,
+ "spacing": 12,
+ "cursor": true
+ }
+}
diff --git a/modules/hyprland/workspaces.jsonc b/modules/hyprland/workspaces.jsonc
new file mode 100644
index 0000000..f32d497
--- /dev/null
+++ b/modules/hyprland/workspaces.jsonc
@@ -0,0 +1,26 @@
+// Workspaces.
+//
+// The icons are NOT here. waybar's format-icons takes text, not paths, so
+// eight themed icons can only arrive as CSS background images; see
+// styles/modules.css. The label is emptied rather than removed, because an
+// empty label still gives each button its box to draw the icon in.
+//
+// Faking this with eight custom modules would have put the icons in config,
+// at the cost of click-to-switch and the active/urgent states. Keeping the
+// real module and moving the icons to CSS keeps the behaviour.
+{
+ "hyprland/workspaces": {
+ "format": "{icon}",
+ "format-icons": {
+ "1": "", "2": "", "3": "", "4": "",
+ "5": "", "6": "", "7": "", "8": "",
+ "urgent": "", "focused": "", "default": ""
+ },
+ "persistent-workspaces": {
+ "*": [1, 2, 3, 4, 5, 6, 7, 8]
+ },
+ "on-scroll-up": "hyprctl dispatch workspace +1",
+ "on-scroll-down": "hyprctl dispatch workspace -1",
+ "cursor": true
+ }
+}
diff --git a/modules/image/idle_inhibitor.jsonc b/modules/image/idle_inhibitor.jsonc
new file mode 100644
index 0000000..e026be8
--- /dev/null
+++ b/modules/image/idle_inhibitor.jsonc
@@ -0,0 +1,15 @@
+// Presentation mode.
+//
+// waybar's own idle_inhibitor module draws its state with format-icons, which
+// is text, so this is an `image` module driving the same inhibitor through
+// hyprland. The state lives in a file because the icon has to be resolved by
+// a separate process from the one that toggles it.
+{
+ "image#idle": {
+ "exec": "$HOME/bin/wb-idle status",
+ "size": 22,
+ "interval": 5,
+ "signal": 9,
+ "on-click": "$HOME/bin/wb-idle toggle"
+ }
+}
diff --git a/modules/image/language.jsonc b/modules/image/language.jsonc
new file mode 100644
index 0000000..48343b7
--- /dev/null
+++ b/modules/image/language.jsonc
@@ -0,0 +1,18 @@
+// Keyboard layout, drawn as a flag.
+//
+// This is an `image` module rather than hyprland/language, because that module
+// styles as #language and nothing else: it has no per-language CSS class, so a
+// flag cannot be picked by a selector, and its format-<lang> takes text rather
+// than a path. wb-lang reads the layout from hyprctl and prints the flag.
+//
+// The two flags are shipped by this repo: the icon theme has 13 and neither
+// Italian nor British is among them.
+{
+ "image#lang": {
+ "exec": "$HOME/bin/wb-lang",
+ "size": 20,
+ "interval": 2,
+ "signal": 10,
+ "tooltip": true
+ }
+}
diff --git a/modules/image/volume.jsonc b/modules/image/volume.jsonc
new file mode 100644
index 0000000..1788643
--- /dev/null
+++ b/modules/image/volume.jsonc
@@ -0,0 +1,44 @@
+// Volume, as a drawer: the output icon is always visible and the microphone
+// slides out on hover, the same shape as the current bar's wireplumber group.
+//
+// These are `image` modules rather than wireplumber ones because wireplumber
+// draws its state with format-icons, which is text. `image` runs a command and
+// reads a path, so wb-icon resolves the state to a themed icon instead. The
+// cost is that the percentage is in the tooltip rather than on the bar, which
+// is the trade an icons-only bar makes.
+//
+// signal 8 is raised by volume.sh after every change, so the icon updates on
+// the keypress rather than up to two seconds later.
+{
+ "group/volume": {
+ "orientation": "horizontal",
+ "modules": [
+ "image#volume",
+ "image#mic"
+ ],
+ "drawer": {
+ "transition-left-to-right": false
+ }
+ },
+
+ "image#volume": {
+ "exec": "$HOME/bin/wb-icon volume",
+ "size": 22,
+ "interval": 2,
+ "signal": 8,
+ "on-click": "$HOME/.config/waybar/scripts/volume.sh output mute",
+ "on-click-right": "pavucontrol-qt",
+ "on-scroll-up": "$HOME/.config/waybar/scripts/volume.sh output raise",
+ "on-scroll-down": "$HOME/.config/waybar/scripts/volume.sh output lower"
+ },
+
+ "image#mic": {
+ "exec": "$HOME/bin/wb-icon mic",
+ "size": 22,
+ "interval": 2,
+ "signal": 8,
+ "on-click": "$HOME/.config/waybar/scripts/volume.sh input mute",
+ "on-scroll-up": "$HOME/.config/waybar/scripts/volume.sh input raise",
+ "on-scroll-down": "$HOME/.config/waybar/scripts/volume.sh input lower"
+ }
+}
diff --git a/styles/fonts.css b/styles/fonts.css
new file mode 100644
index 0000000..8cd59ce
--- /dev/null
+++ b/styles/fonts.css
@@ -0,0 +1,19 @@
+/* Fonts.
+ *
+ * Only three modules show text at all on this bar: the clock pair and the
+ * tooltips. Everything else is an icon, which is the point of the theme.
+ *
+ * Inconsolata is the desktop's monospace face, and monospace is deliberate
+ * for a clock: proportional digits jitter as the minute changes.
+ */
+
+* {
+ font-family: "Inconsolata Nerd Font Mono", monospace;
+ font-weight: bold;
+ font-size: 16px;
+}
+
+tooltip label {
+ font-weight: normal;
+ font-size: 14px;
+}
diff --git a/styles/global.css b/styles/global.css
new file mode 100644
index 0000000..9886761
--- /dev/null
+++ b/styles/global.css
@@ -0,0 +1,31 @@
+/* The bar itself, and the chrome that is not a pill.
+ *
+ * The bar is transparent: the pills are the only thing that draws, which is
+ * what makes them read as separate capsules over the wallpaper.
+ */
+
+window#waybar {
+ background: transparent;
+ color: @main-fg;
+}
+
+/* GTK draws a button background by default, and it shows through the pill's
+ * own rounded corners as square shoulders. */
+button {
+ background: transparent;
+ border: none;
+ box-shadow: none;
+ text-shadow: none;
+ min-height: 0;
+}
+
+tooltip {
+ background: @main-bg;
+ border: 1px solid @outline;
+ border-radius: 8px;
+}
+
+tooltip label {
+ color: @main-fg;
+ padding: 2px;
+}
diff --git a/styles/modules.css b/styles/modules.css
new file mode 100644
index 0000000..647465f
--- /dev/null
+++ b/styles/modules.css
@@ -0,0 +1,180 @@
+/* What is different about each module.
+ *
+ * The icons for the workspaces, the clock pair, the launcher and the language
+ * live here rather than in the config, because waybar's format-icons takes
+ * text and not paths. @ICONS@ is substituted by install.sh with the icon
+ * theme's absolute path, so the theme can move without editing this file.
+ */
+
+/* ------------------------------------------------------------------ *
+ * Launcher: the one always-coloured pill, and the desktop's identity. *
+ * ------------------------------------------------------------------ */
+
+#custom-launcher {
+ background: @accent;
+ background-image: url("@CONFIG@/slackware.svg");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 18px 18px;
+ min-width: 22px;
+ padding: 2px 10px;
+}
+
+/* The accent pill inverts on hover rather than lightening: it is already the
+ * brightest thing on the bar. */
+#custom-launcher:hover {
+ background-color: @main-bg;
+}
+
+/* ---------------------------------------------- *
+ * Clock: the only text, each with a leading icon. *
+ * ---------------------------------------------- */
+
+#clock.date,
+#clock.time {
+ background-repeat: no-repeat;
+ background-position: 10px center;
+ background-size: 16px 16px;
+ padding-left: 32px;
+}
+
+#clock.date {
+ background-image: url("@ICONS@/mimetypes/22/x-office-calendar.svg");
+}
+
+#clock.time {
+ background-image: url("@ICONS@/categories/22/clock.svg");
+}
+
+/* ------------------------------------------------------------- *
+ * Workspaces: eight themed icons, one per button, set by CSS. *
+ * *
+ * The module's labels are empty (see the module config), so each *
+ * button is an icon-sized box and nothing else. *
+ * ------------------------------------------------------------- */
+
+#workspaces button {
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 18px 18px;
+ min-width: 26px;
+ min-height: 26px;
+ margin: 0 1px;
+ border-radius: 10px;
+ /* Dim by default; the active workspace is the one at full strength. */
+ opacity: 0.45;
+ transition: opacity 0.3s ease, background-color 0.3s ease;
+}
+
+#workspaces button:hover {
+ background-color: @hover-bg;
+ opacity: 0.8;
+}
+
+#workspaces button.active,
+#workspaces button.focused {
+ background-color: @hover-bg;
+ opacity: 1;
+}
+
+#workspaces button.urgent {
+ background-color: @critical;
+ opacity: 1;
+}
+
+/* The eight. Generic names, so the bar does not lie when the application on a
+ * workspace changes. */
+#workspaces button:nth-child(1) { background-image: url("@ICONS@/categories/22/web-browser.svg"); }
+#workspaces button:nth-child(2) { background-image: url("@ICONS@/categories/22/utilities-terminal.svg"); }
+#workspaces button:nth-child(3) { background-image: url("@ICONS@/categories/22/text-editor.svg"); }
+#workspaces button:nth-child(4) { background-image: url("@ICONS@/devices/22/network-server.svg"); }
+#workspaces button:nth-child(5) { background-image: url("@ICONS@/actions/22/document-edit.svg"); }
+#workspaces button:nth-child(6) { background-image: url("@ICONS@/categories/22/applications-graphics.svg"); }
+#workspaces button:nth-child(7) { background-image: url("@ICONS@/categories/22/internet-chat.svg"); }
+#workspaces button:nth-child(8) { background-image: url("@ICONS@/devices/22/input-gaming.svg"); }
+
+/* ------------------------------------------------ *
+ * Language: a flag, shipped by this repo. *
+ * *
+ * An image module, not hyprland/language: that one *
+ * has no per-language CSS class, so the flag could *
+ * not be selected. wb-lang picks the file instead. *
+ * ------------------------------------------------ */
+
+#image.lang {
+ padding: 2px 10px;
+ min-width: 22px;
+}
+
+/* ------------------------------------------- *
+ * Dots: coloured by their scripts, not by CSS. *
+ * ------------------------------------------- */
+
+/* The privacy pill only exists while something is capturing: an empty label
+ * would otherwise leave a bare capsule sitting on the bar. */
+#custom-privacy_dots {
+ padding: 2px 10px;
+}
+
+#custom-privacy_dots.mic-off.cam-off.loc-off.scr-off {
+ padding: 0;
+ margin: 0;
+ opacity: 0;
+}
+
+#custom-vmdot {
+ padding: 2px 10px;
+}
+
+/* ---------------------------------------- *
+ * Volume: a drawer, the microphone hidden. *
+ * ---------------------------------------- */
+
+#volume {
+ padding: 2px 8px;
+}
+
+#volume image {
+ padding: 0 4px;
+}
+
+/* --------------------------------------------------------- *
+ * Taskbar: application icons, with the idea2 underline. *
+ * --------------------------------------------------------- */
+
+#taskbar button {
+ background: transparent;
+ padding: 0 8px;
+ margin: 0 1px;
+ border-bottom: 3px solid transparent;
+ transition: border-bottom-color 0.3s ease-out, background-color 0.3s ease;
+}
+
+#taskbar button:hover {
+ background: @hover-bg;
+ border-bottom-color: @accent;
+}
+
+#taskbar button.active {
+ border-bottom-color: @accent;
+}
+
+/* --------------------------- *
+ * Tray and presentation mode. *
+ * --------------------------- */
+
+#tray {
+ padding: 2px 10px;
+}
+
+#tray menu {
+ background: @main-bg;
+ color: @main-fg;
+ border: 1px solid @outline;
+ border-radius: 8px;
+}
+
+#image.idle {
+ padding: 2px 10px;
+ min-width: 22px;
+}
diff --git a/styles/pills.css b/styles/pills.css
new file mode 100644
index 0000000..83e5d36
--- /dev/null
+++ b/styles/pills.css
@@ -0,0 +1,102 @@
+/* The pills, and the way they move.
+ *
+ * Geometry from idea1: modules are rounded capsules grouped into clusters,
+ * with gaps between clusters and none inside one, so the bar reads as a few
+ * objects rather than a continuous strip.
+ *
+ * Motion from idea2: background and colour cross-fade on hover over 0.3s.
+ * That is the whole effect. No keyframes anywhere, because GTK's CSS engine
+ * supports transitions on these properties directly and an animation would
+ * run forever whether or not anyone is looking at it.
+ */
+
+/* Every pill. Shared geometry lives here exactly once; modules.css only says
+ * what is different about each one. */
+#custom-launcher,
+#clock.date,
+#clock.time,
+#workspaces,
+#custom-vmdot,
+#taskbar,
+#custom-privacy_dots,
+#volume,
+#tray,
+#image.idle,
+#image.lang {
+ background: @main-bg;
+ color: @main-fg;
+ border-radius: 14px;
+ padding: 2px 12px;
+ margin: 4px 0;
+ transition: background-color 0.3s ease, color 0.3s ease;
+}
+
+/* Hover. The one rule that makes the bar feel alive, and the reason every
+ * pill declares a transition above. */
+#custom-launcher:hover,
+#clock.date:hover,
+#clock.time:hover,
+#custom-vmdot:hover,
+#custom-privacy_dots:hover,
+#volume:hover,
+#tray:hover,
+#image.idle:hover,
+#image.lang:hover {
+ background: @hover-bg;
+ color: @hover-fg;
+}
+
+/* Clusters.
+ *
+ * Neighbours inside a cluster sit flush and share one outline; only the ends
+ * of a cluster round off. The gap between clusters is a margin on the last
+ * pill of each, so adding a module to a cluster means editing one rule.
+ */
+
+/* Left: launcher | date time | workspaces | vm dots */
+#custom-launcher {
+ margin-left: 6px;
+ margin-right: 6px;
+}
+
+#clock.date {
+ border-radius: 14px 0 0 14px;
+ padding-right: 8px;
+}
+
+#clock.time {
+ border-radius: 0 14px 14px 0;
+ padding-left: 8px;
+ margin-right: 6px;
+}
+
+#workspaces {
+ padding: 2px 6px;
+ margin-right: 6px;
+}
+
+/* Right: privacy | volume | tray | presentation | language */
+#custom-privacy_dots {
+ margin-right: 6px;
+}
+
+#volume {
+ margin-right: 6px;
+}
+
+#tray {
+ margin-right: 6px;
+}
+
+#image.idle {
+ margin-right: 6px;
+}
+
+#image.lang {
+ margin-right: 6px;
+}
+
+/* Center: the taskbar is one pill holding a row of buttons. */
+#taskbar {
+ padding: 2px 6px;
+}
diff --git a/styles/style.css b/styles/style.css
new file mode 100644
index 0000000..d25e5fc
--- /dev/null
+++ b/styles/style.css
@@ -0,0 +1,19 @@
+/* waybar-theme-udt: stylesheet entry point.
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * Licensed under the GNU General Public License v2 only.
+ *
+ * Load order matters: theme.css defines the colours every later file names,
+ * and modules.css overrides the shared pill geometry per module.
+ */
+
+/* Ignore the GTK theme: waybar inherits it otherwise, and it fights the
+ * pills for padding and background. */
+* {
+ all: initial;
+}
+
+@import "theme.css"; /* generated by udt-palette, gitignored */
+@import "fonts.css";
+@import "global.css";
+@import "pills.css";
+@import "modules.css";