config: implement map-pointer command

This command takes a mode, modifiers, button/event name, and pointer
action as arguments. It stores these in the config data structure.

The currently available pointer actions are move-view and resize-view,
which replace the previously hard-coded functionality.

Closing the hovered view with middle click has temorarily been removed
until it is decided if we wish to make this another special pointer
action or perhaps allow running any arbitrary command (which would of
course include close).
This commit is contained in:
Isaac Freund 2020-08-24 14:52:47 +02:00
parent fd8c5e6410
commit 989e7aaeda
No known key found for this signature in database
GPG Key ID: 86DED400DDFD7A11
14 changed files with 240 additions and 86 deletions

View File

@ -28,6 +28,7 @@ installed:
- wayland-protocols - wayland-protocols
- [wlroots](https://github.com/swaywm/wlroots) 0.11.0 - [wlroots](https://github.com/swaywm/wlroots) 0.11.0
- xkbcommon - xkbcommon
- libevdev
- pixman - pixman
- pkg-config - pkg-config
- scdoc (optional, but required for man page generation) - scdoc (optional, but required for man page generation)

View File

@ -112,6 +112,7 @@ fn addServerDeps(exe: *std.build.LibExeObjStep) void {
exe.addIncludeDir("."); exe.addIncludeDir(".");
exe.linkLibC(); exe.linkLibC();
exe.linkSystemLibrary("libevdev");
exe.linkSystemLibrary("pixman-1"); exe.linkSystemLibrary("pixman-1");
exe.linkSystemLibrary("wayland-server"); exe.linkSystemLibrary("wayland-server");
exe.linkSystemLibrary("wlroots"); exe.linkSystemLibrary("wlroots");

View File

@ -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 H mod-master-count +1
riverctl map normal $mod+Shift L 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 for i in $(seq 1 9); do
tagmask=$((1 << ($i - 1))) tagmask=$((1 << ($i - 1)))

View File

@ -136,6 +136,23 @@ that tag 1 through 9 are visible.
A mapping without modifiers can be created by passing an empty string as A mapping without modifiers can be created by passing an empty string as
the modifiers argument. 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_ *outer-padding* _pixels_
Set the padding around the edge of the screen to _pixels_. Set the padding around the edge of the screen to _pixels_.

View File

@ -23,7 +23,7 @@ const c = @import("c.zig");
const util = @import("util.zig"); const util = @import("util.zig");
const Server = @import("Server.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) /// 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 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), mode_to_id: std.StringHashMap(usize),
/// All user-defined keymap modes, indexed by mode id /// 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 /// List of app_ids which will be started floating
float_filter: std.ArrayList([]const u8), float_filter: std.ArrayList([]const u8),
@ -56,22 +56,20 @@ float_filter: std.ArrayList([]const u8),
csd_filter: std.ArrayList([]const u8), csd_filter: std.ArrayList([]const u8),
pub fn init() !Self { pub fn init() !Self {
var mode_to_id = std.StringHashMap(usize).init(util.gpa); var self = Self{
errdefer mode_to_id.deinit(); .mode_to_id = std.StringHashMap(usize).init(util.gpa),
const owned_slice = try std.mem.dupe(util.gpa, u8, "normal"); .modes = std.ArrayList(Mode).init(util.gpa),
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,
.float_filter = std.ArrayList([]const u8).init(util.gpa), .float_filter = std.ArrayList([]const u8).init(util.gpa),
.csd_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 { 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); while (it.next()) |kv| util.gpa.free(kv.key);
self.mode_to_id.deinit(); self.mode_to_id.deinit();
for (self.modes.items) |mode| { for (self.modes.items) |mode| mode.deinit();
for (mode.items) |mapping| mapping.deinit(util.gpa);
mode.deinit();
}
self.modes.deinit(); self.modes.deinit();
self.float_filter.deinit(); self.float_filter.deinit();

View File

@ -397,25 +397,13 @@ fn handleButton(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
// perhaps enter move/resize mode. // perhaps enter move/resize mode.
if (View.fromWlrSurface(wlr_surface)) |view| { if (View.fromWlrSurface(wlr_surface)) |view| {
if (event.state == .WLR_BUTTON_PRESSED and self.pressed_count == 1) { if (event.state == .WLR_BUTTON_PRESSED and self.pressed_count == 1) {
// If the button is pressed and the pointer modifier is // If there is an active mapping for this button which is
// active, enter cursor mode or close view and return. // handled we are done here
const fullscreen = view.current.fullscreen or view.pending.fullscreen; if (self.handlePointerMapping(event, view)) return;
if (self.seat.pointer_modifier) { // Otherwise enter cursor down mode
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); Mode.enter(self, .down, event, view);
} }
} }
}
_ = c.wlr_seat_pointer_notify_button( _ = c.wlr_seat_pointer_notify_button(
self.seat.wlr_seat, self.seat.wlr_seat,
@ -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 { 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 // This event is forwarded by the cursor when a pointer emits an frame
// event. Frame events are sent after regular pointer events to group // event. Frame events are sent after regular pointer events to group

View File

@ -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); c.wlr_seat_set_keyboard(self.seat.wlr_seat, self.wlr_input_device);
// Send modifiers to the client. // Send modifiers to the client.
c.wlr_seat_keyboard_notify_modifiers( c.wlr_seat_keyboard_notify_modifiers(self.seat.wlr_seat, &self.wlr_keyboard.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;
} }
/// Handle any builtin, harcoded compsitor mappings such as VT switching. /// Handle any builtin, harcoded compsitor mappings such as VT switching.

41
river/Mode.zig Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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();
}

25
river/PointerMapping.zig Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
pub const Action = enum {
move,
resize,
};
event_code: u32,
modifiers: u32,
action: Action,

View File

@ -65,9 +65,6 @@ focus_stack: ViewStack(*View) = ViewStack(*View){},
/// List of status tracking objects relaying changes to this seat to clients. /// List of status tracking objects relaying changes to this seat to clients.
status_trackers: std.SinglyLinkedList(SeatStatus) = std.SinglyLinkedList(SeatStatus).init(), 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, listen_request_set_selection: c.wl_listener = undefined,
pub fn init(self: *Self, input_manager: *InputManager, name: [*:0]const u8) !void { 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 /// Returns true if the key was handled
pub fn handleMapping(self: *Self, keysym: c.xkb_keysym_t, modifiers: u32) bool { pub fn handleMapping(self: *Self, keysym: c.xkb_keysym_t, modifiers: u32) bool {
const modes = &self.input_manager.server.config.modes; 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) { if (modifiers == mapping.modifiers and keysym == mapping.keysym) {
// Execute the bound command // Execute the bound command
const args = mapping.command_args; const args = mapping.command_args;

View File

@ -24,6 +24,7 @@ pub usingnamespace @cImport({
@cInclude("unistd.h"); @cInclude("unistd.h");
@cInclude("linux/input-event-codes.h"); @cInclude("linux/input-event-codes.h");
@cInclude("libevdev/libevdev.h");
@cInclude("wayland-server-core.h"); @cInclude("wayland-server-core.h");
//@cInclude("wlr/backend.h"); //@cInclude("wlr/backend.h");

View File

@ -45,6 +45,7 @@ const str_to_impl_fn = [_]struct {
.{ .name = "focus-view", .impl = @import("command/focus_view.zig").focusView }, .{ .name = "focus-view", .impl = @import("command/focus_view.zig").focusView },
.{ .name = "layout", .impl = @import("command/layout.zig").layout }, .{ .name = "layout", .impl = @import("command/layout.zig").layout },
.{ .name = "map", .impl = @import("command/map.zig").map }, .{ .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-count", .impl = @import("command/mod_master_count.zig").modMasterCount },
.{ .name = "mod-master-factor", .impl = @import("command/mod_master_factor.zig").modMasterFactor }, .{ .name = "mod-master-factor", .impl = @import("command/mod_master_factor.zig").modMasterFactor },
.{ .name = "outer-padding", .impl = @import("command/config.zig").outerPadding }, .{ .name = "outer-padding", .impl = @import("command/config.zig").outerPadding },

View File

@ -19,6 +19,7 @@ const std = @import("std");
const util = @import("../util.zig"); const util = @import("../util.zig");
const Mode = @import("../Mode.zig");
const Error = @import("../command.zig").Error; const Error = @import("../command.zig").Error;
const Mapping = @import("../Mapping.zig"); const Mapping = @import("../Mapping.zig");
const Seat = @import("../Seat.zig"); const Seat = @import("../Seat.zig");
@ -45,9 +46,9 @@ pub fn declareMode(
return Error.Other; 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); const owned_name = try std.mem.dupe(util.gpa, u8, new_mode_name);
errdefer util.gpa.free(owned_name); errdefer util.gpa.free(owned_name);
try config.mode_to_id.putNoClobber(owned_name, config.modes.items.len); try config.mode_to_id.putNoClobber(owned_name, config.modes.items.len);
errdefer _ = config.mode_to_id.remove(owned_name); config.modes.appendAssumeCapacity(Mode.init());
try config.modes.append(std.ArrayList(Mapping).init(util.gpa));
} }

View File

@ -22,6 +22,7 @@ const util = @import("../util.zig");
const Error = @import("../command.zig").Error; const Error = @import("../command.zig").Error;
const Mapping = @import("../Mapping.zig"); const Mapping = @import("../Mapping.zig");
const PointerMapping = @import("../PointerMapping.zig");
const Seat = @import("../Seat.zig"); const Seat = @import("../Seat.zig");
const modifier_names = [_]struct { const modifier_names = [_]struct {
@ -49,22 +50,114 @@ pub fn map(
args: []const []const u8, args: []const []const u8,
out: *?[]const u8, out: *?[]const u8,
) Error!void { ) Error!void {
if (args.len < 4) return Error.NotEnoughArguments; if (args.len < 5) return Error.NotEnoughArguments;
// Parse the mode const mode_id = try modeNameToId(allocator, seat, args[1], out);
const config = seat.input_manager.server.config; const modifiers = try parseModifiers(allocator, args[2], out);
const target_mode = args[1];
const mode_id = config.mode_to_id.getValue(target_mode) orelse { // 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( out.* = try std.fmt.allocPrint(
allocator, allocator,
"cannot add mapping to non-existant mode '{}p'", "invalid keysym '{}'",
.{target_mode}, .{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; return Error.Other;
}; };
// Parse the modifiers try mode_pointer_mappings.append(.{
var it = std.mem.split(args[2], "+"); .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; var modifiers: u32 = 0;
while (it.next()) |mod_name| { while (it.next()) |mod_name| {
for (modifier_names) |def| { for (modifier_names) |def| {
@ -81,32 +174,5 @@ pub fn map(
return Error.Other; return Error.Other;
} }
} }
return modifiers;
// 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..]));
} }