add multimonitor
This commit is contained in:
+29
-1
@@ -46,6 +46,11 @@ have_usable: bool = false,
|
||||
tags: u32 = config.default_tags,
|
||||
prev_tags: u32 = config.default_tags,
|
||||
|
||||
/// The tags this output owns. Every tag when `config.split_tags` is off or this
|
||||
/// is the only screen; one slice of the set otherwise. `Wm.assignTagRanges`
|
||||
/// is what divides them up.
|
||||
owned_tags: u32 = action.all_tags,
|
||||
|
||||
/// One slot per tag, plus slot 0 for views of more than one tag. Indexed
|
||||
/// through `state()`, never directly.
|
||||
tag_state: [action.tag_count + 1]TagState = @splat(.{}),
|
||||
@@ -119,13 +124,36 @@ pub fn setLayout(self: *Output, mode: action.Layout) void {
|
||||
st.layout = mode;
|
||||
}
|
||||
|
||||
/// Show a set of tags. Tags belonging to another screen are dropped rather than
|
||||
/// shown here, so `Mod+0` means "everything on this screen" and not "everything
|
||||
/// everywhere"; a mask with nothing of this output's in it leaves the view alone.
|
||||
pub fn setTags(self: *Output, tags: u32) void {
|
||||
const masked = tags & action.all_tags;
|
||||
const masked = tags & self.owned_tags;
|
||||
if (masked == 0 or masked == self.tags) return;
|
||||
self.prev_tags = self.tags;
|
||||
self.tags = masked;
|
||||
}
|
||||
|
||||
/// Hand this output its slice of the tag set.
|
||||
///
|
||||
/// A view left with nothing it owns falls back to the lowest tag it does, which
|
||||
/// is what settles a screen the moment it is plugged in — it starts on
|
||||
/// `default_tags` like every other, and only here finds out that tag is not
|
||||
/// its to show.
|
||||
pub fn setOwnedTags(self: *Output, mask: u32) void {
|
||||
const owned = mask & action.all_tags;
|
||||
if (owned == 0 or owned == self.owned_tags) return;
|
||||
self.owned_tags = owned;
|
||||
|
||||
self.tags = clampView(self.tags, owned);
|
||||
self.prev_tags = clampView(self.prev_tags, owned);
|
||||
}
|
||||
|
||||
fn clampView(tags: u32, owned: u32) u32 {
|
||||
const kept = tags & owned;
|
||||
return if (kept != 0) kept else action.lowestTag(owned);
|
||||
}
|
||||
|
||||
fn onEvent(_: *river.OutputV1, event: river.OutputV1.Event, self: *Output) void {
|
||||
switch (event) {
|
||||
.removed => {
|
||||
|
||||
+47
-2
@@ -76,6 +76,10 @@ pointer_y: i32 = 0,
|
||||
/// The window the pointer is currently inside.
|
||||
hovered: ?*Window = null,
|
||||
|
||||
/// A screen to warp the pointer onto in the next manage sequence. Set when
|
||||
/// focus moves to an output with no window on it to warp to.
|
||||
pending_warp_output: ?*Output = null,
|
||||
|
||||
op: ?Op = null,
|
||||
/// An operation to start in the next manage sequence.
|
||||
pending_op: ?Op = null,
|
||||
@@ -165,12 +169,19 @@ fn createBindings(self: *Seat) !void {
|
||||
std.log.info("registered {d}/{d} pointer bindings", .{ self.buttons.items.len, config.buttons.len });
|
||||
}
|
||||
|
||||
/// The output this seat is working on: the one holding the focused window,
|
||||
/// else the one under the pointer.
|
||||
/// The output this seat is working on: the one holding the focused window, else
|
||||
/// the one focus was last moved to, else the one under the pointer.
|
||||
///
|
||||
/// That middle case is what makes an empty screen a place the user can be.
|
||||
/// Moving to a screen with nothing on it clears the focused window, and with
|
||||
/// only the pointer to fall back on the seat would go on reporting the screen it
|
||||
/// came from — so the tag keys, the layout keys and the next window spawned
|
||||
/// would all land back on the monitor just left.
|
||||
pub fn currentOutput(self: *Seat) ?*Output {
|
||||
if (self.focused) |win| {
|
||||
if (win.output) |out| return out;
|
||||
}
|
||||
if (self.wm.focused_output) |out| return out;
|
||||
return self.wm.outputAt(self.pointer_x, self.pointer_y) orelse self.wm.firstOutput();
|
||||
}
|
||||
|
||||
@@ -256,6 +267,11 @@ pub fn applyManage(self: *Seat) void {
|
||||
self.pending_clear_focus = false;
|
||||
self.wm.ipcDirty();
|
||||
}
|
||||
|
||||
if (self.pending_warp_output) |out| {
|
||||
self.warpToOutput(out);
|
||||
self.pending_warp_output = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pull the pointer to the middle of a newly focused window.
|
||||
@@ -274,6 +290,25 @@ fn warpTo(self: *Seat, win: *Window) void {
|
||||
);
|
||||
}
|
||||
|
||||
/// Pull the pointer onto a screen the keyboard has just moved to, when there is
|
||||
/// no window there to warp to instead.
|
||||
///
|
||||
/// Without it, moving to an empty screen leaves the cursor on the one before —
|
||||
/// and with `focus_follows_mouse` on, the first window the pointer then brushes
|
||||
/// past takes the focus straight back. Manage sequence only.
|
||||
fn warpToOutput(self: *Seat, out: *Output) void {
|
||||
if (!config.warp_cursor) return;
|
||||
if (self.op != null) return;
|
||||
const area = out.layoutArea();
|
||||
if (area.width <= 0 or area.height <= 0) return;
|
||||
if (area.contains(self.pointer_x, self.pointer_y)) return;
|
||||
|
||||
self.seat.pointerWarp(
|
||||
area.x + @divTrunc(area.width, 2),
|
||||
area.y + @divTrunc(area.height, 2),
|
||||
);
|
||||
}
|
||||
|
||||
fn onEvent(_: *river.SeatV1, event: river.SeatV1.Event, self: *Seat) void {
|
||||
switch (event) {
|
||||
.removed => {
|
||||
@@ -289,6 +324,16 @@ fn onEvent(_: *river.SeatV1, event: river.SeatV1.Event, self: *Seat) void {
|
||||
.pointer_position => |ev| {
|
||||
self.pointer_x = ev.x;
|
||||
self.pointer_y = ev.y;
|
||||
|
||||
// Sloppy focus crosses screens too, as dwm's motion handler does:
|
||||
// the pointer leaving a monitor is what moves the seat to the next
|
||||
// one, so the tag and layout keys follow the cursor even over a
|
||||
// screen with no window on it to focus.
|
||||
if (config.focus_follows_mouse and self.op == null) {
|
||||
if (self.wm.outputAt(ev.x, ev.y)) |out| {
|
||||
if (self.wm.focused_output != out) self.wm.enterOutput(self, out);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
.pointer_enter => |ev| {
|
||||
|
||||
+173
-11
@@ -107,6 +107,8 @@ tab_face_tried: bool = false,
|
||||
scratch_windows: std.ArrayList(*Window) = .empty,
|
||||
scratch_cells: std.ArrayList(Box) = .empty,
|
||||
scratch_order: std.ArrayList(*Window) = .empty,
|
||||
/// Outputs in desk order; see `orderedOutputs`.
|
||||
scratch_outputs: std.ArrayList(*Output) = .empty,
|
||||
|
||||
pub fn init(gpa: Allocator, environ: std.process.Environ) !*Wm {
|
||||
const display = try wl.Display.connect(null);
|
||||
@@ -261,6 +263,7 @@ pub fn deinit(self: *Wm) void {
|
||||
self.scratch_windows.deinit(gpa);
|
||||
self.scratch_cells.deinit(gpa);
|
||||
self.scratch_order.deinit(gpa);
|
||||
self.scratch_outputs.deinit(gpa);
|
||||
|
||||
self.ipc.deinit();
|
||||
sys.close(self.repeat_fd);
|
||||
@@ -519,6 +522,8 @@ fn manage(self: *Wm) void {
|
||||
defer self.in_manage = false;
|
||||
|
||||
self.reap();
|
||||
self.assignTagRanges();
|
||||
self.rehomeWindows();
|
||||
self.assignOutputs();
|
||||
self.focusNewWindows();
|
||||
self.arrangeAll();
|
||||
@@ -587,6 +592,7 @@ fn reap(self: *Wm) void {
|
||||
if (seat.focused) |f| if (f.output == null) {
|
||||
seat.focused = null;
|
||||
};
|
||||
if (seat.pending_warp_output == out) seat.pending_warp_output = null;
|
||||
}
|
||||
out.destroy();
|
||||
self.ipcDirty();
|
||||
@@ -608,6 +614,103 @@ fn reap(self: *Wm) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Divide the tag set among the outputs, left to right.
|
||||
///
|
||||
/// Recomputed every manage sequence rather than only when a screen comes or
|
||||
/// goes, because dragging a monitor to the other side of the desk reorders them
|
||||
/// too and the ranges have to follow. `setOwnedTags` is a no-op when nothing
|
||||
/// has actually changed, which is the common case.
|
||||
fn assignTagRanges(self: *Wm) void {
|
||||
const ordered = self.orderedOutputs();
|
||||
for (ordered, 0..) |out, i| {
|
||||
const mask = if (config.split_tags) act.tagsForOutput(i, ordered.len) else act.all_tags;
|
||||
out.setOwnedTags(mask);
|
||||
}
|
||||
}
|
||||
|
||||
/// Move windows to the screen that owns their tags.
|
||||
///
|
||||
/// This is what makes the split work in one direction and heal in the other:
|
||||
/// `Mod+Shift+7` retags a window and the window follows tag 7 to the screen it
|
||||
/// lives on, and unplugging a monitor hands its tags to a neighbour, which the
|
||||
/// windows wearing them follow rather than being stranded on tags their new
|
||||
/// screen cannot show. Plug the monitor back in and they go home.
|
||||
///
|
||||
/// A window keeps the screen it is on for as long as that screen owns any of
|
||||
/// its tags, so putting a window on every tag — `Mod+Shift+0` — does not yank
|
||||
/// it away to whichever screen owns tag 1.
|
||||
fn rehomeWindows(self: *Wm) void {
|
||||
if (!config.split_tags) return;
|
||||
|
||||
for (self.windows.items) |win| {
|
||||
if (win.closed or win.tags == 0) continue;
|
||||
if (win.output) |out| {
|
||||
if (win.tags & out.owned_tags != 0) continue;
|
||||
}
|
||||
const target = self.outputForTags(win.tags) orelse continue;
|
||||
if (win.output == target) continue;
|
||||
|
||||
win.output = target;
|
||||
// The new screen is a different size, so the size we last proposed
|
||||
// says nothing about the size it should have there.
|
||||
win.proposed_width = -1;
|
||||
win.proposed_height = -1;
|
||||
self.windowLeftOutput(win);
|
||||
self.ipcDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep a seat where it is when the window it was focusing moves to another
|
||||
/// screen.
|
||||
///
|
||||
/// `Mod+Shift+7` is a way of getting a window off the screen, not of following
|
||||
/// it across to the next one, and `validateFocus` on its own would do the
|
||||
/// opposite: it resolves against the focused window's output, which is now the
|
||||
/// far screen. So the keyboard goes to whatever is left where the user still is.
|
||||
fn windowLeftOutput(self: *Wm, win: *Window) void {
|
||||
const out = self.focused_output orelse return;
|
||||
for (self.seats.items) |seat| {
|
||||
if (seat.focused != win and seat.pending_focus != win) continue;
|
||||
// `win` has already been moved, so it is not a candidate here.
|
||||
if (self.topOnTags(out, out.tags)) |next| seat.focus(next) else seat.focus(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// The output owning the lowest tag in a mask.
|
||||
pub fn outputForTags(self: *Wm, tags: u32) ?*Output {
|
||||
const t = act.lowestTag(tags);
|
||||
if (t == 0) return null;
|
||||
for (self.outputs.items) |out| {
|
||||
if (out.owned_tags & t != 0) return out;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The outputs in the order they are arranged on the desk: left to right, then
|
||||
/// top to bottom.
|
||||
///
|
||||
/// This is the order `focus_output` steps through and the order the tag set is
|
||||
/// split in, which is what ties the two together — `Mod+l` moves to the screen
|
||||
/// on the right, and the screen on the right is the one holding the higher
|
||||
/// tags. Connection order would put either wherever the cables happened to go.
|
||||
///
|
||||
/// Returns a scratch buffer, valid until the next call.
|
||||
fn orderedOutputs(self: *Wm) []*Output {
|
||||
self.scratch_outputs.clearRetainingCapacity();
|
||||
// Unsorted is a poor order but a working one; nothing here is worth
|
||||
// failing a manage sequence over.
|
||||
self.scratch_outputs.appendSlice(self.gpa, self.outputs.items) catch return self.outputs.items;
|
||||
// A stable sort, so two screens stacked exactly on top of each other keep
|
||||
// connection order rather than swapping about between frames.
|
||||
std.mem.sort(*Output, self.scratch_outputs.items, {}, lessByPosition);
|
||||
return self.scratch_outputs.items;
|
||||
}
|
||||
|
||||
fn lessByPosition(_: void, a: *Output, b: *Output) bool {
|
||||
if (a.box.x != b.box.x) return a.box.x < b.box.x;
|
||||
return a.box.y < b.box.y;
|
||||
}
|
||||
|
||||
/// Give new windows an output and a tag set.
|
||||
fn assignOutputs(self: *Wm) void {
|
||||
const fallback = self.focused_output orelse self.firstOutput();
|
||||
@@ -704,6 +807,57 @@ fn validateFocus(self: *Wm, seat: *Seat) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// View a set of tags, on whichever screen owns them.
|
||||
///
|
||||
/// With the tag set split across displays, `Mod+7` is as much "go to the screen
|
||||
/// tag 7 lives on" as it is "show tag 7" — the two are the same thing, and it is
|
||||
/// what makes the tag keys on their own enough to drive a multi-monitor desk.
|
||||
/// A mask the current screen owns any part of stays where it is, so `Mod+0`
|
||||
/// still means "everything here".
|
||||
fn viewTags(self: *Wm, seat: *Seat, mask: u32) void {
|
||||
const current = seat.currentOutput();
|
||||
const target = blk: {
|
||||
if (!config.split_tags) break :blk current orelse return;
|
||||
if (current) |out| {
|
||||
if (out.owned_tags & mask != 0) break :blk out;
|
||||
}
|
||||
break :blk self.outputForTags(mask) orelse current orelse return;
|
||||
};
|
||||
|
||||
target.setTags(mask);
|
||||
if (target != current) self.enterOutput(seat, target);
|
||||
self.ipcDirty();
|
||||
}
|
||||
|
||||
/// Move the seat to another screen: the keyboard goes to the window most
|
||||
/// recently focused there, and if there is none the pointer is warped instead so
|
||||
/// that an empty screen is still somewhere the user can be.
|
||||
pub fn enterOutput(self: *Wm, seat: *Seat, out: *Output) void {
|
||||
self.focusOutput(out);
|
||||
if (self.topOnTags(out, out.tags)) |win| {
|
||||
seat.focus(win);
|
||||
} else {
|
||||
seat.focus(null);
|
||||
seat.pending_warp_output = out;
|
||||
}
|
||||
}
|
||||
|
||||
/// The most recently focused window a view of `tags` on `out` would show.
|
||||
///
|
||||
/// Unlike `topVisible` this asks the tags rather than the `visible` flags, which
|
||||
/// are only as fresh as the last layout pass — a binding that changes what is
|
||||
/// being viewed and then wants to know what to focus is asking about a view that
|
||||
/// has not been arranged yet.
|
||||
fn topOnTags(self: *Wm, out: *Output, tags: u32) ?*Window {
|
||||
var best: ?*Window = null;
|
||||
for (self.windows.items) |win| {
|
||||
if (win.output != out or win.closed or !win.mapped) continue;
|
||||
if ((win.tags & tags) == 0) continue;
|
||||
if (best == null or win.focus_serial > best.?.focus_serial) best = win;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// The most recently focused visible window on an output.
|
||||
fn topVisible(self: *Wm, out: *Output) ?*Window {
|
||||
var best: ?*Window = null;
|
||||
@@ -1080,12 +1234,11 @@ pub fn perform(self: *Wm, seat: *Seat, action: act.Action) void {
|
||||
.swap => |dir| self.swapWindow(seat, dir),
|
||||
.zoom => self.zoom(seat),
|
||||
|
||||
.view => |mask| if (seat.currentOutput()) |out| {
|
||||
out.setTags(mask);
|
||||
self.ipcDirty();
|
||||
},
|
||||
.view => |mask| self.viewTags(seat, mask),
|
||||
.toggle_view => |mask| if (seat.currentOutput()) |out| {
|
||||
out.setTags(out.tags ^ mask);
|
||||
// Only this screen's own tags can be toggled into its view; the
|
||||
// rest are somewhere else entirely.
|
||||
out.setTags(out.tags ^ (mask & out.owned_tags));
|
||||
self.ipcDirty();
|
||||
},
|
||||
.view_prev => if (seat.currentOutput()) |out| {
|
||||
@@ -1296,30 +1449,39 @@ fn moveToFront(self: *Wm, win: *Window, before: *Window) void {
|
||||
};
|
||||
}
|
||||
|
||||
/// Step to the next screen along, or send the focused window there.
|
||||
///
|
||||
/// The step is through `orderedOutputs`, so `prev` is the screen to the left and
|
||||
/// `next` the one to the right whatever order the monitors were plugged in.
|
||||
fn cycleOutput(self: *Wm, seat: *Seat, dir: act.Direction, send: bool) void {
|
||||
if (self.outputs.items.len < 2) return;
|
||||
const ordered = self.orderedOutputs();
|
||||
if (ordered.len < 2) return;
|
||||
const current = seat.currentOutput() orelse return;
|
||||
|
||||
var idx: usize = 0;
|
||||
for (self.outputs.items, 0..) |out, i| {
|
||||
for (ordered, 0..) |out, i| {
|
||||
if (out == current) idx = i;
|
||||
}
|
||||
const n = self.outputs.items.len;
|
||||
const n = ordered.len;
|
||||
const next = switch (dir) {
|
||||
.next => (idx + 1) % n,
|
||||
.prev => (idx + n - 1) % n,
|
||||
};
|
||||
const target = self.outputs.items[next];
|
||||
const target = ordered[next];
|
||||
|
||||
if (send) {
|
||||
const win = seat.focused orelse return;
|
||||
win.output = target;
|
||||
// Onto the tags that screen is showing, which under a split tag set is
|
||||
// also what keeps the window there rather than being rehomed straight
|
||||
// back to where it came from.
|
||||
win.tags = target.tags;
|
||||
win.proposed_width = -1;
|
||||
win.proposed_height = -1;
|
||||
// dwm's tagmon leaves you on the monitor you were on, and so does this.
|
||||
self.windowLeftOutput(win);
|
||||
} else {
|
||||
self.focusOutput(target);
|
||||
if (self.topVisible(target)) |win| seat.focus(win) else seat.focus(null);
|
||||
self.enterOutput(seat, target);
|
||||
}
|
||||
self.ipcDirty();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,46 @@ pub fn tagSlot(tags: u32) usize {
|
||||
return @ctz(t) + 1;
|
||||
}
|
||||
|
||||
/// The tags belonging to output `index` of `count`, when the tag set is split
|
||||
/// across the displays.
|
||||
///
|
||||
/// The tags are divided into contiguous ranges in the order the outputs are
|
||||
/// arranged on the desk, so with two monitors the left one owns 1–5 and the
|
||||
/// right 6–9. Contiguous rather than interleaved because the keys are what the
|
||||
/// user reaches for: 1–5 under the left hand for the left screen reads as one
|
||||
/// screen's worth of workspaces, 1,3,5,7,9 does not.
|
||||
///
|
||||
/// A lone output owns every tag, which is what makes the split invisible on a
|
||||
/// laptop with nothing plugged in.
|
||||
pub fn tagsForOutput(index: usize, count: usize) u32 {
|
||||
if (count <= 1 or index >= count) return all_tags;
|
||||
|
||||
// More outputs than tags: one each, and the outputs left over share the
|
||||
// last tag rather than getting none. An output owning no tag could show no
|
||||
// window at all, which is worse than two screens showing the same one.
|
||||
if (count >= tag_count) {
|
||||
return @as(u32, 1) << @intCast(@min(index, tag_count - 1));
|
||||
}
|
||||
|
||||
// Earlier outputs take one of the leftover tags each, so the ranges differ
|
||||
// by at most one and it is never the first screen that comes up short.
|
||||
const base = tag_count / count;
|
||||
const rem = tag_count % count;
|
||||
const start = index * base + @min(index, rem);
|
||||
const len = base + @as(usize, @intFromBool(index < rem));
|
||||
|
||||
const ones: u32 = (@as(u32, 1) << @intCast(len)) - 1;
|
||||
return ones << @intCast(start);
|
||||
}
|
||||
|
||||
/// The lowest tag in a mask, as a mask of its own. What a view falls back to
|
||||
/// when the tags it was showing have moved to another screen.
|
||||
pub fn lowestTag(tags: u32) u32 {
|
||||
const t = tags & all_tags;
|
||||
if (t == 0) return 0;
|
||||
return @as(u32, 1) << @intCast(@ctz(t));
|
||||
}
|
||||
|
||||
/// Keyboard modifiers, matching the values of river_seat_v1.modifiers so the
|
||||
/// mask can be bit-cast straight into the protocol type.
|
||||
pub const Mods = struct {
|
||||
|
||||
+24
-9
@@ -60,9 +60,22 @@ pub const mfact: f32 = 0.55;
|
||||
pub const mfact_min: f32 = 0.05;
|
||||
pub const mfact_max: f32 = 0.95;
|
||||
|
||||
/// Tags visible on a newly connected output.
|
||||
/// Tags visible on a newly connected output. Clamped to the tags that output
|
||||
/// owns when `split_tags` is on, so a second screen starts on the first tag of
|
||||
/// its own range rather than on this one.
|
||||
pub const default_tags: u32 = 1;
|
||||
|
||||
/// Split the tag set across the connected screens, so that every tag lives on
|
||||
/// exactly one of them: with two monitors the left owns tags 1–5 and the right
|
||||
/// 6–9, `Mod+7` moves to the right-hand screen and shows tag 7 there, and
|
||||
/// `Mod+Shift+7` sends the focused window over to it. Screens are ordered left
|
||||
/// to right by where they sit in the output layout, not by the order they were
|
||||
/// plugged in.
|
||||
///
|
||||
/// Set this false for dwm's model instead, where every screen has a full set of
|
||||
/// nine tags of its own and the tag keys never leave the one you are on.
|
||||
pub const split_tags = true;
|
||||
|
||||
/// Names exported over IPC for bars to label tags with.
|
||||
pub const tag_names = [action.tag_count][]const u8{
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
@@ -188,9 +201,10 @@ pub const keys = tagKeys() ++ [_]Key{
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.k, .action = .{ .swap = .prev } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.Return, .action = .zoom },
|
||||
|
||||
// Master area.
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.h, .action = .{ .mfact = .{ .relative = -0.05 } } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.l, .action = .{ .mfact = .{ .relative = 0.05 } } },
|
||||
// Master area. dwm puts these on Mod+h/l; those are the monitor keys here,
|
||||
// so the master area takes the pair dwm gives the monitors.
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.comma, .action = .{ .mfact = .{ .relative = -0.05 } } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.period, .action = .{ .mfact = .{ .relative = 0.05 } } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.i, .action = .{ .nmaster = .{ .relative = 1 } } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.d, .action = .{ .nmaster = .{ .relative = -1 } } },
|
||||
|
||||
@@ -208,11 +222,12 @@ pub const keys = tagKeys() ++ [_]Key{
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.Tab, .action = .view_prev },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.Escape, .action = .view_prev },
|
||||
|
||||
// Outputs.
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.comma, .action = .{ .focus_output = .prev } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.period, .action = .{ .focus_output = .next } },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.comma, .action = .{ .send_to_output = .prev } },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.period, .action = .{ .send_to_output = .next } },
|
||||
// Outputs. Left and right on the same keys as the h/l of vi, since that is
|
||||
// the direction they move in: `prev` is the screen to the left.
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.h, .action = .{ .focus_output = .prev } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.l, .action = .{ .focus_output = .next } },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.h, .action = .{ .send_to_output = .prev } },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.l, .action = .{ .send_to_output = .next } },
|
||||
};
|
||||
|
||||
/// dwm's TAGKEYS macro: Mod+N views, Mod+Shift+N tags, Mod+Ctrl+N toggles the
|
||||
|
||||
+5
-1
@@ -319,10 +319,14 @@ fn encodeState(wm: *Wm, gpa: Allocator, out: *std.ArrayList(u8)) !void {
|
||||
try w.writeAll("{\"name\":");
|
||||
try writeJsonString(w, output.displayName());
|
||||
try w.print(
|
||||
",\"focused\":{s},\"tags\":{d},\"occupied\":{d},\"layout\":\"{s}\",\"layout_symbol\":",
|
||||
",\"focused\":{s},\"tags\":{d},\"owned_tags\":{d},\"occupied\":{d},\"layout\":\"{s}\",\"layout_symbol\":",
|
||||
.{
|
||||
if (wm.focused_output == output) "true" else "false",
|
||||
output.tags,
|
||||
// Which tags belong to this screen at all. Every one of them
|
||||
// unless the tag set is split, in which case a bar wants to
|
||||
// draw its screen's slice and not all nine.
|
||||
output.owned_tags,
|
||||
occupied & act.all_tags,
|
||||
@tagName(st.layout),
|
||||
},
|
||||
|
||||
@@ -244,6 +244,78 @@ test "bits above the tag range do not affect the slot" {
|
||||
try testing.expectEqual(act.tag_count, act.tagSlot(@as(u32, 1) << (act.tag_count - 1)));
|
||||
}
|
||||
|
||||
// ─── splitting the tag set across screens ────────────────────────────────────
|
||||
|
||||
test "a lone screen owns every tag" {
|
||||
try testing.expectEqual(act.all_tags, act.tagsForOutput(0, 1));
|
||||
// No screens at all is not a case callers should have to think about.
|
||||
try testing.expectEqual(act.all_tags, act.tagsForOutput(0, 0));
|
||||
}
|
||||
|
||||
test "two screens split the tags into contiguous halves" {
|
||||
// Nine tags do not halve evenly; the left screen takes the extra one.
|
||||
try testing.expectEqual(@as(u32, 0b000011111), act.tagsForOutput(0, 2));
|
||||
try testing.expectEqual(@as(u32, 0b111100000), act.tagsForOutput(1, 2));
|
||||
}
|
||||
|
||||
test "three screens split the tags into thirds" {
|
||||
try testing.expectEqual(@as(u32, 0b000000111), act.tagsForOutput(0, 3));
|
||||
try testing.expectEqual(@as(u32, 0b000111000), act.tagsForOutput(1, 3));
|
||||
try testing.expectEqual(@as(u32, 0b111000000), act.tagsForOutput(2, 3));
|
||||
}
|
||||
|
||||
test "every tag lands on exactly one screen" {
|
||||
// The whole point of the split: no tag is shared and none goes missing,
|
||||
// whatever the screen count.
|
||||
for (1..act.tag_count + 1) |count| {
|
||||
var seen: u32 = 0;
|
||||
for (0..count) |i| {
|
||||
const mask = act.tagsForOutput(i, count);
|
||||
try testing.expect(mask != 0);
|
||||
try testing.expectEqual(@as(u32, 0), seen & mask);
|
||||
seen |= mask;
|
||||
}
|
||||
try testing.expectEqual(act.all_tags, seen);
|
||||
}
|
||||
}
|
||||
|
||||
test "ranges are contiguous and in screen order" {
|
||||
for (1..act.tag_count + 1) |count| {
|
||||
var next: u32 = 0;
|
||||
for (0..count) |i| {
|
||||
const mask = act.tagsForOutput(i, count);
|
||||
// Contiguous: a run of ones, starting where the last range ended.
|
||||
const lowest = @ctz(mask);
|
||||
const past_highest = 32 - @clz(mask);
|
||||
try testing.expectEqual(next, lowest);
|
||||
try testing.expectEqual(past_highest - lowest, @popCount(mask));
|
||||
next = past_highest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "more screens than tags leaves no screen without one" {
|
||||
// Twelve monitors and nine tags: the ones past the end share the last tag
|
||||
// rather than being handed an empty mask, which could show nothing at all.
|
||||
const count = act.tag_count + 3;
|
||||
for (0..count) |i| {
|
||||
try testing.expect(act.tagsForOutput(i, count) != 0);
|
||||
}
|
||||
try testing.expectEqual(act.tagsForOutput(act.tag_count - 1, count), act.tagsForOutput(count - 1, count));
|
||||
}
|
||||
|
||||
test "a screen index past the end owns everything rather than nothing" {
|
||||
try testing.expectEqual(act.all_tags, act.tagsForOutput(2, 2));
|
||||
}
|
||||
|
||||
test "the lowest tag is what a stranded view falls back to" {
|
||||
try testing.expectEqual(@as(u32, 0b100000), act.lowestTag(0b111100000));
|
||||
try testing.expectEqual(@as(u32, 1), act.lowestTag(act.all_tags));
|
||||
try testing.expectEqual(@as(u32, 0), act.lowestTag(0));
|
||||
// Junk above the tag range is not a tag to fall back to.
|
||||
try testing.expectEqual(@as(u32, 0), act.lowestTag(~act.all_tags));
|
||||
}
|
||||
|
||||
// ─── command parsing ─────────────────────────────────────────────────────────
|
||||
|
||||
fn parseOk(argv: []const []const u8) act.Action {
|
||||
|
||||
Reference in New Issue
Block a user