Something went wrong. Try again.
window manager for the river wayland compositor
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129// SPDX-FileCopyrightText: © 2026 mtmn//// SPDX-License-Identifier: BSD-3-Clause
const Command = @This();
const std = @import("std");
pub const arguments_max = 32;pub const size_max = 4096;
// Word end offsets are u16, so the buffer must always fit.comptime { std.debug.assert(size_max <= std.math.maxInt(u16));}
/// fromText and fromSlices fill every field. They reset only count: just/// the bytes up to the last word end are ever read, so the 4 KiB buffer/// needs no clearing between uses.buffer: [size_max]u8,/// End offset of each word inside buffer. Offsets, not pointers, so the/// struct stays valid after a by-value copy into the pending queue.ends: [arguments_max]u16,count: usize,/// Set by the queueing site, not by fromText or fromSlices: commands built/// from the static config file must not trigger the tag OSD, so a session/// start or reload never flashes it.from_config: bool = false,
pub const Cursor = struct { command: *const Command, index: usize = 0, start: usize = 0,
pub fn next(cursor: *Cursor) ?[]const u8 { if (cursor.index == cursor.command.count) return null; const end = cursor.command.ends[cursor.index]; const argument = cursor.command.buffer[cursor.start..end]; cursor.start = end; cursor.index += 1; return argument; }};
pub fn arguments(command: *const Command) Cursor { return .{ .command = command };}
test "fromText splits arguments" { const testing = std.testing; var command: Command = undefined; try command.fromText("map normal Super Q close"); try testing.expectEqual(@as(usize, 5), command.count); var cursor = command.arguments(); try testing.expectEqualStrings("map", cursor.next().?); try testing.expectEqualStrings("normal", cursor.next().?); try testing.expectEqualStrings("Super", cursor.next().?); try testing.expectEqualStrings("Q", cursor.next().?); try testing.expectEqualStrings("close", cursor.next().?); try testing.expectEqual(@as(?[]const u8, null), cursor.next());}
test "fromSlices stores arguments" { const testing = std.testing; var command: Command = undefined; try command.fromSlices(&.{ "map", "normal", "Super", "Q", "close" }); try testing.expectEqual(@as(usize, 5), command.count); var cursor = command.arguments(); try testing.expectEqualStrings("map", cursor.next().?); try testing.expectEqualStrings("normal", cursor.next().?); try testing.expectEqualStrings("Super", cursor.next().?); try testing.expectEqualStrings("Q", cursor.next().?); try testing.expectEqualStrings("close", cursor.next().?); try testing.expectEqual(@as(?[]const u8, null), cursor.next());}
test "command survives a by-value copy" { const testing = std.testing; var command: Command = undefined; try command.fromSlices(&.{ "map", "normal", "Super", "Q", "close" }); const copied = command; var cursor = copied.arguments(); try testing.expectEqualStrings("map", cursor.next().?); try testing.expectEqualStrings("normal", cursor.next().?); try testing.expectEqualStrings("Super", cursor.next().?); try testing.expectEqualStrings("Q", cursor.next().?); try testing.expectEqualStrings("close", cursor.next().?); try testing.expectEqual(@as(?[]const u8, null), cursor.next());}
/// Split text on spaces into an owned command. Used to run bound/// commands, which may be shell spawns or window-manager commands.pub fn fromText( command: *Command, text: []const u8,) error{ CommandTooLong, TooManyArguments }!void { command.count = 0; if (text.len > size_max) return error.CommandTooLong; var used: usize = 0; var parts = std.mem.splitScalar(u8, text, ' '); while (parts.next()) |part| { if (part.len == 0) continue; if (command.count == arguments_max) return error.TooManyArguments; if (part.len > size_max - used) return error.CommandTooLong; @memcpy(command.buffer[used..][0..part.len], part); used += part.len; command.ends[command.count] = @intCast(used); command.count += 1; }}
/// Store argument slices in the command buffer. Used to build commands/// programmatically, for example from the static config file.pub fn fromSlices( command: *Command, argument_list: []const []const u8,) error{ CommandTooLong, TooManyArguments }!void { command.count = 0; if (argument_list.len > arguments_max) return error.TooManyArguments; var used: usize = 0; for (argument_list) |argument| { if (argument.len > size_max - used) return error.CommandTooLong; @memcpy(command.buffer[used..][0..argument.len], argument); used += argument.len; command.ends[command.count] = @intCast(used); command.count += 1; }}