675 lines
26 KiB
Zig
675 lines
26 KiB
Zig
const std = @import("std");
|
|
const context_mod = @import("context.zig");
|
|
const diagnostics = @import("diagnostics.zig");
|
|
const input = @import("input.zig");
|
|
const job = @import("job.zig");
|
|
const layout = @import("layout.zig");
|
|
const leader = @import("leader.zig");
|
|
const lsp = @import("lsp.zig");
|
|
const mobile_acceptance = @import("mobile_acceptance.zig");
|
|
const panel = @import("panel.zig");
|
|
const pi_bridge = @import("pi_bridge.zig");
|
|
const profile = @import("profile.zig");
|
|
const protocol = @import("protocol.zig");
|
|
const replay = @import("replay.zig");
|
|
const repo = @import("repo.zig");
|
|
const session = @import("session.zig");
|
|
const socket = @import("socket.zig");
|
|
const symbol = @import("symbol.zig");
|
|
const syntax = @import("syntax.zig");
|
|
const terminal_debug = @import("terminal_debug.zig");
|
|
const tui = @import("tui.zig");
|
|
|
|
pub const version = "0.1.0-dev";
|
|
|
|
const help_text =
|
|
\\mim - mobile-first terminal code editor for SSH sessions
|
|
\\
|
|
\\Usage:
|
|
\\ mim [path]
|
|
\\ mim [--help]
|
|
\\ mim [--version]
|
|
\\ mim context [task] [intent]
|
|
\\ mim profile
|
|
\\ mim mimctl context <socket|-> [task] [intent]
|
|
\\ mim --mimctl <socket|-> <request...>
|
|
\\
|
|
\\With no path, mim opens the current directory. Existing files open in the
|
|
\\editor; missing paths start a new buffer; directories open the file browser.
|
|
\\This early build also exposes compact local context and mimctl for trusted tools.
|
|
\\
|
|
;
|
|
|
|
const Command = union(enum) {
|
|
help,
|
|
version,
|
|
open: []const u8,
|
|
};
|
|
|
|
fn parseArgs(args: []const []const u8) Command {
|
|
if (args.len <= 1) return .{ .open = "." };
|
|
|
|
const arg = args[1];
|
|
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) return .help;
|
|
if (std.mem.eql(u8, arg, "--version")) return .version;
|
|
return .{ .open = arg };
|
|
}
|
|
|
|
fn versionText() []const u8 {
|
|
return "mim " ++ version ++ "\n";
|
|
}
|
|
|
|
pub fn main(init: std.process.Init) !u8 {
|
|
var debug_allocator = std.heap.DebugAllocator(.{}){};
|
|
defer _ = debug_allocator.deinit();
|
|
const allocator = debug_allocator.allocator();
|
|
|
|
var args = std.process.Args.Iterator.init(init.minimal.args);
|
|
_ = args.skip();
|
|
|
|
const stdout = std.Io.File.stdout();
|
|
const stderr = std.Io.File.stderr();
|
|
|
|
const first_arg = args.next();
|
|
if (first_arg) |arg| {
|
|
if (std.mem.eql(u8, arg, "context")) {
|
|
try printLocalContext(allocator, init.io, &args, stdout);
|
|
return 0;
|
|
}
|
|
if (std.mem.eql(u8, arg, "profile")) {
|
|
try printProfile(allocator, init.io, stdout);
|
|
return 0;
|
|
}
|
|
if (isProfileStub(arg)) {
|
|
try printProfileStub(init.io, stderr, arg);
|
|
return 69;
|
|
}
|
|
if (std.mem.eql(u8, arg, "mimctl")) {
|
|
if (!profile.local_socket) {
|
|
try printProfileStub(init.io, stderr, "mimctl");
|
|
return 69;
|
|
}
|
|
const sub = args.next() orelse {
|
|
try stderr.writeStreamingAll(init.io, "mim: mimctl requires a subcommand\n");
|
|
return 64;
|
|
};
|
|
if (!std.mem.eql(u8, sub, "context")) {
|
|
try stderr.writeStreamingAll(init.io, "mim: unknown mimctl subcommand\n");
|
|
return 64;
|
|
}
|
|
const socket_path = args.next() orelse {
|
|
try stderr.writeStreamingAll(init.io, "mim: mimctl context requires a socket path or '-'\n");
|
|
return 64;
|
|
};
|
|
const line = try collectContextRequest(allocator, &args);
|
|
defer allocator.free(line);
|
|
requestMimctl(allocator, init.io, init.minimal.environ, stdout, stderr, socket_path, line) catch return 69;
|
|
return 0;
|
|
}
|
|
|
|
if (std.mem.eql(u8, arg, "--mimctl")) {
|
|
if (!profile.local_socket) {
|
|
try printProfileStub(init.io, stderr, "mimctl");
|
|
return 69;
|
|
}
|
|
const socket_path = args.next() orelse {
|
|
try stderr.writeStreamingAll(init.io, "mim: --mimctl requires a socket path\n");
|
|
return 64;
|
|
};
|
|
const line = try collectRemainingArgs(allocator, &args);
|
|
defer allocator.free(line);
|
|
if (line.len == 0) {
|
|
try stderr.writeStreamingAll(init.io, "mim: --mimctl requires a request\n");
|
|
return 64;
|
|
}
|
|
requestMimctl(allocator, init.io, init.minimal.environ, stdout, stderr, socket_path, line) catch return 69;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
var e2e_prelude_path: ?[]const u8 = null;
|
|
var open_arg = first_arg;
|
|
if (first_arg) |arg| {
|
|
if (std.mem.eql(u8, arg, "--e2e-prelude-file")) {
|
|
e2e_prelude_path = args.next() orelse {
|
|
try stderr.writeStreamingAll(init.io, "mim: --e2e-prelude-file requires a path\n");
|
|
return 64;
|
|
};
|
|
open_arg = args.next();
|
|
}
|
|
}
|
|
const command = if (open_arg) |arg|
|
|
parseArgs(&.{ "mim", arg })
|
|
else
|
|
parseArgs(&.{"mim"});
|
|
|
|
switch (command) {
|
|
.help => try stdout.writeStreamingAll(init.io, help_text),
|
|
.version => try stdout.writeStreamingAll(init.io, versionText()),
|
|
.open => |path| try openLocalEditor(allocator, init.io, stdout, stderr, path, e2e_prelude_path),
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
fn openLocalEditor(
|
|
allocator: std.mem.Allocator,
|
|
io: std.Io,
|
|
stdout: std.Io.File,
|
|
stderr: std.Io.File,
|
|
path: []const u8,
|
|
e2e_prelude_path: ?[]const u8,
|
|
) !void {
|
|
var client = try tui.Client.init(allocator, .{ .width = 80, .height = 22 });
|
|
defer client.deinit();
|
|
|
|
const stat = std.Io.Dir.cwd().statFile(io, path, .{}) catch |err| switch (err) {
|
|
error.FileNotFound => null,
|
|
else => {
|
|
try stderr.writeStreamingAll(io, "mim: could not inspect path\n");
|
|
return err;
|
|
},
|
|
};
|
|
|
|
if (stat) |metadata| {
|
|
switch (metadata.kind) {
|
|
.directory => try openDirectoryPreview(allocator, io, &client, path),
|
|
.file => {
|
|
if (std.fs.path.dirname(path)) |parent| seedDirectoryRepo(allocator, io, &client, parent) catch {};
|
|
const bytes = std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(diagnostics.max_file_bytes)) catch |err| switch (err) {
|
|
error.StreamTooLong => {
|
|
try client.handleTraceLine("open diagnostic:file_too_large");
|
|
return runLocalEditor(allocator, io, stdout, stderr, &client, path, false, e2e_prelude_path);
|
|
},
|
|
else => return err,
|
|
};
|
|
defer allocator.free(bytes);
|
|
const command = try std.fmt.allocPrint(allocator, "open {s}", .{bytes});
|
|
defer allocator.free(command);
|
|
try client.handleTraceLine(command);
|
|
},
|
|
else => try client.handleTraceLine("open diagnostic:unsupported_file"),
|
|
}
|
|
} else {
|
|
try client.handleTraceLine("open ");
|
|
}
|
|
|
|
try runLocalEditor(allocator, io, stdout, stderr, &client, path, stat != null and stat.?.kind == .directory, e2e_prelude_path);
|
|
}
|
|
|
|
fn seedDirectoryRepo(allocator: std.mem.Allocator, io: std.Io, client: *tui.Client, path: []const u8) !void {
|
|
var dir = try std.Io.Dir.cwd().openDir(io, path, .{ .iterate = true });
|
|
defer dir.close(io);
|
|
|
|
var iterator = dir.iterate();
|
|
var count: usize = 0;
|
|
while (try iterator.next(io)) |entry| {
|
|
if (count >= 80) break;
|
|
if (entry.kind != .file and entry.kind != .directory) continue;
|
|
const relative_path = try std.fmt.allocPrint(allocator, "{s}/{s}{s}", .{ path, entry.name, if (entry.kind == .directory) "/" else "" });
|
|
defer allocator.free(relative_path);
|
|
if (entry.kind == .file) {
|
|
const bytes = std.Io.Dir.cwd().readFileAlloc(io, relative_path, allocator, .limited(diagnostics.max_file_bytes)) catch |err| switch (err) {
|
|
error.StreamTooLong, error.AccessDenied, error.FileNotFound, error.NotDir => null,
|
|
else => null,
|
|
};
|
|
if (bytes) |content| {
|
|
defer allocator.free(content);
|
|
client.addRepoFileContent(relative_path, content) catch {};
|
|
} else {
|
|
client.addRepoFileContent(relative_path, "") catch {};
|
|
}
|
|
} else {
|
|
client.addRepoFileContent(relative_path, "") catch {};
|
|
}
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
fn openDirectoryPreview(allocator: std.mem.Allocator, io: std.Io, client: *tui.Client, path: []const u8) !void {
|
|
try seedDirectoryRepo(allocator, io, client, path);
|
|
try client.handleTraceLine("file_picker");
|
|
}
|
|
|
|
fn runLocalEditor(
|
|
allocator: std.mem.Allocator,
|
|
io: std.Io,
|
|
stdout: std.Io.File,
|
|
stderr: std.Io.File,
|
|
client: *tui.Client,
|
|
path: []const u8,
|
|
is_dir: bool,
|
|
e2e_prelude_path: ?[]const u8,
|
|
) !void {
|
|
const stdin_file = std.Io.File.stdin();
|
|
const interactive = stdin_file.isTty(io) catch false;
|
|
|
|
var last_saved = try client.snapshotBytesAlloc(allocator);
|
|
defer allocator.free(last_saved);
|
|
var last_client_saved: ?[]u8 = null;
|
|
defer if (last_client_saved) |bytes| allocator.free(bytes);
|
|
|
|
if (e2e_prelude_path) |prelude_path| {
|
|
const prelude = try std.Io.Dir.cwd().readFileAlloc(io, prelude_path, allocator, .limited(64 * 1024));
|
|
defer allocator.free(prelude);
|
|
var lines = std.mem.splitScalar(u8, prelude, '\n');
|
|
while (lines.next()) |line| if (line.len != 0) try client.handleTraceLine(line);
|
|
}
|
|
|
|
if (!interactive) {
|
|
try renderLocalFrame(allocator, io, stdout, client, path, is_dir, false);
|
|
return;
|
|
}
|
|
|
|
if (is_dir) try client.openFilePicker();
|
|
|
|
var raw_terminal = try RawTerminal.enable(stdin_file.handle);
|
|
defer raw_terminal.restore();
|
|
|
|
while (true) {
|
|
try renderLocalFrame(allocator, io, stdout, client, path, is_dir, true);
|
|
const raw = try readEditorInputFdAlloc(allocator, stdin_file.handle);
|
|
defer if (raw) |bytes| allocator.free(bytes);
|
|
if (raw == null) return;
|
|
|
|
client.handleInput(raw.?) catch |err| {
|
|
if (err == tui.Error.ClientQuit) return;
|
|
try stderr.writeStreamingAll(io, "mim: input failed: ");
|
|
try stderr.writeStreamingAll(io, @errorName(err));
|
|
try stderr.writeStreamingAll(io, "\n");
|
|
continue;
|
|
};
|
|
|
|
const after = try client.snapshotBytesAlloc(allocator);
|
|
defer allocator.free(after);
|
|
var dirty = !std.mem.eql(u8, after, last_saved);
|
|
if (client.saved()) |saved_bytes| {
|
|
const new_save_request = last_client_saved == null or !std.mem.eql(u8, saved_bytes, last_client_saved.?);
|
|
if (new_save_request) {
|
|
if (is_dir and client.currentPath() == null) {
|
|
client.setStatusMessage("directory browser has nothing to save");
|
|
} else {
|
|
const save_path = try resolveLocalSavePath(allocator, path, client.currentPath());
|
|
defer allocator.free(save_path);
|
|
try persistLocalSave(allocator, io, save_path, saved_bytes, &last_saved, &last_client_saved);
|
|
dirty = false;
|
|
client.setStatusMessage("saved");
|
|
}
|
|
}
|
|
} else |_| {}
|
|
|
|
if (client.requestedQuit()) {
|
|
if (dirty and !client.requestedDiscardQuit()) {
|
|
client.clearQuit();
|
|
client.setStatusMessage("dirty buffer: Space w saves, Space Q discards");
|
|
continue;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn resolveLocalSavePath(allocator: std.mem.Allocator, launch_path: []const u8, current_path: ?[]const u8) ![]u8 {
|
|
const selected = current_path orelse launch_path;
|
|
if (std.fs.path.isAbsolute(selected) or std.mem.indexOfScalar(u8, selected, '/') != null) return allocator.dupe(u8, selected);
|
|
const base = std.fs.path.dirname(launch_path) orelse ".";
|
|
return std.fs.path.join(allocator, &.{ base, selected });
|
|
}
|
|
|
|
fn persistLocalSave(
|
|
allocator: std.mem.Allocator,
|
|
io: std.Io,
|
|
path: []const u8,
|
|
bytes: []const u8,
|
|
last_saved: *[]u8,
|
|
last_client_saved: *?[]u8,
|
|
) !void {
|
|
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = path, .data = bytes });
|
|
const saved_copy = try allocator.dupe(u8, bytes);
|
|
errdefer allocator.free(saved_copy);
|
|
const client_copy = try allocator.dupe(u8, bytes);
|
|
allocator.free(last_saved.*);
|
|
last_saved.* = saved_copy;
|
|
if (last_client_saved.*) |old| allocator.free(old);
|
|
last_client_saved.* = client_copy;
|
|
}
|
|
|
|
const RawTerminal = struct {
|
|
fd: std.posix.fd_t,
|
|
original: std.posix.termios,
|
|
|
|
fn enable(fd: std.posix.fd_t) !RawTerminal {
|
|
const original = try std.posix.tcgetattr(fd);
|
|
var raw = original;
|
|
raw.iflag.BRKINT = false;
|
|
raw.iflag.ICRNL = false;
|
|
raw.iflag.INPCK = false;
|
|
raw.iflag.ISTRIP = false;
|
|
raw.iflag.IXON = false;
|
|
raw.oflag.OPOST = false;
|
|
raw.cflag.CSIZE = .CS8;
|
|
raw.lflag.ECHO = false;
|
|
raw.lflag.ICANON = false;
|
|
raw.lflag.IEXTEN = false;
|
|
raw.lflag.ISIG = false;
|
|
raw.cc[@intFromEnum(std.posix.V.MIN)] = 1;
|
|
raw.cc[@intFromEnum(std.posix.V.TIME)] = 0;
|
|
try std.posix.tcsetattr(fd, .FLUSH, raw);
|
|
return .{ .fd = fd, .original = original };
|
|
}
|
|
|
|
fn restore(self: *RawTerminal) void {
|
|
std.posix.tcsetattr(self.fd, .FLUSH, self.original) catch {};
|
|
}
|
|
};
|
|
|
|
fn readEditorInputFdAlloc(allocator: std.mem.Allocator, fd: std.posix.fd_t) !?[]u8 {
|
|
var one: [1]u8 = undefined;
|
|
const n = try std.posix.read(fd, &one);
|
|
if (n == 0) return null;
|
|
var bytes = std.ArrayList(u8).empty;
|
|
errdefer bytes.deinit(allocator);
|
|
try bytes.append(allocator, one[0]);
|
|
if (one[0] != 0x1b) return try bytes.toOwnedSlice(allocator);
|
|
|
|
var fds = [_]std.posix.pollfd{.{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }};
|
|
const ready = std.posix.poll(&fds, 30) catch 0;
|
|
if (ready == 0 or (fds[0].revents & std.posix.POLL.IN) == 0) return try bytes.toOwnedSlice(allocator);
|
|
const second_n = try std.posix.read(fd, &one);
|
|
if (second_n == 0) return try bytes.toOwnedSlice(allocator);
|
|
try bytes.append(allocator, one[0]);
|
|
if (one[0] == '[') {
|
|
while (bytes.items.len < 8) {
|
|
const next_ready = std.posix.poll(&fds, 30) catch 0;
|
|
if (next_ready == 0 or (fds[0].revents & std.posix.POLL.IN) == 0) break;
|
|
const next_n = try std.posix.read(fd, &one);
|
|
if (next_n == 0) break;
|
|
try bytes.append(allocator, one[0]);
|
|
if (one[0] >= '@' and one[0] <= '~') break;
|
|
}
|
|
}
|
|
return try bytes.toOwnedSlice(allocator);
|
|
}
|
|
|
|
fn readEditorInputAlloc(allocator: std.mem.Allocator, stdin: *std.Io.Reader, fd: std.posix.fd_t) !?[]u8 {
|
|
const first = stdin.takeByte() catch |err| switch (err) {
|
|
error.EndOfStream => return null,
|
|
else => return err,
|
|
};
|
|
var bytes = std.ArrayList(u8).empty;
|
|
errdefer bytes.deinit(allocator);
|
|
try bytes.append(allocator, first);
|
|
|
|
if (first == 0x1b) {
|
|
var fds = [_]std.posix.pollfd{.{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }};
|
|
const ready: usize = if (fd < 0) 1 else std.posix.poll(&fds, 30) catch 0;
|
|
if (ready == 0 or (fd >= 0 and (fds[0].revents & std.posix.POLL.IN) == 0)) return try bytes.toOwnedSlice(allocator);
|
|
const second = stdin.takeByte() catch return try bytes.toOwnedSlice(allocator);
|
|
try bytes.append(allocator, second);
|
|
if (second == '[') {
|
|
const third_ready: usize = if (fd < 0) 1 else std.posix.poll(&fds, 30) catch 0;
|
|
if (third_ready == 0 or (fd >= 0 and (fds[0].revents & std.posix.POLL.IN) == 0)) return try bytes.toOwnedSlice(allocator);
|
|
const third = stdin.takeByte() catch return try bytes.toOwnedSlice(allocator);
|
|
try bytes.append(allocator, third);
|
|
}
|
|
return try bytes.toOwnedSlice(allocator);
|
|
}
|
|
|
|
const len = std.unicode.utf8ByteSequenceLength(first) catch 1;
|
|
var i: usize = 1;
|
|
while (i < len) : (i += 1) {
|
|
const next = stdin.takeByte() catch break;
|
|
try bytes.append(allocator, next);
|
|
}
|
|
return try bytes.toOwnedSlice(allocator);
|
|
}
|
|
|
|
fn renderLocalFrame(allocator: std.mem.Allocator, io: std.Io, stdout: std.Io.File, client: *tui.Client, path: []const u8, is_dir: bool, interactive: bool) !void {
|
|
const frame = try client.render(allocator);
|
|
defer allocator.free(frame);
|
|
if (interactive) {
|
|
try stdout.writeStreamingAll(io, "\x1b[H\x1b[2J");
|
|
try writeTerminalText(allocator, io, stdout, frame);
|
|
try writeTerminalText(allocator, io, stdout, "\n\n");
|
|
try writeTerminalText(allocator, io, stdout, "mim opened: ");
|
|
try writeTerminalText(allocator, io, stdout, path);
|
|
try writeTerminalText(allocator, io, stdout, if (is_dir)
|
|
"\nDirectory browser. Space opens commands; o/Enter opens panel items where available; q quits when clean.\n"
|
|
else
|
|
"\nEditor starts in normal mode. Press i to insert; Space commands work in normal mode; Space w saves; Space q quits when clean; Space Q discards dirty changes.\n");
|
|
} else {
|
|
try stdout.writeStreamingAll(io, frame);
|
|
try stdout.writeStreamingAll(io, "\n\n");
|
|
try stdout.writeStreamingAll(io, "mim opened: ");
|
|
try stdout.writeStreamingAll(io, path);
|
|
try stdout.writeStreamingAll(io, if (is_dir)
|
|
"\nDirectory browser. Space opens commands; o/Enter opens panel items where available; q quits when clean.\n"
|
|
else
|
|
"\nEditor starts in normal mode. Press i to insert; Space commands work in normal mode; Space w saves; Space q quits when clean; Space Q discards dirty changes.\n");
|
|
}
|
|
}
|
|
|
|
fn writeTerminalText(allocator: std.mem.Allocator, io: std.Io, stdout: std.Io.File, bytes: []const u8) !void {
|
|
const converted = try terminalCrlfAlloc(allocator, bytes);
|
|
defer allocator.free(converted);
|
|
try stdout.writeStreamingAll(io, converted);
|
|
}
|
|
|
|
fn terminalCrlfAlloc(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
|
|
var out = std.ArrayList(u8).empty;
|
|
errdefer out.deinit(allocator);
|
|
var start: usize = 0;
|
|
for (bytes, 0..) |byte, i| {
|
|
if (byte != '\n') continue;
|
|
if (start < i) try out.appendSlice(allocator, bytes[start..i]);
|
|
try out.appendSlice(allocator, "\r\n");
|
|
start = i + 1;
|
|
}
|
|
if (start < bytes.len) try out.appendSlice(allocator, bytes[start..]);
|
|
return out.toOwnedSlice(allocator);
|
|
}
|
|
|
|
fn printLocalContext(allocator: std.mem.Allocator, io: std.Io, args: *std.process.Args.Iterator, stdout: std.Io.File) !void {
|
|
const task = args.next();
|
|
const intent = args.next();
|
|
const text = try context_mod.localContextAlloc(allocator, ".", task, intent, .{});
|
|
defer allocator.free(text);
|
|
try stdout.writeStreamingAll(io, text);
|
|
}
|
|
|
|
fn printProfile(allocator: std.mem.Allocator, io: std.Io, stdout: std.Io.File) !void {
|
|
const text = try profile.statusAlloc(allocator);
|
|
defer allocator.free(text);
|
|
try stdout.writeStreamingAll(io, text);
|
|
}
|
|
|
|
fn isProfileStub(arg: []const u8) bool {
|
|
return std.mem.eql(u8, arg, "remote") or
|
|
std.mem.eql(u8, arg, "mimctl") or
|
|
std.mem.eql(u8, arg, "plugin") or
|
|
std.mem.eql(u8, arg, "plugins") or
|
|
std.mem.eql(u8, arg, "background-agent") or
|
|
std.mem.eql(u8, arg, "bundled-pi");
|
|
}
|
|
|
|
fn printProfileStub(io: std.Io, stderr: std.Io.File, arg: []const u8) !void {
|
|
try stderr.writeStreamingAll(io, "mim: ");
|
|
try stderr.writeStreamingAll(io, arg);
|
|
try stderr.writeStreamingAll(io, " not built in this profile\n");
|
|
}
|
|
|
|
fn requestMimctl(
|
|
allocator: std.mem.Allocator,
|
|
io: std.Io,
|
|
environ: std.process.Environ,
|
|
stdout: std.Io.File,
|
|
stderr: std.Io.File,
|
|
socket_path: []const u8,
|
|
line: []const u8,
|
|
) !void {
|
|
const resolved_socket_path = if (std.mem.eql(u8, socket_path, "-"))
|
|
std.process.Environ.getAlloc(environ, allocator, socket.socket_env_name) catch {
|
|
try stderr.writeStreamingAll(io, "mim: MIM_SOCKET is not set\n");
|
|
return error.MimctlFailed;
|
|
}
|
|
else
|
|
try allocator.dupe(u8, socket_path);
|
|
defer allocator.free(resolved_socket_path);
|
|
|
|
const response = socket.request(allocator, resolved_socket_path, line) catch {
|
|
try stderr.writeStreamingAll(io, "mim: mimctl request failed\n");
|
|
return error.MimctlFailed;
|
|
};
|
|
defer allocator.free(response);
|
|
try stdout.writeStreamingAll(io, response);
|
|
}
|
|
|
|
fn collectContextRequest(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) ![]u8 {
|
|
var line = std.ArrayList(u8).empty;
|
|
errdefer line.deinit(allocator);
|
|
try line.appendSlice(allocator, "context");
|
|
while (args.next()) |arg| {
|
|
try line.append(allocator, ' ');
|
|
try line.appendSlice(allocator, arg);
|
|
}
|
|
return line.toOwnedSlice(allocator);
|
|
}
|
|
|
|
fn collectRemainingArgs(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) ![]u8 {
|
|
var line = std.ArrayList(u8).empty;
|
|
errdefer line.deinit(allocator);
|
|
var first = true;
|
|
while (args.next()) |arg| {
|
|
if (!first) try line.append(allocator, ' ');
|
|
try line.appendSlice(allocator, arg);
|
|
first = false;
|
|
}
|
|
return line.toOwnedSlice(allocator);
|
|
}
|
|
|
|
test {
|
|
_ = context_mod;
|
|
_ = diagnostics;
|
|
_ = input;
|
|
_ = job;
|
|
_ = layout;
|
|
_ = leader;
|
|
_ = lsp;
|
|
_ = mobile_acceptance;
|
|
_ = panel;
|
|
_ = pi_bridge;
|
|
_ = profile;
|
|
_ = protocol;
|
|
_ = replay;
|
|
_ = repo;
|
|
_ = session;
|
|
_ = socket;
|
|
_ = symbol;
|
|
_ = syntax;
|
|
_ = tui;
|
|
}
|
|
|
|
test "regular: help text names the binary and open path" {
|
|
try std.testing.expect(std.mem.indexOf(u8, help_text, "mim - mobile-first terminal code editor") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, help_text, "mim [path]") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, help_text, "mim context") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, help_text, "mim profile") != null);
|
|
}
|
|
|
|
test "regular: excluded profile capabilities have explicit stubs" {
|
|
try std.testing.expect(isProfileStub("remote"));
|
|
try std.testing.expect(isProfileStub("mimctl"));
|
|
try std.testing.expect(isProfileStub("plugin"));
|
|
try std.testing.expect(isProfileStub("background-agent"));
|
|
try std.testing.expect(!isProfileStub("context"));
|
|
}
|
|
|
|
test "regular: version output is stable enough for smoke checks" {
|
|
try std.testing.expectEqualStrings("mim 0.1.0-dev\n", versionText());
|
|
}
|
|
|
|
test "regular: no args opens current directory" {
|
|
const argv = [_][]const u8{"mim"};
|
|
const parsed = parseArgs(&argv);
|
|
switch (parsed) {
|
|
.open => |path| try std.testing.expectEqualStrings(".", path),
|
|
else => return error.ExpectedOpenPath,
|
|
}
|
|
}
|
|
|
|
test "regular: positional path opens file directory or new buffer" {
|
|
const argv = [_][]const u8{ "mim", "some-file.zig" };
|
|
const parsed = parseArgs(&argv);
|
|
switch (parsed) {
|
|
.open => |path| try std.testing.expectEqualStrings("some-file.zig", path),
|
|
else => return error.ExpectedOpenPath,
|
|
}
|
|
}
|
|
|
|
test "regular: local editor input reader preserves text utf8 and arrows" {
|
|
var ascii = std.Io.Reader.fixed("a");
|
|
const ascii_event = (try readEditorInputAlloc(std.testing.allocator, &ascii, -1)).?;
|
|
defer std.testing.allocator.free(ascii_event);
|
|
try std.testing.expectEqualStrings("a", ascii_event);
|
|
|
|
var utf8 = std.Io.Reader.fixed("é");
|
|
const utf8_event = (try readEditorInputAlloc(std.testing.allocator, &utf8, -1)).?;
|
|
defer std.testing.allocator.free(utf8_event);
|
|
try std.testing.expectEqualStrings("é", utf8_event);
|
|
|
|
var arrow = std.Io.Reader.fixed("\x1b[D");
|
|
const arrow_event = (try readEditorInputAlloc(std.testing.allocator, &arrow, -1)).?;
|
|
defer std.testing.allocator.free(arrow_event);
|
|
try std.testing.expectEqualStrings("\x1b[D", arrow_event);
|
|
}
|
|
|
|
test "regular: local editor client exposes save and dirty quit guards" {
|
|
var client = try tui.Client.init(std.testing.allocator, .{ .width = 32, .height = 8 });
|
|
defer client.deinit();
|
|
|
|
try client.handleTraceLine("open ");
|
|
const initial = try client.snapshotBytesAlloc(std.testing.allocator);
|
|
defer std.testing.allocator.free(initial);
|
|
try std.testing.expectEqualStrings("", initial);
|
|
|
|
try client.handleInput("i");
|
|
try client.handleInput("h");
|
|
try client.handleInput("e");
|
|
try client.handleInput("l");
|
|
try client.handleInput("l");
|
|
try client.handleInput("o");
|
|
const edited = try client.snapshotBytesAlloc(std.testing.allocator);
|
|
defer std.testing.allocator.free(edited);
|
|
try std.testing.expectEqualStrings("hello", edited);
|
|
|
|
try client.handleInput(" ");
|
|
try client.handleInput("x");
|
|
const with_space = try client.snapshotBytesAlloc(std.testing.allocator);
|
|
defer std.testing.allocator.free(with_space);
|
|
try std.testing.expectEqualStrings("hello x", with_space);
|
|
try client.handleTraceLine("key escape");
|
|
try std.testing.expectEqualStrings("normal", client.modeName());
|
|
try client.handleInput(" ");
|
|
try client.handleInput("w");
|
|
try std.testing.expectEqualStrings("hello x", try client.saved());
|
|
|
|
try client.handleInput("i");
|
|
try client.handleInput("!");
|
|
try client.handleTraceLine("key escape");
|
|
try client.handleInput(" ");
|
|
try client.handleInput("q");
|
|
try std.testing.expect(client.requestedQuit());
|
|
client.clearQuit();
|
|
try std.testing.expect(!client.requestedQuit());
|
|
}
|
|
|
|
test "terminal debug helpers are included in main test root" {
|
|
_ = terminal_debug;
|
|
}
|
|
|
|
test "regular: raw terminal output converts newline to carriage-return newline" {
|
|
const converted = try terminalCrlfAlloc(std.testing.allocator, "a\nb\n");
|
|
defer std.testing.allocator.free(converted);
|
|
try std.testing.expectEqualStrings("a\r\nb\r\n", converted);
|
|
}
|