├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── build.zig ├── scheme ├── startup.scm ├── winter │ └── server.scm ├── wl.scm └── wl │ ├── server.scm │ └── view.scm ├── shell.nix └── src ├── C.zig ├── Cursor.zig ├── Keyboard.zig ├── Output.zig ├── Server.zig ├── View.zig ├── glue.c ├── main.zig └── wrapper.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | *~ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/zig-wlroots"] 2 | path = deps/zig-wlroots 3 | url = https://github.com/swaywm/zig-wlroots 4 | [submodule "deps/zig-pixman"] 5 | path = deps/zig-pixman 6 | url = https://github.com/ifreund/zig-pixman 7 | [submodule "deps/zig-xkbcommon"] 8 | path = deps/zig-xkbcommon 9 | url = https://github.com/ifreund/zig-xkbcommon 10 | [submodule "deps/zig-wayland"] 11 | path = deps/zig-wayland 12 | url = https://github.com/ifreund/zig-wayland 13 | [submodule "protocol/wlr-protocols"] 14 | path = protocol/wlr-protocols 15 | url = https://gitlab.freedesktop.org/wlroots/wlr-protocols.git 16 | [submodule "protocol/plasma-wayland-protocols"] 17 | path = protocol/plasma-wayland-protocols 18 | url = https://github.com/KDE/plasma-wayland-protocols 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ashlynn Anderson 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 | # winter 2 | A lispy wayland compositor. 3 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Builder = std.build.Builder; 3 | const Pkg = std.build.Pkg; 4 | const InstallDirectoryOptions = std.build.InstallDirectoryOptions; 5 | 6 | const ScanProtocolsStep = @import("deps/zig-wayland/build.zig").ScanProtocolsStep; 7 | 8 | pub fn build(b: *Builder) void { 9 | const target = b.standardTargetOptions(.{}); 10 | const mode = b.standardReleaseOptions(); 11 | 12 | const options = b.addOptions(); 13 | 14 | const scheme: InstallDirectoryOptions = .{ 15 | .source_dir = "scheme", 16 | .install_dir = .prefix, 17 | .install_subdir = "share/winter", 18 | }; 19 | 20 | b.installDirectory(scheme); 21 | options.addOption( 22 | []const u8, 23 | "scheme_dir", 24 | b.getInstallPath(scheme.install_dir, scheme.install_subdir), 25 | ); 26 | 27 | const scanner = ScanProtocolsStep.create(b); 28 | scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml"); 29 | scanner.addSystemProtocol("unstable/xdg-output/xdg-output-unstable-v1.xml"); 30 | //scanner.addProtocolPath("protocol/wlr-protocols/unstable/wlr-layer-shell-unstable-v1.xml"); 31 | 32 | const wayland = Pkg{ 33 | .name = "wayland", 34 | .path = .{ .generated = &scanner.result }, 35 | }; 36 | const xkbcommon = Pkg{ 37 | .name = "xkbcommon", 38 | .path = .{ .path = "deps/zig-xkbcommon/src/xkbcommon.zig" }, 39 | }; 40 | const pixman = Pkg{ 41 | .name = "pixman", 42 | .path = .{ .path = "deps/zig-pixman/pixman.zig" }, 43 | }; 44 | const wlroots = Pkg{ 45 | .name = "wlroots", 46 | .path = .{ .path = "deps/zig-wlroots/src/wlroots.zig" }, 47 | .dependencies = &[_]Pkg{ wayland, xkbcommon, pixman }, 48 | }; 49 | 50 | const exe = b.addExecutable("winter", "src/wrapper.zig"); 51 | exe.setTarget(target); 52 | exe.setBuildMode(mode); 53 | 54 | exe.addOptions("build_options", options); 55 | 56 | exe.addCSourceFiles(&.{ 57 | "src/glue.c", 58 | }, &.{}); 59 | 60 | exe.linkLibC(); 61 | 62 | exe.addPackage(wayland); 63 | exe.linkSystemLibrary("wayland-server"); 64 | 65 | exe.addPackage(xkbcommon); 66 | exe.linkSystemLibrary("xkbcommon"); 67 | 68 | exe.addPackage(wlroots); 69 | exe.linkSystemLibrary("wlroots"); 70 | 71 | exe.addPackage(pixman); 72 | exe.linkSystemLibrary("pixman-1"); 73 | 74 | exe.linkSystemLibrary("guile-3.0"); 75 | 76 | exe.step.dependOn(&scanner.step); 77 | // https://github.com/ziglang/zig/issues/131 78 | scanner.addCSource(exe); 79 | 80 | exe.install(); 81 | 82 | const run_cmd = exe.run(); 83 | run_cmd.step.dependOn(b.getInstallStep()); 84 | if (b.args) |args| { 85 | run_cmd.addArgs(args); 86 | } 87 | 88 | const run_step = b.step("run", "Run the app"); 89 | run_step.dependOn(&run_cmd.step); 90 | 91 | // // I'm not entirely sure how testing should work, since I need to 92 | // // enter guile mode before starting... 93 | // const exe_tests = b.addTest("src/wrapper.zig"); 94 | // exe_tests.setTarget(target); 95 | // exe_tests.setBuildMode(mode); 96 | 97 | // const test_step = b.step("test", "Run unit tests"); 98 | // test_step.dependOn(&exe_tests.step); 99 | } 100 | -------------------------------------------------------------------------------- /scheme/startup.scm: -------------------------------------------------------------------------------- 1 | (use-modules 2 | ((winter server) 3 | #:select (run) 4 | #:prefix server:)) 5 | 6 | (server:run) 7 | -------------------------------------------------------------------------------- /scheme/winter/server.scm: -------------------------------------------------------------------------------- 1 | (define-module (winter server) 2 | #:use-module ((wl) 3 | #:select (bind!) 4 | #:prefix wl:) 5 | #:use-module ((wl server) 6 | #:select (make-server 7 | server-outputs 8 | server-views 9 | server-socket 10 | run-server)) 11 | #:use-module ((wl view) 12 | #:select (view-enable!)) 13 | #:use-module ((ice-9 pretty-print) 14 | #:select ((pretty-print . pp))) 15 | #:export (server 16 | outputs 17 | views 18 | run)) 19 | 20 | (define server 21 | (make-parameter (make-server))) 22 | 23 | (define (outputs) 24 | (server-outputs (server))) 25 | 26 | (define (views) 27 | (server-views (server))) 28 | 29 | (define (run) 30 | (let ([server (server)]) 31 | (wl:bind! server 'new-output new-output-fn) 32 | (wl:bind! server 'new-view new-view-fn) 33 | 34 | (begin 35 | (display "Running on WAYLAND_DISPLAY=") 36 | (display (server-socket server)) 37 | (newline)) 38 | 39 | (run-server server))) 40 | 41 | (define (new-output-fn server output) 42 | (display "New output: ") (pp output)) 43 | 44 | (define (new-view-fn server view) 45 | (wl:bind! view 'map view-map-fn) 46 | (display "New view: ") (pp view)) 47 | 48 | (define (view-map-fn view) 49 | (view-enable! view)) 50 | -------------------------------------------------------------------------------- /scheme/wl.scm: -------------------------------------------------------------------------------- 1 | (define-module (wl) 2 | #:use-module ((wl internal) 3 | #:select (bind!) 4 | #:prefix internal:) 5 | #:export (bind!)) 6 | 7 | (define (bind! wl-object event function) 8 | "Binds FUNCTION to EVENT of WL-OBJECT." 9 | (internal:bind! wl-object event function)) 10 | -------------------------------------------------------------------------------- /scheme/wl/server.scm: -------------------------------------------------------------------------------- 1 | (define-module (wl server) 2 | #:use-module ((wl server internal) 3 | #:select (make-server 4 | server-outputs 5 | server-views 6 | server-socket 7 | run-server) 8 | #:prefix internal:) 9 | #:export (make-server 10 | server-outputs 11 | server-views 12 | server-socket 13 | run-server)) 14 | 15 | (define (make-server) 16 | "Creates a new wayland server." 17 | (internal:make-server)) 18 | 19 | (define (server-outputs server) 20 | "Returns the list of outputs attached to SERVER." 21 | (internal:server-outputs server)) 22 | 23 | (define (server-views server) 24 | "Returns the list of views held by SERVER. This includes unmapped and 25 | disabled views." 26 | (internal:server-views server)) 27 | 28 | (define (server-socket server) 29 | "Returns SERVER's socket name." 30 | (internal:server-socket server)) 31 | 32 | (define (run-server server) 33 | "Runs SERVER. Does not return until SERVER stops." 34 | (internal:run-server server)) 35 | -------------------------------------------------------------------------------- /scheme/wl/view.scm: -------------------------------------------------------------------------------- 1 | (define-module (wl view) 2 | #:use-module ((wl view internal) 3 | #:select (view-enable! 4 | view-disable!) 5 | #:prefix internal:) 6 | #:export (view-enable! 7 | view-disable!)) 8 | 9 | (define (view-enable! view) 10 | "Enables VIEW." 11 | (internal:view-enable! view)) 12 | 13 | (define (view-disable! view) 14 | "Disables VIEW." 15 | (internal:view-disable! view)) 16 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | let 4 | debug-guile = pkgs.enableDebugging (pkgs.guile_3_0.overrideAttrs (old: { 5 | dontStrip = true; 6 | })); 7 | in pkgs.mkShell { 8 | packages = with pkgs; [ 9 | pkg-config 10 | 11 | wayland wayland-protocols wayland-scanner.dev 12 | wlroots libxkbcommon pixman 13 | 14 | debug-guile 15 | ]; 16 | } 17 | -------------------------------------------------------------------------------- /src/C.zig: -------------------------------------------------------------------------------- 1 | // Disabling inlining makes zig translate-c's job much easier 2 | const C = @cImport({ 3 | @cInclude("libguile/scmconfig.h"); 4 | @cUndef("SCM_C_INLINE"); 5 | @cInclude("libguile.h"); 6 | }); 7 | 8 | pub usingnamespace C; 9 | 10 | // glue.c 11 | pub extern fn scm_is_a_p(C.SCM, C.SCM) bool; 12 | -------------------------------------------------------------------------------- /src/Cursor.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | 4 | const wl = @import("wayland").server.wl; 5 | 6 | const wlr = @import("wlroots"); 7 | const xkb = @import("xkbcommon"); 8 | 9 | const C = @import("C.zig"); 10 | 11 | const ally = std.heap.c_allocator; 12 | 13 | const Server = @import("Server.zig"); 14 | 15 | pub var scm_cursor_type: C.SCM = undefined; 16 | 17 | pub fn scm_init() void { 18 | _ = C.scm_c_define_module( 19 | "wl cursor internal", 20 | scm_initModule, 21 | null, 22 | ); 23 | } 24 | 25 | fn scm_initModule(_: ?*anyopaque) callconv(.C) void { 26 | scm_cursor_type = C.scm_make_foreign_object_type( 27 | C.scm_from_utf8_symbol("cursor"), 28 | C.scm_list_1( 29 | C.scm_from_utf8_symbol("ptr"), 30 | ), 31 | null, 32 | ); 33 | } 34 | 35 | pub fn cursorToScm(cursor: *Cursor) C.SCM { 36 | return C.scm_make_foreign_object_1( 37 | scm_cursor_type, 38 | cursor, 39 | ); 40 | } 41 | 42 | pub fn scmToCursor(scm_cursor: C.SCM) *Cursor { 43 | C.scm_assert_foreign_object_type(scm_cursor_type, scm_cursor); 44 | 45 | return @ptrCast( 46 | *Cursor, 47 | @alignCast( 48 | @alignOf(*Cursor), 49 | C.scm_foreign_object_ref(scm_cursor, 0).?, 50 | ), 51 | ); 52 | } 53 | 54 | const Cursor = @This(); // { 55 | 56 | server: *Server, 57 | seat: *wlr.Seat, 58 | 59 | wlr_cursor: *wlr.Cursor, 60 | cursor_mgr: *wlr.XcursorManager, 61 | 62 | // TODO: add grabbing and resizing 63 | mode: enum { passthrough } = .passthrough, 64 | 65 | request_set_listener: wl.Listener(*wlr.Seat.event.RequestSetCursor), 66 | 67 | motion_listener: wl.Listener(*wlr.Pointer.event.Motion), 68 | motion_absolute_listener: wl.Listener(*wlr.Pointer.event.MotionAbsolute), 69 | button_listener: wl.Listener(*wlr.Pointer.event.Button), 70 | axis_listener: wl.Listener(*wlr.Pointer.event.Axis), 71 | frame_listener: wl.Listener(*wlr.Cursor), 72 | 73 | pub fn init(self: *Cursor, server: *Server) !void { 74 | self.* = .{ 75 | .server = server, 76 | .seat = server.seat, 77 | 78 | .wlr_cursor = try wlr.Cursor.create(), 79 | .cursor_mgr = try wlr.XcursorManager.create(null, 24), 80 | 81 | // listeners 82 | .request_set_listener = wl.Listener(*wlr.Seat.event.RequestSetCursor).init( 83 | onRequestSet, 84 | ), 85 | 86 | .motion_listener = wl.Listener(*wlr.Pointer.event.Motion).init( 87 | onMotion, 88 | ), 89 | .motion_absolute_listener = wl.Listener(*wlr.Pointer.event.MotionAbsolute).init( 90 | onMotionAbsolute, 91 | ), 92 | .button_listener = wl.Listener(*wlr.Pointer.event.Button).init( 93 | onButton, 94 | ), 95 | .axis_listener = wl.Listener(*wlr.Pointer.event.Axis).init( 96 | onAxis, 97 | ), 98 | .frame_listener = wl.Listener(*wlr.Cursor).init( 99 | onFrame, 100 | ), 101 | }; 102 | 103 | self.wlr_cursor.attachOutputLayout(server.output_layout); 104 | 105 | // TODO: actually handle scaling correctly 106 | try self.cursor_mgr.load(1); 107 | 108 | self.seat.events.request_set_cursor.add(&self.request_set_listener); 109 | 110 | self.wlr_cursor.events.motion.add(&self.motion_listener); 111 | self.wlr_cursor.events.motion_absolute.add(&self.motion_absolute_listener); 112 | self.wlr_cursor.events.button.add(&self.button_listener); 113 | self.wlr_cursor.events.axis.add(&self.axis_listener); 114 | self.wlr_cursor.events.frame.add(&self.frame_listener); 115 | } 116 | 117 | //fn destroy(self: *Cursor) {} 118 | 119 | pub fn attachInputDevice(self: *Cursor, input_device: *wlr.InputDevice) void { 120 | self.wlr_cursor.attachInputDevice(input_device); 121 | } 122 | 123 | fn onRequestSet( 124 | listener: *wl.Listener(*wlr.Seat.event.RequestSetCursor), 125 | event: *wlr.Seat.event.RequestSetCursor, 126 | ) void { 127 | const self = @fieldParentPtr(Cursor, "request_set_listener", listener); 128 | 129 | if (event.seat_client == self.seat.pointer_state.focused_client) { 130 | self.wlr_cursor.setSurface( 131 | event.surface, 132 | event.hotspot_x, 133 | event.hotspot_y, 134 | ); 135 | } 136 | } 137 | 138 | fn onMotion( 139 | listener: *wl.Listener(*wlr.Pointer.event.Motion), 140 | event: *wlr.Pointer.event.Motion, 141 | ) void { 142 | const self = @fieldParentPtr(Cursor, "motion_listener", listener); 143 | 144 | self.wlr_cursor.move(event.device, event.delta_x, event.delta_y); 145 | self.processMotion(event.time_msec); 146 | } 147 | 148 | fn onMotionAbsolute( 149 | listener: *wl.Listener(*wlr.Pointer.event.MotionAbsolute), 150 | event: *wlr.Pointer.event.MotionAbsolute, 151 | ) void { 152 | const self = @fieldParentPtr(Cursor, "motion_absolute_listener", listener); 153 | 154 | self.wlr_cursor.warpAbsolute(event.device, event.x, event.y); 155 | self.processMotion(event.time_msec); 156 | } 157 | 158 | fn processMotion(self: *Cursor, time_msec: u32) void { 159 | switch (self.mode) { 160 | .passthrough => if (self.server.viewAt( 161 | self.wlr_cursor.x, 162 | self.wlr_cursor.y, 163 | )) |result| { 164 | self.seat.pointerNotifyEnter( 165 | result.surface, 166 | result.relative_x, 167 | result.relative_y, 168 | ); 169 | self.seat.pointerNotifyMotion( 170 | time_msec, 171 | result.relative_x, 172 | result.relative_y, 173 | ); 174 | } else { 175 | self.cursor_mgr.setCursorImage("left_ptr", self.wlr_cursor); 176 | self.seat.pointerClearFocus(); 177 | }, 178 | } 179 | } 180 | 181 | fn onButton( 182 | listener: *wl.Listener(*wlr.Pointer.event.Button), 183 | event: *wlr.Pointer.event.Button, 184 | ) void { 185 | const self = @fieldParentPtr(Cursor, "button_listener", listener); 186 | 187 | _ = self.seat.pointerNotifyButton(event.time_msec, event.button, event.state); 188 | if (event.state == .released) { 189 | self.mode = .passthrough; 190 | } else if (self.server.viewAt(self.wlr_cursor.x, self.wlr_cursor.y)) |result| { 191 | _ = result; 192 | // self.server.focusView(result.view, result.surface); 193 | } 194 | } 195 | 196 | fn onAxis( 197 | listener: *wl.Listener(*wlr.Pointer.event.Axis), 198 | event: *wlr.Pointer.event.Axis, 199 | ) void { 200 | const self = @fieldParentPtr(Cursor, "axis_listener", listener); 201 | self.seat.pointerNotifyAxis( 202 | event.time_msec, 203 | event.orientation, 204 | event.delta, 205 | event.delta_discrete, 206 | event.source, 207 | ); 208 | } 209 | 210 | fn onFrame( 211 | listener: *wl.Listener(*wlr.Cursor), 212 | _: *wlr.Cursor, 213 | ) void { 214 | const self = @fieldParentPtr(Cursor, "frame_listener", listener); 215 | self.seat.pointerNotifyFrame(); 216 | } 217 | 218 | // } 219 | -------------------------------------------------------------------------------- /src/Keyboard.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | 4 | const wl = @import("wayland").server.wl; 5 | 6 | const wlr = @import("wlroots"); 7 | const xkb = @import("xkbcommon"); 8 | 9 | const C = @import("C.zig"); 10 | 11 | const ally = std.heap.c_allocator; 12 | 13 | const Server = @import("Server.zig"); 14 | 15 | pub var scm_keyboard_type: C.SCM = undefined; 16 | 17 | pub fn scm_init() void { 18 | _ = C.scm_c_define_module( 19 | "wl keyboard internal", 20 | scm_initModule, 21 | null, 22 | ); 23 | } 24 | 25 | fn scm_initModule(_: ?*anyopaque) callconv(.C) void { 26 | scm_keyboard_type = C.scm_make_foreign_object_type( 27 | C.scm_from_utf8_symbol("keyboard"), 28 | C.scm_list_1( 29 | C.scm_from_utf8_symbol("ptr"), 30 | ), 31 | null, 32 | ); 33 | } 34 | 35 | pub fn keyboardToScm(keyboard: *Keyboard) C.SCM { 36 | return C.scm_make_foreign_object_1( 37 | scm_keyboard_type, 38 | keyboard, 39 | ); 40 | } 41 | 42 | pub fn scmToKeyboard(scm_keyboard: C.SCM) *Keyboard { 43 | C.scm_assert_foreign_object_type(scm_keyboard_type, scm_keyboard); 44 | 45 | return @ptrCast( 46 | *Keyboard, 47 | @alignCast( 48 | @alignOf(*Keyboard), 49 | C.scm_foreign_object_ref(scm_keyboard, 0).?, 50 | ), 51 | ); 52 | } 53 | 54 | const Keyboard = @This(); // { 55 | 56 | server: *Server, 57 | seat: *wlr.Seat, 58 | 59 | link: wl.list.Link = undefined, 60 | input_device: *wlr.InputDevice, 61 | 62 | modifiers_listener: wl.Listener(*wlr.Keyboard), 63 | key_listener: wl.Listener(*wlr.Keyboard.event.Key), 64 | 65 | fn create(server: *Server, input_device: *wlr.InputDevice) !*Keyboard { 66 | const self = try ally.create(Keyboard); 67 | errdefer ally.destroy(self); 68 | 69 | self.* = .{ 70 | .server = server, 71 | .seat = server.seat, 72 | 73 | .input_device = input_device, 74 | 75 | // listeners 76 | .modifiers_listener = wl.Listener(*wlr.Keyboard).init( 77 | onModifiers, 78 | ), 79 | .key_listener = wl.Listener(*wlr.Keyboard.event.Key).init( 80 | onKey, 81 | ), 82 | }; 83 | 84 | const context = xkb.Context.new(.no_flags) orelse { 85 | return error.ContextFailed; 86 | }; 87 | defer context.unref(); 88 | const keymap = xkb.Keymap.newFromString( 89 | context, 90 | .{}, 91 | .no_flags, 92 | ) orelse return error.KeymapFailed; 93 | defer keymap.unref(); 94 | 95 | const wlr_keyboard = self.input_device.device.keyboard; 96 | if (!wlr_keyboard.setKeymap(keymap)) return error.SetKeymapFailed; 97 | wlr_keyboard.setRepeatInfo(25, 600); 98 | 99 | wlr_keyboard.events.modifiers.add(&self.modifiers_listener); 100 | wlr_keyboard.events.key.add(&self.key_listener); 101 | 102 | self.seat.setKeyboard(input_device); 103 | } 104 | 105 | fn onModifiers(listener: *wl.Listener(*wlr.Keyboard), wlr_keyboard: *wlr.Keyboard) void { 106 | const self = @fieldParentPtr(Keyboard, "modifiers_listener", listener); 107 | 108 | self.seat.setKeyboard(keyboard.device); 109 | self.seat.keyboardNotifyModifiers(&wlr_keyboard.modifiers); 110 | } 111 | 112 | fn onKey( 113 | listener: *wl.Listener(*wlr.Keyboard.event.Key), 114 | event: *wlr.Keyboard.event.Key, 115 | ) void { 116 | const self = @fieldParentPtr(Keyboard, "key_listener", listener); 117 | const wlr_keyboard = self.input_device.device.keyboard; 118 | 119 | // // Translate libinput keycode -> xkbcommon 120 | // const keycode = event.keycode + 8; 121 | 122 | var handled = false; 123 | 124 | // potentially handle key 125 | 126 | if (!handled) { 127 | self.seat.setKeyboard(keyboard.device); 128 | self.seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Output.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | 4 | const wl = @import("wayland").server.wl; 5 | 6 | const wlr = @import("wlroots"); 7 | const xkb = @import("xkbcommon"); 8 | 9 | const C = @import("C.zig"); 10 | 11 | const ally = std.heap.c_allocator; 12 | 13 | const Server = @import("Server.zig"); 14 | 15 | pub var scm_output_type: C.SCM = undefined; 16 | 17 | pub fn scm_init() void { 18 | _ = C.scm_c_define_module( 19 | "wl output internal", 20 | scm_initModule, 21 | null, 22 | ); 23 | } 24 | 25 | fn scm_initModule(_: ?*anyopaque) callconv(.C) void { 26 | scm_output_type = C.scm_make_foreign_object_type( 27 | C.scm_from_utf8_symbol("output"), 28 | C.scm_list_1( 29 | C.scm_from_utf8_symbol("ptr"), 30 | ), 31 | // All resources but the server are managed by wayland. 32 | null, 33 | ); 34 | } 35 | 36 | pub fn outputToScm(output: *Output) C.SCM { 37 | return C.scm_make_foreign_object_1( 38 | scm_output_type, 39 | output, 40 | ); 41 | } 42 | 43 | pub fn scmToOutput(scm_output: C.SCM) *Output { 44 | C.scm_assert_foreign_object_type(scm_output_type, scm_output); 45 | 46 | return @ptrCast( 47 | *Output, 48 | @alignCast( 49 | @alignOf(*Output), 50 | C.scm_foreign_object_ref(scm_output, 0).?, 51 | ), 52 | ); 53 | } 54 | 55 | const Output = @This(); // { 56 | 57 | server: *Server, 58 | 59 | link: wl.list.Link = undefined, 60 | wlr_output: *wlr.Output, 61 | 62 | frame_listener: wl.Listener(*wlr.Output), 63 | destroy_listener: wl.Listener(*wlr.Output), 64 | 65 | pub fn create(server: *Server, wlr_output: *wlr.Output) !*Output { 66 | const self = try ally.create(Output); 67 | errdefer ally.destroy(self); 68 | 69 | self.* = .{ 70 | .server = server, 71 | 72 | .wlr_output = wlr_output, 73 | 74 | // listeners 75 | .frame_listener = wl.Listener(*wlr.Output).init(onFrame), 76 | .destroy_listener = wl.Listener(*wlr.Output).init(onDestroy), 77 | }; 78 | 79 | wlr_output.events.frame.add(&self.frame_listener); 80 | wlr_output.events.destroy.add(&self.destroy_listener); 81 | 82 | return self; 83 | } 84 | 85 | pub fn bindScmFunction( 86 | self: *Output, 87 | scm_event_symbol: C.SCM, 88 | scm_procedure: C.SCM, 89 | ) C.SCM { 90 | _ = self; 91 | _ = scm_event_symbol; 92 | _ = scm_procedure; 93 | 94 | // Currently nothing worth binding. 95 | 96 | return C.SCM_BOOL_F; 97 | } 98 | 99 | fn onFrame( 100 | listener: *wl.Listener(*wlr.Output), 101 | wlr_output: *wlr.Output, 102 | ) void { 103 | const self = @fieldParentPtr(Output, "frame_listener", listener); 104 | 105 | const scene_output = self.server.scene.getSceneOutput(wlr_output).?; 106 | _ = scene_output.commit(); 107 | 108 | var now: os.timespec = undefined; 109 | os.clock_gettime(os.CLOCK.MONOTONIC, &now) catch { 110 | @panic("CLOCK_MONOTONIC not supported"); 111 | }; 112 | scene_output.sendFrameDone(&now); 113 | } 114 | 115 | fn onDestroy( 116 | listener: *wl.Listener(*wlr.Output), 117 | _: *wlr.Output, 118 | ) void { 119 | const self = @fieldParentPtr(Output, "destroy_listener", listener); 120 | 121 | self.frame_listener.link.remove(); 122 | self.destroy_listener.link.remove(); 123 | 124 | self.link.remove(); 125 | 126 | ally.destroy(self); 127 | } 128 | -------------------------------------------------------------------------------- /src/Server.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | 4 | const wl = @import("wayland").server.wl; 5 | 6 | const wlr = @import("wlroots"); 7 | const xkb = @import("xkbcommon"); 8 | 9 | const C = @import("C.zig"); 10 | 11 | const ally = std.heap.c_allocator; 12 | 13 | const Output = @import("Output.zig"); 14 | const View = @import("View.zig"); 15 | const Keyboard = @import("Keyboard.zig"); 16 | const Cursor = @import("Cursor.zig"); 17 | 18 | pub var scm_server_type: C.SCM = undefined; 19 | 20 | pub fn scm_init() void { 21 | _ = C.scm_c_define_module( 22 | "wl server internal", 23 | scm_initModule, 24 | null, 25 | ); 26 | } 27 | 28 | fn scm_initModule(_: ?*anyopaque) callconv(.C) void { 29 | scm_server_type = C.scm_make_foreign_object_type( 30 | C.scm_from_utf8_symbol("server"), 31 | C.scm_list_1( 32 | C.scm_from_utf8_symbol("ptr"), 33 | ), 34 | finalizeServer, 35 | ); 36 | 37 | _ = C.scm_c_define_gsubr( 38 | "make-server", 39 | 0, 40 | 0, 41 | 0, 42 | @intToPtr(?*anyopaque, @ptrToInt(scm_makeServer)), 43 | ); 44 | 45 | _ = C.scm_c_define_gsubr( 46 | "server-outputs", 47 | 1, 48 | 0, 49 | 0, 50 | @intToPtr(?*anyopaque, @ptrToInt(scm_serverOutputs)), 51 | ); 52 | 53 | _ = C.scm_c_define_gsubr( 54 | "server-views", 55 | 1, 56 | 0, 57 | 0, 58 | @intToPtr(?*anyopaque, @ptrToInt(scm_serverViews)), 59 | ); 60 | 61 | _ = C.scm_c_define_gsubr( 62 | "server-socket", 63 | 1, 64 | 0, 65 | 0, 66 | @intToPtr(?*anyopaque, @ptrToInt(scm_serverSocket)), 67 | ); 68 | 69 | _ = C.scm_c_define_gsubr( 70 | "run-server", 71 | 1, 72 | 0, 73 | 0, 74 | @intToPtr(?*anyopaque, @ptrToInt(scm_runServer)), 75 | ); 76 | 77 | // scm_c_export is broken with @cImport so we use 78 | // scm_module_export instead. 79 | var scm_exports = C.SCM_EOL; 80 | for ([_][:0]const u8{ 81 | "make-server", 82 | "server-outputs", 83 | "server-views", 84 | "server-socket", 85 | "run-server", 86 | }) |symbol_name| { 87 | scm_exports = C.scm_cons( 88 | C.scm_from_utf8_symbol(symbol_name.ptr), 89 | scm_exports, 90 | ); 91 | } 92 | _ = C.scm_module_export( 93 | C.scm_current_module(), 94 | scm_exports, 95 | ); 96 | } 97 | 98 | fn scm_makeServer() callconv(.C) C.SCM { 99 | const server = Server.create() catch { 100 | C.scm_error( 101 | C.scm_misc_error_key, 102 | "make-server", 103 | "Failed to create server", 104 | null, 105 | C.SCM_BOOL_F, 106 | ); 107 | }; 108 | 109 | return serverToScm(server); 110 | } 111 | 112 | fn scm_serverOutputs(scm_server: C.SCM) callconv(.C) C.SCM { 113 | const server = scmToServer(scm_server); 114 | 115 | var list = C.SCM_EOL; 116 | var iter = server.outputs.iterator(.reverse); 117 | while (iter.next()) |output| { 118 | list = C.scm_cons( 119 | Output.outputToScm(output), 120 | list, 121 | ); 122 | } 123 | 124 | return list; 125 | } 126 | 127 | fn scm_serverViews(scm_server: C.SCM) callconv(.C) C.SCM { 128 | const server = scmToServer(scm_server); 129 | 130 | var list = C.SCM_EOL; 131 | var iter = server.views.iterator(.reverse); 132 | while (iter.next()) |view| { 133 | list = C.scm_cons( 134 | View.viewToScm(view), 135 | list, 136 | ); 137 | } 138 | 139 | return list; 140 | } 141 | 142 | fn scm_serverSocket(scm_server: C.SCM) callconv(.C) C.SCM { 143 | const server = scmToServer(scm_server); 144 | 145 | return C.scm_from_utf8_string(server.socket[0..server.socket.len]); 146 | } 147 | 148 | fn scm_runServer(scm_server: C.SCM) callconv(.C) C.SCM { 149 | const server = scmToServer(scm_server); 150 | 151 | server.backend.start() catch { 152 | C.scm_error( 153 | C.scm_misc_error_key, 154 | "run-server", 155 | "Failed to start server backend", 156 | null, 157 | C.SCM_BOOL_F, 158 | ); 159 | }; 160 | 161 | server.wl_server.run(); 162 | 163 | return scm_server; 164 | } 165 | 166 | fn finalizeServer(scm_server: C.SCM) callconv(.C) void { 167 | const server = scmToServer(scm_server); 168 | server.destroy(); 169 | } 170 | 171 | pub fn serverToScm(server: *Server) C.SCM { 172 | return C.scm_make_foreign_object_1( 173 | scm_server_type, 174 | server, 175 | ); 176 | } 177 | 178 | pub fn scmToServer(scm_server: C.SCM) *Server { 179 | C.scm_assert_foreign_object_type(scm_server_type, scm_server); 180 | 181 | return @ptrCast( 182 | *Server, 183 | @alignCast( 184 | @alignOf(*Server), 185 | C.scm_foreign_object_ref(scm_server, 0).?, 186 | ), 187 | ); 188 | } 189 | 190 | const Server = @This(); // { 191 | 192 | // Filled with 0s since guile likes null terminated strings 193 | socket: [11]u8 = [11]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 194 | 195 | wl_server: *wl.Server, 196 | backend: *wlr.Backend, 197 | renderer: *wlr.Renderer, 198 | allocator: *wlr.Allocator, 199 | scene: *wlr.Scene, 200 | 201 | output_layout: *wlr.OutputLayout, 202 | xdg_shell: *wlr.XdgShell, 203 | seat: *wlr.Seat, 204 | 205 | outputs: wl.list.Head(Output, "link") = undefined, 206 | views: wl.list.Head(View, "link") = undefined, 207 | keyboards: wl.list.Head(Keyboard, "link") = undefined, 208 | cursor: Cursor = undefined, 209 | 210 | new_output_listener: wl.Listener(*wlr.Output), 211 | on_new_output_function: ?C.SCM = null, 212 | new_xdg_surface_listener: wl.Listener(*wlr.XdgSurface), 213 | on_new_view_function: ?C.SCM = null, 214 | new_input_listener: wl.Listener(*wlr.InputDevice), 215 | 216 | pub fn create() !*Server { 217 | const self = try ally.create(Server); 218 | errdefer ally.destroy(self); 219 | 220 | const wl_server = try wl.Server.create(); 221 | const backend = try wlr.Backend.autocreate(wl_server); 222 | const renderer = try wlr.Renderer.autocreate(backend); 223 | 224 | var buf: [11]u8 = undefined; 225 | const socket = try wl_server.addSocketAuto(&buf); 226 | 227 | self.* = .{ 228 | .wl_server = wl_server, 229 | .backend = backend, 230 | .renderer = renderer, 231 | .allocator = try wlr.Allocator.autocreate(backend, renderer), 232 | .scene = try wlr.Scene.create(), 233 | 234 | .output_layout = try wlr.OutputLayout.create(), 235 | .xdg_shell = try wlr.XdgShell.create(wl_server), 236 | .seat = try wlr.Seat.create(wl_server, "default"), 237 | 238 | //listeners 239 | .new_output_listener = wl.Listener(*wlr.Output).init( 240 | onNewOutput, 241 | ), 242 | .new_xdg_surface_listener = wl.Listener(*wlr.XdgSurface).init( 243 | onNewXdgSurface, 244 | ), 245 | .new_input_listener = wl.Listener(*wlr.InputDevice).init( 246 | onNewInput, 247 | ), 248 | }; 249 | 250 | // Copy the socket name into our server struct so it isn't 251 | // destroyed with our stack. 252 | for (socket[0..socket.len]) |b, i| self.socket[i] = b; 253 | 254 | try renderer.initServer(wl_server); 255 | try self.scene.attachOutputLayout(self.output_layout); 256 | 257 | _ = try wlr.Compositor.create(self.wl_server, self.renderer); 258 | _ = try wlr.DataDeviceManager.create(self.wl_server); 259 | 260 | self.backend.events.new_output.add(&self.new_output_listener); 261 | self.xdg_shell.events.new_surface.add(&self.new_xdg_surface_listener); 262 | self.backend.events.new_input.add(&self.new_input_listener); 263 | 264 | self.outputs.init(); 265 | self.views.init(); 266 | self.keyboards.init(); 267 | try self.cursor.init(self); 268 | 269 | return self; 270 | } 271 | 272 | pub fn destroy(self: *Server) void { 273 | self.wl_server.destroyClients(); 274 | self.wl_server.destroy(); 275 | 276 | ally.destroy(self); 277 | } 278 | 279 | pub fn bindScmFunction( 280 | self: *Server, 281 | scm_event_symbol: C.SCM, 282 | scm_procedure: C.SCM, 283 | ) C.SCM { 284 | inline for ([_][2][:0]const u8{ 285 | // symbol name field name 286 | .{ "new-output", "on_new_output_function" }, 287 | .{ "new-view", "on_new_view_function" }, 288 | }) |event| { 289 | if (C.SCM_BOOL_T == C.scm_eqv_p( 290 | scm_event_symbol, 291 | C.scm_from_utf8_symbol(event[0].ptr), 292 | )) { 293 | if (@field(self, event[1])) |scm_fn| { 294 | _ = C.scm_gc_unprotect_object(scm_fn); 295 | } 296 | @field(self, event[1]) = C.scm_gc_protect_object( 297 | scm_procedure, 298 | ); 299 | return scm_procedure; 300 | } 301 | } 302 | // else: 303 | return C.SCM_BOOL_F; 304 | } 305 | 306 | pub const ViewAtResult = struct { 307 | view: *View, 308 | surface: *wlr.Surface, 309 | relative_x: f64, 310 | relative_y: f64, 311 | }; 312 | 313 | pub fn viewAt(self: *Server, absolute_x: f64, absolute_y: f64) ?ViewAtResult { 314 | var relative_y: f64 = undefined; 315 | var relative_x: f64 = undefined; 316 | 317 | if (self.scene.node.at( 318 | absolute_x, 319 | absolute_y, 320 | &relative_x, 321 | &relative_y, 322 | )) |node| { 323 | if (node.type != .surface) return null; 324 | const surface = wlr.SceneSurface.fromNode(node).surface; 325 | 326 | var it: ?*wlr.SceneNode = node; 327 | while (it) |n| : (it = n.parent) { 328 | if (@intToPtr(?*View, n.data)) |view| { 329 | return ViewAtResult{ 330 | .view = view, 331 | .surface = surface, 332 | .relative_x = relative_x, 333 | .relative_y = relative_y, 334 | }; 335 | } 336 | } 337 | } 338 | 339 | return null; 340 | } 341 | 342 | fn onNewOutput( 343 | listener: *wl.Listener(*wlr.Output), 344 | wlr_output: *wlr.Output, 345 | ) void { 346 | const self = @fieldParentPtr(Server, "new_output_listener", listener); 347 | 348 | if (!wlr_output.initRender(self.allocator, self.renderer)) { 349 | std.log.err("Failed to init renderer for output.", .{}); 350 | return; 351 | } 352 | 353 | if (wlr_output.preferredMode()) |mode| { 354 | wlr_output.setMode(mode); 355 | wlr_output.enable(true); 356 | wlr_output.commit() catch |err| { 357 | std.log.err("Failed to set output mode:\n{}", .{err}); 358 | if (@errorReturnTrace()) |trace| { 359 | std.debug.dumpStackTrace(trace.*); 360 | } 361 | return; 362 | }; 363 | } 364 | 365 | //_ = try self.cursor.cursor_mgr.load(wlr_output.scale); 366 | 367 | const output = Output.create(self, wlr_output) catch |err| { 368 | std.log.err("Failed to create output:\n{}", .{err}); 369 | if (@errorReturnTrace()) |trace| { 370 | std.debug.dumpStackTrace(trace.*); 371 | } 372 | return; 373 | }; 374 | 375 | self.outputs.prepend(output); 376 | self.output_layout.addAuto(wlr_output); 377 | 378 | // FIXME: handle guile errors so a scheme error doesn't cause a 379 | // crash. 380 | if (self.on_new_output_function) |scm_fn| { 381 | _ = C.scm_call_2( 382 | scm_fn, 383 | serverToScm(self), 384 | Output.outputToScm(output), 385 | ); 386 | } 387 | } 388 | 389 | fn onNewXdgSurface( 390 | listener: *wl.Listener(*wlr.XdgSurface), 391 | xdg_surface: *wlr.XdgSurface, 392 | ) void { 393 | const self = @fieldParentPtr(Server, "new_xdg_surface_listener", listener); 394 | 395 | switch (xdg_surface.role) { 396 | .toplevel => { 397 | const view = View.create(self, xdg_surface) catch |err| { 398 | std.log.err("Failed to create view:\n{}", .{err}); 399 | if (@errorReturnTrace()) |trace| { 400 | std.debug.dumpStackTrace(trace.*); 401 | } 402 | return; 403 | }; 404 | 405 | self.views.prepend(view); 406 | 407 | // FIXME: handle guile errors so a scheme error doesn't 408 | // cause a crash. 409 | if (self.on_new_view_function) |scm_fn| { 410 | _ = C.scm_call_2( 411 | scm_fn, 412 | serverToScm(self), 413 | View.viewToScm(view), 414 | ); 415 | } 416 | }, 417 | .popup => { 418 | // Since we only support XDG shell this is a safe assert 419 | const parent = wlr.XdgSurface.fromWlrSurface( 420 | xdg_surface.role_data.popup.parent.?, 421 | ); 422 | const parent_node = @intToPtr( 423 | ?*wlr.SceneNode, 424 | parent.data, 425 | ) orelse return; 426 | const scene_node = parent_node.createSceneXdgSurface( 427 | xdg_surface, 428 | ) catch |err| { 429 | std.log.err("Failed to allocate XDG popup node:\n{}", .{err}); 430 | if (@errorReturnTrace()) |trace| { 431 | std.debug.dumpStackTrace(trace.*); 432 | } 433 | return; 434 | }; 435 | xdg_surface.data = @ptrToInt(scene_node); 436 | }, 437 | .none => unreachable, 438 | } 439 | } 440 | 441 | fn onNewInput( 442 | listener: *wl.Listener(*wlr.InputDevice), 443 | input_device: *wlr.InputDevice, 444 | ) void { 445 | const self = @fieldParentPtr(Server, "new_input_listener", listener); 446 | switch (input_device.type) { 447 | .keyboard => { 448 | const keyboard = Keyboard.create(self, input_device) catch |err| { 449 | std.log.err("Failed to create keyboard:\n{}", .{err}); 450 | if (@errorReturnTrace()) |trace| { 451 | std.debug.dumpStackTrace(trace.*); 452 | } 453 | return; 454 | }; 455 | 456 | self.keyboards.prepend(keyboard); 457 | }, 458 | .pointer => self.cursor.attachInputDevice(input_device), 459 | else => {}, 460 | } 461 | 462 | self.seat.setCapabilities(.{ 463 | .pointer = true, 464 | .keyboard = false, 465 | }); 466 | } 467 | -------------------------------------------------------------------------------- /src/View.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const wl = @import("wayland").server.wl; 4 | const wlr = @import("wlroots"); 5 | 6 | const C = @import("C.zig"); 7 | 8 | const ally = std.heap.c_allocator; 9 | 10 | const Server = @import("Server.zig"); 11 | 12 | pub var scm_view_type: C.SCM = undefined; 13 | 14 | pub fn scm_init() void { 15 | _ = C.scm_c_define_module( 16 | "wl view internal", 17 | scm_initModule, 18 | null, 19 | ); 20 | } 21 | 22 | fn scm_initModule(_: ?*anyopaque) callconv(.C) void { 23 | scm_view_type = C.scm_make_foreign_object_type( 24 | C.scm_from_utf8_symbol("view"), 25 | C.scm_list_1( 26 | C.scm_from_utf8_symbol("ptr"), 27 | ), 28 | // All resources but the server are managed by wayland. 29 | null, 30 | ); 31 | 32 | _ = C.scm_c_define_gsubr( 33 | "view-enable!", 34 | 1, 35 | 0, 36 | 0, 37 | @intToPtr(?*anyopaque, @ptrToInt(scm_viewEnableX)), 38 | ); 39 | 40 | _ = C.scm_c_define_gsubr( 41 | "view-disable!", 42 | 1, 43 | 0, 44 | 0, 45 | @intToPtr(?*anyopaque, @ptrToInt(scm_viewDisableX)), 46 | ); 47 | 48 | _ = C.scm_module_export( 49 | C.scm_current_module(), 50 | C.scm_list_2( 51 | C.scm_from_utf8_symbol("view-enable!"), 52 | C.scm_from_utf8_symbol("view-disable!"), 53 | ), 54 | ); 55 | } 56 | 57 | fn scm_viewEnableX(scm_view: C.SCM) callconv(.C) C.SCM { 58 | const view = scmToView(scm_view); 59 | 60 | view.setEnabled(true); 61 | return scm_view; 62 | } 63 | 64 | fn scm_viewDisableX(scm_view: C.SCM) callconv(.C) C.SCM { 65 | const view = scmToView(scm_view); 66 | 67 | view.setEnabled(false); 68 | return scm_view; 69 | } 70 | 71 | pub fn viewToScm(view: *View) C.SCM { 72 | return C.scm_make_foreign_object_1( 73 | scm_view_type, 74 | view, 75 | ); 76 | } 77 | 78 | pub fn scmToView(scm_view: C.SCM) *View { 79 | C.scm_assert_foreign_object_type(scm_view_type, scm_view); 80 | 81 | return @ptrCast( 82 | *View, 83 | @alignCast( 84 | @alignOf(*View), 85 | C.scm_foreign_object_ref(scm_view, 0).?, 86 | ), 87 | ); 88 | } 89 | 90 | const View = @This(); // { 91 | 92 | server: *Server, 93 | 94 | link: wl.list.Link = undefined, 95 | xdg_surface: *wlr.XdgSurface, 96 | scene_node: *wlr.SceneNode, 97 | 98 | mapped: bool = false, 99 | x: i32 = 0, 100 | y: i32 = 0, 101 | 102 | map_listener: wl.Listener(*wlr.XdgSurface), 103 | on_map_function: ?C.SCM = null, 104 | unmap_listener: wl.Listener(*wlr.XdgSurface), 105 | on_unmap_function: ?C.SCM = null, 106 | destroy_listener: wl.Listener(*wlr.XdgSurface), 107 | on_destroy_function: ?C.SCM = null, 108 | 109 | pub fn create(server: *Server, xdg_surface: *wlr.XdgSurface) !*View { 110 | const self = try ally.create(View); 111 | errdefer ally.destroy(self); 112 | 113 | self.* = .{ 114 | .server = server, 115 | 116 | .xdg_surface = xdg_surface, 117 | .scene_node = try server.scene.node.createSceneXdgSurface(xdg_surface), 118 | 119 | .map_listener = wl.Listener(*wlr.XdgSurface).init(onMap), 120 | .unmap_listener = wl.Listener(*wlr.XdgSurface).init(onUnmap), 121 | .destroy_listener = wl.Listener(*wlr.XdgSurface).init(onDestroy), 122 | }; 123 | 124 | self.scene_node.data = @ptrToInt(self); 125 | self.xdg_surface.data = @ptrToInt(self.scene_node); 126 | 127 | self.xdg_surface.events.map.add(&self.map_listener); 128 | self.xdg_surface.events.unmap.add(&self.unmap_listener); 129 | self.xdg_surface.events.destroy.add(&self.destroy_listener); 130 | 131 | return self; 132 | } 133 | 134 | pub fn bindScmFunction( 135 | self: *View, 136 | scm_event_symbol: C.SCM, 137 | scm_procedure: C.SCM, 138 | ) C.SCM { 139 | inline for ([_][2][:0]const u8{ 140 | // symbol name field name 141 | .{ "map", "on_map_function" }, 142 | .{ "unmap", "on_unmap_function" }, 143 | .{ "destroy", "on_destroy_function" }, 144 | }) |event| { 145 | if (C.SCM_BOOL_T == C.scm_eqv_p( 146 | scm_event_symbol, 147 | C.scm_from_utf8_symbol(event[0].ptr), 148 | )) { 149 | if (@field(self, event[1])) |scm_fn| { 150 | _ = C.scm_gc_unprotect_object(scm_fn); 151 | } 152 | @field(self, event[1]) = C.scm_gc_protect_object( 153 | scm_procedure, 154 | ); 155 | return scm_procedure; 156 | } 157 | } 158 | // else: 159 | return C.SCM_BOOL_F; 160 | } 161 | 162 | pub fn setEnabled(self: *View, enabled: bool) void { 163 | self.scene_node.setEnabled(enabled); 164 | } 165 | 166 | fn onMap( 167 | listener: *wl.Listener(*wlr.XdgSurface), 168 | xdg_surface: *wlr.XdgSurface, 169 | ) void { 170 | const self = @fieldParentPtr(View, "map_listener", listener); 171 | 172 | self.mapped = true; 173 | self.setEnabled(false); 174 | 175 | // FIXME: handle guile errors so a scheme error doesn't cause a 176 | // crash. 177 | if (self.on_map_function) |scm_fn| { 178 | _ = C.scm_call_1( 179 | scm_fn, 180 | viewToScm(self), 181 | ); 182 | } 183 | 184 | // temporary 185 | // focus 186 | if (self.server.seat.keyboard_state.focused_surface) |previous_surface| { 187 | if (previous_surface == xdg_surface.surface) return; 188 | if (previous_surface.isXdgSurface()) { 189 | const prev_xdg_surface = wlr.XdgSurface.fromWlrSurface(previous_surface); 190 | _ = prev_xdg_surface.role_data.toplevel.setActivated(false); 191 | } 192 | } 193 | self.scene_node.raiseToTop(); 194 | _ = xdg_surface.role_data.toplevel.setActivated(true); 195 | } 196 | 197 | fn onUnmap( 198 | listener: *wl.Listener(*wlr.XdgSurface), 199 | _: *wlr.XdgSurface, 200 | ) void { 201 | const self = @fieldParentPtr(View, "unmap_listener", listener); 202 | 203 | self.mapped = false; 204 | 205 | // FIXME: handle guile errors so a scheme error doesn't cause a 206 | // crash. 207 | if (self.on_unmap_function) |scm_fn| { 208 | _ = C.scm_call_1( 209 | scm_fn, 210 | viewToScm(self), 211 | ); 212 | } 213 | } 214 | 215 | fn onDestroy( 216 | listener: *wl.Listener(*wlr.XdgSurface), 217 | _: *wlr.XdgSurface, 218 | ) void { 219 | const self = @fieldParentPtr(View, "destroy_listener", listener); 220 | 221 | // FIXME: handle guile errors so a scheme error doesn't cause a 222 | // crash. 223 | if (self.on_destroy_function) |scm_fn| { 224 | _ = C.scm_call_1( 225 | scm_fn, 226 | viewToScm(self), 227 | ); 228 | } 229 | 230 | self.map_listener.link.remove(); 231 | self.unmap_listener.link.remove(); 232 | self.destroy_listener.link.remove(); 233 | 234 | self.link.remove(); 235 | 236 | ally.destroy(self); 237 | } 238 | 239 | // } 240 | -------------------------------------------------------------------------------- /src/glue.c: -------------------------------------------------------------------------------- 1 | /* 2 | Zig has issues translating some macros, so this file defines glue 3 | functions that wrap the macros we need. 4 | */ 5 | 6 | #include 7 | 8 | extern int 9 | scm_is_a_p (SCM val, SCM type) 10 | { 11 | return SCM_IS_A_P (val, type); 12 | } 13 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const wlr = @import("wlroots"); 3 | 4 | const build_options = @import("build_options"); 5 | 6 | const C = @import("C.zig"); 7 | 8 | const Server = @import("Server.zig"); 9 | const Output = @import("Output.zig"); 10 | const View = @import("View.zig"); 11 | const Cursor = @import("Cursor.zig"); 12 | const Keyboard = @import("Keyboard.zig"); 13 | 14 | const ally = std.heap.c_allocator; 15 | 16 | fn scm_init() void { 17 | _ = C.scm_c_define_module( 18 | "wl internal", 19 | scm_initModule, 20 | null, 21 | ); 22 | 23 | Server.scm_init(); 24 | Output.scm_init(); 25 | View.scm_init(); 26 | Cursor.scm_init(); 27 | Keyboard.scm_init(); 28 | } 29 | 30 | fn scm_initModule(_: ?*anyopaque) callconv(.C) void { 31 | _ = C.scm_c_define_gsubr( 32 | "bind!", 33 | 3, 34 | 0, 35 | 0, 36 | @intToPtr(?*anyopaque, @ptrToInt(scm_bindX)), 37 | ); 38 | 39 | _ = C.scm_module_export( 40 | C.scm_current_module(), 41 | C.scm_list_1( 42 | C.scm_from_utf8_symbol("bind!"), 43 | ), 44 | ); 45 | } 46 | 47 | fn scm_bindX( 48 | scm_wl_object: C.SCM, 49 | scm_event_symbol: C.SCM, 50 | scm_procedure: C.SCM, 51 | ) C.SCM { 52 | if (C.scm_is_a_p(scm_wl_object, Server.scm_server_type)) { 53 | return Server.scmToServer(scm_wl_object).bindScmFunction( 54 | scm_event_symbol, 55 | scm_procedure, 56 | ); 57 | } 58 | if (C.scm_is_a_p(scm_wl_object, Output.scm_output_type)) { 59 | return Output.scmToOutput(scm_wl_object).bindScmFunction( 60 | scm_event_symbol, 61 | scm_procedure, 62 | ); 63 | } 64 | if (C.scm_is_a_p(scm_wl_object, View.scm_view_type)) { 65 | return View.scmToView(scm_wl_object).bindScmFunction( 66 | scm_event_symbol, 67 | scm_procedure, 68 | ); 69 | } 70 | 71 | // no match 72 | return C.SCM_BOOL_F; 73 | } 74 | 75 | pub fn main() anyerror!void { 76 | wlr.log.init(.debug); 77 | 78 | scm_init(); 79 | 80 | const scm_load_path = C.scm_c_lookup("%load-path"); 81 | 82 | _ = C.scm_variable_set_x( 83 | scm_load_path, 84 | C.scm_cons( 85 | C.scm_from_utf8_string(build_options.scheme_dir.ptr), 86 | C.scm_variable_ref(scm_load_path), 87 | ), 88 | ); 89 | 90 | // We want an error if this fails to load, so we don't use a 91 | // handler. 92 | _ = C.scm_c_primitive_load( 93 | try std.fs.path.joinZ(ally, &.{ build_options.scheme_dir, "startup.scm" }), 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/wrapper.zig: -------------------------------------------------------------------------------- 1 | //! Since Guile requires programs embedding it to enter [guile 2 | //! mode](https://www.gnu.org/software/guile/manual/html_node/Initialization.html), 3 | //! wrapper.zig's `main` is the actual entrypoint of our program and it 4 | //! enters guile mode with `scm_with_guile` before calling main.zig's 5 | //! `main` function. 6 | 7 | const std = @import("std"); 8 | const C = @import("C.zig"); 9 | 10 | const guile_mode_main = @import("main.zig").main; 11 | 12 | pub fn main() anyerror!void { 13 | _ = C.scm_with_guile(innerMain, null); 14 | } 15 | 16 | fn innerMain(_: ?*anyopaque) callconv(.C) ?*anyopaque { 17 | switch (@typeInfo(@TypeOf(guile_mode_main))) { 18 | .Fn => |fn_info| { 19 | if (fn_info.return_type) |return_type| { 20 | switch (@typeInfo(return_type)) { 21 | .Void => guile_mode_main(), 22 | .ErrorUnion => guile_mode_main() catch |err| { 23 | std.debug.print("{}\n", .{err}); 24 | if (@errorReturnTrace()) |trace| { 25 | std.debug.dumpStackTrace(trace.*); 26 | } 27 | }, 28 | else => unreachable, 29 | } 30 | } else { 31 | guile_mode_main(); 32 | } 33 | }, 34 | else => @compileError("main must be a function"), 35 | } 36 | return null; 37 | } 38 | --------------------------------------------------------------------------------