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;
}