diff --git a/api.lua b/api.lua index 3b8d7fd..cb25ff9 100644 --- a/api.lua +++ b/api.lua @@ -80,4 +80,11 @@ mez.options = { } } +---------------- Keybinds ---------------- +mez.add_keybind("modifier", "keycode", function() + -- callback +end, { + -- additional options +}) + -- Virtual terminal switching diff --git a/build.zig b/build.zig index afb4a2a..ff1f01c 100644 --- a/build.zig +++ b/build.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const Scanner = @import("wayland").Scanner; @@ -6,6 +7,13 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + // TODO: this will probably change based on the install paths, make this a var + // that can be passed at comptime? + const runtime_path_prefix = switch (builtin.mode) { + .Debug => "runtime/", + else => "/usr/share", + }; + // If instead your goal is to create an executable, consider if users might // be interested in also being able to embed the core functionality of your // program in their own executable in order to avoid the overhead involved in @@ -33,6 +41,7 @@ pub fn build(b: *std.Build) void { const xkbcommon = b.dependency("xkbcommon", .{}).module("xkbcommon"); const pixman = b.dependency("pixman", .{}).module("pixman"); const wlroots = b.dependency("wlroots", .{}).module("wlroots"); + const zlua = b.dependency("zlua", .{}).module("zlua"); wlroots.addImport("wayland", wayland); wlroots.addImport("xkbcommon", xkbcommon); @@ -55,11 +64,16 @@ pub fn build(b: *std.Build) void { mez.root_module.addImport("wayland", wayland); mez.root_module.addImport("xkbcommon", xkbcommon); mez.root_module.addImport("wlroots", wlroots); + mez.root_module.addImport("zlua", zlua); mez.linkSystemLibrary("wayland-server"); mez.linkSystemLibrary("xkbcommon"); mez.linkSystemLibrary("pixman-1"); + const options = b.addOptions(); + options.addOption([]const u8, "runtime_path_prefix", runtime_path_prefix); + mez.root_module.addOptions("config", options); + b.installArtifact(mez); const run_step = b.step("run", "Run the app"); diff --git a/build.zig.zon b/build.zig.zon index 9715c07..6424066 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,6 +20,10 @@ .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.19.3.tar.gz", .hash = "wlroots-0.19.3-jmOlcuL_AwBHhLCwpFsXbTizE3q9BugFmGX-XIxqcPMc", }, + .zlua = .{ + .url = "git+https://github.com/natecraddock/ziglua#39f8df588d0864070deffa308ef575bf492777a0", + .hash = "zlua-0.1.0-hGRpC6E9BQDBGKPqzmCRsI6Xd8jH9KohccmX69-L6HyS", + }, }, .paths = .{ "build.zig", diff --git a/runtime/share/mezzaluna/init.lua b/runtime/share/mezzaluna/init.lua new file mode 100644 index 0000000..f11c827 --- /dev/null +++ b/runtime/share/mezzaluna/init.lua @@ -0,0 +1,16 @@ +local env_conf = os.getenv("XDG_CONFIG_HOME") +if not env_conf then + env_conf = os.getenv("HOME") + if not env_conf then + error("Couldn't determine potential config directory is $HOME set?") + end + env_conf = mez.fs.joinpath(env_conf, ".config") +end + +mez.path.config = mez.fs.joinpath(env_conf, "mez", "init.lua") +package.path = package.path..";"..mez.fs.joinpath(env_conf, "mez", "lua", "?.lua") + +-- this is an example +-- mez.api.add_keymap("ctrl", "a", function() +-- print("hello from my keymap") +-- end) diff --git a/src/keyboard.zig b/src/keyboard.zig index c3a6e2c..c2af9ea 100644 --- a/src/keyboard.zig +++ b/src/keyboard.zig @@ -6,6 +6,7 @@ const Keyboard = @This(); const std = @import("std"); const gpa = std.heap.c_allocator; const server = &@import("main.zig").server; +const Keymap = @import("keymap.zig"); const wl = @import("wayland").server.wl; const wlr = @import("wlroots"); @@ -76,17 +77,17 @@ fn handleModifiers(_: *wl.Listener(*wlr.Keyboard), wlr_keyboard: *wlr.Keyboard) fn handleKey(_: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void { // Translate libinput keycode -> xkbcommon - // const keycode = event.keycode + 8; + const keycode = event.keycode + 8; - // TODO: lua handle keybinds here - const handled = false; - if (server.keyboard.wlr_keyboard.getModifiers().alt and event.state == .pressed) { - // for (wlr_keyboard.xkb_state.?.keyGetSyms(keycode)) |sym| { - // if (keyboard.server.handleKeybind(sym)) { - // handled = true; - // break; - // } - // } + var handled = false; + const modifiers = server.keyboard.wlr_keyboard.getModifiers(); + for (server.keyboard.wlr_keyboard.xkb_state.?.keyGetSyms(keycode)) |sym| { + if (server.keymaps.get(Keymap.hash(modifiers, sym))) |map| { + if (event.state == .pressed and !map.options.on_release) { + map.callback(); + handled = true; + } + } } if (!handled) { diff --git a/src/keymap.zig b/src/keymap.zig new file mode 100644 index 0000000..c6363d6 --- /dev/null +++ b/src/keymap.zig @@ -0,0 +1,37 @@ +//! This is a simple way to define a keymap. To keep hashing consistent the +//! hash is generated here. +const Keymap = @This(); + +const std = @import("std"); + +const xkb = @import("xkbcommon"); +const wlr = @import("wlroots"); +const zlua = @import("zlua"); + +const Lua = &@import("main.zig").lua; + +modifier: wlr.Keyboard.ModifierMask, +keycode: xkb.Keysym, +/// This is the location of the lua function in the lua registry +lua_ref_idx: i32, +options: struct { + /// if false the callback is called on press + on_release: bool, +}, + +pub fn callback(self: *const Keymap) void { + const t = Lua.state.rawGetIndex(zlua.registry_index, self.lua_ref_idx); + if (t != zlua.LuaType.function) { + std.log.err("Failed to call keybind, it doesn't have a callback.", .{}); + return; + } + Lua.state.pushValue(1); + Lua.state.call(.{ .args = 0, .results = 0 }); + Lua.state.pop(-1); +} + +pub fn hash(modifier: wlr.Keyboard.ModifierMask, keycode: xkb.Keysym) u64 { + const mod_val: u32 = @bitCast(modifier); + const key_val: u32 = @intFromEnum(keycode); + return (@as(u64, mod_val) << 32) | @as(u64, key_val); +} diff --git a/src/lua/api.zig b/src/lua/api.zig new file mode 100644 index 0000000..0f097c6 --- /dev/null +++ b/src/lua/api.zig @@ -0,0 +1,91 @@ +const Api = @This(); + +const std = @import("std"); +const server = &@import("../main.zig").server; +const Keymap = @import("../keymap.zig"); + +const zlua = @import("zlua"); +const xkb = @import("xkbcommon"); +const wlr = @import("wlroots"); + +const gpa = std.heap.c_allocator; + +pub fn add_keymap(L: *zlua.Lua) i32 { + const nargs: i32 = L.getTop(); + if (nargs < 3) { + L.raiseErrorStr("Expected at least three arguments", .{}); + return 0; + } + + // ensure the first three agrs of the correct types + L.checkType(1, .string); + L.checkType(2, .string); + L.checkType(3, .function); + + var keymap: Keymap = undefined; + + const mod = L.toString(1) catch { + L.raiseErrorStr("Lua error check your config", .{}); + return 0; + }; + var it = std.mem.splitScalar(u8, mod, '|'); + var modifiers = wlr.Keyboard.ModifierMask{}; + while (it.next()) |m| { + // TODO: can we generate this at comptime? + if (std.mem.eql(u8, m, "shift")) { + modifiers.shift = true; + } else if (std.mem.eql(u8, m, "caps")) { + modifiers.caps = true; + } else if (std.mem.eql(u8, m, "ctrl")) { + modifiers.ctrl = true; + } else if (std.mem.eql(u8, m, "alt")) { + modifiers.alt = true; + } else if (std.mem.eql(u8, m, "mod2")) { + modifiers.mod2 = true; + } else if (std.mem.eql(u8, m, "mod3")) { + modifiers.mod3 = true; + } else if (std.mem.eql(u8, m, "logo")) { + modifiers.logo = true; + } else if (std.mem.eql(u8, m, "mod5")) { + modifiers.mod5 = true; + } + } + keymap.modifier = modifiers; + + const key = L.toString(2) catch { + L.raiseErrorStr("Lua error check your config", .{}); + return 0; + }; + keymap.keycode = xkb.Keysym.fromName(key, .no_flags); + + L.checkType(3, .function); + keymap.lua_ref_idx = L.ref(zlua.registry_index) catch { + L.raiseErrorStr("Lua error check your config", .{}); + return 0; + }; + + // FIXME: for som reason I can't seem to get this to validate that the 4th + // argument exists unless there's a 5th argument. It doesn't seem to matter + // what type the 5th is just that it's there. + if (nargs == 4) { + // L.checkType(4, .table); + // _ = L.pushString("on_release"); + // _ = L.getTable(4); + // const b = L.toBoolean(-1); + // L.pop(-1); + // L.pop(-1); + } + + const hash = Keymap.hash(keymap.modifier, keymap.keycode); + server.keymaps.put(hash, keymap) catch |err| { + std.log.err("Failed to add keymap to keymaps: {}", .{err}); + return 0; + }; + + return 0; +} + +pub fn get_keybind(L: *zlua.Lua) i32 { + _ = L; + return 0; +} diff --git a/src/lua/bridge.zig b/src/lua/bridge.zig new file mode 100644 index 0000000..37868d3 --- /dev/null +++ b/src/lua/bridge.zig @@ -0,0 +1,28 @@ +const Bridge = @This(); + +const std = @import("std"); +const Lua = @import("lua.zig"); + +const gpa = std.heap.c_allocator; + +pub fn getNestedField(L: *Lua, path: []u8) bool { + var tokens = std.mem.tokenizeScalar(u8, path, '.'); + var first = true; + + while (tokens.next()) |token| { + const tok = gpa.dupeZ(u8, token) catch return false; + if (first) { + _ = L.state.getGlobal(tok) catch return false; + first = false; + } else { + _ = L.state.getField(-1, tok); + L.state.remove(-2); + } + + if (L.state.isNil(-1)) { + return false; + } + } + + return true; +} diff --git a/src/lua/fs.zig b/src/lua/fs.zig new file mode 100644 index 0000000..3e5ea2c --- /dev/null +++ b/src/lua/fs.zig @@ -0,0 +1,43 @@ +const Fs = @This(); + +const std = @import("std"); +const zlua = @import("zlua"); + +const Lua = @import("lua.zig"); + +const gpa = std.heap.c_allocator; + +pub fn joinpath(L: *zlua.Lua) i32 { + const nargs: i32 = L.getTop(); + if (nargs < 2) { + L.raiseErrorStr("Expected at least two paths to join", .{}); + return 0; + } + + var paths = std.ArrayList([:0]const u8).initCapacity(gpa, @intCast(nargs)) catch { + return 0; + }; + defer paths.deinit(gpa); + + var i: u8 = 1; + while (i <= nargs) : (i += 1) { + if (!L.isString(i)) { + L.raiseErrorStr("Expected string at argument %d", .{i}); + return 0; + } + + const partial_path = L.toString(i) catch unreachable; + paths.append(gpa, partial_path) catch { + // TODO: tell lua? + return 0; + }; + } + + const final_path: []const u8 = std.fs.path.join(gpa, paths.items) catch { + // TODO: tell lua? + return 0; + }; + _ = L.pushString(final_path); + + return 1; +} diff --git a/src/lua/lua.zig b/src/lua/lua.zig new file mode 100644 index 0000000..e63df2d --- /dev/null +++ b/src/lua/lua.zig @@ -0,0 +1,84 @@ +const Lua = @This(); + +const std = @import("std"); +const config = @import("config"); +const zlua = @import("zlua"); + +const Bridge = @import("bridge.zig"); +const Fs = @import("fs.zig"); +const Api = @import("api.zig"); + +const gpa = std.heap.c_allocator; + +state: *zlua.Lua, + +fn loadRuntimeDir(self: *Lua) !void { + const tmppath = try std.fs.path.join(gpa, &[_][]const u8{ + config.runtime_path_prefix, + "share", + "mezzaluna", + "init.lua", + }); + const path = try gpa.dupeZ(u8, tmppath); + + self.state.doFile(path) catch { + const err = try self.state.toString(-1); + std.log.debug("Failed to run lua file: {s}", .{err}); + }; +} + +fn loadConfigDir(self: *Lua) !void { + const lua_path = "mez.path.config"; + if (!Bridge.getNestedField(self, @constCast(lua_path[0..]))) { + std.log.err("Config path not found. is your runtime dir setup?", .{}); + return; + } + const path = self.state.toString(-1) catch |err| { + std.log.err("Failed to pop the config path from the lua stack. {}", .{err}); + return; + }; + self.state.pop(-1); + try self.state.doFile(path); +} + +pub fn init(self: *Lua) !void { + self.state = try zlua.Lua.init(gpa); + errdefer self.state.deinit(); + self.state.openLibs(); + + { + self.state.newTable(); + defer _ = self.state.setGlobal("mez"); + { + self.state.newTable(); + defer _ = self.state.setField(-2, "path"); + } + { + const fs_funcs = zlua.fnRegsFromType(Fs); + self.state.newLib(fs_funcs); + self.state.setField(-2, "fs"); + } + { + const api_funcs = zlua.fnRegsFromType(Api); + self.state.newLib(api_funcs); + self.state.setField(-2, "api"); + } + } + + loadRuntimeDir(self) catch |err| { + if (err == error.LuaRuntime) { + std.log.warn("{s}", .{try self.state.toString(-1)}); + } + }; + loadConfigDir(self) catch |err| { + if (err == error.LuaRuntime) { + std.log.warn("{s}", .{try self.state.toString(-1)}); + } + }; + + std.log.debug("Loaded lua", .{}); +} + +pub fn deinit(self: *Lua) void { + self.state.deinit(); +} diff --git a/src/main.zig b/src/main.zig index 604a51d..ab4bb16 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,10 +2,12 @@ const std = @import("std"); const wlr = @import("wlroots"); const Server = @import("server.zig"); +const Lua = @import("lua/lua.zig"); const gpa = std.heap.c_allocator; pub var server: Server = undefined; +pub var lua: Lua = undefined; pub fn main() !void { wlr.log.init(.err, null); @@ -14,6 +16,7 @@ pub fn main() !void { server.init(); defer server.deinit(); + try lua.init(); var buf: [11]u8 = undefined; const socket = try server.wl_server.addSocketAuto(&buf); diff --git a/src/server.zig b/src/server.zig index 88384de..4b7a32c 100644 --- a/src/server.zig +++ b/src/server.zig @@ -11,6 +11,7 @@ const Keyboard = @import("keyboard.zig"); const Output = @import("output.zig"); const View = @import("view.zig"); const Utils = @import("utils.zig"); +const Keymap = @import("keymap.zig"); const gpa = std.heap.c_allocator; const server = &@import("main.zig").server; @@ -34,6 +35,7 @@ root: Root, seat: Seat, cursor: Cursor, keyboard: Keyboard, +keymaps: std.AutoHashMap(u64, Keymap), // Backend listeners new_input: wl.Listener(*wlr.InputDevice) = .init(handleNewInput), @@ -89,6 +91,7 @@ pub fn init(self: *Server) void { .seat = undefined, .cursor = undefined, .keyboard = undefined, + .keymaps = .init(gpa), }; self.renderer.initServer(wl_server) catch {