├── .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 | 
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 |
--------------------------------------------------------------------------------