├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── TODO.md ├── build.zig ├── build.zig.zon ├── protocol ├── river-control-unstable-v1.xml ├── river-status-unstable-v1.xml └── wlr-layer-shell-unstable-v1.xml └── src ├── Bar.zig ├── Buffer.zig ├── Input.zig ├── Loop.zig ├── Monitor.zig ├── Seat.zig ├── Tags.zig ├── Wayland.zig ├── Widget.zig ├── flags.zig ├── main.zig └── render.zig /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmeum/creek/5417ceab36f4b9aa433f85fb54c11cbb797d65b3/.gitmodules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Andrea Feletto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## README 2 | 3 | Creek is a [dwm]-inspired [malleable] and minimalist status bar for the [River] Wayland compositor. 4 | The implementation is a hard fork of version 0.1.3 of the [levee] status bar. 5 | Compared to levee, the main objective is to ease [recombination and reuse][malleable reuse] by providing a simpler interface for adding custom information to the status bar. 6 | The original version of levee only provides builtin support for certain [modules][levee modules], these have to be written in Zig and compiled into levee. 7 | This fork pursues an alternative direction by allowing arbitrary text to be written to standard input of the status bar process, this text is then displayed in the status bar. 8 | 9 | Additionally, the following new features have been added: 10 | 11 | * Support for tracking the current window title in the status bar 12 | * Highlighting of tags containing urgent windows (see [xdg-activation]) 13 | * Basic run-time configuration support via command-line flags 14 | 15 | ### Screenshot 16 | 17 | ![Screenshot of River with a creek status bar](https://files.8pit.net/img/creek-screenshot-20240302.png) 18 | 19 | The screenshot features three active tags: tag 2 is currently focused and has one active window, tag 4 is not focused but is occupied (i.e. has windows), and tag 9 has an urgent window. 20 | In the middle of the status bar, the current title of the selected window on the focused tag is displayed. 21 | On the right-hand side, the current time is shown, this is information is generated using `date(1)` (see usage example below). 22 | 23 | ### Build 24 | 25 | The following dependencies need to be installed: 26 | 27 | * [zig] 0.13.0 28 | * [wayland] 1.21.0 29 | * [pixman] 0.42.0 30 | * [fcft] 3.1.5 (with [utf8proc] support) 31 | 32 | Afterwards, creek can be build as follows 33 | 34 | $ git clone https://git.8pit.net/creek.git 35 | $ cd creek 36 | $ zig build 37 | 38 | ### Configuration 39 | 40 | This version of creek can be configured using several command-line options: 41 | 42 | * `-fn`: The font used in the status bar 43 | * `-hg`: The total height of the status bar 44 | * `-nf`: Normal foreground color 45 | * `-nb`: Normal background color 46 | * `-ff`: Foreground color for focused tags 47 | * `-fb`: Background color for focused tags 48 | 49 | Example: 50 | 51 | $ creek -fn Terminus:size=12 -hg 18 -nf 0xffffff -nb 0x000000 52 | 53 | ### Usage Example 54 | 55 | In order to display the current time in the top-right corner, invoke creek as follows: 56 | 57 | $ ( while date; do sleep 1; done ) | creek 58 | 59 | Note that for more complex setups, a shell script may [not be the best option](https://flak.tedunangst.com/post/rough-idling). 60 | 61 | [dwm]: https://dwm.suckless.org/ 62 | [River]: https://github.com/riverwm/river/ 63 | [malleable]: https://malleable.systems/ 64 | [malleable reuse]: https://malleable.systems/mission/#2-arbitrary-recombination-and-reuse 65 | [levee]: https://sr.ht/~andreafeletto/levee 66 | [levee modules]: https://git.sr.ht/~andreafeletto/levee/tree/v0.1.3/item/src/modules 67 | [xdg-activation]: https://wayland.app/protocols/xdg-activation-v1 68 | [zig]: https://ziglang.org/ 69 | [wayland]: https://wayland.freedesktop.org/ 70 | [pixman]: http://pixman.org/ 71 | [fcft]: https://codeberg.org/dnkl/fcft/ 72 | [utf8proc]: https://juliastrings.github.io/utf8proc/ 73 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * Improve font rendering 2 | * fcft's font rendering is a bit meh 3 | * Especially compared to bemenu which uses pango/cairo 4 | * Fix some bugs with resizing 5 | * E.g. if river is running in a window 6 | * Resize will sometimes cause issues with status text 7 | * Find a better way to create the Seat in `src/Wayland.zig` 8 | * Improve tag handling 9 | * Currently 9 tags are hardcoded 10 | * Using more/less tags is currently not easily possible 11 | * Unfortunately, not possible to determine the maximum amount of tags with the current River protocol 12 | * Probably requires an additional command-line flag or something (meh) 13 | * Consider displaying floating/tiling status next to the tags 14 | * IIRC this is what vanilla dwm does 15 | * Report that Guix's Zig will link against systemc libc when not run in container 16 | * Upgrade to latest and greatest Zig version (requires work on the Guix side) 17 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Scanner = @import("wayland").Scanner; 4 | 5 | pub fn build(b: *std.Build) void { 6 | const target = b.standardTargetOptions(.{}); 7 | const optimize = b.standardOptimizeOption(.{}); 8 | const pie = b.option(bool, "pie", "Build a position-independent executable") orelse false; 9 | 10 | const scanner = Scanner.create(b, .{}); 11 | 12 | scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml"); 13 | scanner.addSystemProtocol("stable/viewporter/viewporter.xml"); 14 | scanner.addSystemProtocol("staging/single-pixel-buffer/single-pixel-buffer-v1.xml"); 15 | scanner.addCustomProtocol(b.path("protocol/wlr-layer-shell-unstable-v1.xml")); 16 | scanner.addCustomProtocol(b.path("protocol/river-status-unstable-v1.xml")); 17 | scanner.addCustomProtocol(b.path("protocol/river-control-unstable-v1.xml")); 18 | 19 | scanner.generate("wl_compositor", 4); 20 | scanner.generate("wl_subcompositor", 1); 21 | scanner.generate("wl_shm", 1); 22 | scanner.generate("wl_output", 3); 23 | scanner.generate("wl_seat", 5); 24 | scanner.generate("wp_single_pixel_buffer_manager_v1", 1); 25 | scanner.generate("wp_viewporter", 1); 26 | scanner.generate("zwlr_layer_shell_v1", 1); 27 | scanner.generate("zriver_status_manager_v1", 2); 28 | scanner.generate("zriver_control_v1", 1); 29 | 30 | const wayland = b.createModule(.{ .root_source_file = scanner.result }); 31 | const pixman = b.dependency("pixman", .{}).module("pixman"); 32 | const fcft = b.dependency("fcft", .{}).module("fcft"); 33 | 34 | const exe = b.addExecutable(.{ 35 | .name = "creek", 36 | .root_source_file = b.path("src/main.zig"), 37 | .target = target, 38 | .optimize = optimize, 39 | }); 40 | exe.pie = pie; 41 | 42 | exe.linkLibC(); 43 | exe.root_module.addImport("wayland", wayland); 44 | exe.linkSystemLibrary("wayland-client"); 45 | 46 | exe.root_module.addImport("pixman", pixman); 47 | exe.linkSystemLibrary("pixman-1"); 48 | 49 | exe.root_module.addImport("fcft", fcft); 50 | exe.linkSystemLibrary("fcft"); 51 | 52 | b.installArtifact(exe); 53 | 54 | const run = b.addRunArtifact(exe); 55 | run.step.dependOn(b.getInstallStep()); 56 | if (b.args) |args| { 57 | run.addArgs(args); 58 | } 59 | 60 | const run_step = b.step("run", "Run creek"); 61 | run_step.dependOn(&run.step); 62 | } 63 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .creek, 3 | .version = "0.4.0-dev", 4 | .fingerprint = 0x5570bc6e8cc47d94, 5 | .paths = .{""}, 6 | .dependencies = .{ 7 | .pixman = .{ 8 | .url = "https://codeberg.org/ifreund/zig-pixman/archive/v0.3.0.tar.gz", 9 | .hash = "pixman-0.3.0-LClMnz2VAAAs7QSCGwLimV5VUYx0JFnX5xWU6HwtMuDX", 10 | }, 11 | .wayland = .{ 12 | .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.3.0.tar.gz", 13 | .hash = "wayland-0.3.0-lQa1kjPIAQDmhGYpY-zxiRzQJFHQ2VqhJkQLbKKdt5wl", 14 | }, 15 | .fcft = .{ 16 | .url = "https://git.sr.ht/~novakane/zig-fcft/archive/v2.0.0.tar.gz", 17 | .hash = "fcft-2.0.0-zcx6C5EaAADIEaQzDg5D4UvFFMjSEwDE38vdE9xObeN9", 18 | }, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /protocol/river-control-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright 2020 The River Developers 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | 18 | 19 | 20 | 21 | This interface allows clients to run compositor commands and receive a 22 | success/failure response with output or a failure message respectively. 23 | 24 | Each command is built up in a series of add_argument requests and 25 | executed with a run_command request. The first argument is the command 26 | to be run. 27 | 28 | A complete list of commands should be made available in the man page of 29 | the compositor. 30 | 31 | 32 | 33 | 34 | This request indicates that the client will not use the 35 | river_control object any more. Objects that have been created 36 | through this instance are not affected. 37 | 38 | 39 | 40 | 41 | 42 | Arguments are stored by the server in the order they were sent until 43 | the run_command request is made. 44 | 45 | 46 | 47 | 48 | 49 | 50 | Execute the command built up using the add_argument request for the 51 | given seat. 52 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | This object is created by the run_command request. Exactly one of the 62 | success or failure events will be sent. This object will be destroyed 63 | by the compositor after one of the events is sent. 64 | 65 | 66 | 67 | 68 | Sent when the command has been successfully received and executed by 69 | the compositor. Some commands may produce output, in which case the 70 | output argument will be a non-empty string. 71 | 72 | 73 | 74 | 75 | 76 | 77 | Sent when the command could not be carried out. This could be due to 78 | sending a non-existent command, no command, not enough arguments, too 79 | many arguments, invalid arguments, etc. 80 | 81 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /protocol/river-status-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright 2020 The River Developers 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | 18 | 19 | 20 | 21 | A global factory for objects that receive status information specific 22 | to river. It could be used to implement, for example, a status bar. 23 | 24 | 25 | 26 | 27 | This request indicates that the client will not use the 28 | river_status_manager object any more. Objects that have been created 29 | through this instance are not affected. 30 | 31 | 32 | 33 | 34 | 35 | This creates a new river_output_status object for the given wl_output. 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | This creates a new river_seat_status object for the given wl_seat. 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | This interface allows clients to receive information about the current 53 | windowing state of an output. 54 | 55 | 56 | 57 | 58 | This request indicates that the client will not use the 59 | river_output_status object any more. 60 | 61 | 62 | 63 | 64 | 65 | Sent once binding the interface and again whenever the tag focus of 66 | the output changes. 67 | 68 | 69 | 70 | 71 | 72 | 73 | Sent once on binding the interface and again whenever the tag state 74 | of the output changes. 75 | 76 | 77 | 78 | 79 | 80 | 81 | Sent once on binding the interface and again whenever the set of 82 | tags with at least one urgent view changes. 83 | 84 | 85 | 86 | 87 | 88 | 89 | Sent once on binding the interface should a layout name exist and again 90 | whenever the name changes. 91 | 92 | 93 | 94 | 95 | 96 | 97 | Sent when the current layout name has been removed without a new one 98 | being set, for example when the active layout generator disconnects. 99 | 100 | 101 | 102 | 103 | 104 | 105 | This interface allows clients to receive information about the current 106 | focus of a seat. Note that (un)focused_output events will only be sent 107 | if the client has bound the relevant wl_output globals. 108 | 109 | 110 | 111 | 112 | This request indicates that the client will not use the 113 | river_seat_status object any more. 114 | 115 | 116 | 117 | 118 | 119 | Sent on binding the interface and again whenever an output gains focus. 120 | 121 | 122 | 123 | 124 | 125 | 126 | Sent whenever an output loses focus. 127 | 128 | 129 | 130 | 131 | 132 | 133 | Sent once on binding the interface and again whenever the focused 134 | view or a property thereof changes. The title may be an empty string 135 | if no view is focused or the focused view did not set a title. 136 | 137 | 138 | 139 | 140 | 141 | 142 | Sent once on binding the interface and again whenever a new mode 143 | is entered (e.g. with riverctl enter-mode foobar). 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /protocol/wlr-layer-shell-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2017 Drew DeVault 5 | 6 | Permission to use, copy, modify, distribute, and sell this 7 | software and its documentation for any purpose is hereby granted 8 | without fee, provided that the above copyright notice appear in 9 | all copies and that both that copyright notice and this permission 10 | notice appear in supporting documentation, and that the name of 11 | the copyright holders not be used in advertising or publicity 12 | pertaining to distribution of the software without specific, 13 | written prior permission. The copyright holders make no 14 | representations about the suitability of this software for any 15 | purpose. It is provided "as is" without express or implied 16 | warranty. 17 | 18 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | THIS SOFTWARE. 26 | 27 | 28 | 29 | 30 | Clients can use this interface to assign the surface_layer role to 31 | wl_surfaces. Such surfaces are assigned to a "layer" of the output and 32 | rendered with a defined z-depth respective to each other. They may also be 33 | anchored to the edges and corners of a screen and specify input handling 34 | semantics. This interface should be suitable for the implementation of 35 | many desktop shell components, and a broad number of other applications 36 | that interact with the desktop. 37 | 38 | 39 | 40 | 41 | Create a layer surface for an existing surface. This assigns the role of 42 | layer_surface, or raises a protocol error if another role is already 43 | assigned. 44 | 45 | Creating a layer surface from a wl_surface which has a buffer attached 46 | or committed is a client error, and any attempts by a client to attach 47 | or manipulate a buffer prior to the first layer_surface.configure call 48 | must also be treated as errors. 49 | 50 | After creating a layer_surface object and setting it up, the client 51 | must perform an initial commit without any buffer attached. 52 | The compositor will reply with a layer_surface.configure event. 53 | The client must acknowledge it and is then allowed to attach a buffer 54 | to map the surface. 55 | 56 | You may pass NULL for output to allow the compositor to decide which 57 | output to use. Generally this will be the one that the user most 58 | recently interacted with. 59 | 60 | Clients can specify a namespace that defines the purpose of the layer 61 | surface. 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | These values indicate which layers a surface can be rendered in. They 79 | are ordered by z depth, bottom-most first. Traditional shell surfaces 80 | will typically be rendered between the bottom and top layers. 81 | Fullscreen shell surfaces are typically rendered at the top layer. 82 | Multiple surfaces can share a single layer, and ordering within a 83 | single layer is undefined. 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | This request indicates that the client will not use the layer_shell 97 | object any more. Objects that have been created through this instance 98 | are not affected. 99 | 100 | 101 | 102 | 103 | 104 | 105 | An interface that may be implemented by a wl_surface, for surfaces that 106 | are designed to be rendered as a layer of a stacked desktop-like 107 | environment. 108 | 109 | Layer surface state (layer, size, anchor, exclusive zone, 110 | margin, interactivity) is double-buffered, and will be applied at the 111 | time wl_surface.commit of the corresponding wl_surface is called. 112 | 113 | Attaching a null buffer to a layer surface unmaps it. 114 | 115 | Unmapping a layer_surface means that the surface cannot be shown by the 116 | compositor until it is explicitly mapped again. The layer_surface 117 | returns to the state it had right after layer_shell.get_layer_surface. 118 | The client can re-map the surface by performing a commit without any 119 | buffer attached, waiting for a configure event and handling it as usual. 120 | 121 | 122 | 123 | 124 | Sets the size of the surface in surface-local coordinates. The 125 | compositor will display the surface centered with respect to its 126 | anchors. 127 | 128 | If you pass 0 for either value, the compositor will assign it and 129 | inform you of the assignment in the configure event. You must set your 130 | anchor to opposite edges in the dimensions you omit; not doing so is a 131 | protocol error. Both values are 0 by default. 132 | 133 | Size is double-buffered, see wl_surface.commit. 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | Requests that the compositor anchor the surface to the specified edges 142 | and corners. If two orthogonal edges are specified (e.g. 'top' and 143 | 'left'), then the anchor point will be the intersection of the edges 144 | (e.g. the top left corner of the output); otherwise the anchor point 145 | will be centered on that edge, or in the center if none is specified. 146 | 147 | Anchor is double-buffered, see wl_surface.commit. 148 | 149 | 150 | 151 | 152 | 153 | 154 | Requests that the compositor avoids occluding an area with other 155 | surfaces. The compositor's use of this information is 156 | implementation-dependent - do not assume that this region will not 157 | actually be occluded. 158 | 159 | A positive value is only meaningful if the surface is anchored to one 160 | edge or an edge and both perpendicular edges. If the surface is not 161 | anchored, anchored to only two perpendicular edges (a corner), anchored 162 | to only two parallel edges or anchored to all edges, a positive value 163 | will be treated the same as zero. 164 | 165 | A positive zone is the distance from the edge in surface-local 166 | coordinates to consider exclusive. 167 | 168 | Surfaces that do not wish to have an exclusive zone may instead specify 169 | how they should interact with surfaces that do. If set to zero, the 170 | surface indicates that it would like to be moved to avoid occluding 171 | surfaces with a positive exclusive zone. If set to -1, the surface 172 | indicates that it would not like to be moved to accommodate for other 173 | surfaces, and the compositor should extend it all the way to the edges 174 | it is anchored to. 175 | 176 | For example, a panel might set its exclusive zone to 10, so that 177 | maximized shell surfaces are not shown on top of it. A notification 178 | might set its exclusive zone to 0, so that it is moved to avoid 179 | occluding the panel, but shell surfaces are shown underneath it. A 180 | wallpaper or lock screen might set their exclusive zone to -1, so that 181 | they stretch below or over the panel. 182 | 183 | The default value is 0. 184 | 185 | Exclusive zone is double-buffered, see wl_surface.commit. 186 | 187 | 188 | 189 | 190 | 191 | 192 | Requests that the surface be placed some distance away from the anchor 193 | point on the output, in surface-local coordinates. Setting this value 194 | for edges you are not anchored to has no effect. 195 | 196 | The exclusive zone includes the margin. 197 | 198 | Margin is double-buffered, see wl_surface.commit. 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | Types of keyboard interaction possible for layer shell surfaces. The 209 | rationale for this is twofold: (1) some applications are not interested 210 | in keyboard events and not allowing them to be focused can improve the 211 | desktop experience; (2) some applications will want to take exclusive 212 | keyboard focus. 213 | 214 | 215 | 216 | 217 | This value indicates that this surface is not interested in keyboard 218 | events and the compositor should never assign it the keyboard focus. 219 | 220 | This is the default value, set for newly created layer shell surfaces. 221 | 222 | This is useful for e.g. desktop widgets that display information or 223 | only have interaction with non-keyboard input devices. 224 | 225 | 226 | 227 | 228 | Request exclusive keyboard focus if this surface is above the shell surface layer. 229 | 230 | For the top and overlay layers, the seat will always give 231 | exclusive keyboard focus to the top-most layer which has keyboard 232 | interactivity set to exclusive. If this layer contains multiple 233 | surfaces with keyboard interactivity set to exclusive, the compositor 234 | determines the one receiving keyboard events in an implementation- 235 | defined manner. In this case, no guarantee is made when this surface 236 | will receive keyboard focus (if ever). 237 | 238 | For the bottom and background layers, the compositor is allowed to use 239 | normal focus semantics. 240 | 241 | This setting is mainly intended for applications that need to ensure 242 | they receive all keyboard events, such as a lock screen or a password 243 | prompt. 244 | 245 | 246 | 247 | 248 | This requests the compositor to allow this surface to be focused and 249 | unfocused by the user in an implementation-defined manner. The user 250 | should be able to unfocus this surface even regardless of the layer 251 | it is on. 252 | 253 | Typically, the compositor will want to use its normal mechanism to 254 | manage keyboard focus between layer shell surfaces with this setting 255 | and regular toplevels on the desktop layer (e.g. click to focus). 256 | Nevertheless, it is possible for a compositor to require a special 257 | interaction to focus or unfocus layer shell surfaces (e.g. requiring 258 | a click even if focus follows the mouse normally, or providing a 259 | keybinding to switch focus between layers). 260 | 261 | This setting is mainly intended for desktop shell components (e.g. 262 | panels) that allow keyboard interaction. Using this option can allow 263 | implementing a desktop shell that can be fully usable without the 264 | mouse. 265 | 266 | 267 | 268 | 269 | 270 | 271 | Set how keyboard events are delivered to this surface. By default, 272 | layer shell surfaces do not receive keyboard events; this request can 273 | be used to change this. 274 | 275 | This setting is inherited by child surfaces set by the get_popup 276 | request. 277 | 278 | Layer surfaces receive pointer, touch, and tablet events normally. If 279 | you do not want to receive them, set the input region on your surface 280 | to an empty region. 281 | 282 | Keyboard interactivity is double-buffered, see wl_surface.commit. 283 | 284 | 285 | 286 | 287 | 288 | 289 | This assigns an xdg_popup's parent to this layer_surface. This popup 290 | should have been created via xdg_surface::get_popup with the parent set 291 | to NULL, and this request must be invoked before committing the popup's 292 | initial state. 293 | 294 | See the documentation of xdg_popup for more details about what an 295 | xdg_popup is and how it is used. 296 | 297 | 298 | 299 | 300 | 301 | 302 | When a configure event is received, if a client commits the 303 | surface in response to the configure event, then the client 304 | must make an ack_configure request sometime before the commit 305 | request, passing along the serial of the configure event. 306 | 307 | If the client receives multiple configure events before it 308 | can respond to one, it only has to ack the last configure event. 309 | 310 | A client is not required to commit immediately after sending 311 | an ack_configure request - it may even ack_configure several times 312 | before its next surface commit. 313 | 314 | A client may send multiple ack_configure requests before committing, but 315 | only the last request sent before a commit indicates which configure 316 | event the client really is responding to. 317 | 318 | 319 | 320 | 321 | 322 | 323 | This request destroys the layer surface. 324 | 325 | 326 | 327 | 328 | 329 | The configure event asks the client to resize its surface. 330 | 331 | Clients should arrange their surface for the new states, and then send 332 | an ack_configure request with the serial sent in this configure event at 333 | some point before committing the new surface. 334 | 335 | The client is free to dismiss all but the last configure event it 336 | received. 337 | 338 | The width and height arguments specify the size of the window in 339 | surface-local coordinates. 340 | 341 | The size is a hint, in the sense that the client is free to ignore it if 342 | it doesn't resize, pick a smaller size (to satisfy aspect ratio or 343 | resize in steps of NxM pixels). If the client picks a smaller size and 344 | is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the 345 | surface will be centered on this axis. 346 | 347 | If the width or height arguments are zero, it means the client should 348 | decide its own window dimension. 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | The closed event is sent by the compositor when the surface will no 358 | longer be shown. The output may have been destroyed or the user may 359 | have asked for it to be removed. Further changes to the surface will be 360 | ignored. The client should destroy the resource after receiving this 361 | event, and create a new surface if they so choose. 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | Change the layer that the surface is rendered on. 384 | 385 | Layer is double-buffered, see wl_surface.commit. 386 | 387 | 388 | 389 | 390 | 391 | -------------------------------------------------------------------------------- /src/Bar.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log; 3 | const mem = std.mem; 4 | 5 | const fcft = @import("fcft"); 6 | const wl = @import("wayland").client.wl; 7 | const wp = @import("wayland").client.wp; 8 | const zwlr = @import("wayland").client.zwlr; 9 | 10 | const Buffer = @import("Buffer.zig"); 11 | const Monitor = @import("Monitor.zig"); 12 | const render = @import("render.zig"); 13 | const Widget = @import("Widget.zig"); 14 | const Bar = @This(); 15 | 16 | const state = &@import("root").state; 17 | 18 | monitor: *Monitor, 19 | 20 | layer_surface: *zwlr.LayerSurfaceV1, 21 | background: struct { 22 | surface: *wl.Surface, 23 | viewport: *wp.Viewport, 24 | buffer: *wl.Buffer, 25 | }, 26 | 27 | title: Widget, 28 | tags: Widget, 29 | text: Widget, 30 | 31 | tags_width: u16, 32 | text_width: u16, 33 | 34 | abbrev_width: u16, 35 | abbrev_run: *const fcft.TextRun, 36 | 37 | text_padding: i32, 38 | configured: bool, 39 | width: u16, 40 | height: u16, 41 | 42 | // Convert a pixman u16 color to a 32-bit color with a pre-multiplied 43 | // alpha channel as used by the "Single-pixel buffer" Wayland protocol. 44 | fn toRgba(color: u16) u32 { 45 | return (@as(u32, color) >> 8) << 24 | 0xffffff; 46 | } 47 | 48 | pub fn create(monitor: *Monitor) !*Bar { 49 | const bg_color = &state.config.normalBgColor; 50 | const self = try state.gpa.create(Bar); 51 | self.monitor = monitor; 52 | self.configured = false; 53 | 54 | const compositor = state.wayland.compositor.?; 55 | const viewporter = state.wayland.viewporter.?; 56 | const spb_manager = state.wayland.single_pixel_buffer_manager.?; 57 | const layer_shell = state.wayland.layer_shell.?; 58 | 59 | self.background.surface = try compositor.createSurface(); 60 | self.background.viewport = try viewporter.getViewport(self.background.surface); 61 | self.background.buffer = try spb_manager.createU32RgbaBuffer(toRgba(bg_color.red), toRgba(bg_color.green), toRgba(bg_color.blue), 0xffffffff); 62 | 63 | self.layer_surface = try layer_shell.getLayerSurface(self.background.surface, monitor.output, .top, "creek"); 64 | 65 | self.title = try Widget.init(self.background.surface); 66 | self.tags = try Widget.init(self.background.surface); 67 | self.text = try Widget.init(self.background.surface); 68 | 69 | // calculate right padding for status text 70 | const font = state.config.font; 71 | const char_run = try font.rasterizeTextRunUtf32(&[_]u32{' '}, .default); 72 | self.text_padding = char_run.glyphs[0].advance.x; 73 | char_run.destroy(); 74 | 75 | // rasterize abbreviation glyphs for window ttile. 76 | self.abbrev_run = try font.rasterizeTextRunUtf32(&[_]u32{'…'}, .default); 77 | self.abbrev_width = 0; 78 | var i: usize = 0; 79 | while (i < self.abbrev_run.count) : (i += 1) { 80 | self.abbrev_width += @intCast(self.abbrev_run.glyphs[i].advance.x); 81 | } 82 | 83 | // setup layer surface 84 | self.layer_surface.setSize(0, state.config.height); 85 | self.layer_surface.setAnchor( 86 | .{ .top = true, .left = true, .right = true, .bottom = false }, 87 | ); 88 | self.layer_surface.setExclusiveZone(state.config.height); 89 | self.layer_surface.setMargin(0, 0, 0, 0); 90 | self.layer_surface.setListener(*Bar, layerSurfaceListener, self); 91 | 92 | self.tags.surface.commit(); 93 | self.title.surface.commit(); 94 | self.text.surface.commit(); 95 | self.background.surface.commit(); 96 | 97 | self.tags_width = 0; 98 | self.text_width = 0; 99 | 100 | return self; 101 | } 102 | 103 | pub fn destroy(self: *Bar) void { 104 | self.abbrev_run.destroy(); 105 | self.monitor.bar = null; 106 | 107 | self.layer_surface.destroy(); 108 | 109 | self.background.buffer.destroy(); 110 | self.background.viewport.destroy(); 111 | self.background.surface.destroy(); 112 | 113 | self.title.deinit(); 114 | self.tags.deinit(); 115 | self.text.deinit(); 116 | state.gpa.destroy(self); 117 | } 118 | 119 | fn layerSurfaceListener( 120 | layerSurface: *zwlr.LayerSurfaceV1, 121 | event: zwlr.LayerSurfaceV1.Event, 122 | bar: *Bar, 123 | ) void { 124 | switch (event) { 125 | .configure => |data| { 126 | layerSurface.ackConfigure(data.serial); 127 | 128 | const w: u16 = @intCast(data.width); 129 | const h: u16 = @intCast(data.height); 130 | if (bar.configured and bar.width == w and bar.height == h) { 131 | return; 132 | } 133 | 134 | bar.configured = true; 135 | bar.width = w; 136 | bar.height = h; 137 | 138 | const bg = &bar.background; 139 | bg.surface.attach(bg.buffer, 0, 0); 140 | bg.surface.damageBuffer(0, 0, bar.width, bar.height); 141 | bg.viewport.setDestination(bar.width, bar.height); 142 | 143 | render.renderTags(bar) catch |err| { 144 | log.err("renderTags failed for monitor {}: {s}", .{ bar.monitor.globalName, @errorName(err) }); 145 | return; 146 | }; 147 | 148 | bar.tags.surface.commit(); 149 | bar.title.surface.commit(); 150 | bar.text.surface.commit(); 151 | bar.background.surface.commit(); 152 | }, 153 | .closed => bar.destroy(), 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Buffer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const posix = std.posix; 4 | const linux = std.os.linux; 5 | 6 | const pixman = @import("pixman"); 7 | const wl = @import("wayland").client.wl; 8 | 9 | const Buffer = @This(); 10 | 11 | mmap: ?[]align(4096) u8 = null, 12 | data: ?[]u32 = null, 13 | buffer: ?*wl.Buffer = null, 14 | pix: ?*pixman.Image = null, 15 | 16 | busy: bool = false, 17 | width: u31 = 0, 18 | height: u31 = 0, 19 | size: u31 = 0, 20 | 21 | pub fn resize(self: *Buffer, shm: *wl.Shm, width: u31, height: u31) !void { 22 | if (width == 0 or height == 0) return; 23 | 24 | self.busy = true; 25 | self.width = width; 26 | self.height = height; 27 | 28 | const fd = try posix.memfd_create("creek-shm", linux.MFD.CLOEXEC); 29 | defer posix.close(fd); 30 | 31 | const stride = width * 4; 32 | self.size = stride * height; 33 | try posix.ftruncate(fd, self.size); 34 | 35 | self.mmap = try posix.mmap(null, self.size, posix.PROT.READ | posix.PROT.WRITE, .{ .TYPE = .SHARED }, fd, 0); 36 | self.data = mem.bytesAsSlice(u32, self.mmap.?); 37 | 38 | const pool = try shm.createPool(fd, self.size); 39 | defer pool.destroy(); 40 | 41 | self.buffer = try pool.createBuffer(0, width, height, stride, .argb8888); 42 | errdefer self.buffer.?.destroy(); 43 | self.buffer.?.setListener(*Buffer, listener, self); 44 | 45 | self.pix = pixman.Image.createBitsNoClear(.a8r8g8b8, width, height, self.data.?.ptr, stride); 46 | } 47 | 48 | pub fn deinit(self: *Buffer) void { 49 | if (self.pix) |pix| _ = pix.unref(); 50 | if (self.buffer) |buf| buf.destroy(); 51 | if (self.mmap) |mmap| posix.munmap(mmap); 52 | } 53 | 54 | fn listener(_: *wl.Buffer, event: wl.Buffer.Event, buffer: *Buffer) void { 55 | switch (event) { 56 | .release => buffer.busy = false, 57 | } 58 | } 59 | 60 | pub fn nextBuffer(pool: *[2]Buffer, shm: *wl.Shm, width: u16, height: u16) !*Buffer { 61 | if (pool[0].busy and pool[1].busy) { 62 | return error.NoAvailableBuffers; 63 | } 64 | const buffer = if (!pool[0].busy) &pool[0] else &pool[1]; 65 | 66 | if (buffer.width != width or buffer.height != height) { 67 | buffer.deinit(); 68 | try buffer.resize(shm, width, height); 69 | } 70 | return buffer; 71 | } 72 | -------------------------------------------------------------------------------- /src/Input.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log; 3 | 4 | const wl = @import("wayland").client.wl; 5 | 6 | const Bar = @import("Bar.zig"); 7 | const Input = @This(); 8 | 9 | const state = &@import("root").state; 10 | 11 | globalName: u32, 12 | 13 | pointer: struct { 14 | pointer: ?*wl.Pointer, 15 | x: i32, 16 | y: i32, 17 | bar: ?*Bar, 18 | surface: ?*wl.Surface, 19 | }, 20 | 21 | pub fn create(name: u32) !*Input { 22 | const self = try state.gpa.create(Input); 23 | const seat = state.wayland.seat.?; 24 | 25 | self.globalName = name; 26 | self.pointer.pointer = null; 27 | self.pointer.bar = null; 28 | self.pointer.surface = null; 29 | 30 | seat.setListener(*Input, listener, self); 31 | return self; 32 | } 33 | 34 | pub fn destroy(self: *Input) void { 35 | if (self.pointer.pointer) |pointer| { 36 | pointer.release(); 37 | } 38 | state.gpa.destroy(self); 39 | } 40 | 41 | fn listener(seat: *wl.Seat, event: wl.Seat.Event, input: *Input) void { 42 | switch (event) { 43 | .capabilities => |data| { 44 | if (input.pointer.pointer) |pointer| { 45 | pointer.release(); 46 | input.pointer.pointer = null; 47 | } 48 | if (data.capabilities.pointer) { 49 | input.pointer.pointer = seat.getPointer() catch |err| { 50 | log.err("cannot obtain seat pointer: {s}", .{@errorName(err)}); 51 | return; 52 | }; 53 | input.pointer.pointer.?.setListener( 54 | *Input, 55 | pointerListener, 56 | input, 57 | ); 58 | } 59 | }, 60 | .name => {}, 61 | } 62 | } 63 | 64 | fn pointerListener( 65 | _: *wl.Pointer, 66 | event: wl.Pointer.Event, 67 | input: *Input, 68 | ) void { 69 | switch (event) { 70 | .enter => |data| { 71 | input.pointer.x = data.surface_x.toInt(); 72 | input.pointer.y = data.surface_y.toInt(); 73 | const bar = state.wayland.findBar(data.surface); 74 | input.pointer.bar = bar; 75 | input.pointer.surface = data.surface; 76 | }, 77 | .leave => |_| { 78 | input.pointer.bar = null; 79 | input.pointer.surface = null; 80 | }, 81 | .motion => |data| { 82 | input.pointer.x = data.surface_x.toInt(); 83 | input.pointer.y = data.surface_y.toInt(); 84 | }, 85 | .button => |data| { 86 | if (data.state != .pressed) return; 87 | if (input.pointer.bar) |bar| { 88 | if (!bar.configured) return; 89 | 90 | const tagsSurface = bar.tags.surface; 91 | if (input.pointer.surface != tagsSurface) return; 92 | 93 | const x: u32 = @intCast(input.pointer.x); 94 | if (x < bar.height * @as(u16, bar.monitor.tags.tags.len)) { 95 | bar.monitor.tags.handleClick(x) catch |err| { 96 | log.err("handleClick failed for monitor {}: {s}", 97 | .{bar.monitor.globalName, @errorName(err)}); 98 | return; 99 | }; 100 | } 101 | } 102 | }, 103 | else => {}, 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Loop.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log; 3 | const mem = std.mem; 4 | const posix = std.posix; 5 | const linux = std.os.linux; 6 | const io = std.io; 7 | 8 | const render = @import("render.zig"); 9 | const Loop = @This(); 10 | 11 | const state = &@import("root").state; 12 | 13 | sfd: posix.fd_t, 14 | 15 | pub fn init() !Loop { 16 | var mask = posix.empty_sigset; 17 | linux.sigaddset(&mask, linux.SIG.INT); 18 | linux.sigaddset(&mask, linux.SIG.TERM); 19 | linux.sigaddset(&mask, linux.SIG.QUIT); 20 | 21 | _ = linux.sigprocmask(linux.SIG.BLOCK, &mask, null); 22 | const sfd = linux.signalfd(-1, &mask, linux.SFD.NONBLOCK); 23 | 24 | return Loop{ .sfd = @intCast(sfd) }; 25 | } 26 | 27 | pub fn run(self: *Loop) !void { 28 | const wayland = &state.wayland; 29 | 30 | var fds = [_]posix.pollfd{ 31 | .{ 32 | .fd = self.sfd, 33 | .events = posix.POLL.IN, 34 | .revents = undefined, 35 | }, 36 | .{ 37 | .fd = wayland.fd, 38 | .events = posix.POLL.IN, 39 | .revents = undefined, 40 | }, 41 | .{ 42 | .fd = posix.STDIN_FILENO, 43 | .events = posix.POLL.IN, 44 | .revents = undefined, 45 | }, 46 | }; 47 | 48 | var reader = io.getStdIn().reader(); 49 | while (true) { 50 | while (true) { 51 | const ret = wayland.display.dispatchPending(); 52 | _ = wayland.display.flush(); 53 | if (ret == .SUCCESS) break; 54 | } 55 | 56 | _ = posix.poll(&fds, -1) catch |err| { 57 | log.err("poll failed: {s}", .{@errorName(err)}); 58 | return; 59 | }; 60 | 61 | for (fds) |fd| { 62 | if (fd.revents & posix.POLL.HUP != 0 or fd.revents & posix.POLL.ERR != 0) { 63 | return; 64 | } 65 | } 66 | 67 | // signals 68 | if (fds[0].revents & posix.POLL.IN != 0) { 69 | return; 70 | } 71 | 72 | // wayland 73 | if (fds[1].revents & posix.POLL.IN != 0) { 74 | const errno = wayland.display.dispatch(); 75 | if (errno != .SUCCESS) return; 76 | } 77 | if (fds[1].revents & posix.POLL.OUT != 0) { 78 | const errno = wayland.display.flush(); 79 | if (errno != .SUCCESS) return; 80 | } 81 | 82 | // status input 83 | if (fds[2].revents & posix.POLL.IN != 0) { 84 | if (state.wayland.river_seat) |seat| { 85 | if (seat.focusedBar()) |bar| { 86 | seat.status_text.reset(); 87 | try reader.streamUntilDelimiter(seat.status_text.writer(), '\n', null); 88 | 89 | render.renderText(bar, seat.status_text.getWritten()) catch |err| { 90 | log.err("renderText failed for monitor {}: {s}", 91 | .{bar.monitor.globalName, @errorName(err)}); 92 | continue; 93 | }; 94 | 95 | bar.text.surface.commit(); 96 | bar.background.surface.commit(); 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Monitor.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log; 3 | 4 | const wl = @import("wayland").client.wl; 5 | 6 | const Bar = @import("Bar.zig"); 7 | const Tags = @import("Tags.zig"); 8 | const Seat = @import("Seat.zig"); 9 | pub const Monitor = @This(); 10 | 11 | const state = &@import("root").state; 12 | 13 | output: *wl.Output, 14 | globalName: u32, 15 | scale: i32, 16 | 17 | bar: ?*Bar, 18 | tags: *Tags, 19 | 20 | pub fn create(registry: *wl.Registry, name: u32) !*Monitor { 21 | const self = try state.gpa.create(Monitor); 22 | self.output = try registry.bind(name, wl.Output, 4); 23 | self.globalName = name; 24 | self.scale = 1; 25 | 26 | self.bar = null; 27 | self.tags = try Tags.create(self); 28 | 29 | self.output.setListener(*Monitor, listener, self); 30 | return self; 31 | } 32 | 33 | pub fn destroy(self: *Monitor) void { 34 | if (self.bar) |bar| { 35 | bar.destroy(); 36 | } 37 | self.tags.destroy(); 38 | state.gpa.destroy(self); 39 | } 40 | 41 | pub fn confBar(self: *Monitor) ?*Bar { 42 | if (self.bar) |bar| { 43 | if (bar.configured) { 44 | return bar; 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | 51 | fn listener(_: *wl.Output, event: wl.Output.Event, monitor: *Monitor) void { 52 | switch (event) { 53 | .scale => |scale| { 54 | monitor.scale = scale.factor; 55 | }, 56 | .geometry => {}, 57 | .mode => {}, 58 | .done => { 59 | if (monitor.bar) |_| { 60 | return; 61 | } 62 | monitor.bar = Bar.create(monitor) catch |err| { 63 | log.err("cannot create bar for monitor {}: {s}", 64 | .{monitor.globalName, @errorName(err)}); 65 | return; 66 | }; 67 | }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Seat.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log; 3 | const Mutex = std.Thread.Mutex; 4 | 5 | const wl = @import("wayland").client.wl; 6 | 7 | const Bar = @import("Bar.zig"); 8 | const Monitor = @import("Monitor.zig"); 9 | const render = @import("render.zig"); 10 | const zriver = @import("wayland").client.zriver; 11 | const state = &@import("root").state; 12 | 13 | pub const Seat = @This(); 14 | 15 | seat_status: *zriver.SeatStatusV1, 16 | current_output: ?*wl.Output, 17 | window_title: ?[:0]u8, 18 | status_buffer: [4096]u8 = undefined, 19 | status_text: std.io.FixedBufferStream([]u8), 20 | mtx: Mutex, 21 | 22 | pub fn create() !*Seat { 23 | const self = try state.gpa.create(Seat); 24 | const manager = state.wayland.status_manager.?; 25 | const seat = state.wayland.seat.?; 26 | 27 | self.mtx = Mutex{}; 28 | self.current_output = null; 29 | self.window_title = null; 30 | self.seat_status = try manager.getRiverSeatStatus(seat); 31 | self.seat_status.setListener(*Seat, seatListener, self); 32 | 33 | self.status_text = std.io.fixedBufferStream(&self.status_buffer); 34 | return self; 35 | } 36 | 37 | pub fn destroy(self: *Seat) void { 38 | self.mtx.lock(); 39 | if (self.window_title) |w| { 40 | state.gpa.free(w); 41 | } 42 | self.mtx.unlock(); 43 | 44 | self.seat_status.destroy(); 45 | state.gpa.destroy(self); 46 | } 47 | 48 | pub fn focusedMonitor(self: *Seat) ?*Monitor { 49 | // If there is no current monitor, e.g. on startup use the first one. 50 | // 51 | // TODO: Find a better way to do this. 52 | if (self.current_output == null) { 53 | const items = state.wayland.monitors.items; 54 | if (items.len > 0) { 55 | return items[0]; 56 | } 57 | } 58 | 59 | for (state.wayland.monitors.items) |monitor| { 60 | if (monitor.output == self.current_output) { 61 | return monitor; 62 | } 63 | } 64 | 65 | return null; 66 | } 67 | 68 | pub fn focusedBar(self: *Seat) ?*Bar { 69 | if (self.focusedMonitor()) |m| { 70 | return m.confBar(); 71 | } 72 | 73 | return null; 74 | } 75 | 76 | fn updateTitle(self: *Seat, data: [*:0]const u8) void { 77 | const title = std.mem.sliceTo(data, 0); 78 | 79 | self.mtx.lock(); 80 | defer self.mtx.unlock(); 81 | 82 | if (self.window_title) |t| { 83 | state.gpa.free(t); 84 | } 85 | if (title.len == 0) { 86 | self.window_title = null; 87 | } else { 88 | const vz = state.gpa.allocSentinel(u8, title.len, 0) catch |err| { 89 | log.err("allocSentinel failed for window title: {s}\n", .{@errorName(err)}); 90 | return; 91 | }; 92 | @memcpy(vz[0..vz.len], title); 93 | self.window_title = vz; 94 | } 95 | } 96 | 97 | fn focusedOutput(self: *Seat, output: *wl.Output) void { 98 | var monitor: ?*Monitor = null; 99 | for (state.wayland.monitors.items) |m| { 100 | if (m.output == output) { 101 | monitor = m; 102 | break; 103 | } 104 | } 105 | 106 | if (monitor) |m| { 107 | if (m.confBar()) |bar| { 108 | self.current_output = m.output; 109 | render.renderText(bar, self.status_text.getWritten()) catch |err| { 110 | log.err("renderText failed on focus for monitor {}: {s}", 111 | .{m.globalName, @errorName(err)}); 112 | return; 113 | }; 114 | 115 | bar.text.surface.commit(); 116 | bar.background.surface.commit(); 117 | } 118 | } else { 119 | log.err("seatListener: couldn't find focused output", .{}); 120 | } 121 | } 122 | 123 | fn unfocusedOutput(self: *Seat, output: *wl.Output) void { 124 | var monitor: ?*Monitor = null; 125 | for (state.wayland.monitors.items) |m| { 126 | if (m.output == output) { 127 | monitor = m; 128 | break; 129 | } 130 | } 131 | 132 | if (monitor) |m| { 133 | if (m.confBar()) |bar| { 134 | render.resetText(bar) catch |err| { 135 | log.err("resetText failed for monitor {}: {s}", 136 | .{bar.monitor.globalName, @errorName(err)}); 137 | }; 138 | bar.text.surface.commit(); 139 | 140 | render.renderTitle(bar, null) catch |err| { 141 | log.err("renderTitle failed on unfocus for monitor {}: {s}", 142 | .{bar.monitor.globalName, @errorName(err)}); 143 | return; 144 | }; 145 | 146 | bar.title.surface.commit(); 147 | bar.background.surface.commit(); 148 | } 149 | } else { 150 | log.err("seatListener: couldn't find unfocused output", .{}); 151 | } 152 | 153 | self.current_output = null; 154 | } 155 | 156 | fn focusedView(self: *Seat, title: [*:0]const u8) void { 157 | self.updateTitle(title); 158 | if (self.focusedBar()) |bar| { 159 | render.renderTitle(bar, self.window_title) catch |err| { 160 | log.err("renderTitle failed on focused view for monitor {}: {s}", 161 | .{bar.monitor.globalName, @errorName(err)}); 162 | return; 163 | }; 164 | 165 | bar.title.surface.commit(); 166 | bar.background.surface.commit(); 167 | } 168 | } 169 | 170 | fn seatListener( 171 | _: *zriver.SeatStatusV1, 172 | event: zriver.SeatStatusV1.Event, 173 | seat: *Seat, 174 | ) void { 175 | switch (event) { 176 | .focused_output => |data| seat.focusedOutput(data.output.?), 177 | .unfocused_output => |data| seat.unfocusedOutput(data.output.?), 178 | .focused_view => |data| seat.focusedView(data.title), 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Tags.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log; 3 | 4 | const zriver = @import("wayland").client.zriver; 5 | const pixman = @import("pixman"); 6 | 7 | const Monitor = @import("Monitor.zig"); 8 | const render = @import("render.zig"); 9 | const Input = @import("Input.zig"); 10 | const Tags = @This(); 11 | 12 | const state = &@import("root").state; 13 | 14 | monitor: *Monitor, 15 | output_status: *zriver.OutputStatusV1, 16 | tags: [9]Tag, 17 | 18 | pub const Tag = struct { 19 | label: u8, 20 | focused: bool = false, 21 | occupied: bool = false, 22 | urgent: bool = false, 23 | 24 | pub fn bgColor(self: *const Tag) *pixman.Color { 25 | if (self.focused) { 26 | return &state.config.focusBgColor; 27 | } else if (self.urgent) { 28 | return &state.config.normalFgColor; 29 | } else { 30 | return &state.config.normalBgColor; 31 | } 32 | } 33 | 34 | pub fn fgColor(self: *const Tag) *pixman.Color { 35 | if (self.focused) { 36 | return &state.config.focusFgColor; 37 | } else if (self.urgent) { 38 | return &state.config.normalBgColor; 39 | } else { 40 | return &state.config.normalFgColor; 41 | } 42 | } 43 | }; 44 | 45 | pub fn create(monitor: *Monitor) !*Tags { 46 | const self = try state.gpa.create(Tags); 47 | const manager = state.wayland.status_manager.?; 48 | 49 | self.monitor = monitor; 50 | self.output_status = try manager.getRiverOutputStatus(monitor.output); 51 | for (&self.tags, 0..) |*tag, i| { 52 | tag.label = '1' + @as(u8, @intCast(i)); 53 | } 54 | 55 | self.output_status.setListener(*Tags, outputStatusListener, self); 56 | return self; 57 | } 58 | 59 | pub fn destroy(self: *Tags) void { 60 | self.output_status.destroy(); 61 | state.gpa.destroy(self); 62 | } 63 | 64 | fn outputStatusListener( 65 | _: *zriver.OutputStatusV1, 66 | event: zriver.OutputStatusV1.Event, 67 | tags: *Tags, 68 | ) void { 69 | switch (event) { 70 | .focused_tags => |data| { 71 | for (&tags.tags, 0..) |*tag, i| { 72 | const mask = @as(u32, 1) << @as(u5, @intCast(i)); 73 | tag.focused = data.tags & mask != 0; 74 | } 75 | }, 76 | .urgent_tags => |data| { 77 | for (&tags.tags, 0..) |*tag, i| { 78 | const mask = @as(u32, 1) << @as(u5, @intCast(i)); 79 | tag.urgent = data.tags & mask != 0; 80 | } 81 | }, 82 | .view_tags => |data| { 83 | for (&tags.tags) |*tag| { 84 | tag.occupied = false; 85 | } 86 | for (data.tags.slice(u32)) |view| { 87 | for (&tags.tags, 0..) |*tag, i| { 88 | const mask = @as(u32, 1) << @as(u5, @intCast(i)); 89 | if (view & mask != 0) tag.occupied = true; 90 | } 91 | } 92 | }, 93 | } 94 | if (tags.monitor.confBar()) |bar| { 95 | render.renderTags(bar) catch |err| { 96 | log.err("renderTags failed for monitor {}: {s}", .{ tags.monitor.globalName, @errorName(err) }); 97 | return; 98 | }; 99 | 100 | bar.tags.surface.commit(); 101 | bar.background.surface.commit(); 102 | } 103 | } 104 | 105 | pub fn handleClick(self: *Tags, x: u32) !void { 106 | const control = state.wayland.control.?; 107 | 108 | if (self.monitor.bar) |bar| { 109 | const index = x / bar.height; 110 | const payload = try std.fmt.allocPrintZ( 111 | state.gpa, 112 | "{d}", 113 | .{@as(u32, 1) << @as(u5, @intCast(index))}, 114 | ); 115 | defer state.gpa.free(payload); 116 | 117 | control.addArgument("set-focused-tags"); 118 | control.addArgument(payload); 119 | const callback = try control.runCommand(state.wayland.seat.?); 120 | _ = callback; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Wayland.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log; 3 | const mem = std.mem; 4 | const meta = std.meta; 5 | const posix = std.posix; 6 | 7 | const wl = @import("wayland").client.wl; 8 | const wp = @import("wayland").client.wp; 9 | const zwlr = @import("wayland").client.zwlr; 10 | const zriver = @import("wayland").client.zriver; 11 | 12 | const Bar = @import("Bar.zig"); 13 | const Input = @import("Input.zig"); 14 | const Monitor = @import("Monitor.zig"); 15 | const Seat = @import("Seat.zig"); 16 | const Wayland = @This(); 17 | 18 | const state = &@import("root").state; 19 | 20 | display: *wl.Display, 21 | registry: *wl.Registry, 22 | fd: posix.fd_t, 23 | 24 | compositor: ?*wl.Compositor = null, 25 | subcompositor: ?*wl.Subcompositor = null, 26 | seat: ?*wl.Seat = null, 27 | shm: ?*wl.Shm = null, 28 | single_pixel_buffer_manager: ?*wp.SinglePixelBufferManagerV1 = null, 29 | viewporter: ?*wp.Viewporter = null, 30 | layer_shell: ?*zwlr.LayerShellV1 = null, 31 | status_manager: ?*zriver.StatusManagerV1 = null, 32 | control: ?*zriver.ControlV1 = null, 33 | 34 | river_seat: ?*Seat = null, 35 | monitors: std.ArrayList(*Monitor), 36 | inputs: std.ArrayList(*Input), 37 | 38 | pub fn init() !Wayland { 39 | const display = try wl.Display.connect(null); 40 | const wfd: posix.fd_t = @intCast(display.getFd()); 41 | const registry = try display.getRegistry(); 42 | 43 | return Wayland{ 44 | .display = display, 45 | .registry = registry, 46 | .fd = wfd, 47 | .monitors = std.ArrayList(*Monitor).init(state.gpa), 48 | .inputs = std.ArrayList(*Input).init(state.gpa), 49 | }; 50 | } 51 | 52 | pub fn deinit(self: *Wayland) void { 53 | for (self.monitors.items) |monitor| monitor.destroy(); 54 | for (self.inputs.items) |input| input.destroy(); 55 | 56 | if (self.river_seat) |s| s.destroy(); 57 | self.monitors.deinit(); 58 | self.inputs.deinit(); 59 | 60 | if (self.compositor) |global| global.destroy(); 61 | if (self.subcompositor) |global| global.destroy(); 62 | if (self.shm) |global| global.destroy(); 63 | if (self.viewporter) |global| global.destroy(); 64 | if (self.single_pixel_buffer_manager) |global| global.destroy(); 65 | if (self.layer_shell) |global| global.destroy(); 66 | if (self.status_manager) |global| global.destroy(); 67 | if (self.control) |global| global.destroy(); 68 | // TODO: Do we need to .release() the seat? 69 | if (self.seat) |global| global.destroy(); 70 | 71 | self.registry.destroy(); 72 | self.display.disconnect(); 73 | } 74 | 75 | pub fn registerGlobals(self: *Wayland) !void { 76 | self.registry.setListener(*Wayland, registryListener, self); 77 | 78 | const errno = self.display.roundtrip(); 79 | if (errno != .SUCCESS) { 80 | return error.RoundtripFailed; 81 | } 82 | } 83 | 84 | pub fn findBar(self: *Wayland, wlSurface: ?*wl.Surface) ?*Bar { 85 | if (wlSurface == null) { 86 | return null; 87 | } 88 | for (self.monitors.items) |monitor| { 89 | if (monitor.bar) |bar| { 90 | if (bar.background.surface == wlSurface or 91 | bar.title.surface == wlSurface or 92 | bar.tags.surface == wlSurface or 93 | bar.text.surface == wlSurface) 94 | { 95 | return bar; 96 | } 97 | } 98 | } 99 | return null; 100 | } 101 | 102 | fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Wayland) void { 103 | switch (event) { 104 | .global => |g| { 105 | self.bindGlobal(registry, g.name, g.interface) catch unreachable; 106 | }, 107 | .global_remove => |g| { 108 | for (self.monitors.items, 0..) |monitor, i| { 109 | if (monitor.globalName == g.name) { 110 | self.monitors.swapRemove(i).destroy(); 111 | break; 112 | } 113 | } 114 | for (self.inputs.items, 0..) |input, i| { 115 | if (input.globalName == g.name) { 116 | self.inputs.swapRemove(i).destroy(); 117 | break; 118 | } 119 | } 120 | }, 121 | } 122 | } 123 | 124 | fn bindGlobal(self: *Wayland, registry: *wl.Registry, name: u32, iface: [*:0]const u8) !void { 125 | if (mem.orderZ(u8, iface, wl.Compositor.interface.name) == .eq) { 126 | self.compositor = try registry.bind(name, wl.Compositor, 4); 127 | } else if (mem.orderZ(u8, iface, wl.Subcompositor.interface.name) == .eq) { 128 | self.subcompositor = try registry.bind(name, wl.Subcompositor, 1); 129 | } else if (mem.orderZ(u8, iface, wl.Shm.interface.name) == .eq) { 130 | self.shm = try registry.bind(name, wl.Shm, 1); 131 | } else if (mem.orderZ(u8, iface, wp.Viewporter.interface.name) == .eq) { 132 | self.viewporter = try registry.bind(name, wp.Viewporter, 1); 133 | } else if (mem.orderZ(u8, iface, wp.SinglePixelBufferManagerV1.interface.name) == .eq) { 134 | self.single_pixel_buffer_manager = try registry.bind(name, wp.SinglePixelBufferManagerV1, 1); 135 | } else if (mem.orderZ(u8, iface, zwlr.LayerShellV1.interface.name) == .eq) { 136 | self.layer_shell = try registry.bind(name, zwlr.LayerShellV1, 1); 137 | } else if (mem.orderZ(u8, iface, zriver.StatusManagerV1.interface.name) == .eq) { 138 | self.status_manager = try registry.bind(name, zriver.StatusManagerV1, 2); 139 | self.river_seat = try Seat.create(); // TODO: find a better way to do this 140 | } else if (mem.orderZ(u8, iface, zriver.ControlV1.interface.name) == .eq) { 141 | self.control = try registry.bind(name, zriver.ControlV1, 1); 142 | } else if (mem.orderZ(u8, iface, wl.Output.interface.name) == .eq) { 143 | const monitor = try Monitor.create(registry, name); 144 | try self.monitors.append(monitor); 145 | } else if (mem.orderZ(u8, iface, wl.Seat.interface.name) == .eq) { 146 | self.seat = try registry.bind(name, wl.Seat, 5); 147 | try self.inputs.append(try Input.create(name)); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Widget.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | 4 | const wl = @import("wayland").client.wl; 5 | 6 | const Buffer = @import("Buffer.zig"); 7 | const Widget = @This(); 8 | 9 | const state = &@import("root").state; 10 | 11 | surface: *wl.Surface, 12 | subsurface: *wl.Subsurface, 13 | buffers: [2]Buffer, 14 | 15 | pub fn init(background: *wl.Surface) !Widget { 16 | const compositor = state.wayland.compositor.?; 17 | const subcompositor = state.wayland.subcompositor.?; 18 | 19 | const surface = try compositor.createSurface(); 20 | const subsurface = try subcompositor.getSubsurface(surface, background); 21 | 22 | return Widget{ 23 | .surface = surface, 24 | .subsurface = subsurface, 25 | .buffers = .{ .{}, .{} }, 26 | }; 27 | } 28 | 29 | pub fn deinit(self: *Widget) void { 30 | self.subsurface.destroy(); 31 | self.surface.destroy(); 32 | self.buffers[0].deinit(); 33 | self.buffers[1].deinit(); 34 | } 35 | -------------------------------------------------------------------------------- /src/flags.zig: -------------------------------------------------------------------------------- 1 | // Zero allocation argument parsing for unix-like systems (taken from River). 2 | // Includes a minor modifications for error handling on unknown flags. 3 | // 4 | // Released under the Zero Clause BSD (0BSD) license: 5 | // 6 | // Copyright 2023 Isaac Freund 7 | // 8 | // Permission to use, copy, modify, and/or distribute this software for any 9 | // purpose with or without fee is hereby granted. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 14 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 17 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | 19 | const std = @import("std"); 20 | const mem = std.mem; 21 | 22 | pub const Flag = struct { 23 | name: [:0]const u8, 24 | kind: enum { boolean, arg }, 25 | }; 26 | 27 | pub fn parser(comptime Arg: type, comptime flags: []const Flag) type { 28 | switch (Arg) { 29 | // TODO consider allowing []const u8 30 | [:0]const u8, [*:0]const u8 => {}, // ok 31 | else => @compileError("invalid argument type: " ++ @typeName(Arg)), 32 | } 33 | return struct { 34 | pub const Result = struct { 35 | /// Remaining args after the recognized flags 36 | args: []const Arg, 37 | /// Data obtained from parsed flags 38 | flags: Flags, 39 | 40 | pub const Flags = flags_type: { 41 | var fields: []const std.builtin.Type.StructField = &.{}; 42 | for (flags) |flag| { 43 | const field: std.builtin.Type.StructField = switch (flag.kind) { 44 | .boolean => .{ 45 | .name = flag.name, 46 | .type = bool, 47 | .default_value = &false, 48 | .is_comptime = false, 49 | .alignment = @alignOf(bool), 50 | }, 51 | .arg => .{ 52 | .name = flag.name, 53 | .type = ?[:0]const u8, 54 | .default_value_ptr = &@as(?[:0]const u8, null), 55 | .is_comptime = false, 56 | .alignment = @alignOf(?[:0]const u8), 57 | }, 58 | }; 59 | fields = fields ++ [_]std.builtin.Type.StructField{field}; 60 | } 61 | break :flags_type @Type(.{ .@"struct" = .{ 62 | .layout = .auto, 63 | .fields = fields, 64 | .decls = &.{}, 65 | .is_tuple = false, 66 | } }); 67 | }; 68 | }; 69 | 70 | pub fn parse(args: []const Arg) !Result { 71 | var result_flags: Result.Flags = .{}; 72 | 73 | var i: usize = 0; 74 | outer: while (i < args.len) : (i += 1) { 75 | const arg = switch (Arg) { 76 | [*:0]const u8 => mem.sliceTo(args[i], 0), 77 | [:0]const u8 => args[i], 78 | else => unreachable, 79 | }; 80 | if (arg[0] != '-') { 81 | continue; 82 | } 83 | 84 | var flag_found = false; 85 | inline for (flags) |flag| { 86 | if (mem.eql(u8, "-" ++ flag.name, arg)) { 87 | flag_found = true; 88 | switch (flag.kind) { 89 | .boolean => @field(result_flags, flag.name) = true, 90 | .arg => { 91 | i += 1; 92 | if (i == args.len) { 93 | std.log.err("option '-" ++ flag.name ++ 94 | "' requires an argument but none was provided!", .{}); 95 | return error.MissingFlagArgument; 96 | } 97 | @field(result_flags, flag.name) = switch (Arg) { 98 | [*:0]const u8 => mem.sliceTo(args[i], 0), 99 | [:0]const u8 => args[i], 100 | else => unreachable, 101 | }; 102 | }, 103 | } 104 | continue :outer; 105 | } 106 | } 107 | if (!flag_found) { 108 | std.log.err("option '{s}' is unknown", .{arg}); 109 | return error.UnknownFlag; 110 | } 111 | break; 112 | } 113 | 114 | return Result{ 115 | .args = args[i..], 116 | .flags = result_flags, 117 | }; 118 | } 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const heap = std.heap; 3 | const io = std.io; 4 | const log = std.log; 5 | const mem = std.mem; 6 | const posix = std.posix; 7 | const os = std.os; 8 | const fmt = std.fmt; 9 | const process = std.process; 10 | 11 | const fcft = @import("fcft"); 12 | const pixman = @import("pixman"); 13 | 14 | const flags = @import("flags.zig"); 15 | const Loop = @import("Loop.zig"); 16 | const Wayland = @import("Wayland.zig"); 17 | 18 | pub const Config = struct { 19 | height: u16, 20 | normalFgColor: pixman.Color, 21 | normalBgColor: pixman.Color, 22 | focusFgColor: pixman.Color, 23 | focusBgColor: pixman.Color, 24 | font: *fcft.Font, 25 | }; 26 | 27 | pub const State = struct { 28 | gpa: mem.Allocator, 29 | config: Config, 30 | wayland: Wayland, 31 | loop: Loop, 32 | }; 33 | 34 | pub var state: State = undefined; 35 | 36 | fn parseColor(str: []const u8) !pixman.Color { 37 | // Color string needs to contain a base prefix. 38 | // For example: 0xRRGGBB. 39 | const val = try fmt.parseInt(u24, str, 0); 40 | 41 | const r: u8 = @truncate(val >> 16); 42 | const g: u8 = @truncate(val >> 8); 43 | const b: u8 = @truncate(val); 44 | 45 | return pixman.Color{ 46 | .red = @as(u16, r) << 8 | 0xff, 47 | .green = @as(u16, g) << 8 | 0xff, 48 | .blue = @as(u16, b) << 8 | 0xff, 49 | .alpha = 0xffff, 50 | }; 51 | } 52 | 53 | fn parseColorFlag(flg: ?[]const u8, def: []const u8) !pixman.Color { 54 | if (flg) |raw| { 55 | return parseColor(raw); 56 | } else { 57 | return parseColor(def); 58 | } 59 | } 60 | 61 | fn parseFlags(args: [][*:0]u8) !Config { 62 | const result = flags.parser([*:0]const u8, &.{ 63 | .{ .name = "hg", .kind = .arg }, // height 64 | .{ .name = "fn", .kind = .arg }, // font name 65 | .{ .name = "nf", .kind = .arg }, // normal foreground 66 | .{ .name = "nb", .kind = .arg }, // normal background 67 | .{ .name = "ff", .kind = .arg }, // focused foreground 68 | .{ .name = "fb", .kind = .arg }, // focused background 69 | }).parse(args) catch { 70 | usage(); 71 | }; 72 | 73 | var font_names = if (result.flags.@"fn") |raw| blk: { 74 | break :blk [_][*:0]const u8{raw}; 75 | } else blk: { 76 | break :blk [_][*:0]const u8{"monospace:size=10"}; 77 | }; 78 | 79 | const font = try fcft.Font.fromName(&font_names, null); 80 | const height: u16 = if (result.flags.hg) |raw| blk: { 81 | break :blk try fmt.parseUnsigned(u16, raw, 10); 82 | } else blk: { 83 | break :blk @intFromFloat(@as(f32, @floatFromInt(font.height)) * 1.5); 84 | }; 85 | 86 | return Config{ 87 | .font = font, 88 | .height = @intCast(height), 89 | .normalFgColor = try parseColorFlag(result.flags.nf, "0xb8b8b8"), 90 | .normalBgColor = try parseColorFlag(result.flags.nb, "0x282828"), 91 | .focusFgColor = try parseColorFlag(result.flags.ff, "0x181818"), 92 | .focusBgColor = try parseColorFlag(result.flags.fb, "0x7cafc2"), 93 | }; 94 | } 95 | 96 | pub fn usage() noreturn { 97 | const desc = 98 | \\usage: creek [-hg HEIGHT] [-fn FONT] [-nf COLOR] [-nb COLOR] 99 | \\ [-ff COLOR] [-fb COLOR] 100 | \\ 101 | ; 102 | 103 | io.getStdErr().writeAll(desc) catch |err| { 104 | std.debug.panic("{s}", .{@errorName(err)}); 105 | }; 106 | 107 | process.exit(1); 108 | } 109 | 110 | pub fn main() anyerror!void { 111 | var gpa: heap.GeneralPurposeAllocator(.{}) = .{}; 112 | defer _ = gpa.deinit(); 113 | 114 | _ = fcft.init(.auto, false, .warning); 115 | if (fcft.capabilities() & fcft.Capabilities.text_run_shaping == 0) { 116 | @panic("Support for text run shaping required in fcft and not present"); 117 | } 118 | 119 | state.gpa = gpa.allocator(); 120 | state.wayland = try Wayland.init(); 121 | state.loop = try Loop.init(); 122 | state.config = parseFlags(os.argv[1..]) catch |err| { 123 | log.err("Option parsing failed with: {s}", .{@errorName(err)}); 124 | usage(); 125 | }; 126 | 127 | defer { 128 | state.wayland.deinit(); 129 | } 130 | 131 | try state.wayland.registerGlobals(); 132 | try state.loop.run(); 133 | } 134 | -------------------------------------------------------------------------------- /src/render.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const unicode = std.unicode; 4 | 5 | const fcft = @import("fcft"); 6 | const pixman = @import("pixman"); 7 | const time = @cImport(@cInclude("time.h")); 8 | 9 | const Buffer = @import("Buffer.zig"); 10 | const Bar = @import("Bar.zig"); 11 | const Tag = @import("Tags.zig").Tag; 12 | 13 | const state = &@import("root").state; 14 | 15 | pub const RenderFn = fn (*Bar) anyerror!void; 16 | 17 | pub fn toUtf8(gpa: mem.Allocator, bytes: []const u8) ![]u32 { 18 | const utf8 = try unicode.Utf8View.init(bytes); 19 | var iter = utf8.iterator(); 20 | 21 | var runes = try std.ArrayList(u32).initCapacity(gpa, bytes.len); 22 | var i: usize = 0; 23 | while (iter.nextCodepoint()) |rune| : (i += 1) { 24 | runes.appendAssumeCapacity(rune); 25 | } 26 | 27 | return runes.toOwnedSlice(); 28 | } 29 | 30 | pub fn renderTags(bar: *Bar) !void { 31 | const surface = bar.tags.surface; 32 | const tags = bar.monitor.tags.tags; 33 | 34 | const buffers = &bar.tags.buffers; 35 | const shm = state.wayland.shm.?; 36 | 37 | const width = bar.height * @as(u16, tags.len + 1); 38 | const buffer = try Buffer.nextBuffer(buffers, shm, width, bar.height); 39 | if (buffer.buffer == null) return; 40 | buffer.busy = true; 41 | 42 | for (&tags, 0..) |*tag, i| { 43 | const offset: i16 = @intCast(bar.height * i); 44 | try renderTag(buffer.pix.?, tag, bar.height, offset); 45 | } 46 | 47 | // Separator tag to visually separate last focused tag from 48 | // focused window title (both use the same background color). 49 | const offset: i16 = @intCast(bar.height * tags.len); 50 | try renderTag(buffer.pix.?, &Tag{ .label = '|' }, bar.height, offset); 51 | 52 | bar.tags_width = width; 53 | surface.setBufferScale(bar.monitor.scale); 54 | surface.damageBuffer(0, 0, width, bar.height); 55 | surface.attach(buffer.buffer, 0, 0); 56 | } 57 | 58 | fn renderRun(start: i32, buffer: *Buffer, image: *pixman.Image, bar: *Bar, glyphs: [*]*const fcft.Glyph, count: usize) !i32 { 59 | const font_height: u32 = @intCast(state.config.font.height); 60 | const y_offset: i32 = @intCast((bar.height - font_height) / 2); 61 | 62 | var i: usize = 0; 63 | var x: i32 = start; 64 | while (i < count) : (i += 1) { 65 | const glyph = glyphs[i]; 66 | x += @intCast(glyph.x); 67 | const y = (state.config.font.ascent - @as(i32, @intCast(glyph.y))) + y_offset; 68 | pixman.Image.composite32(.over, image, glyph.pix, buffer.pix.?, 0, 0, 0, 0, x, y, glyph.width, glyph.height); 69 | x += glyph.advance.x - @as(i32, @intCast(glyph.x)); 70 | } 71 | 72 | return x; 73 | } 74 | 75 | pub fn renderTitle(bar: *Bar, title: ?[]const u8) !void { 76 | const surface = bar.title.surface; 77 | const shm = state.wayland.shm.?; 78 | 79 | var runes: ?[]u32 = null; 80 | if (title) |t| { 81 | if (t.len > 0) 82 | runes = try toUtf8(state.gpa, t); 83 | } 84 | defer { 85 | if (runes) |r| state.gpa.free(r); 86 | } 87 | 88 | // calculate width 89 | const title_start = bar.tags_width; 90 | const text_start = if (bar.text_width == 0) blk: { 91 | break :blk 0; 92 | } else blk: { 93 | break :blk bar.width - bar.text_width - bar.text_padding; 94 | }; 95 | const width: u16 = if (text_start > 0) blk: { 96 | break :blk @intCast(text_start - title_start - bar.text_padding); 97 | } else blk: { 98 | break :blk bar.width - title_start; 99 | }; 100 | 101 | // set subsurface offset 102 | const x_offset = bar.tags_width; 103 | const y_offset = 0; 104 | bar.title.subsurface.setPosition(x_offset, y_offset); 105 | 106 | const buffers = &bar.title.buffers; 107 | const buffer = try Buffer.nextBuffer(buffers, shm, width, bar.height); 108 | if (buffer.buffer == null) return; 109 | buffer.busy = true; 110 | 111 | var bg_color = state.config.normalBgColor; 112 | if (title) |t| { 113 | if (t.len > 0) bg_color = state.config.focusBgColor; 114 | } 115 | const bg_area = [_]pixman.Rectangle16{ 116 | .{ .x = 0, .y = 0, .width = width, .height = bar.height }, 117 | }; 118 | _ = pixman.Image.fillRectangles(.src, buffer.pix.?, &bg_color, 1, &bg_area); 119 | 120 | if (runes) |r| { 121 | const font = state.config.font; 122 | const run = try font.rasterizeTextRunUtf32(r, .default); 123 | defer run.destroy(); 124 | 125 | // calculate maximum amount of glyphs that can be displayed 126 | var max_x: i32 = bar.text_padding; 127 | var max_glyphs: u16 = 0; 128 | var i: usize = 0; 129 | while (i < run.count) : (i += 1) { 130 | const glyph = run.glyphs[i]; 131 | max_x += @intCast(glyph.x); 132 | if (max_x >= width - (2 * bar.text_padding) - bar.abbrev_width) { 133 | break; 134 | } 135 | max_x += glyph.advance.x - @as(i32, @intCast(glyph.x)); 136 | max_glyphs += 1; 137 | } 138 | 139 | var x: i32 = bar.text_padding; 140 | const color = pixman.Image.createSolidFill(&state.config.focusFgColor).?; 141 | x += try renderRun(bar.text_padding, buffer, color, bar, run.glyphs, max_glyphs); 142 | if (run.count > max_glyphs) { // if abbreviated 143 | _ = try renderRun(x, buffer, color, bar, bar.abbrev_run.glyphs, bar.abbrev_run.count); 144 | } 145 | } 146 | 147 | surface.setBufferScale(bar.monitor.scale); 148 | surface.damageBuffer(0, 0, width, bar.height); 149 | surface.attach(buffer.buffer, 0, 0); 150 | } 151 | 152 | pub fn resetText(bar: *Bar) !void { 153 | const surface = bar.text.surface; 154 | const shm = state.wayland.shm.?; 155 | 156 | const buffers = &bar.text.buffers; 157 | const buffer = try Buffer.nextBuffer(buffers, shm, bar.text_width, bar.height); 158 | if (buffer.buffer == null) return; 159 | buffer.busy = true; 160 | 161 | const text_to_bottom: u16 = 162 | @intCast(state.config.font.height + bar.text_padding); 163 | const bg_area = [_]pixman.Rectangle16{ 164 | .{ .x = 0, .y = 0, .width = bar.text_width, .height = text_to_bottom }, 165 | }; 166 | var bg_color = state.config.normalBgColor; 167 | _ = pixman.Image.fillRectangles(.src, buffer.pix.?, &bg_color, 1, &bg_area); 168 | 169 | surface.setBufferScale(bar.monitor.scale); 170 | surface.damageBuffer(0, 0, bar.text_width, bar.height); 171 | surface.attach(buffer.buffer, 0, 0); 172 | } 173 | 174 | pub fn renderText(bar: *Bar, text: []const u8) !void { 175 | const surface = bar.text.surface; 176 | const shm = state.wayland.shm.?; 177 | 178 | // utf8 encoding 179 | const runes = try toUtf8(state.gpa, text); 180 | defer state.gpa.free(runes); 181 | 182 | // rasterize 183 | const font = state.config.font; 184 | const run = try font.rasterizeTextRunUtf32(runes, .default); 185 | defer run.destroy(); 186 | 187 | // compute total width 188 | var i: usize = 0; 189 | var width: u16 = 0; 190 | while (i < run.count) : (i += 1) { 191 | width += @intCast(run.glyphs[i].advance.x); 192 | } 193 | 194 | // set subsurface offset 195 | const font_height: u32 = @intCast(state.config.font.height); 196 | const x_offset: i32 = @intCast(bar.width - width - bar.text_padding); 197 | const y_offset: i32 = @intCast(@divFloor(bar.height - font_height, 2)); 198 | bar.text.subsurface.setPosition(x_offset, y_offset); 199 | 200 | const buffers = &bar.text.buffers; 201 | const buffer = try Buffer.nextBuffer(buffers, shm, width, bar.height); 202 | if (buffer.buffer == null) return; 203 | buffer.busy = true; 204 | 205 | const bg_area = [_]pixman.Rectangle16{ 206 | .{ .x = 0, .y = 0, .width = width, .height = bar.height }, 207 | }; 208 | const bg_color = mem.zeroes(pixman.Color); 209 | _ = pixman.Image.fillRectangles(.src, buffer.pix.?, &bg_color, 1, &bg_area); 210 | 211 | var x: i32 = 0; 212 | i = 0; 213 | const color = pixman.Image.createSolidFill(&state.config.normalFgColor).?; 214 | while (i < run.count) : (i += 1) { 215 | const glyph = run.glyphs[i]; 216 | x += @intCast(glyph.x); 217 | const y = state.config.font.ascent - @as(i32, @intCast(glyph.y)); 218 | pixman.Image.composite32(.over, color, glyph.pix, buffer.pix.?, 0, 0, 0, 0, x, y, glyph.width, glyph.height); 219 | x += glyph.advance.x - @as(i32, @intCast(glyph.x)); 220 | } 221 | 222 | surface.setBufferScale(bar.monitor.scale); 223 | surface.damageBuffer(0, 0, width, bar.height); 224 | surface.attach(buffer.buffer, 0, 0); 225 | 226 | // render title again if text width changed 227 | if (width != bar.text_width) { 228 | bar.text_width = width; 229 | 230 | if (state.wayland.river_seat) |seat| { 231 | seat.mtx.lock(); 232 | defer seat.mtx.unlock(); 233 | 234 | try renderTitle(bar, seat.window_title); 235 | bar.title.surface.commit(); 236 | bar.background.surface.commit(); 237 | } 238 | } 239 | } 240 | 241 | fn renderTag( 242 | pix: *pixman.Image, 243 | tag: *const Tag, 244 | size: u16, 245 | offset: i16, 246 | ) !void { 247 | const outer = [_]pixman.Rectangle16{ 248 | .{ .x = offset, .y = 0, .width = size, .height = size }, 249 | }; 250 | const outer_color = tag.bgColor(); 251 | _ = pixman.Image.fillRectangles(.over, pix, outer_color, 1, &outer); 252 | 253 | if (tag.occupied) { 254 | const font_height: u16 = @intCast(state.config.font.height); 255 | 256 | // Constants taken from dwm-6.3 drawbar function. 257 | const boxs: i16 = @intCast(font_height / 9); 258 | const boxw: u16 = font_height / 6 + 2; 259 | 260 | const box = pixman.Rectangle16{ 261 | .x = offset + boxs, 262 | .y = boxs, 263 | .width = boxw, 264 | .height = boxw, 265 | }; 266 | 267 | const box_color = if (tag.focused) blk: { 268 | break :blk &state.config.normalBgColor; 269 | } else blk: { 270 | break :blk tag.fgColor(); 271 | }; 272 | 273 | _ = pixman.Image.fillRectangles(.over, pix, box_color, 1, &[_]pixman.Rectangle16{box}); 274 | if (!tag.focused) { 275 | const border = 1; // size of the border 276 | const inner = pixman.Rectangle16{ 277 | .x = box.x + border, 278 | .y = box.y + border, 279 | .width = box.width - (2 * border), 280 | .height = box.height - (2 * border), 281 | }; 282 | 283 | const inner_color = tag.bgColor(); 284 | _ = pixman.Image.fillRectangles(.over, pix, inner_color, 1, &[_]pixman.Rectangle16{inner}); 285 | } 286 | } 287 | 288 | const glyph_color = tag.fgColor(); 289 | const font = state.config.font; 290 | const char = pixman.Image.createSolidFill(glyph_color).?; 291 | const glyph = try font.rasterizeCharUtf32(tag.label, .default); 292 | const x = offset + @divFloor(size - glyph.width, 2); 293 | const y = @divFloor(size - glyph.height, 2); 294 | pixman.Image.composite32(.over, char, glyph.pix, pix, 0, 0, 0, 0, x, y, glyph.width, glyph.height); 295 | } 296 | --------------------------------------------------------------------------------