diff --git a/README.md b/README.md index 0cdb98f..3900eed 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ installed: - wayland-protocols - [wlroots](https://github.com/swaywm/wlroots) 0.11.0 - xkbcommon +- libevdev - pixman - pkg-config - scdoc (optional, but required for man page generation) diff --git a/build.zig b/build.zig index 8a9572b..23aa3e6 100644 --- a/build.zig +++ b/build.zig @@ -112,6 +112,7 @@ fn addServerDeps(exe: *std.build.LibExeObjStep) void { exe.addIncludeDir("."); exe.linkLibC(); + exe.linkSystemLibrary("libevdev"); exe.linkSystemLibrary("pixman-1"); exe.linkSystemLibrary("wayland-server"); exe.linkSystemLibrary("wlroots"); diff --git a/contrib/config.sh b/contrib/config.sh index 6b2cd72..261ed4b 100755 --- a/contrib/config.sh +++ b/contrib/config.sh @@ -37,6 +37,12 @@ riverctl map normal $mod L mod-master-factor +0.05 riverctl map normal $mod+Shift H mod-master-count +1 riverctl map normal $mod+Shift L mod-master-count -1 +# Mod + Left Mouse Button to move views +riverctl map-pointer normal $mod BTN_LEFT move-view + +# Mod + Right Mouse Button to resize views +riverctl map-pointer normal $mod BTN_RIGHT resize-view + for i in $(seq 1 9); do tagmask=$((1 << ($i - 1))) diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index 3b7da3c..3632827 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -136,6 +136,23 @@ that tag 1 through 9 are visible. A mapping without modifiers can be created by passing an empty string as the modifiers argument. +*map-pointer* _mode_ _modifiers_ _button_ _action_ + _mode_ and _modifiers_ are the same as for *map*. + + _button_ is the name of a linux input event code. The most commonly used + values are: + + - BTN_LEFT - left mouse button + - BTN_RIGHT - right mouse button + - BTN_MIDDLE - middle mouse button + + A complete list may be found in _/usr/include/linux/input-event-codes.h_ + + _action_ is one of the following values: + + - move-view + - resize-view + *outer-padding* _pixels_ Set the padding around the edge of the screen to _pixels_. diff --git a/river/Config.zig b/river/Config.zig index a2814f1..900dabc 100644 --- a/river/Config.zig +++ b/river/Config.zig @@ -23,7 +23,7 @@ const c = @import("c.zig"); const util = @import("util.zig"); const Server = @import("Server.zig"); -const Mapping = @import("Mapping.zig"); +const Mode = @import("Mode.zig"); /// Color of background in RGBA (alpha should only affect nested sessions) background_color: [4]f32 = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }, // Solarized base03 @@ -47,7 +47,7 @@ outer_padding: u32 = 8, mode_to_id: std.StringHashMap(usize), /// All user-defined keymap modes, indexed by mode id -modes: std.ArrayList(std.ArrayList(Mapping)), +modes: std.ArrayList(Mode), /// List of app_ids which will be started floating float_filter: std.ArrayList([]const u8), @@ -56,22 +56,20 @@ float_filter: std.ArrayList([]const u8), csd_filter: std.ArrayList([]const u8), pub fn init() !Self { - var mode_to_id = std.StringHashMap(usize).init(util.gpa); - errdefer mode_to_id.deinit(); - const owned_slice = try std.mem.dupe(util.gpa, u8, "normal"); - errdefer util.gpa.free(owned_slice); - try mode_to_id.putNoClobber(owned_slice, 0); - - var modes = std.ArrayList(std.ArrayList(Mapping)).init(util.gpa); - errdefer modes.deinit(); - try modes.append(std.ArrayList(Mapping).init(util.gpa)); - - return Self{ - .mode_to_id = mode_to_id, - .modes = modes, + var self = Self{ + .mode_to_id = std.StringHashMap(usize).init(util.gpa), + .modes = std.ArrayList(Mode).init(util.gpa), .float_filter = std.ArrayList([]const u8).init(util.gpa), .csd_filter = std.ArrayList([]const u8).init(util.gpa), }; + + // Start with a single, empty mode called normal + errdefer self.deinit(); + const owned_slice = try std.mem.dupe(util.gpa, u8, "normal"); + try self.mode_to_id.putNoClobber(owned_slice, 0); + try self.modes.append(Mode.init()); + + return self; } pub fn deinit(self: Self) void { @@ -79,10 +77,7 @@ pub fn deinit(self: Self) void { while (it.next()) |kv| util.gpa.free(kv.key); self.mode_to_id.deinit(); - for (self.modes.items) |mode| { - for (mode.items) |mapping| mapping.deinit(util.gpa); - mode.deinit(); - } + for (self.modes.items) |mode| mode.deinit(); self.modes.deinit(); self.float_filter.deinit(); diff --git a/river/Cursor.zig b/river/Cursor.zig index 1ca75d7..c84c3af 100644 --- a/river/Cursor.zig +++ b/river/Cursor.zig @@ -397,23 +397,11 @@ fn handleButton(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { // perhaps enter move/resize mode. if (View.fromWlrSurface(wlr_surface)) |view| { if (event.state == .WLR_BUTTON_PRESSED and self.pressed_count == 1) { - // If the button is pressed and the pointer modifier is - // active, enter cursor mode or close view and return. - const fullscreen = view.current.fullscreen or view.pending.fullscreen; - if (self.seat.pointer_modifier) { - switch (event.button) { - c.BTN_LEFT => if (!fullscreen) Mode.enter(self, .move, event, view), - c.BTN_MIDDLE => view.close(), - c.BTN_RIGHT => if (!fullscreen) Mode.enter(self, .resize, event, view), - - // TODO Some mice have additional buttons. These - // could also be bound to some useful action. - else => {}, - } - return; - } else { - Mode.enter(self, .down, event, view); - } + // If there is an active mapping for this button which is + // handled we are done here + if (self.handlePointerMapping(event, view)) return; + // Otherwise enter cursor down mode + Mode.enter(self, .down, event, view); } } @@ -426,6 +414,26 @@ fn handleButton(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { } } +/// Handle the mapping for the passed button if any. Returns true if there +/// was a mapping and the button was handled. +fn handlePointerMapping(self: *Self, event: *c.wlr_event_pointer_button, view: *View) bool { + const wlr_keyboard = c.wlr_seat_get_keyboard(self.seat.wlr_seat); + const modifiers = c.wlr_keyboard_get_modifiers(wlr_keyboard); + + const fullscreen = view.current.fullscreen or view.pending.fullscreen; + + const config = self.seat.input_manager.server.config; + return for (config.modes.items[self.seat.mode_id].pointer_mappings.items) |mapping| { + if (event.button == mapping.event_code and modifiers == mapping.modifiers) { + switch (mapping.action) { + .move => if (!fullscreen) Mode.enter(self, .move, event, view), + .resize => if (!fullscreen) Mode.enter(self, .resize, event, view), + } + break true; + } + } else false; +} + fn handleFrame(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { // This event is forwarded by the cursor when a pointer emits an frame // event. Frame events are sent after regular pointer events to group diff --git a/river/Keyboard.zig b/river/Keyboard.zig index cb8dc12..f5c065f 100644 --- a/river/Keyboard.zig +++ b/river/Keyboard.zig @@ -153,13 +153,7 @@ fn handleModifiers(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void c.wlr_seat_set_keyboard(self.seat.wlr_seat, self.wlr_input_device); // Send modifiers to the client. - c.wlr_seat_keyboard_notify_modifiers( - self.seat.wlr_seat, - &self.wlr_keyboard.modifiers, - ); - - const modifiers = c.wlr_keyboard_get_modifiers(self.wlr_keyboard); - self.seat.pointer_modifier = modifiers == c.WLR_MODIFIER_LOGO; + c.wlr_seat_keyboard_notify_modifiers(self.seat.wlr_seat, &self.wlr_keyboard.modifiers); } /// Handle any builtin, harcoded compsitor mappings such as VT switching. diff --git a/river/Mode.zig b/river/Mode.zig new file mode 100644 index 0000000..136e19d --- /dev/null +++ b/river/Mode.zig @@ -0,0 +1,41 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// 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, either version 3 of the License, or +// (at your option) any later version. +// +// 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 Self = @This(); + +const std = @import("std"); +const util = @import("util.zig"); + +const Mapping = @import("Mapping.zig"); +const PointerMapping = @import("PointerMapping.zig"); + +// TODO: use unmanaged array lists here to save memory +mappings: std.ArrayList(Mapping), +pointer_mappings: std.ArrayList(PointerMapping), + +pub fn init() Self { + return .{ + .mappings = std.ArrayList(Mapping).init(util.gpa), + .pointer_mappings = std.ArrayList(PointerMapping).init(util.gpa), + }; +} + +pub fn deinit(self: Self) void { + for (self.mappings.items) |m| m.deinit(util.gpa); + self.mappings.deinit(); + self.pointer_mappings.deinit(); +} diff --git a/river/PointerMapping.zig b/river/PointerMapping.zig new file mode 100644 index 0000000..161d17a --- /dev/null +++ b/river/PointerMapping.zig @@ -0,0 +1,25 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// 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, either version 3 of the License, or +// (at your option) any later version. +// +// 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 . + +pub const Action = enum { + move, + resize, +}; + +event_code: u32, +modifiers: u32, +action: Action, diff --git a/river/Seat.zig b/river/Seat.zig index 7ab0fb6..6a2c15e 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -65,9 +65,6 @@ focus_stack: ViewStack(*View) = ViewStack(*View){}, /// List of status tracking objects relaying changes to this seat to clients. status_trackers: std.SinglyLinkedList(SeatStatus) = std.SinglyLinkedList(SeatStatus).init(), -/// State of pointer modifier; Used for pointer operations such as move ans resize. -pointer_modifier: bool = false, - listen_request_set_selection: c.wl_listener = undefined, pub fn init(self: *Self, input_manager: *InputManager, name: [*:0]const u8) !void { @@ -258,7 +255,7 @@ pub fn handleViewUnmap(self: *Self, view: *View) void { /// Returns true if the key was handled pub fn handleMapping(self: *Self, keysym: c.xkb_keysym_t, modifiers: u32) bool { const modes = &self.input_manager.server.config.modes; - for (modes.items[self.mode_id].items) |mapping| { + for (modes.items[self.mode_id].mappings.items) |mapping| { if (modifiers == mapping.modifiers and keysym == mapping.keysym) { // Execute the bound command const args = mapping.command_args; diff --git a/river/c.zig b/river/c.zig index 5a09aaa..113df91 100644 --- a/river/c.zig +++ b/river/c.zig @@ -24,6 +24,7 @@ pub usingnamespace @cImport({ @cInclude("unistd.h"); @cInclude("linux/input-event-codes.h"); + @cInclude("libevdev/libevdev.h"); @cInclude("wayland-server-core.h"); //@cInclude("wlr/backend.h"); diff --git a/river/command.zig b/river/command.zig index 8527c9f..cd9489c 100644 --- a/river/command.zig +++ b/river/command.zig @@ -45,6 +45,7 @@ const str_to_impl_fn = [_]struct { .{ .name = "focus-view", .impl = @import("command/focus_view.zig").focusView }, .{ .name = "layout", .impl = @import("command/layout.zig").layout }, .{ .name = "map", .impl = @import("command/map.zig").map }, + .{ .name = "map-pointer", .impl = @import("command/map.zig").mapPointer }, .{ .name = "mod-master-count", .impl = @import("command/mod_master_count.zig").modMasterCount }, .{ .name = "mod-master-factor", .impl = @import("command/mod_master_factor.zig").modMasterFactor }, .{ .name = "outer-padding", .impl = @import("command/config.zig").outerPadding }, diff --git a/river/command/declare_mode.zig b/river/command/declare_mode.zig index 27e5483..9dc2dfc 100644 --- a/river/command/declare_mode.zig +++ b/river/command/declare_mode.zig @@ -19,6 +19,7 @@ const std = @import("std"); const util = @import("../util.zig"); +const Mode = @import("../Mode.zig"); const Error = @import("../command.zig").Error; const Mapping = @import("../Mapping.zig"); const Seat = @import("../Seat.zig"); @@ -45,9 +46,9 @@ pub fn declareMode( return Error.Other; } + try config.modes.ensureCapacity(config.modes.items.len + 1); const owned_name = try std.mem.dupe(util.gpa, u8, new_mode_name); errdefer util.gpa.free(owned_name); try config.mode_to_id.putNoClobber(owned_name, config.modes.items.len); - errdefer _ = config.mode_to_id.remove(owned_name); - try config.modes.append(std.ArrayList(Mapping).init(util.gpa)); + config.modes.appendAssumeCapacity(Mode.init()); } diff --git a/river/command/map.zig b/river/command/map.zig index 9946ce6..d21cb33 100644 --- a/river/command/map.zig +++ b/river/command/map.zig @@ -22,6 +22,7 @@ const util = @import("../util.zig"); const Error = @import("../command.zig").Error; const Mapping = @import("../Mapping.zig"); +const PointerMapping = @import("../PointerMapping.zig"); const Seat = @import("../Seat.zig"); const modifier_names = [_]struct { @@ -49,22 +50,114 @@ pub fn map( args: []const []const u8, out: *?[]const u8, ) Error!void { - if (args.len < 4) return Error.NotEnoughArguments; + if (args.len < 5) return Error.NotEnoughArguments; - // Parse the mode - const config = seat.input_manager.server.config; - const target_mode = args[1]; - const mode_id = config.mode_to_id.getValue(target_mode) orelse { + const mode_id = try modeNameToId(allocator, seat, args[1], out); + const modifiers = try parseModifiers(allocator, args[2], out); + + // Parse the keysym + const keysym_name = try std.cstr.addNullByte(allocator, args[3]); + defer allocator.free(keysym_name); + const keysym = c.xkb_keysym_from_name(keysym_name, .XKB_KEYSYM_CASE_INSENSITIVE); + if (keysym == c.XKB_KEY_NoSymbol) { out.* = try std.fmt.allocPrint( allocator, - "cannot add mapping to non-existant mode '{}p'", - .{target_mode}, + "invalid keysym '{}'", + .{args[3]}, + ); + return Error.Other; + } + + // Check if the mapping already exists + const mode_mappings = &seat.input_manager.server.config.modes.items[mode_id].mappings; + for (mode_mappings.items) |existant_mapping| { + if (existant_mapping.modifiers == modifiers and existant_mapping.keysym == keysym) { + out.* = try std.fmt.allocPrint( + allocator, + "a mapping for modifiers '{}' and keysym '{}' already exists", + .{ args[2], args[3] }, + ); + return Error.Other; + } + } + + try mode_mappings.append(try Mapping.init(util.gpa, keysym, modifiers, args[4..])); +} + +/// Create a new pointer mapping for a given mode +/// +/// Example: +/// map-pointer normal Mod4 BTN_LEFT move-view +pub fn mapPointer( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + out: *?[]const u8, +) Error!void { + if (args.len < 5) return Error.NotEnoughArguments; + if (args.len > 5) return Error.TooManyArguments; + + const mode_id = try modeNameToId(allocator, seat, args[1], out); + const modifiers = try parseModifiers(allocator, args[2], out); + + const event_code = blk: { + const event_code_name = try std.cstr.addNullByte(allocator, args[3]); + defer allocator.free(event_code_name); + const ret = c.libevdev_event_code_from_name(c.EV_KEY, event_code_name); + if (ret < 1) { + out.* = try std.fmt.allocPrint(allocator, "unknown button {}", .{args[3]}); + return Error.Other; + } + break :blk @intCast(u32, ret); + }; + + // Check if the mapping already exists + const mode_pointer_mappings = &seat.input_manager.server.config.modes.items[mode_id].pointer_mappings; + for (mode_pointer_mappings.items) |existing| { + if (existing.event_code == event_code and existing.modifiers == modifiers) { + out.* = try std.fmt.allocPrint( + allocator, + "a pointer mapping for modifiers '{}' and button '{}' already exists", + .{ args[2], args[3] }, + ); + return Error.Other; + } + } + + const action = if (std.mem.eql(u8, args[4], "move-view")) + PointerMapping.Action.move + else if (std.mem.eql(u8, args[4], "resize-view")) + PointerMapping.Action.resize + else { + out.* = try std.fmt.allocPrint( + allocator, + "invalid pointer action {}, must be move-view or resize-view", + .{args[4]}, ); return Error.Other; }; - // Parse the modifiers - var it = std.mem.split(args[2], "+"); + try mode_pointer_mappings.append(.{ + .event_code = event_code, + .modifiers = modifiers, + .action = action, + }); +} + +fn modeNameToId(allocator: *std.mem.Allocator, seat: *Seat, mode_name: []const u8, out: *?[]const u8) !usize { + const config = seat.input_manager.server.config; + return config.mode_to_id.getValue(mode_name) orelse { + out.* = try std.fmt.allocPrint( + allocator, + "cannot add mapping to non-existant mode '{}p'", + .{mode_name}, + ); + return Error.Other; + }; +} + +fn parseModifiers(allocator: *std.mem.Allocator, modifiers_str: []const u8, out: *?[]const u8) !u32 { + var it = std.mem.split(modifiers_str, "+"); var modifiers: u32 = 0; while (it.next()) |mod_name| { for (modifier_names) |def| { @@ -81,32 +174,5 @@ pub fn map( return Error.Other; } } - - // Parse the keysym - const keysym_name = try std.cstr.addNullByte(allocator, args[3]); - defer allocator.free(keysym_name); - const keysym = c.xkb_keysym_from_name(keysym_name, .XKB_KEYSYM_CASE_INSENSITIVE); - if (keysym == c.XKB_KEY_NoSymbol) { - out.* = try std.fmt.allocPrint( - allocator, - "invalid keysym '{}'", - .{args[3]}, - ); - return Error.Other; - } - - // Check if the mapping already exists - const mode_mappings = &config.modes.items[mode_id]; - for (mode_mappings.items) |existant_mapping| { - if (existant_mapping.modifiers == modifiers and existant_mapping.keysym == keysym) { - out.* = try std.fmt.allocPrint( - allocator, - "a mapping for modifiers '{}' and keysym '{}' already exists", - .{ args[2], args[3] }, - ); - return Error.Other; - } - } - - try mode_mappings.append(try Mapping.init(util.gpa, keysym, modifiers, args[4..])); + return modifiers; }