diff --git a/.gitmodules b/.gitmodules index 6b5fe4b..81a74c8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "deps/zig-wayland"] path = deps/zig-wayland url = https://github.com/ifreund/zig-wayland.git +[submodule "deps/zig-args"] + path = deps/zig-args + url = https://github.com/MasterQ32/zig-args.git diff --git a/build.zig b/build.zig index 7ab9733..8141a54 100644 --- a/build.zig +++ b/build.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const ScanProtocolsStep = @import("deps/zig-wayland/build.zig").ScanProtocolsStep; pub fn build(b: *std.build.Builder) void { // Standard target options allows the person running `zig build` to choose @@ -11,9 +12,49 @@ pub fn build(b: *std.build.Builder) void { // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. const mode = b.standardReleaseOptions(); - const exe = b.addExecutable("zlurp", "src/main.zig"); + const scanner = ScanProtocolsStep.create(b); + scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml"); + scanner.addSystemProtocol("unstable/xdg-output/xdg-output-unstable-v1.xml"); + scanner.addProtocolPath("protocols/wlr-layer-shell-unstable-v1.xml"); + + // Pass the maximum version implemented by your wayland server or client. + // Requests, events, enums, etc. from newer versions will not be generated, + // ensuring forwards compatibility with newer protocol xml. + // This will also generate code for interfaces created using the provided + // global interface, in this example wl_keyboard, wl_pointer, xdg_surface, + // xdg_toplevel, etc. would be generated. + scanner.generate("wl_compositor", 4); + scanner.generate("zxdg_output_manager_v1", 1); + scanner.generate("zwlr_layer_shell_v1", 1); + scanner.generate("wl_shm", 1); + scanner.generate("wl_output", 3); + scanner.generate("wl_seat", 2); + scanner.generate("xdg_wm_base", 1); + + const zig_args = std.build.Pkg{ + .name = "zig-args", + .source = .{.path = "deps/zig-args/args.zig"}, + }; + + const wayland = std.build.Pkg{ + .name = "wayland", + .source = .{ .generated = &scanner.result }, + }; + + const exe = b.addExecutable("zlurp", "main.zig"); exe.setTarget(target); exe.setBuildMode(mode); + + exe.step.dependOn(&scanner.step); + exe.addPackage(wayland); + exe.addPackage(zig_args); + exe.linkLibC(); + exe.linkSystemLibrary("wayland-client"); + exe.linkSystemLibrary("xkbcommon"); + exe.linkSystemLibrary("cairo"); + + scanner.addCSource(exe); + exe.install(); const run_cmd = exe.run(); @@ -25,7 +66,8 @@ pub fn build(b: *std.build.Builder) void { const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); - const exe_tests = b.addTest("src/main.zig"); + // TESTS + const exe_tests = b.addTest("main.zig"); exe_tests.setTarget(target); exe_tests.setBuildMode(mode); diff --git a/deps/zig-args b/deps/zig-args new file mode 160000 index 0000000..f272f2f --- /dev/null +++ b/deps/zig-args @@ -0,0 +1 @@ +Subproject commit f272f2fb25ff86c00fa67051175b8db76b23e402 diff --git a/main.zig b/main.zig new file mode 100644 index 0000000..b773f5d --- /dev/null +++ b/main.zig @@ -0,0 +1,328 @@ +const std = @import("std"); +const wayland = @import("zig-wayland/wayland.zig"); +const zig_args = @import("zig-args"); +const wl = wayland.client.wl; +const xdg = wayland.client.xdg; +const zxdg = wayland.client.zxdg; +const zwlr = wayland.client.zwlr; +const list = wayland.server.wl.list; + +const xkb = @cImport({ + @cInclude("xkbcommon/xkbcommon.h"); +}); + +const cairo = @cImport({ + @cInclude("cairo/cairo.h"); +}); + +const Point = struct { + x: i32, + y: i32, +}; + +const Size = struct { + const Self = @This(); + width: u32, + height: u32, + + + fn size(self: *Self) usize { + return self.width * self.height; + } +}; + +const Box = struct { + const Self = @This(); + + position: Point, + extents: Size, + + fn intersects(self: *const Self, other: *const Self) bool { + const x1 = self.position.x; + const y1 = self.position.y; + const w1 = self.extents.width; + const h1 = self.extents.height; + + const x2 = other.position.x; + const y2 = other.position.y; + const w2 = other.extents.width; + const h2 = other.extents.height; + + // return self.x < other.x + other.width and + // self.x + self.width > other.x and + // self.y < other.y + other.height and + // self.height + self.y > other.y; + + return x1 < x2 + w2 and + x1 + w1 > x2 and + y1 < y1 + h2 and + y1 + h1 > y2; + } + + fn contains(self: *Self, point: Point) bool { + const x1 = self.position.x; + const y1 = self.position.y; + const w1 = self.extents.width; + const h1 = self.extents.height; + + const x2 = point.x; + const y2 = point.y; + + // return self.x <= point.x and self.x + self.width > point.x and + // self.y <= point.y and self.y + self.height > point.y; + + return x1 <= x2 and x1 + w1 > x2 and + y1 <= y2 and y1 + h1 > y2; + } + + fn size(self: *Self) usize { + return self.extents.size(); + } +}; + +fn ListItem(comptime T: type) type { + return struct { + const Self = @This(); + + link: list.Link, + value: T, + + fn Head() type { + list.Head(Self, "link"); + } + + fn fromLink(link: *list.Link) *Self { + return @fieldParentPtr(Self, "link", link); + } + + fn remove(self: *Self) void { + self.link.remove(); + } + + fn next(self: *Self) ?*list.Link { + self.link.next(); + } + + fn prev(self: *Self) ?*list.Link { + self.link.prev(); + } + + fn getLink(self: *Self) *list.Link { + return &self.link; + } + + fn get(self: *Self) *T { + return &self.value; + } + + fn getConst(self: *const Self) *const T { + return &self.value; + } + }; +} + +const Options = struct { + const Self = @This(); + + help: bool = false, + @"display-dimensions": bool = false, + background: ?u32 = null, + border: ?u32 = null, + selection: ?u32 = null, + choice: ?u32 = null, + format: ?[]const u8 = null, + @"font-family": ?[]const u8 = null, + @"border-weight": ?u32 = null, + @"single-point": bool = false, + @"output-boxes": bool = false, + @"restrict-selection": bool = false, + // TODO: parse this in the format of W:H instead of a fraction + @"aspect-ratio": ?f64 = null, + + pub const shorthands = .{ + .h = "help", + .b = "background", + .c = "border", + .s = "selection", + .B = "choice", + .f = "format", + .F = "font-family", + .w = "border-weight", + .p = "single-point", + .o = "output-boxes", + .r = "restrict-selection", + .a = "aspect-ratio", + }; + + const usage = @embedFile("usage"); + + fn parse() !zig_args.ParseArgsResult(Self, null) { + const self = try zig_args.parseForCurrentProcess(Self, std.heap.page_allocator, .print); + // TODO: print help + + return self; + } +}; + +const State = struct { + const Uninit = struct { + const Self = @This(); + + ally: std.mem.Allocator = std.heap.c_allocator, + + dpy: *wl.Display = undefined, + registry: *wl.Registry = undefined, + + shm: ?*wl.Shm = null, + compositor: ?*wl.Compositor = null, + layer_shell: ?*zwlr.LayerShellV1 = null, + xdg_output_manager: ?*zxdg.OutputManagerV1 = null, + + fn create() !*Self { + const ally = std.heap.c_allocator; + + const dpy = try wl.Display.connect(null); + const registry = try dpy.getRegistry(); + + const self = try ally.create(Self); + + self.* = .{ + .ally = ally, + .dpy = dpy, + .registry = registry, + }; + + registry.setListener(*Self, registryListener, self); + if (dpy.roundtrip() != .SUCCESS) return error.RoundtripFailed; + + return self; + } + + fn deinit(self: *Self) !void { + self.ally.destroy(self); + } + + fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Self) void { + switch (event) { + .global => |global| { + if (std.cstr.cmp(global.interface, wl.Compositor.getInterface().name) == 0) { + self.compositor = registry.bind(global.name, wl.Compositor, 4) catch return; + } else if (std.cstr.cmp(global.interface, wl.Shm.getInterface().name) == 0) { + self.shm = registry.bind(global.name, wl.Shm, 1) catch return; + } else if (std.cstr.cmp(global.interface, zwlr.LayerShellV1.getInterface().name) == 0) { + self.layer_shell = registry.bind(global.name, zwlr.LayerShellV1, 1) catch return; + } else if (std.cstr.cmp(global.interface, zxdg.OutputManagerV1.getInterface().name) == 0) { + self.xdg_output_manager = registry.bind(global.name, zxdg.OutputManagerV1, 2) catch + return; + } + }, + .global_remove => {}, + } + } + + fn intoInit(self: *Self) !*Init { + // free self at end of this function + defer self.ally.destroy(self); + + const init = try self.ally.create(Init); + // free allocated memory if not returning it + errdefer self.ally.destroy(init); + + const xkb_context = xkb.xkb_context_new(xkb.XKB_CONTEXT_NO_FLAGS) orelse + return error.NoXkbContext; + // free xkb_context if we fail to initialize the state + errdefer xkb.xkb_context_unref(xkb_context); + + const cursor_theme = std.process.getEnvVarOwned(self.ally, "XCURSOR_THEME") catch ""; + // only free cursor_theme if bailing + errdefer self.ally.free(cursor_theme); + + const buf = try std.process.getEnvVarOwned(self.ally, "XCURSOR_SIZE"); + defer self.ally.free(buf); + + const cursor_size = std.fmt.parseInt(u32, buf, 10) catch return error.InvalidXCursorSize; + + // i hope this is fine but appears to be requried to reset the listener on the registry + self.registry.destroy(); + const registry = try self.dpy.getRegistry(); + + init.* = Init { + .ally = self.ally, + .dpy = self.dpy, + .registry = registry, + + // these 4 fields are set by the registry listener so they may be null at this point + // treat that as a hard error tho. + .shm = self.shm orelse return error.NoWlShm, + .compositor = self.compositor orelse return error.NoWlCompositor, + .layer_shell = self.layer_shell orelse return error.NoWlrLayerShell, + .xdg_output_manager = self.xdg_output_manager orelse return error.NoXdgOutputManager, + + // these 3 fields should never be null because init() will bail if these functions fail + .xkb_context = xkb_context, + .cursor_theme = cursor_theme, + .cursor_size = cursor_size, + }; + + init.registry.setListener(*Init, Init.registryListener, init); + + return init; + } + }; + + const Init = struct { + const Self = @This(); + + ally: std.mem.Allocator = std.heap.c_allocator, + + dpy: *wl.Display = undefined, + registry: *wl.Registry = undefined, + + shm: *wl.Shm = undefined, + compositor: *wl.Compositor = undefined, + layer_shell: *zwlr.LayerShellV1 = undefined, + xdg_output_manager: *zxdg.OutputManagerV1 = undefined, + + xkb_context: *xkb.xkb_context = undefined, + + cursor_theme: []const u8 = undefined, + cursor_size: u32 = undefined, + + fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Self) void { + _ = registry; + _ = self; + std.debug.print("registryListener called after init", .{}); + switch (event) { + .global => {}, + .global_remove => { + }, + } + } + + fn deinit(self: *Self) void { + defer self.ally.destroy(self); + + self.shm.destroy(); + self.compositor.destroy(); + self.layer_shell.destroy(); + self.xdg_output_manager.destroy(); + xkb.xkb_context_unref(self.xkb_context); + self.registry.destroy(); + + self.ally.free(self.cursor_theme); + } + }; +}; + + +pub fn main() !void { + // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) + std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); + + const state = try State.Uninit.create(); + const init = try state.intoInit(); + + _ = try std.io.getStdIn().reader().readByte(); + + defer init.deinit(); +} diff --git a/protocols/wlr-layer-shell-unstable-v1.xml b/protocols/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..6a5d5d3 --- /dev/null +++ b/protocols/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,285 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (size, anchor, exclusive zone, margin, interactivity) + is double-buffered, and will be applied at the time wl_surface.commit of + the corresponding wl_surface is called. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthoginal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area of the surface + with other surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to an + edge, rather than a corner. The zone is the number of surface-local + coordinates from the edge that are considered exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive excluzive zone. If set to -1, the surface + indicates that it would not like to be moved to accomodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Set to 1 to request that the seat send keyboard events to this layer + surface. For layers below the shell surface layer, the seat will use + normal focus semantics. For layers above the shell surface layers, the + seat will always give exclusive keyboard focus to the top-most layer + which has keyboard interactivity set to true. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Events is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + diff --git a/src/main.zig b/src/main.zig deleted file mode 100644 index 5837faf..0000000 --- a/src/main.zig +++ /dev/null @@ -1,57 +0,0 @@ -const std = @import("std"); - -const VecN = std.meta.Vector; - -const Rgba32 = union { - comps: struct { r: u32, g: u32, b: u32, a: u32 }, - rgba: u64, -}; - -const State = struct { - colors: struct { background: Rgba32, border: Rgba32, selection: Rgba32, choice: Rgba32 }, - border_weight: u8, - display_dimensions: bool, - restrict_selection: bool, - fixed_aspect_ratio: bool, - aspect_ratio: f32, - // font_family -}; - -fn defaultState() State { - return State{ - .colors = .{ - .background = .{ .rgba = 0xFFFFFF40 }, - .border = .{ .rgba = 0x000000FF }, - .selection = .{ .rgba = 0x00000000 }, - .choice = .{ .rgba = 0xFFFFFF40 }, - }, - .border_weight = 2, - .display_dimensions = false, - .restrict_selection = false, - .fixed_aspect_ratio = false, - .aspect_ratio = 0, - }; -} - -pub fn main() !void { - // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) - std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); - - // stdout is for the actual output of your application, for example if you - // are implementing gzip, then only the compressed bytes should be sent to - // stdout, not any debugging messages. - const stdout_file = std.io.getStdOut().writer(); - var bw = std.io.bufferedWriter(stdout_file); - const stdout = bw.writer(); - - try stdout.print("Run `zig build test` to run the tests.\n", .{}); - - try bw.flush(); // don't forget to flush! -} - -test "simple test" { - var list = std.ArrayList(i32).init(std.testing.allocator); - defer list.deinit(); // try commenting this out and see if zig detects the memory leak! - try list.append(42); - try std.testing.expectEqual(@as(i32, 42), list.pop()); -} diff --git a/usage b/usage new file mode 100644 index 0000000..a7cd448 --- /dev/null +++ b/usage @@ -0,0 +1,15 @@ +Usage: slurp [options...] + + --help -h Show help message and quit. + --display-dimensions -d Display dimensions of selection. + --background -b #rrggbbaa Set background color. + --border -c #rrggbbaa Set border color. + --selection -s #rrggbbaa Set selection color. + --choice -B #rrggbbaa Set option box color. + --font-family -F s Set the font family for the dimensions. + --border-weight -w n Set border weight. + --format -f s Set output format. + --output-boxes -o Select a display output. + --single-point -p Select a single point. + --restrict-selection -r Restrict selection to predefined boxes. + --aspect-ratio -a w:h Force aspect ratio. \ No newline at end of file diff --git a/zig-wayland b/zig-wayland new file mode 120000 index 0000000..0065b3b --- /dev/null +++ b/zig-wayland @@ -0,0 +1 @@ +zig-cache/zig-wayland \ No newline at end of file