// This file is part of river, a dynamic tiling wayland compositor. // // Copyright 2020 The River Developers // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 3. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . const std = @import("std"); const fmt = std.fmt; const mem = std.mem; const meta = std.meta; const wlr = @import("wlroots"); const xkb = @import("xkbcommon"); const flags = @import("flags"); const c = @import("../c.zig"); const server = &@import("../main.zig").server; const util = @import("../util.zig"); const Error = @import("../command.zig").Error; const Mapping = @import("../Mapping.zig"); const PointerMapping = @import("../PointerMapping.zig"); const SwitchMapping = @import("../SwitchMapping.zig"); const Switch = @import("../Switch.zig"); const Seat = @import("../Seat.zig"); /// Create a new mapping for a given mode /// /// Example: /// map normal Mod4+Shift Return spawn foot pub fn map( seat: *Seat, args: []const [:0]const u8, out: *?[]const u8, ) Error!void { const result = flags.parser([:0]const u8, &.{ .{ .name = "release", .kind = .boolean }, .{ .name = "repeat", .kind = .boolean }, .{ .name = "layout", .kind = .arg }, }).parse(args[1..]) catch { return error.InvalidValue; }; if (result.args.len < 4) return Error.NotEnoughArguments; if (result.flags.release and result.flags.repeat) return Error.ConflictingOptions; const layout_index = blk: { if (result.flags.layout) |layout_raw| { break :blk try fmt.parseInt(u32, layout_raw, 10); } else { break :blk null; } }; const mode_raw = result.args[0]; const modifiers_raw = result.args[1]; const keysym_raw = result.args[2]; const command = result.args[3..]; const mode_id = try modeNameToId(mode_raw, out); const modifiers = try parseModifiers(modifiers_raw, out); const keysym = try parseKeysym(keysym_raw, out); const mode_mappings = &server.config.modes.items[mode_id].mappings; const new = try Mapping.init( keysym, modifiers, command, .{ .release = result.flags.release, .repeat = result.flags.repeat, .layout_index = layout_index, }, ); errdefer new.deinit(); if (mappingExists(mode_mappings, modifiers, keysym, result.flags.release)) |current| { mode_mappings.items[current].deinit(); mode_mappings.items[current] = new; // Warn user if they overwrote an existing keybinding using riverctl. const opts = if (result.flags.release) "-release " else ""; out.* = try fmt.allocPrint( util.gpa, "overwrote an existing keybinding: {s} {s}{s} {s}", .{ mode_raw, opts, modifiers_raw, keysym_raw }, ); } else { // Repeating mappings borrow the Mapping directly. To prevent a // possible crash if the Mapping ArrayList is reallocated, stop any // currently repeating mappings. seat.clearRepeatingMapping(); try mode_mappings.append(util.gpa, new); } } /// Create a new switch mapping for a given mode /// /// Example: /// map-switch normal lid close spawn "wlr-randr --output eDP-1 --off" pub fn mapSwitch( _: *Seat, args: []const [:0]const u8, out: *?[]const u8, ) Error!void { if (args.len < 5) return Error.NotEnoughArguments; const mode_id = try modeNameToId(args[1], out); const switch_type = try parseSwitchType(args[2], out); const switch_state = try parseSwitchState(switch_type, args[3], out); const new = try SwitchMapping.init(switch_type, switch_state, args[4..]); errdefer new.deinit(); const mode_mappings = &server.config.modes.items[mode_id].switch_mappings; if (switchMappingExists(mode_mappings, switch_type, switch_state)) |current| { mode_mappings.items[current].deinit(); mode_mappings.items[current] = new; // Warn user if they overwrote an existing keybinding using riverctl. out.* = try std.fmt.allocPrint( util.gpa, "overwrote an existing keybinding: map-switch {s} {s} {s}", .{ args[1], args[2], args[3] }, ); } else { try mode_mappings.append(util.gpa, new); } } /// Create a new pointer mapping for a given mode /// /// Example: /// map-pointer normal Mod4 BTN_LEFT move-view pub fn mapPointer( _: *Seat, args: []const [:0]const u8, out: *?[]const u8, ) Error!void { if (args.len < 5) return Error.NotEnoughArguments; const mode_id = try modeNameToId(args[1], out); const modifiers = try parseModifiers(args[2], out); const event_code = try parseEventCode(args[3], out); const action: meta.Tag(PointerMapping.Action) = blk: { if (mem.eql(u8, args[4], "move-view")) { break :blk .move; } else if (mem.eql(u8, args[4], "resize-view")) { break :blk .resize; } else { break :blk .command; } }; if (action != .command and args.len > 5) return Error.TooManyArguments; var new = try PointerMapping.init( event_code, modifiers, action, args[4..], ); errdefer new.deinit(); const mode_pointer_mappings = &server.config.modes.items[mode_id].pointer_mappings; if (pointerMappingExists(mode_pointer_mappings, modifiers, event_code)) |current| { mode_pointer_mappings.items[current].deinit(); mode_pointer_mappings.items[current] = new; } else { try mode_pointer_mappings.append(util.gpa, new); } } fn modeNameToId(mode_name: []const u8, out: *?[]const u8) !usize { const config = &server.config; return config.mode_to_id.get(mode_name) orelse { out.* = try fmt.allocPrint( util.gpa, "cannot add/remove mapping to/from non-existant mode '{s}'", .{mode_name}, ); return Error.Other; }; } /// Returns the index of the Mapping with matching modifiers, keysym and release, if any. fn mappingExists( mappings: *std.ArrayListUnmanaged(Mapping), modifiers: wlr.Keyboard.ModifierMask, keysym: xkb.Keysym, release: bool, ) ?usize { for (mappings.items, 0..) |mapping, i| { if (meta.eql(mapping.modifiers, modifiers) and mapping.keysym == keysym and mapping.options.release == release) { return i; } } return null; } /// Returns the index of the SwitchMapping with matching switch_type and switch_state, if any. fn switchMappingExists( switch_mappings: *std.ArrayListUnmanaged(SwitchMapping), switch_type: Switch.Type, switch_state: Switch.State, ) ?usize { for (switch_mappings.items, 0..) |mapping, i| { if (mapping.switch_type == switch_type and meta.eql(mapping.switch_state, switch_state)) { return i; } } return null; } /// Returns the index of the PointerMapping with matching modifiers and event code, if any. fn pointerMappingExists( pointer_mappings: *std.ArrayListUnmanaged(PointerMapping), modifiers: wlr.Keyboard.ModifierMask, event_code: u32, ) ?usize { for (pointer_mappings.items, 0..) |mapping, i| { if (meta.eql(mapping.modifiers, modifiers) and mapping.event_code == event_code) { return i; } } return null; } fn parseEventCode(name: [:0]const u8, out: *?[]const u8) !u32 { const event_code = c.libevdev_event_code_from_name(c.EV_KEY, name.ptr); if (event_code < 1) { out.* = try fmt.allocPrint(util.gpa, "unknown button {s}", .{name}); return Error.Other; } return @intCast(event_code); } fn parseKeysym(name: [:0]const u8, out: *?[]const u8) !xkb.Keysym { const keysym = xkb.Keysym.fromName(name, .case_insensitive); if (keysym == .NoSymbol) { out.* = try fmt.allocPrint(util.gpa, "invalid keysym '{s}'", .{name}); return Error.Other; } // The case insensitive matching done by xkbcommon returns the first // lowercase match found if there are multiple matches that differ only in // case. This works great for alphabetic keys for example but there is one // problematic exception we handle specially here. For some reason there // exist both uppercase and lowercase versions of XF86ScreenSaver with // different keysym values for example. Switching to a case-sensitive match // would be too much of a breaking change at this point so fix this by // special-casing this exception. // // This has been fixed upstream in libxkbcommon 1.7.0 // https://github.com/xkbcommon/libxkbcommon/pull/465 // TODO remove the workaround once libxkbcommon 1.7.0 is widely distributed. if (@intFromEnum(keysym) == xkb.Keysym.XF86Screensaver) { if (mem.eql(u8, name, "XF86Screensaver")) { return keysym; } else if (mem.eql(u8, name, "XF86ScreenSaver")) { return @enumFromInt(xkb.Keysym.XF86ScreenSaver); } else { out.* = try fmt.allocPrint(util.gpa, "ambiguous keysym name '{s}'", .{name}); return Error.Other; } } return keysym; } fn parseModifiers(modifiers_str: []const u8, out: *?[]const u8) !wlr.Keyboard.ModifierMask { var it = mem.split(u8, modifiers_str, "+"); var modifiers = wlr.Keyboard.ModifierMask{}; outer: while (it.next()) |mod_name| { if (mem.eql(u8, mod_name, "None")) continue; inline for ([_]struct { name: []const u8, field_name: []const u8 }{ .{ .name = "Shift", .field_name = "shift" }, .{ .name = "Control", .field_name = "ctrl" }, .{ .name = "Mod1", .field_name = "alt" }, .{ .name = "Alt", .field_name = "alt" }, .{ .name = "Mod3", .field_name = "mod3" }, .{ .name = "Mod4", .field_name = "logo" }, .{ .name = "Super", .field_name = "logo" }, .{ .name = "Mod5", .field_name = "mod5" }, }) |def| { if (mem.eql(u8, def.name, mod_name)) { @field(modifiers, def.field_name) = true; continue :outer; } } out.* = try fmt.allocPrint(util.gpa, "invalid modifier '{s}'", .{mod_name}); return Error.Other; } return modifiers; } fn parseSwitchType( switch_type_str: []const u8, out: *?[]const u8, ) !Switch.Type { return meta.stringToEnum(Switch.Type, switch_type_str) orelse { out.* = try std.fmt.allocPrint( util.gpa, "invalid switch '{s}', must be 'lid' or 'tablet'", .{switch_type_str}, ); return Error.Other; }; } fn parseSwitchState( switch_type: Switch.Type, switch_state_str: []const u8, out: *?[]const u8, ) !Switch.State { switch (switch_type) { .lid => { const lid_state = meta.stringToEnum( Switch.LidState, switch_state_str, ) orelse { out.* = try std.fmt.allocPrint( util.gpa, "invalid lid state '{s}', must be 'close' or 'open'", .{switch_state_str}, ); return Error.Other; }; return Switch.State{ .lid = lid_state }; }, .tablet => { const tablet_state = meta.stringToEnum( Switch.TabletState, switch_state_str, ) orelse { out.* = try std.fmt.allocPrint( util.gpa, "invalid tablet state '{s}', must be 'on' or 'off'", .{switch_state_str}, ); return Error.Other; }; return Switch.State{ .tablet = tablet_state }; }, } } /// Remove a mapping from a given mode /// /// Example: /// unmap normal Mod4+Shift Return pub fn unmap(seat: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void { const result = flags.parser([:0]const u8, &.{ .{ .name = "release", .kind = .boolean }, }).parse(args[1..]) catch { return error.InvalidValue; }; if (result.args.len < 3) return Error.NotEnoughArguments; if (result.args.len > 3) return Error.TooManyArguments; const mode_id = try modeNameToId(result.args[0], out); const modifiers = try parseModifiers(result.args[1], out); const keysym = try parseKeysym(result.args[2], out); const mode_mappings = &server.config.modes.items[mode_id].mappings; const mapping_idx = mappingExists( mode_mappings, modifiers, keysym, result.flags.release, ) orelse return; // Repeating mappings borrow the Mapping directly. To prevent a possible // crash if the Mapping ArrayList is reallocated, stop any currently // repeating mappings. seat.clearRepeatingMapping(); var mapping = mode_mappings.swapRemove(mapping_idx); mapping.deinit(); } /// Remove a switch mapping from a given mode /// /// Example: /// unmap-switch normal tablet on pub fn unmapSwitch( _: *Seat, args: []const [:0]const u8, out: *?[]const u8, ) Error!void { if (args.len < 4) return Error.NotEnoughArguments; const mode_id = try modeNameToId(args[1], out); const switch_type = try parseSwitchType(args[2], out); const switch_state = try parseSwitchState(switch_type, args[3], out); const mode_mappings = &server.config.modes.items[mode_id].switch_mappings; const mapping_idx = switchMappingExists(mode_mappings, switch_type, switch_state) orelse return; var mapping = mode_mappings.swapRemove(mapping_idx); mapping.deinit(); } /// Remove a pointer mapping for a given mode /// /// Example: /// unmap-pointer normal Mod4 BTN_LEFT pub fn unmapPointer(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void { if (args.len < 4) return Error.NotEnoughArguments; if (args.len > 4) return Error.TooManyArguments; const mode_id = try modeNameToId(args[1], out); const modifiers = try parseModifiers(args[2], out); const event_code = try parseEventCode(args[3], out); const mode_pointer_mappings = &server.config.modes.items[mode_id].pointer_mappings; const mapping_idx = pointerMappingExists(mode_pointer_mappings, modifiers, event_code) orelse return; var mapping = mode_pointer_mappings.swapRemove(mapping_idx); mapping.deinit(); }