~novakane/zelbar

79788da8f9657f2b9df0943c1b769656fba990d2 — Hugo Machet 1 year, 3 months ago 39453b8
Implement pointers events and  action on click

New format:
  %{A:action}
  Send <action> to STDOUT
6 files changed, 216 insertions(+), 19 deletions(-)

M README.md
M build.zig
M doc/zelbar.1
M src/Backend.zig
M src/Tokenizer.zig
M src/data.zig
M README.md => README.md +7 -13
@@ 2,18 2,6 @@

Wayland statusbar reading input from STDIN, inspired by [lemonbar].

## Status

Under active development, expect a lot of breaking changes.

TODO:

-   Seat pointers events, and then allow action on click.
-   Better idiomatic zig if needed.
-   Improve memory usage, performance, the bar need to be lightweight.

Contributions welcome!

## Building

Requirements:


@@ 27,7 15,7 @@ Init submodules:

Build, `e.g.`

    zig build --prefix ~/.local
    zig build -Drelease-safe --prefix ~/.local

## Usage



@@ 69,12 57,18 @@ after each text block.
%{O:[0xRRGGBB[AA]:size]}     Overline color:size in pixel
%{U:[0xRRGGBB[AA]:size]}     Underline color:size in pixel
%{X:size}                    Offset in pixel in the alignment direction
%{A:action}                  Text to send to the STDOUT on mouse click
%{l|c|r}                     Alignment of the text (left|center|right) in
                               the bar
```

See **zelbar**(1) man page for more informations.

### Output

Output of an action on mouse button released is send to the STDOUT followed by
a newline.

## Contributing

See [CONTRIBUTING.md]

M build.zig => build.zig +3 -2
@@ 60,10 60,10 @@ pub fn build(b: *Builder) !void {

    exe.addPackagePath("flags", "common/flags.zig");

    scanner.generate("wl_compositor", 4);
    scanner.generate("wl_compositor", 5);
    scanner.generate("wl_shm", 1);
    scanner.generate("wl_output", 4);
    scanner.generate("wl_seat", 7);
    scanner.generate("wl_seat", 8);
    scanner.generate("zwlr_layer_shell_v1", 4);

    const wayland = std.build.Pkg{


@@ 73,6 73,7 @@ pub fn build(b: *Builder) !void {
    exe.addPackage(wayland);
    exe.linkLibC();
    exe.linkSystemLibrary("wayland-client");
    exe.linkSystemLibrary("wayland-cursor");

    const pixman = std.build.Pkg{
        .name = "pixman",

M doc/zelbar.1 => doc/zelbar.1 +4 -0
@@ 220,6 220,10 @@ Do
for text block with alignment set to
.BR center .
.TP
.B %{A:action}
Action on mouse click. The command is send to STDOUT followed by a newline.
Allowing to pipe it to a script, execute it or ignore it.
.TP
.B %{l|c|r}
Alignment in the bar of the following text block.
.IP

M src/Backend.zig => src/Backend.zig +171 -4
@@ 17,6 17,7 @@
const std = @import("std");
const assert = std.debug.assert;
const builtin = @import("builtin");
const io = std.io;
const mem = std.mem;
const os = std.os;



@@ 74,11 75,46 @@ const Output = struct {
    }
};

// Most code for pointers have been taken and adapted from https://git.sr.ht/~leon_plickat/wayprompt
// same license.
const HotSpot = struct {
    action: []const u8,
    x: u31,
    y: u31,
    width: u31,
    height: u31,

    fn contains_point(hotspot: HotSpot, x: u31, y: u31) bool {
        return x >= hotspot.x and x <= hotspot.x +| hotspot.width and
            y >= hotspot.y and y <= hotspot.y +| hotspot.height;
    }

    fn act(hotspot: HotSpot) !void {
        var bw = io.bufferedWriter(io.getStdOut().writer());
        const writer = bw.writer();
        try writer.writeAll(hotspot.action);
        try writer.writeByte('\n');
        try bw.flush();
    }
};

const Seat = struct {
    const CursorShape = enum { none, arrow, hand };

    wl_seat: *wl.Seat,
    name: ?[]const u8 = null,
    id: u32,

    // Pointer related objects.
    wl_pointer: ?*wl.Pointer = null,
    pointer_x: u31 = 0,
    pointer_y: u31 = 0,
    cursor_shape: CursorShape = .none,
    cursor_theme: ?*wl.CursorTheme = null,
    cursor_surface: ?*wl.Surface = null,
    last_enter_serial: u32 = undefined,
    press_hotspot: ?*HotSpot = null,

    fn init(seat: *Seat, wl_seat: *wl.Seat, id: u32) !void {
        seat.* = .{
            .wl_seat = wl_seat,


@@ 89,14 125,20 @@ const Seat = struct {
    }

    fn deinit(seat: *Seat) void {
        seat.release_pointer();
        seat.wl_seat.release();
        if (seat.name) |name| ctx.gpa.free(name);
    }

    fn seat_listener(_: *wl.Seat, event: wl.Seat.Event, seat: *Seat) void {
        switch (event) {
            // TODO
            .capabilities => {},
            .capabilities => |ev| {
                if (ev.capabilities.pointer) {
                    seat.bind_pointer() catch {};
                } else {
                    seat.release_pointer();
                }
            },
            .name => |ev| {
                seat.name = ctx.gpa.dupe(u8, mem.span(ev.name)) catch |err| switch (err) {
                    error.OutOfMemory => {


@@ 107,12 149,129 @@ const Seat = struct {
            },
        }
    }

    fn bind_pointer(seat: *Seat) !void {
        if (seat.wl_pointer != null) return;
        seat.wl_pointer = try seat.wl_seat.getPointer();
        seat.wl_pointer.?.setListener(*Seat, pointer_listener, seat);
    }

    fn release_pointer(seat: *Seat) void {
        seat.cursor_shape = .none;
        seat.press_hotspot = null;
        if (seat.cursor_theme) |t| {
            t.destroy();
            seat.cursor_theme = null;
        }
        if (seat.cursor_surface) |s| {
            s.destroy();
            seat.cursor_surface = null;
        }
        if (seat.wl_pointer) |p| {
            p.release();
            seat.wl_pointer = null;
        }
    }

    fn pointer_listener(_: *wl.Pointer, event: wl.Pointer.Event, seat: *Seat) void {
        switch (event) {
            .enter => |ev| seat.update_pointer(ev.surface_x, ev.surface_y, ev.serial),
            .motion => |ev| seat.update_pointer(ev.surface_x, ev.surface_y, null),
            .button => |ev| {
                // Only activating a button on release is the better UX, IMO.
                switch (ev.state) {
                    .pressed => {
                        seat.press_hotspot = ctx.backend.surface.?.hotspot_from_point(seat.pointer_x, seat.pointer_y);
                    },
                    .released => {
                        if (seat.press_hotspot == null) return;
                        if (ctx.backend.surface.?.hotspot_from_point(seat.pointer_x, seat.pointer_y)) |hs| {
                            if (hs == seat.press_hotspot.?) hs.act() catch return;
                        }
                        seat.press_hotspot = null;
                    },
                    else => {},
                }
            },
            else => {},
        }
    }

    fn update_pointer(seat: *Seat, x: wl.Fixed, y: wl.Fixed, serial: ?u32) void {
        const X = x.toInt();
        seat.pointer_x = if (X > 0) @intCast(u31, X) else 0;

        const Y = y.toInt();
        seat.pointer_y = if (Y > 0) @intCast(u31, Y) else 0;

        if (serial) |s| seat.last_enter_serial = s;

        // Sanity check.
        assert(seat.wl_pointer != null);
        assert(ctx.backend.surface != null);

        // Cursor errors shall not be fatal. It's fairly expectable for
        // something to go wrong there and it's not exactly vital to our
        // operation here, so we can roll without setting the cursor.
        if (ctx.backend.surface.?.hotspot_from_point(seat.pointer_x, seat.pointer_y) != null) {
            seat.set_cursor(.hand) catch {};
        } else {
            seat.set_cursor(.arrow) catch {};
        }
    }

    fn set_cursor(seat: *Seat, shape: CursorShape) !void {
        if (seat.cursor_shape == shape) return;

        const name = switch (shape) {
            .none => unreachable,
            .arrow => "default",
            .hand => "pointer",
        };

        const cursor_size = 24 * ctx.backend.scale;

        if (seat.cursor_theme == null) {
            seat.cursor_theme = try wl.CursorTheme.load(null, cursor_size, ctx.backend.shm.?);
        }
        errdefer {
            seat.cursor_theme.?.destroy();
            seat.cursor_theme = null;
        }

        // These just point back to the CursorTheme, no need to keep them.
        const wl_cursor = seat.cursor_theme.?.getCursor(name) orelse return error.NoCursor;
        const cursor_image = wl_cursor.images[0]; // TODO Is this nullable? Not in the bindings, but they may be wrong.
        const wl_buffer = try cursor_image.getBuffer();

        if (seat.cursor_surface == null) {
            seat.cursor_surface = try ctx.backend.compositor.?.createSurface();
        }
        errdefer {
            seat.cursor_surface.?.destroy();
            seat.cursor_surface = null;
        }

        seat.cursor_surface.?.setBufferScale(ctx.backend.scale);
        seat.cursor_surface.?.attach(wl_buffer, 0, 0);
        seat.cursor_surface.?.damageBuffer(0, 0, std.math.maxInt(i31), std.math.maxInt(u31));
        seat.cursor_surface.?.commit();

        seat.wl_pointer.?.setCursor(
            seat.last_enter_serial,
            seat.cursor_surface.?,
            @intCast(i32, @divFloor(cursor_image.hotspot_x, @intCast(u32, ctx.backend.scale))),
            @intCast(i32, @divFloor(cursor_image.hotspot_y, @intCast(u32, ctx.backend.scale))),
        );
    }
};

const Surface = struct {
    wl_surface: ?*wl.Surface = null,
    layer_surface: ?*zwlr.LayerSurfaceV1 = null,

    hotspots: std.ArrayListUnmanaged(HotSpot) = .{},

    configured: bool = false,

    fn init(surface: *Surface) !void {


@@ 145,12 304,20 @@ const Surface = struct {
    }

    fn deinit(surface: *Surface) void {
        surface.hotspots.deinit(ctx.gpa);
        if (surface.layer_surface) |layer_surface| layer_surface.destroy();
        if (surface.wl_surface) |wl_surface| wl_surface.destroy();
        surface.layer_surface = null;
        surface.wl_surface = null;
    }

    fn hotspot_from_point(surface: *Surface, x: u31, y: u31) ?*HotSpot {
        for (surface.hotspots.items) |*hs| {
            if (hs.contains_point(x, y)) return hs;
        }
        return null;
    }

    fn configure_layer(
        layer_surface: *zwlr.LayerSurfaceV1,
        width: u31,


@@ 554,7 721,7 @@ fn registry_event(backend: *Backend, registry: *wl.Registry, event: wl.Registry.
    switch (event) {
        .global => |ev| {
            if (std.cstr.cmp(ev.interface, wl.Compositor.getInterface().name) == 0) {
                backend.compositor = try registry.bind(ev.name, wl.Compositor, 4);
                backend.compositor = try registry.bind(ev.name, wl.Compositor, 5);
            } else if (std.cstr.cmp(ev.interface, wl.Shm.getInterface().name) == 0) {
                backend.shm = try registry.bind(ev.name, wl.Shm, 1);
            } else if (std.cstr.cmp(ev.interface, wl.Output.getInterface().name) == 0) {


@@ 571,7 738,7 @@ fn registry_event(backend: *Backend, registry: *wl.Registry, event: wl.Registry.
            } else if (std.cstr.cmp(ev.interface, wl.Seat.getInterface().name) == 0) {
                if (ev.version < 5) fatal_version(wl.Seat, ev.version, 5);

                const wl_seat = try registry.bind(ev.name, wl.Seat, 7);
                const wl_seat = try registry.bind(ev.name, wl.Seat, 8);
                errdefer wl_seat.release();

                const node = try ctx.gpa.create(std.SinglyLinkedList(Seat).Node);

M src/Tokenizer.zig => src/Tokenizer.zig +16 -0
@@ 65,6 65,10 @@ pub const Tag = enum {
    /// %{X:size}
    attr_offset,

    /// Actions on mouse click
    /// %{A:action}
    attr_action,

    /// End of line
    eol,
};


@@ 90,6 94,7 @@ const State = enum {
    attr_align_c,
    attr_align_r,
    attr_offset,
    attr_action,
};

state: State = .start,


@@ 162,6 167,10 @@ pub fn next(self: *Tokenizer) Token {
                    tok_start = self.index;
                    self.state = .attr_offset;
                },
                'A' => {
                    tok_start = self.index;
                    self.state = .attr_action;
                },
                else => return self.emit_error(.unknown_attribute),
            },



@@ 233,6 242,13 @@ pub fn next(self: *Tokenizer) Token {
                }),
                else => return self.emit_error(.attribute_not_closed),
            },
            .attr_action => switch (c) {
                '}' => return self.emit(.start, .{
                    .tag = .attr_action,
                    .bytes = self.buffer[tok_start..self.index],
                }),
                else => {},
            },

            .text => switch (c) {
                '\n', '\r', '\t' => return self.emit(.start, .{

M src/data.zig => src/data.zig +15 -0
@@ 153,6 153,8 @@ fn parse_input(input: []const u8, height: u16) !Entries {
                entry.offset = try fmt.parseUnsigned(u15, token.bytes[2..], 10);
            },

            .attr_action => entry.action = token.bytes[2..],

            .eol => return entries,

            .invalid => {


@@ 217,6 219,8 @@ const Entry = struct {
    offset: u15,
    width: u16,

    action: ?[]const u8,

    text: Text = undefined,

    pub fn new() Entry {


@@ 227,6 231,7 @@ const Entry = struct {
            .colors = .{},
            .lines = .{},
            .offset = 0,
            .action = null,
        };
    }



@@ 252,6 257,16 @@ const Entry = struct {
        const pix = ctx.bar.image orelse return error.PixmanImageEmpty;
        try decorations.draw_rectangle(pix, x, y, entry.width, height, bg);

        if (entry.action) |action| {
            try ctx.backend.surface.?.hotspots.append(ctx.gpa, .{
                .action = action,
                .x = @intCast(u31, x),
                .y = @intCast(u31, y),
                .width = entry.width,
                .height = height,
            });
        }

        if (entry.lines.get(.over)) |line_o| {
            try decorations.draw_rectangle(
                pix,