src/cli/muxg.zig
Ref: Size: 11.1 KiB History
//! Native workspace entry; subsequent attachments are chosen in the picker.
const std = @import("std");
const native = @import("native");
const client = @import("client");
const term = @import("term");
const cliflags = @import("cliflags");
const sockpath = @import("sockpath");
const xdg = @import("xdg");
const proto = term.protocol;
const hosts = client.hosts;
const PointSize = struct {
value: f64,
pub fn parseCLI(text: []const u8) !PointSize {
return .{ .value = native.font_options.parsePointSize(text) catch return error.Invalid };
}
};
const Color = struct {
value: u32,
pub fn parseCLI(text: []const u8) !Color {
return .{ .value = native.config.parseColor(text) catch return error.Invalid };
}
};
const usage =
\\usage: muxg [TARGET] [--forward LOCAL_PORT:REMOTE_PORT]... [--session NAME] [--sock PATH] [--via CMD] [--key PATH] [--theme NAME|PATH] [--background HEX] [--foreground HEX] [--cursor-color HEX] [--palette N=HEX] [--font-family FAMILY] [--font-size POINTS] [--font-px N]
\\
\\ TARGET HOST (ssh handoff) or quic://HOST[:PORT]; none restores the saved workspace
\\ --session the session name (default: the daemon's default session)
\\ --sock a local daemon's socket path
\\ --via a command whose stdio is the daemon
\\ --key the QUIC key file (or MUX_KEY_FILE)
\\ --forward bind 127.0.0.1:LOCAL_PORT and reach 127.0.0.1:REMOTE_PORT on TARGET; repeatable
\\ --font-px font pixels at 100% display scale (default 16)
\\ --font-family ordered font family; repeat for fallbacks (config: font-family)
\\ --font-size font size in points, 1–192 (config: font-size)
\\ --theme theme filename in config themes directory or absolute path
\\ --background, --foreground, --cursor-color explicit 6-digit colour
\\ --palette indexed colour override, N=RRGGBB; may be repeated
\\ --help --version
\\
;
const Arguments = struct {
sock: ?[]const u8 = null,
via: ?[]const u8 = null,
key: ?[]const u8 = null,
session: ?proto.SessionName = null,
font_px: ?u16 = null,
font_size: ?PointSize = null,
theme: ?[]const u8 = null,
background: ?Color = null,
foreground: ?Color = null,
cursor_color: ?Color = null,
_palette_values: [256]?u32 = [_]?u32{null} ** 256,
_font_families: std.ArrayListUnmanaged([:0]const u8) = .empty,
_forwards: std.ArrayListUnmanaged(client.forward.Rule) = .empty,
_argv_alloc: std.mem.Allocator = undefined,
_font_family_oom: bool = false,
_target: ?[]const u8 = null,
_targets: usize = 0,
pub fn positional(self: *Arguments, word: []const u8) bool {
self._target = word;
self._targets += 1;
return true;
}
pub fn extra(self: *Arguments, rest: []const [:0]const u8) usize {
if (std.mem.eql(u8, rest[0], "--font-family")) {
if (rest.len < 2) return 0;
self._font_families.append(self._argv_alloc, rest[1]) catch {
self._font_family_oom = true;
};
return 2;
}
if (std.mem.eql(u8, rest[0], "--palette")) {
if (rest.len < 2) return 0;
const pair = native.config.parsePalette(rest[1]) catch return 0;
self._palette_values[pair.index] = pair.color;
return 2;
}
if (std.mem.eql(u8, rest[0], "--forward")) {
if (rest.len < 2) return 0;
const rule = client.forward.Rule.parse(rest[1]) catch return 0;
self._forwards.append(self._argv_alloc, rule) catch {
self._font_family_oom = true;
};
return 2;
}
return 0;
}
};
comptime {
cliflags.assertDocumented(Arguments, usage, &.{});
}
pub fn main() !u8 {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) std.debug.print("muxg: LEAK: allocations outlived deinit\n", .{});
const alloc = gpa.allocator();
var argv_arena = std.heap.ArenaAllocator.init(alloc);
defer argv_arena.deinit();
const argv_alloc = argv_arena.allocator();
const args = try std.process.argsAlloc(argv_alloc);
if (try client.resolver.helper(alloc, args[1..])) |code| return code;
var o: Arguments = .{ ._argv_alloc = argv_alloc };
defer o._font_families.deinit(argv_alloc);
defer o._forwards.deinit(argv_alloc);
cliflags.parseStrict(Arguments, &o, args[1..]) catch |e| return cliflags.exitFor(e, usage, "muxg", std.fmt.comptimePrint("{s} ({s})", .{ @import("build_options").version, @tagName(@import("builtin").mode) }));
if (o._font_family_oom) return error.OutOfMemory;
if (o.font_px != null and o.font_size != null) {
std.debug.print("muxg: --font-px and --font-size are ambiguous; choose one\n", .{});
return 2;
}
var settings: native.config.Settings = .{};
defer settings.deinit(argv_alloc);
var config_line: usize = 1;
const config_path = xdg.pathFrom(argv_alloc, std.posix.getenv("XDG_CONFIG_HOME"), std.posix.getenv("HOME"), ".config", "config") catch |err| switch (err) {
error.NoHome => null,
else => return err,
};
if (config_path) |path| settings = native.config.load(argv_alloc, path, &config_line) catch |err| {
const reason = switch (err) {
error.UnknownKey => "unknown key; supported keys: font-family, font-size, theme, foreground, background, cursor-color, palette",
error.InvalidSyntax => "expected one key = value per line; only font-family may repeat",
error.InvalidValue => "font-size must be a finite number between 1 and 192 points",
error.InvalidColor => "colour must be exactly six hexadecimal digits",
error.InvalidPalette => "palette must be N=RRGGBB with N between 0 and 255",
error.InvalidThemeName => "theme must name a file or an absolute path",
error.MissingFamily => "font-family must name an installed monospace family",
else => @errorName(err),
};
std.debug.print("muxg: config {s}:{d}: {s}\n", .{ path, config_line, reason });
return 2;
};
var explicit: native.theme.Overrides = .{
.foreground = if (o.foreground) |v| v.value else settings.foreground,
.background = if (o.background) |v| v.value else settings.background,
.cursor = if (o.cursor_color) |v| v.value else settings.cursor_color,
};
for (settings.palette, 0..) |v, i| explicit.palette[i] = v;
for (o._palette_values, 0..) |v, i| {
if (v) |color| explicit.palette[i] = color;
}
var selected: native.theme.Overrides = .{};
const selection = o.theme orelse if (settings.theme) |v| v else null;
if (selection) |name| {
const theme_path = resolveThemePath(argv_alloc, config_path, name) catch |err| {
if (o.theme == null and config_path != null) {
std.debug.print("muxg: config {s}:{d}: invalid theme '{s}': {s}\n", .{ config_path.?, settings.theme_line, name, @errorName(err) });
} else std.debug.print("muxg: invalid theme '{s}': {s}\n", .{ name, @errorName(err) });
return 2;
};
var theme_line: usize = 1;
var warning_ctx = WarningContext{ .path = theme_path };
selected = native.theme.load(argv_alloc, theme_path, &theme_line, &warning_ctx, themeWarning) catch |err| {
std.debug.print("muxg: theme {s}:{d}: {s}\n", .{ theme_path, theme_line, if (err == error.InvalidValue) "invalid colour" else if (err == error.InvalidSyntax) "expected one key = value per line" else @errorName(err) });
return 2;
};
}
const named: usize = @as(usize, @intFromBool(o.sock != null)) + @intFromBool(o.via != null) + o._targets;
if (named > 1) {
std.debug.print("muxg: name one transport: HOST, --sock, --via or quic://\n{s}", .{usage});
return 2;
}
if (o._forwards.items.len != 0 and named == 0) {
std.debug.print("muxg: --forward requires an explicit TARGET, --sock, or --via\n", .{});
return 2;
}
const cli_points: ?f64 = if (o.font_size) |points| points.value else null;
const font_points = if (o.font_px != null) null else cli_points orelse settings.size_points;
const font_px = o.font_px orelse 16;
if (font_px == 0 or font_px > 256) {
std.debug.print("muxg: --font-px must be between 1 and 256\n", .{});
return 2;
}
const session = if (o.session) |n| n.name else "";
const key = std.posix.getenv("MUX_KEY_FILE");
const font_families: []const [:0]const u8 = if (o._font_families.items.len != 0)
o._font_families.items
else if (settings.families.len != 0)
settings.families
else
native.font.default_families;
const temporary = named != 0 or o.session != null;
const local_path: ?[]const u8 = if (o.sock) |path| path else if (o._target != null or o.via != null) null else if (temporary) (try sockpath.defaultOrExplain(argv_alloc, "muxg") orelse return 1) else sockpath.defaultSockPath(argv_alloc) catch null;
const local: ?client.Target = if (local_path) |path| .{ .sock = path } else null;
const target: ?client.Target = if (!temporary) null else if (o.via) |cmd| .{ .via = cmd } else if (o._target) |word| resolve(argv_alloc, word, o.key orelse key) catch |err| {
std.debug.print("muxg: bad target: {s}\n", .{@errorName(err)});
return 2;
} else local;
return native.run(alloc, .{
.target = target,
.local_target = local,
.state_path = if (temporary) null else try xdg.statePath(argv_alloc, "native-workspace.json"),
.key_path = o.key orelse key,
.session = session,
.font_px = font_px,
.font_families = font_families,
.font_points = font_points,
.appearance = native.theme.merge(native.theme.legacy, selected, explicit),
.test_fifo = std.posix.getenv("MUXG_TEST_FIFO"),
.forwards = o._forwards.items,
}) catch |err| {
std.debug.print("muxg: {s}\n", .{if (err == error.WorkspaceAlreadyOpen) "the saved workspace is already open" else @errorName(err)});
return 2;
};
}
const WarningContext = struct { path: []const u8 };
fn themeWarning(ctx: ?*anyopaque, line: usize, key: []const u8) void {
const info: *WarningContext = @ptrCast(@alignCast(ctx.?));
std.debug.print("{s}:{d}: unsupported theme key {s}; ignored\n", .{ info.path, line, key });
}
fn resolveThemePath(alloc: std.mem.Allocator, config_path: ?[]const u8, name: []const u8) ![]const u8 {
if (name.len == 0) return error.InvalidThemeName;
if (std.fs.path.isAbsolute(name)) return alloc.dupe(u8, name);
if (std.mem.indexOfAny(u8, name, "/\\") != null or std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) return error.InvalidThemeName;
const cfg = config_path orelse return error.NoHome;
const dir = std.fs.path.dirname(cfg) orelse return error.InvalidThemeName;
return std.fmt.allocPrint(alloc, "{s}/themes/{s}", .{ dir, name });
}
fn resolve(alloc: std.mem.Allocator, word: []const u8, key: ?[]const u8) !client.Target {
var target = try client.Target.fromSpec(alloc, try hosts.parse(word), key, client.quic_idle_ms_default, false);
if (target == .hand) target.hand.narrate = true;
return target;
}