1428 lines
46 KiB
Zig
1428 lines
46 KiB
Zig
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 Utils = @import("box.zig");
|
|
const Point = Utils.Point;
|
|
const Box = Utils.Box;
|
|
const Boxes = Utils.Boxes;
|
|
const Size = Utils.Size;
|
|
const Selection = Utils.Selection;
|
|
|
|
const xkb = @cImport({
|
|
@cInclude("xkbcommon/xkbcommon.h");
|
|
});
|
|
|
|
const Cairo = @cImport({
|
|
@cInclude("cairo/cairo.h");
|
|
});
|
|
|
|
|
|
const Buffer = struct {
|
|
const Self = @This();
|
|
|
|
buffer: *wl.Buffer = undefined,
|
|
cairo: struct {
|
|
surface: *Cairo.cairo_surface_t = undefined,
|
|
ctx: *Cairo.cairo_t = undefined,
|
|
} = .{},
|
|
size: Size = .{},
|
|
data: []u8 = undefined,
|
|
in_use: std.atomic.Atomic(bool) = std.atomic.Atomic(bool).init(false),
|
|
|
|
fn randomName() [13]u8 {
|
|
var name = [_]u8{'/', 'z', 'l', 'u','r','p','-','0','0','0','0','0','0'};
|
|
|
|
var rng = std.rand.DefaultPrng.init(@intCast(u64, std.time.milliTimestamp()));
|
|
for (name[7..]) |*c| {
|
|
c.* = rng.random().intRangeAtMost(u8, 'A', 'Z');
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
fn init(self: *Self, shm: *wl.Shm, size: Size) !void {
|
|
const name = randomName();
|
|
const stride = size.width * 4;
|
|
const len = stride * size.height;
|
|
|
|
const fd = try std.os.memfd_create(&name, 0);
|
|
defer std.os.close(fd);
|
|
try std.os.ftruncate(fd, len);
|
|
|
|
const data = try std.os.mmap(
|
|
null,
|
|
len,
|
|
std.os.PROT.READ | std.os.PROT.WRITE,
|
|
std.os.MAP.SHARED,
|
|
fd,
|
|
0,
|
|
);
|
|
|
|
const pool = try shm.createPool(fd, @intCast(i32, len));
|
|
defer pool.destroy();
|
|
|
|
const buffer = try pool.createBuffer(
|
|
0,
|
|
@intCast(i32, size.width),
|
|
@intCast(i32, size.height),
|
|
@intCast(i32, stride),
|
|
.argb8888,
|
|
);
|
|
|
|
const surface = Cairo.cairo_image_surface_create_for_data(
|
|
@ptrCast([*c]u8, data),
|
|
Cairo.CAIRO_FORMAT_ARGB32,
|
|
@intCast(c_int, size.width),
|
|
@intCast(c_int, size.height),
|
|
@intCast(c_int, stride),
|
|
) orelse return error.NoCairoSurface;
|
|
|
|
const cairo_ctx = Cairo.cairo_create(surface) orelse return error.NoCairoContext;
|
|
|
|
self.* = .{
|
|
.buffer = buffer,
|
|
.size = size,
|
|
.data = data,
|
|
.cairo = .{
|
|
.surface = surface,
|
|
.ctx = cairo_ctx,
|
|
},
|
|
};
|
|
|
|
self.buffer.setListener(*Self, bufferListener, self);
|
|
}
|
|
|
|
fn deinit(self: *Self) void {
|
|
std.os.munmap(@alignCast(0x1000, self.data));
|
|
self.buffer.destroy();
|
|
|
|
Cairo.cairo_destroy(self.cairo.ctx);
|
|
Cairo.cairo_surface_destroy(self.cairo.surface);
|
|
}
|
|
|
|
fn bufferListener(buffer: *wl.Buffer, event: wl.Buffer.Event, self: *Self) void {
|
|
_ = buffer;
|
|
|
|
switch (event) {
|
|
.release => {
|
|
self.in_use.store(false, .Monotonic);
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
const Buffers = struct {
|
|
const Self = @This();
|
|
state: *State.Init,
|
|
buffers: [2]?Buffer = undefined,
|
|
|
|
fn init(self: *Self, state: *State.Init) !void {
|
|
self.* = Self{
|
|
.state = state,
|
|
};
|
|
|
|
for (self.buffers) |*buffer| {
|
|
buffer.* = null;
|
|
}
|
|
}
|
|
|
|
// returns any already initialized and not currently used buffer
|
|
fn getAvailableBuffer(self: *Self, size: Size) ?*Buffer {
|
|
for (self.buffers) |*buffer| {
|
|
if (buffer.*) |*buf| {
|
|
if (!buf.in_use.load(.Monotonic)) {
|
|
if (buf.size.width != size.width or buf.size.height != size.height) {
|
|
buf.deinit();
|
|
buf.* = .{};
|
|
buf.init(self.state.shm, size) catch {
|
|
buffer.* = null;
|
|
continue;
|
|
};
|
|
}
|
|
return buf;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (self.buffers) |*buffer| {
|
|
if (buffer.* == null) {
|
|
buffer.* = .{};
|
|
buffer.*.?.init(self.state.shm, size) catch {
|
|
buffer.* = null;
|
|
continue;
|
|
};
|
|
return &buffer.*.?;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
|
|
test "buffers" {
|
|
std.testing.refAllDecls(Buffers);
|
|
}
|
|
|
|
fn ListItem(comptime T: type) type {
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
link: list.Link,
|
|
value: T = undefined,
|
|
|
|
fn init(self: *Self, value: T) void {
|
|
self.value = value;
|
|
}
|
|
|
|
fn Head() type {
|
|
return list.Head(Self, "link");
|
|
}
|
|
|
|
fn fromLink(link: *list.Link) *Self {
|
|
return @fieldParentPtr(Self, "link", link);
|
|
}
|
|
|
|
fn fromValue(value: *T) *Self {
|
|
return @fieldParentPtr(Self, "value", value);
|
|
}
|
|
|
|
/// automatically calls `deinit()` on `data` if a function with that name exists
|
|
fn remove(self: *Self) void {
|
|
if (@hasDecl(T, "deinit")) {
|
|
self.value.deinit();
|
|
}
|
|
|
|
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 Output = struct {
|
|
const Self = @This();
|
|
|
|
state: *State.Init = undefined,
|
|
output: *wl.Output,
|
|
|
|
surface: *wl.Surface = undefined,
|
|
layer_surface: *zwlr.LayerSurfaceV1 = undefined,
|
|
xdg_output: *zxdg.OutputV1 = undefined,
|
|
|
|
geometry: Box = Box.default(),
|
|
logical_geometry: Box = Box.default(),
|
|
scale: i32 = 1,
|
|
|
|
size: Size = Size{},
|
|
|
|
configured: bool = false,
|
|
needs_redraw: bool = false,
|
|
|
|
buffers: Buffers = undefined,
|
|
frame_callback: ?*wl.Callback = null,
|
|
|
|
fn init(self: *Self, state: *State.Init) !void {
|
|
self.state = state;
|
|
try self.buffers.init(state);
|
|
|
|
self.output.setListener(*Self, outputListener, self);
|
|
|
|
self.surface = try state.compositor.createSurface();
|
|
|
|
self.layer_surface = try state.layer_shell.getLayerSurface(
|
|
self.surface,
|
|
self.output,
|
|
.overlay,
|
|
"selection",
|
|
);
|
|
self.layer_surface.setListener(*Self, layerSurfaceListener, self);
|
|
|
|
self.xdg_output = try state.xdg_output_manager.getXdgOutput(self.output);
|
|
self.xdg_output.setListener(*Self, xdgOutputListener, self);
|
|
|
|
self.layer_surface.setAnchor(.{.top = true, .left = true, .right = true, .bottom = true,});
|
|
self.layer_surface.setKeyboardInteractivity(1);
|
|
self.layer_surface.setExclusiveZone(-1);
|
|
self.surface.commit();
|
|
}
|
|
|
|
fn deinit(self: *Self) void {
|
|
self.output.destroy();
|
|
}
|
|
|
|
fn setSourceU32(c: *Cairo.cairo_t, color: u32) void {
|
|
Cairo.cairo_set_source_rgba(
|
|
c,
|
|
@intToFloat(f32, color >> (3 * 8) & 0xff) / 255.0,
|
|
@intToFloat(f32, color >> (2 * 8) & 0xff) / 255.0,
|
|
@intToFloat(f32, color >> (1 * 8) & 0xff) / 255.0,
|
|
@intToFloat(f32, color >> (0 * 8) & 0xff) / 255.0,
|
|
);
|
|
}
|
|
|
|
fn drawRect(buffer: *Buffer, box: Box, color: u32) void {
|
|
const cairo = buffer.cairo.ctx;
|
|
setSourceU32(cairo, color);
|
|
Cairo.cairo_rectangle(
|
|
cairo,
|
|
box.getX(f64),
|
|
box.getY(f64),
|
|
box.getWidth(f64),
|
|
box.getHeight(f64),
|
|
);
|
|
}
|
|
|
|
fn drawDimensions(self: *Self, c: *Cairo.cairo_t, color: u32, box: *const Box) void {
|
|
const opts = self.state.getOptions();
|
|
const font_family = opts.@"font-family".ptr;
|
|
Cairo.cairo_select_font_face(c,
|
|
font_family,
|
|
Cairo.CAIRO_FONT_SLANT_NORMAL,
|
|
Cairo.CAIRO_FONT_WEIGHT_NORMAL,
|
|
);
|
|
Cairo.cairo_set_font_size(c, 14);
|
|
setSourceU32(c, color);
|
|
var buffer = [_]u8{0} ** 12;
|
|
const printed = std.fmt.bufPrint(&buffer,
|
|
"{}x{}",
|
|
.{box.extents.width, box.extents.height}) catch null;
|
|
|
|
if (printed) |str| {
|
|
Cairo.cairo_move_to(c,
|
|
box.getRightMost(f64) + 10.0,
|
|
box.getBottomMost(f64) + 10.0,
|
|
);
|
|
Cairo.cairo_show_text(c, @ptrCast([*c]const u8, str.ptr));
|
|
}
|
|
}
|
|
|
|
fn render(self: *Self, buffer: *Buffer) void {
|
|
const cairo = &buffer.cairo;
|
|
const opts = self.state.getOptions();
|
|
|
|
// clear screen
|
|
Cairo.cairo_set_operator(cairo.ctx, Cairo.CAIRO_OPERATOR_SOURCE);
|
|
setSourceU32(cairo.ctx, opts.background);
|
|
Cairo.cairo_paint(cairo.ctx);
|
|
|
|
// draw in_boxes
|
|
if (self.state.config.in_boxes) |boxes| {
|
|
var iter = boxes.iter();
|
|
while (iter.next()) |box| {
|
|
if (box.intersects(&self.logical_geometry)) {
|
|
const corrected_box = box.translated(self.logical_geometry.position.inverted());
|
|
|
|
drawRect(buffer, corrected_box, opts.choice);
|
|
Cairo.cairo_fill(cairo.ctx);
|
|
|
|
if (opts.@"display-dimensions") {
|
|
self.drawDimensions(cairo.ctx, opts.border, box);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// draw selection for each seat
|
|
var seat_iter = self.state.seats.iterator(.forward);
|
|
while (seat_iter.next()) |link| {
|
|
const seat = link.get();
|
|
if (seat.pointer == null) {
|
|
continue;
|
|
}
|
|
|
|
if (seat.pointer.?.selection.getSelectionBox()) |box| {
|
|
// draw inner box
|
|
const corrected_box = box.translated(self.logical_geometry.position.inverted());
|
|
std.debug.print("selection: {}\n", .{corrected_box});
|
|
|
|
drawRect(buffer, corrected_box, opts.selection);
|
|
Cairo.cairo_fill(cairo.ctx);
|
|
|
|
Cairo.cairo_set_line_width(cairo.ctx, @intToFloat(f64, opts.@"border-weight"));
|
|
drawRect(buffer, corrected_box, opts.border);
|
|
Cairo.cairo_stroke(cairo.ctx);
|
|
|
|
if (opts.@"display-dimensions") {
|
|
self.drawDimensions(cairo.ctx, opts.border, &box);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn commitFrame(self: *Self) void {
|
|
const size = Size{
|
|
.width = self.size.width * @intCast(u32, self.scale),
|
|
.height = self.size.height * @intCast(u32, self.scale),
|
|
};
|
|
|
|
// FIXME: maybe somehow make this retry to get another buffer if the one
|
|
// that was returned has just become in-use?
|
|
if (self.buffers.getAvailableBuffer(size)) |buffer| {
|
|
if (buffer.in_use.compareAndSwap(false, true, .Release, .Monotonic) == null) {
|
|
Cairo.cairo_identity_matrix(buffer.cairo.ctx);
|
|
Cairo.cairo_scale(
|
|
buffer.cairo.ctx,
|
|
@intToFloat(f64, self.scale),
|
|
@intToFloat(f64, self.scale),
|
|
);
|
|
|
|
// RENDER
|
|
self.render(buffer);
|
|
|
|
if (self.frame_callback == null) {
|
|
self.frame_callback = self.surface.frame() catch null;
|
|
if (self.frame_callback) |frame| {
|
|
frame.setListener(*Self, frameCallbackListener, self);
|
|
}
|
|
}
|
|
|
|
self.surface.attach(buffer.buffer, 0, 0);
|
|
self.surface.damage(0, 0,
|
|
@intCast(i32, self.size.width),
|
|
@intCast(i32, self.size.height),
|
|
);
|
|
self.surface.setBufferScale(self.scale);
|
|
self.surface.commit();
|
|
|
|
self.needs_redraw = false;
|
|
} else {
|
|
std.debug.print("failed to CAS in_use.\n", .{});
|
|
}
|
|
} else {
|
|
std.debug.print("No buffer available!\n", .{});
|
|
}
|
|
}
|
|
|
|
fn requestRedraw(self: *Self) !void {
|
|
self.needs_redraw = true;
|
|
|
|
if (self.frame_callback == null) {
|
|
self.frame_callback = try self.surface.frame();
|
|
self.frame_callback.?.setListener(*Self, frameCallbackListener, self);
|
|
self.surface.commit();
|
|
}
|
|
}
|
|
|
|
fn frameCallbackListener(frame: *wl.Callback, event: wl.Callback.Event, self: *Self) void {
|
|
switch (event) {
|
|
.done => {
|
|
frame.destroy();
|
|
self.frame_callback = null;
|
|
|
|
if (self.needs_redraw) {
|
|
self.commitFrame();
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
fn xdgOutputListener(output: *zxdg.OutputV1, event: zxdg.OutputV1.Event, self: *Self) void {
|
|
_ = output;
|
|
|
|
std.debug.print("zxdg_output listener: {}\n", .{event});
|
|
|
|
switch (event) {
|
|
.logical_position => |pos| {
|
|
self.logical_geometry.position = .{.x = pos.x, .y = pos.y,};
|
|
},
|
|
.logical_size => |size| {
|
|
self.logical_geometry.extents = .{
|
|
.width = @intCast(u32, size.width),
|
|
.height = @intCast(u32, size.height),
|
|
};
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn layerSurfaceListener(
|
|
layer: *zwlr.LayerSurfaceV1,
|
|
event: zwlr.LayerSurfaceV1.Event,
|
|
self: *Self,
|
|
) void {
|
|
std.debug.print("zwlr_layer_surface listener: {}\n", .{event});
|
|
|
|
switch (event) {
|
|
.configure => |cfg|{
|
|
self.configured = true;
|
|
self.size = .{
|
|
.width = cfg.width,
|
|
.height = cfg.height,
|
|
};
|
|
|
|
layer.ackConfigure(cfg.serial);
|
|
self.commitFrame();
|
|
},
|
|
.closed => {
|
|
self.state.removeOutput(self);
|
|
},
|
|
}
|
|
}
|
|
|
|
fn outputListener(output: *wl.Output, event: wl.Output.Event, self: *Self) void {
|
|
_ = output;
|
|
|
|
std.debug.print("wl_output listener: {}\n", .{event});
|
|
|
|
switch (event) {
|
|
.geometry => |geom| {
|
|
self.geometry.position = .{.x = geom.x, .y = geom.y};
|
|
},
|
|
.mode => |mode| {
|
|
if (!mode.flags.current) {
|
|
self.geometry.extents = .{
|
|
.width = @intCast(u32, mode.width),
|
|
.height = @intCast(u32, mode.height),
|
|
};
|
|
}
|
|
},
|
|
.scale => |scale| {
|
|
self.scale = scale.factor;
|
|
},
|
|
else => {
|
|
std.debug.print("wl_output listener: unhandled\n", .{});
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
const Seat = struct {
|
|
const Self = @This();
|
|
|
|
state: *State.Init = undefined,
|
|
seat: *wl.Seat,
|
|
|
|
keyboard: ?struct {
|
|
keyboard: *wl.Keyboard,
|
|
xkb: ?struct {
|
|
keymap: *xkb.xkb_keymap,
|
|
state: *xkb.xkb_state,
|
|
} = null,
|
|
} = null,
|
|
|
|
pointer: ?struct {
|
|
pointer: *wl.Pointer,
|
|
current_surface_geom: ?Box = null,
|
|
stage: SelectionStage = .Edit,
|
|
selection: SelectionState = SelectionState.None(),
|
|
} = null,
|
|
|
|
|
|
// awful naming but thats life innit
|
|
// this encodes whether the selection is actively being edited or moved as a whole
|
|
const SelectionStage = enum {
|
|
Edit,
|
|
Move,
|
|
};
|
|
|
|
const SelectionTag = enum {
|
|
none,
|
|
pre,
|
|
updating,
|
|
post,
|
|
};
|
|
|
|
const SelectionState = union(SelectionTag) {
|
|
|
|
none: void,
|
|
pre: Point,
|
|
updating: Selection,
|
|
post: Box,
|
|
|
|
fn None() SelectionState {
|
|
return SelectionState.none;
|
|
}
|
|
|
|
fn translate(self: *SelectionState, delta: Point) void {
|
|
switch (self.*) {
|
|
.pre => |*point| {
|
|
point.add(delta);
|
|
},
|
|
.updating => |*selection| {
|
|
selection.start.add(delta);
|
|
selection.end.add(delta);
|
|
},
|
|
.post => |*box| {
|
|
box.translate(delta);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn getSelectionBox(self: SelectionState) ?Box {
|
|
switch (self) {
|
|
.updating => |selection| {
|
|
return selection.asBox();
|
|
},
|
|
.post => |box| {
|
|
return box;
|
|
},
|
|
else => {return null;},
|
|
}
|
|
}
|
|
};
|
|
|
|
fn init(self: *Self, state: *State.Init) void {
|
|
self.state = state;
|
|
|
|
self.seat.setListener(*Self, seatListener, self);
|
|
}
|
|
|
|
fn calculateRedraws(self: *Self) void {
|
|
if (self.pointer) |ptr| {
|
|
const box = ptr.selection.getSelectionBox();
|
|
|
|
var output_iter = self.state.outputs.iterator(.forward);
|
|
while (output_iter.next()) |link| {
|
|
const output = link.get();
|
|
if (box == null or output.logical_geometry.intersects(&box.?)) {
|
|
output.requestRedraw() catch {};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn pointerListener(pointer: *wl.Pointer, event: wl.Pointer.Event, self: *Self) void {
|
|
_ = pointer;
|
|
|
|
switch (event) {
|
|
.enter => |enter| {
|
|
if (self.state.getOutputForSurface(enter.surface.?)) |output| {
|
|
self.pointer.?.current_surface_geom = output.logical_geometry;
|
|
}
|
|
|
|
std.debug.print("surface geom: {?}\n", .{self.pointer.?.current_surface_geom});
|
|
},
|
|
.leave => {
|
|
self.pointer.?.current_surface_geom = null;
|
|
},
|
|
.motion => |motion| {
|
|
const x = motion.surface_x.toInt();
|
|
const y = motion.surface_y.toInt();
|
|
const xy = Point{.x = x, .y = y,};
|
|
|
|
if (self.pointer.?.current_surface_geom) |geom| {
|
|
const point = xy.added(geom.position);
|
|
|
|
switch (self.pointer.?.stage) {
|
|
.Edit => {
|
|
switch (self.pointer.?.selection) {
|
|
.pre => {
|
|
self.pointer.?.selection = .{.updating = .{.start = point, .end = point}};
|
|
return self.calculateRedraws();
|
|
},
|
|
.updating => |*selection| {
|
|
selection.end = point;
|
|
return self.calculateRedraws();
|
|
},
|
|
else => {},
|
|
}
|
|
},
|
|
.Move => {
|
|
const delta = blk: {
|
|
switch (self.pointer.?.selection) {
|
|
.pre => |end| {
|
|
break :blk point.added(end.inverted());
|
|
},
|
|
.updating => |selection| {
|
|
break :blk point.added(selection.end.inverted());
|
|
},
|
|
else => {break :blk null;},
|
|
}
|
|
};
|
|
|
|
if (delta != null) {
|
|
self.pointer.?.selection.translate(delta.?);
|
|
return self.calculateRedraws();
|
|
}
|
|
},
|
|
}
|
|
|
|
}
|
|
},
|
|
.button => |button| {
|
|
switch (button.state) {
|
|
.pressed => {
|
|
self.pointer.?.selection = SelectionState{.pre = .{}};
|
|
},
|
|
.released => {
|
|
switch (self.pointer.?.selection) {
|
|
.updating => |selection| {
|
|
const box = selection.asBox();
|
|
self.pointer.?.selection = .{.post = box};
|
|
return self.state.finish(box, self.pointer.?.current_surface_geom.?);
|
|
},
|
|
else => {},
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
}
|
|
|
|
fn keyboardListener(keyboard: *wl.Keyboard, event: wl.Keyboard.Event, self: *Self) void {
|
|
_ = keyboard;
|
|
|
|
switch (event) {
|
|
.keymap => |keymap_event| {
|
|
const keymap = blk: {
|
|
switch (keymap_event.format) {
|
|
.no_keymap => {
|
|
break :blk xkb.xkb_keymap_new_from_names(
|
|
self.state.xkb_context,
|
|
null,
|
|
xkb.XKB_KEYMAP_COMPILE_NO_FLAGS,
|
|
);
|
|
},
|
|
.xkb_v1 => {
|
|
const buffer = std.os.mmap(
|
|
null,
|
|
keymap_event.size,
|
|
std.os.PROT.READ,
|
|
std.os.MAP.PRIVATE,
|
|
keymap_event.fd,
|
|
0,
|
|
) catch break :blk null;
|
|
defer std.os.munmap(buffer);
|
|
defer std.os.close(keymap_event.fd);
|
|
|
|
break :blk xkb.xkb_keymap_new_from_buffer(
|
|
self.state.xkb_context,
|
|
@ptrCast([*c]const u8, buffer),
|
|
keymap_event.size - 1,
|
|
xkb.XKB_KEYMAP_FORMAT_TEXT_V1,
|
|
xkb.XKB_KEYMAP_COMPILE_NO_FLAGS,
|
|
);
|
|
|
|
},
|
|
else => unreachable,
|
|
}
|
|
};
|
|
|
|
if (keymap) |map| {
|
|
const state = xkb.xkb_state_new(map);
|
|
if (state) |state_| {
|
|
// SAFETY: keyboard cant be null because we are in the keyboard listener
|
|
self.keyboard.?.xkb = .{
|
|
.keymap = map,
|
|
.state = state_,
|
|
};
|
|
}
|
|
}
|
|
},
|
|
.key => |key| {
|
|
if (self.keyboard.?.xkb) |xkb_| {
|
|
const keysym = xkb.xkb_state_key_get_one_sym(xkb_.state, key.key + 8);
|
|
|
|
switch (key.state) {
|
|
.pressed => {
|
|
switch (keysym) {
|
|
xkb.XKB_KEY_Escape, xkb.XKB_KEY_q => {
|
|
|
|
if (self.pointer) |*ptr| {
|
|
switch (ptr.selection) {
|
|
.none => {
|
|
self.state.running = false;
|
|
return;
|
|
},
|
|
else => {
|
|
ptr.selection = .none;
|
|
return self.calculateRedraws();
|
|
}
|
|
}
|
|
}
|
|
|
|
},
|
|
xkb.XKB_KEY_space => {
|
|
if (self.pointer) |*ptr| {
|
|
ptr.stage = .Move;
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
},
|
|
.released => {
|
|
switch (keysym) {
|
|
xkb.XKB_KEY_space => {
|
|
if (self.pointer) |*ptr| {
|
|
ptr.stage = .Edit;
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn seatListener(seat: *wl.Seat, event: wl.Seat.Event, self: *Self) void {
|
|
switch (event) {
|
|
.capabilities => |value| {
|
|
std.debug.print("seat capabilities: {}\n", .{value.capabilities});
|
|
const capabilities = value.capabilities;
|
|
if (capabilities.keyboard) {
|
|
const keyboard = seat.getKeyboard() catch return;
|
|
self.keyboard = .{
|
|
.keyboard = keyboard,
|
|
};
|
|
|
|
keyboard.setListener(*Self, keyboardListener, self);
|
|
}
|
|
|
|
if (capabilities.pointer) {
|
|
const pointer = seat.getPointer() catch return;
|
|
self.pointer = .{
|
|
.pointer = pointer,
|
|
};
|
|
|
|
pointer.setListener(*Self, pointerListener, self);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn deinit(self: *Self) void {
|
|
if (self.keyboard) |keyboard| {
|
|
keyboard.keyboard.destroy();
|
|
|
|
if (keyboard.xkb) |kb| {
|
|
xkb.xkb_state_unref(kb.state);
|
|
xkb.xkb_keymap_unref(kb.keymap);
|
|
}
|
|
}
|
|
|
|
if (self.pointer) |pointer| {
|
|
pointer.pointer.destroy();
|
|
}
|
|
|
|
|
|
self.seat.destroy();
|
|
}
|
|
};
|
|
|
|
/// helper type to handle dynamic dispatch listeners whatever they're called
|
|
fn Listener(comptime T: type, comptime D: type) type {
|
|
return struct {
|
|
const Self = @This();
|
|
const Fn = *const fn (obj: *T, event: T.Event, data: *D) void;
|
|
|
|
data: *D,
|
|
callback: Fn,
|
|
|
|
fn create(data: *D, callback: Fn) Self {
|
|
return Self{
|
|
.data = data,
|
|
.callback = callback,
|
|
};
|
|
}
|
|
|
|
fn listener(obj: *T, event: T.Event, self: *Self) void {
|
|
return self.callback(obj, event, self.data);
|
|
}
|
|
|
|
fn set(self: *Self, obj: *T) void {
|
|
obj.setListener(*Self, listener, self);
|
|
}
|
|
|
|
fn cast(
|
|
self: *Self,
|
|
comptime D2: type,
|
|
new_data: *D2,
|
|
new_callback: Listener(T, D2).Fn,
|
|
) *Listener(T, D2) {
|
|
const other = @ptrCast(*Listener(T, D2), self);
|
|
other.data = new_data;
|
|
other.callback = new_callback;
|
|
return other;
|
|
}
|
|
};
|
|
}
|
|
|
|
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,
|
|
|
|
registry_listener: *Listener(wl.Registry, Self) = undefined,
|
|
|
|
shm: ?*wl.Shm = null,
|
|
compositor: ?*wl.Compositor = null,
|
|
layer_shell: ?*zwlr.LayerShellV1 = null,
|
|
xdg_output_manager: ?*zxdg.OutputManagerV1 = null,
|
|
|
|
seats: *ListItem(Seat).Head() = undefined,
|
|
outputs: *ListItem(Output).Head() = undefined,
|
|
|
|
fn create(ally: std.mem.Allocator) !*Self {
|
|
|
|
const dpy = try wl.Display.connect(null);
|
|
const registry = try dpy.getRegistry();
|
|
|
|
const self = try ally.create(Self);
|
|
|
|
const listener = try ally.create(Listener(wl.Registry, Self));
|
|
|
|
listener.* = .{
|
|
.data = self,
|
|
.callback = registryListener,
|
|
};
|
|
|
|
self.* = .{
|
|
.ally = ally,
|
|
.dpy = dpy,
|
|
.registry = registry,
|
|
.registry_listener = listener,
|
|
.seats = try ally.create(ListItem(Seat).Head()),
|
|
.outputs = try ally.create(ListItem(Output).Head()),
|
|
};
|
|
|
|
self.seats.init();
|
|
self.outputs.init();
|
|
|
|
self.registry_listener.set(self.registry);
|
|
if (dpy.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
|
|
|
return self;
|
|
}
|
|
|
|
fn deinit(self: *Self) void {
|
|
defer self.ally.destroy(self);
|
|
|
|
var seats_iter = self.seats.safeIterator(.forward);
|
|
while (seats_iter.next()) |next| {
|
|
next.remove();
|
|
self.ally.destroy(next);
|
|
}
|
|
self.ally.destroy(self.seats);
|
|
|
|
|
|
var outputs_iter = self.outputs.safeIterator(.forward);
|
|
while (outputs_iter.next()) |next| {
|
|
next.remove();
|
|
self.ally.destroy(next);
|
|
}
|
|
self.ally.destroy(self.outputs);
|
|
|
|
if (self.shm) |p| {
|
|
p.destroy();
|
|
}
|
|
if (self.compositor) |p| {
|
|
p.destroy();
|
|
}
|
|
if (self.layer_shell) |p| {
|
|
p.destroy();
|
|
}
|
|
if (self.xdg_output_manager) |p| {
|
|
p.destroy();
|
|
}
|
|
|
|
self.registry.destroy();
|
|
self.ally.destroy(
|
|
self.registry_listener
|
|
);
|
|
}
|
|
|
|
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Self) void {
|
|
switch (event) {
|
|
.global => |global| {
|
|
std.debug.print("global: {s}\n", .{global.interface});
|
|
|
|
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;
|
|
|
|
} else if (std.cstr.cmp(global.interface, wl.Seat.getInterface().name) == 0) {
|
|
const seat = registry.bind(global.name, wl.Seat, 1) catch return;
|
|
|
|
if (self.ally.create(ListItem(Seat)) catch null) |ele| {
|
|
ele.init(Seat{.seat = seat});
|
|
self.seats.append(ele);
|
|
}
|
|
} else if (std.cstr.cmp(global.interface, wl.Output.getInterface().name) == 0) {
|
|
const output = registry.bind(global.name, wl.Output, 3) catch return;
|
|
|
|
if (self.ally.create(ListItem(Output)) catch null) |ele| {
|
|
ele.init(Output{.output = output});
|
|
self.outputs.append(ele);
|
|
}
|
|
}
|
|
},
|
|
.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);
|
|
|
|
try init.init(self);
|
|
|
|
return init;
|
|
}
|
|
};
|
|
|
|
const Init = struct {
|
|
const Self = @This();
|
|
|
|
ally: std.mem.Allocator = undefined,
|
|
|
|
running: bool = true,
|
|
|
|
dpy: *wl.Display = undefined,
|
|
registry: *wl.Registry = undefined,
|
|
|
|
registry_listener: *Listener(wl.Registry, Self) = 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,
|
|
|
|
seats: *ListItem(Seat).Head() = undefined,
|
|
outputs: *ListItem(Output).Head() = undefined,
|
|
|
|
config: Input = undefined,
|
|
|
|
fn init(self: *Self, uninit: *Uninit) !void {
|
|
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(uninit.ally, "XCURSOR_THEME") catch "";
|
|
// only free cursor_theme if bailing
|
|
errdefer uninit.ally.free(cursor_theme);
|
|
|
|
const buf = try std.process.getEnvVarOwned(uninit.ally, "XCURSOR_SIZE");
|
|
defer uninit.ally.free(buf);
|
|
|
|
const cursor_size = std.fmt.parseInt(u32, buf, 10) catch return error.InvalidXCursorSize;
|
|
|
|
self.* = Self{
|
|
.ally = uninit.ally,
|
|
.dpy = uninit.dpy,
|
|
.registry = uninit.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 = uninit.shm orelse return error.NoWlShm,
|
|
.compositor = uninit.compositor orelse return error.NoWlCompositor,
|
|
.layer_shell = uninit.layer_shell orelse return error.NoWlrLayerShell,
|
|
.xdg_output_manager = uninit.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,
|
|
|
|
.seats = uninit.seats,
|
|
.outputs = uninit.outputs,
|
|
|
|
.config = try Input.init(uninit.ally),
|
|
};
|
|
|
|
// i hope this is fine but appears to be requried to reset the listener on the registry
|
|
self.registry_listener = uninit.registry_listener.cast(Self, self, Self.registryListener);
|
|
|
|
// init seats
|
|
|
|
var seats_iter = self.seats.safeIterator(.forward);
|
|
while (seats_iter.next()) |item| {
|
|
const seat = item.get();
|
|
seat.init(self);
|
|
}
|
|
|
|
// init outputs
|
|
|
|
var output_iter = self.outputs.safeIterator(.forward);
|
|
while (output_iter.next()) |item| {
|
|
const output = item.get();
|
|
try output.init(self);
|
|
}
|
|
|
|
if (self.dpy.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
|
}
|
|
|
|
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Self) void {
|
|
_ = registry;
|
|
_ = self;
|
|
switch (event) {
|
|
.global => {},
|
|
.global_remove => {},
|
|
}
|
|
}
|
|
|
|
fn getOptions(self: *Self) *Args {
|
|
return &self.config.args.options;
|
|
}
|
|
|
|
fn run(self: *Self) !void {
|
|
while (self.running) {
|
|
if (self.dpy.dispatch() != .SUCCESS) return error.DispatchFailed;
|
|
}
|
|
}
|
|
|
|
fn finish(self: *Self, box: Box, surface_geom: Box) void {
|
|
self.running = false;
|
|
|
|
const format = OutputFormat.fromFormatStr(self.ally, self.getOptions().format) catch return;
|
|
var iter = format.iter();
|
|
|
|
const stdout = std.io.getStdOut().writer();
|
|
while (iter.next()) |entry| {
|
|
if (entry.piece) |piece| {
|
|
_ = stdout.write(piece) catch return;
|
|
}
|
|
if (entry.arg) |arg| {
|
|
switch (arg) {
|
|
.X => {stdout.print("{}", .{box.getX(i32)}) catch return;},
|
|
.Y => {stdout.print("{}", .{box.getY(i32)}) catch return;},
|
|
.Width => {stdout.print("{}", .{box.getWidth(u32)}) catch return;},
|
|
.Height => {stdout.print("{}", .{box.getHeight(u32)}) catch return;},
|
|
.XRel => {stdout.print("{}", .{box.getX(i32) - surface_geom.getX(i32)}) catch return;},
|
|
.YRel => {stdout.print("{}", .{box.getY(i32) - surface_geom.getY(i32)}) catch return;},
|
|
.WidthClamped => {
|
|
stdout.print(
|
|
"{}",
|
|
.{@min(box.getWidth(i32),
|
|
surface_geom.getRightMost(i32) - box.getX(i32))},
|
|
) catch return;
|
|
},
|
|
.HeightClamped=> {
|
|
stdout.print(
|
|
"{}",
|
|
.{@min(box.getHeight(i32),
|
|
surface_geom.getBottomMost(i32) - box.getY(i32))},
|
|
) catch return;
|
|
},
|
|
// TODO: implement label and outputname
|
|
}
|
|
}
|
|
}
|
|
_ = stdout.write("\n") catch null;
|
|
}
|
|
|
|
fn removeOutput(self: *Self, output: *Output) void {
|
|
const link = ListItem(Output).fromValue(output);
|
|
link.remove();
|
|
self.ally.destroy(link);
|
|
}
|
|
|
|
fn getOutputForSurface(self: *Self, surface: *wl.Surface) ?*Output {
|
|
var output_iter = self.outputs.iterator(.forward);
|
|
while (output_iter.next()) |next| {
|
|
const output = next.get();
|
|
if (output.surface == surface) {
|
|
return output;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
fn deinit(self: *Self) void {
|
|
defer self.ally.destroy(self);
|
|
|
|
var seats_iter = self.seats.safeIterator(.forward);
|
|
while (seats_iter.next()) |next| {
|
|
next.remove();
|
|
self.ally.destroy(next);
|
|
}
|
|
self.ally.destroy(self.seats);
|
|
|
|
|
|
var outputs_iter = self.outputs.safeIterator(.forward);
|
|
while (outputs_iter.next()) |next| {
|
|
next.remove();
|
|
self.ally.destroy(next);
|
|
}
|
|
self.ally.destroy(self.outputs);
|
|
|
|
// Make sure the compositor has unmapped our surfaces by the time we exit
|
|
_ = self.dpy.roundtrip();
|
|
|
|
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.destroy(self.registry_listener);
|
|
|
|
self.config.deinit();
|
|
|
|
self.ally.free(self.cursor_theme);
|
|
}
|
|
};
|
|
};
|
|
|
|
const Args = struct {
|
|
const Self = @This();
|
|
|
|
help: bool = false,
|
|
@"display-dimensions": bool = false,
|
|
background: u32 = 0xFFFFFF40,
|
|
border: u32 = 0x000000FF,
|
|
selection: u32 = 0x00000000,
|
|
choice: u32 = 0xFFFFFF40,
|
|
format: []const u8 = "%x,%y %wx%h\n",
|
|
@"font-family": [:0]const u8 = "sans-serif",
|
|
@"border-weight": u32 = 2,
|
|
@"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");
|
|
|
|
const ResultType = zig_args.ParseArgsResult(Self, null);
|
|
|
|
fn parse(ally: std.mem.Allocator) !ResultType {
|
|
const self = try zig_args.parseForCurrentProcess(Self, ally, .print);
|
|
// TODO: print help
|
|
|
|
return self;
|
|
}
|
|
};
|
|
|
|
const Input = struct {
|
|
const Self = @This();
|
|
|
|
args: Args.ResultType,
|
|
in_boxes: ?Boxes = null,
|
|
|
|
fn init(ally: std.mem.Allocator) !Self {
|
|
const args = try Args.parse(ally);
|
|
const in_boxes = try readInBoxes(ally);
|
|
|
|
return Self {
|
|
.in_boxes = in_boxes,
|
|
.args = args,
|
|
};
|
|
}
|
|
|
|
fn deinit(self: Self) void {
|
|
self.args.deinit();
|
|
if (self.in_boxes) |boxes| {
|
|
boxes.deinit();
|
|
}
|
|
}
|
|
|
|
fn readInBoxes(alloc: std.mem.Allocator) !?Boxes {
|
|
var buf = [_]u8{0} ** 256;
|
|
if (!std.io.getStdIn().isTty()) {
|
|
var in_boxes = Boxes.init(alloc);
|
|
errdefer in_boxes.deinit();
|
|
|
|
var reader = std.io.getStdIn().reader();
|
|
while (try reader.readUntilDelimiterOrEof(&buf, '\n')) |line| {
|
|
if (line.len == 0) continue;
|
|
|
|
buf[line.len + 1] = 0;
|
|
const sentineld = buf[0..line.len + 1:0];
|
|
|
|
const result = try Box.parseFromStr(sentineld);
|
|
defer result.destroy();
|
|
|
|
try in_boxes.boxes.append(result.box);
|
|
}
|
|
|
|
return in_boxes;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
|
|
pub fn run(ally: std.mem.Allocator) !void {
|
|
const state = try State.Uninit.create(ally);
|
|
const init = try state.intoInit();
|
|
defer init.deinit();
|
|
|
|
try init.run();
|
|
}
|
|
|
|
pub fn main() !void {
|
|
try run(std.heap.c_allocator);
|
|
}
|
|
|
|
const OutputFormat = struct {
|
|
const Self = @This();
|
|
|
|
const Arg = enum {
|
|
X,
|
|
Y,
|
|
Width,
|
|
Height,
|
|
XRel,
|
|
YRel,
|
|
WidthClamped,
|
|
HeightClamped,
|
|
// TODO:
|
|
// OutputName,
|
|
// SelectionName,
|
|
|
|
fn fromChar(c: u8) ?Arg {
|
|
switch (c) {
|
|
'x' => {return .X;},
|
|
'y' => {return .Y;},
|
|
'X' => {return .XRel;},
|
|
'Y' => {return .YRel;},
|
|
'w' => {return .Width;},
|
|
'h' => {return .Height;},
|
|
'W' => {return .WidthClamped;},
|
|
'H' => {return .HeightClamped;},
|
|
else => {return null;},
|
|
}
|
|
}
|
|
};
|
|
|
|
const Entry = struct {piece: ?[]const u8, arg: Arg};
|
|
|
|
list: std.ArrayList(Entry),
|
|
last_piece: ?[]const u8,
|
|
|
|
const Iterator = struct {
|
|
format: Self,
|
|
i: usize = 0,
|
|
|
|
pub fn next(self: *@This()) ?struct {piece: ?[]const u8, arg: ?Arg} {
|
|
const items = self.format.list.items;
|
|
if (self.i < items.len) {
|
|
const item = &items[self.i];
|
|
self.i += 1;
|
|
|
|
return .{.piece = item.piece, .arg = item.arg};
|
|
} else if (self.i == items.len and self.format.last_piece != null) {
|
|
self.i += 1;
|
|
return .{.piece = self.format.last_piece, .arg = null};
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
|
|
fn iter(self: Self) Iterator {
|
|
return .{
|
|
.format = self,
|
|
};
|
|
}
|
|
|
|
fn deinit(self: Self) void {
|
|
self.list.deinit();
|
|
}
|
|
|
|
/// this does not copy! returned object is only valid as long as 'str' is valid.
|
|
fn fromFormatStr(ally: std.mem.Allocator, str: []const u8) !Self {
|
|
var window: struct {start: usize, end: usize} = .{.start = 0, .end = 0};
|
|
var pieces = std.ArrayList(Entry).init(ally);
|
|
|
|
var i: usize = 0;
|
|
while (i < str.len) {
|
|
defer i += 1;
|
|
const c = str[i];
|
|
|
|
window.end = i;
|
|
|
|
if (c == '%') {
|
|
if (str.len > i) {
|
|
i += 1;
|
|
if (Arg.fromChar(str[i])) |arg| {
|
|
const piece = if (window.start != window.end) str[window.start..window.end] else null;
|
|
window.start = i + 1;
|
|
window.end = i + 1;
|
|
|
|
std.debug.print("piece: '{?s}', arg: {?}\n", .{piece, arg});
|
|
try pieces.append(.{.piece = piece, .arg = arg});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const last_piece = if (window.start != window.end) str[window.start..window.end] else null;
|
|
std.debug.print("piece: '{?s}'\n", .{last_piece});
|
|
|
|
return Self{
|
|
.list = pieces,
|
|
.last_piece = last_piece,
|
|
};
|
|
}
|
|
};
|
|
|
|
test "format parsing" {
|
|
const ally = std.testing.allocator;
|
|
const a = try OutputFormat.fromFormatStr(ally, "%x,%y blob %wx%h");
|
|
defer a.deinit();
|
|
const b = try OutputFormat.fromFormatStr(ally, "%t,%y blob %wx%h");
|
|
defer b.deinit();
|
|
}
|