feat: add commands to execute shell tasks from keybindings
This is the first part of #67.
This commit is contained in:
parent
337b6ce626
commit
cfc99b61dc
5 changed files with 259 additions and 3 deletions
10
build.zig
10
build.zig
|
@ -380,6 +380,15 @@ pub fn build_exe(
|
||||||
break :blk b.addRunArtifact(tests);
|
break :blk b.addRunArtifact(tests);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shell_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/shell.zig"),
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "thespian", .module = thespian_mod },
|
||||||
|
.{ .name = "cbor", .module = cbor_mod },
|
||||||
|
.{ .name = "log", .module = log_mod },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const ripgrep_mod = b.createModule(.{
|
const ripgrep_mod = b.createModule(.{
|
||||||
.root_source_file = b.path("src/ripgrep.zig"),
|
.root_source_file = b.path("src/ripgrep.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
|
@ -444,6 +453,7 @@ pub fn build_exe(
|
||||||
.{ .name = "text_manip", .module = text_manip_mod },
|
.{ .name = "text_manip", .module = text_manip_mod },
|
||||||
.{ .name = "Buffer", .module = Buffer_mod },
|
.{ .name = "Buffer", .module = Buffer_mod },
|
||||||
.{ .name = "keybind", .module = keybind_mod },
|
.{ .name = "keybind", .module = keybind_mod },
|
||||||
|
.{ .name = "shell", .module = shell_mod },
|
||||||
.{ .name = "ripgrep", .module = ripgrep_mod },
|
.{ .name = "ripgrep", .module = ripgrep_mod },
|
||||||
.{ .name = "theme", .module = themes_dep.module("theme") },
|
.{ .name = "theme", .module = themes_dep.module("theme") },
|
||||||
.{ .name = "themes", .module = themes_dep.module("themes") },
|
.{ .name = "themes", .module = themes_dep.module("themes") },
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
{
|
{
|
||||||
|
"project": {
|
||||||
|
"press": [
|
||||||
|
["f5", ["create_scratch_buffer", "*test*"], ["shell_execute_insert", "zig", "build", "test"]],
|
||||||
|
["f7", ["create_scratch_buffer", "*build*"], ["shell_execute_insert", "zig", "build"]]
|
||||||
|
]
|
||||||
|
},
|
||||||
"normal": {
|
"normal": {
|
||||||
|
"inherit": "project",
|
||||||
"press": [
|
"press": [
|
||||||
["ctrl+e", "open_recent"],
|
["ctrl+e", "open_recent"],
|
||||||
["ctrl+r", "open_recent_project"],
|
["ctrl+r", "open_recent_project"],
|
||||||
|
@ -120,12 +127,12 @@
|
||||||
["ctrl+f2", "insert_command_name"],
|
["ctrl+f2", "insert_command_name"],
|
||||||
["f3", "goto_next_match"],
|
["f3", "goto_next_match"],
|
||||||
["f15", "goto_prev_match"],
|
["f15", "goto_prev_match"],
|
||||||
["f5", "toggle_inspector_view"],
|
["ctrl+f5", "dump_current_line_tree"],
|
||||||
["f6", "dump_current_line_tree"],
|
["ctrl+f7", "dump_current_line"],
|
||||||
["f7", "dump_current_line"],
|
|
||||||
["f9", "theme_prev"],
|
["f9", "theme_prev"],
|
||||||
["f10", "theme_next"],
|
["f10", "theme_next"],
|
||||||
["f11", "toggle_panel"],
|
["f11", "toggle_panel"],
|
||||||
|
["ctrl+f11", "toggle_inspector_view"],
|
||||||
["f12", "goto_definition"],
|
["f12", "goto_definition"],
|
||||||
["f34", "toggle_whitespace_mode"],
|
["f34", "toggle_whitespace_mode"],
|
||||||
["escape", "cancel"],
|
["escape", "cancel"],
|
||||||
|
@ -182,6 +189,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"inherit": "project",
|
||||||
"on_match_failure": "ignore",
|
"on_match_failure": "ignore",
|
||||||
"press": [
|
"press": [
|
||||||
["h", "open_help"],
|
["h", "open_help"],
|
||||||
|
|
197
src/shell.zig
Normal file
197
src/shell.zig
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const tp = @import("thespian");
|
||||||
|
const cbor = @import("cbor");
|
||||||
|
const log = @import("log");
|
||||||
|
|
||||||
|
pid: ?tp.pid,
|
||||||
|
stdin_behavior: std.process.Child.StdIo,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
const module_name = @typeName(Self);
|
||||||
|
pub const max_chunk_size = tp.subprocess.max_chunk_size;
|
||||||
|
pub const Writer = std.io.Writer(*Self, Error, write);
|
||||||
|
pub const BufferedWriter = std.io.BufferedWriter(max_chunk_size, Writer);
|
||||||
|
pub const Error = error{ InvalidShellArg0, OutOfMemory, Exit, ThespianSpawnFailed, Closed };
|
||||||
|
|
||||||
|
pub const OutputHandler = fn (parent: tp.pid_ref, arg0: []const u8, output: []const u8) void;
|
||||||
|
pub const ExitHandler = fn (parent: tp.pid_ref, arg0: []const u8, err_msg: []const u8, exit_code: i64) void;
|
||||||
|
|
||||||
|
pub const Handlers = struct {
|
||||||
|
out: *const OutputHandler,
|
||||||
|
err: ?*const OutputHandler = null,
|
||||||
|
exit: *const ExitHandler = log_exit_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn execute(allocator: std.mem.Allocator, argv: tp.message, handlers: Handlers) Error!void {
|
||||||
|
const stdin_behavior = .Close;
|
||||||
|
var pid = try Process.create(allocator, argv, stdin_behavior, handlers);
|
||||||
|
pid.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_pipe(allocator: std.mem.Allocator, argv: tp.message, output_handler: ?OutputHandler, exit_handler: ?ExitHandler) Error!Self {
|
||||||
|
const stdin_behavior = .Pipe;
|
||||||
|
return .{ .pid = try Process.create(allocator, argv, stdin_behavior, output_handler, exit_handler), .stdin_behavior = stdin_behavior };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
if (self.pid) |pid| {
|
||||||
|
if (self.stdin_behavior == .Pipe)
|
||||||
|
pid.send(.{"close"}) catch {};
|
||||||
|
self.pid = null;
|
||||||
|
pid.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, bytes: []const u8) !usize {
|
||||||
|
try self.input(bytes);
|
||||||
|
return bytes.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn input(self: *const Self, bytes: []const u8) !void {
|
||||||
|
const pid = self.pid orelse return error.Closed;
|
||||||
|
var remaining = bytes;
|
||||||
|
while (remaining.len > 0)
|
||||||
|
remaining = loop: {
|
||||||
|
if (remaining.len > max_chunk_size) {
|
||||||
|
try pid.send(.{ "input", remaining[0..max_chunk_size] });
|
||||||
|
break :loop remaining[max_chunk_size..];
|
||||||
|
} else {
|
||||||
|
try pid.send(.{ "input", remaining });
|
||||||
|
break :loop &[_]u8{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(self: *Self) void {
|
||||||
|
self.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writer(self: *Self) Writer {
|
||||||
|
return .{ .context = self };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bufferedWriter(self: *Self) BufferedWriter {
|
||||||
|
return .{ .unbuffered_writer = self.writer() };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_handler(parent: tp.pid_ref, arg0: []const u8, output: []const u8) void {
|
||||||
|
_ = parent;
|
||||||
|
_ = arg0;
|
||||||
|
const logger = log.logger(@typeName(Self));
|
||||||
|
var it = std.mem.splitScalar(u8, output, '\n');
|
||||||
|
while (it.next()) |line| logger.print("{s}", .{line});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_err_handler(parent: tp.pid_ref, arg0: []const u8, output: []const u8) void {
|
||||||
|
_ = parent;
|
||||||
|
const logger = log.logger(@typeName(Self));
|
||||||
|
var it = std.mem.splitScalar(u8, output, '\n');
|
||||||
|
while (it.next()) |line| logger.print_err(arg0, "{s}", .{line});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_exit_handler(parent: tp.pid_ref, arg0: []const u8, err_msg: []const u8, exit_code: i64) void {
|
||||||
|
_ = parent;
|
||||||
|
const logger = log.logger(@typeName(Self));
|
||||||
|
if (exit_code > 0) {
|
||||||
|
logger.print_err(arg0, "'{s}' terminated {s} exitcode: {d}", .{ arg0, err_msg, exit_code });
|
||||||
|
} else {
|
||||||
|
logger.print("'{s}' exited", .{arg0});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Process = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
arg0: [:0]const u8,
|
||||||
|
argv: tp.message,
|
||||||
|
receiver: Receiver,
|
||||||
|
sp: ?tp.subprocess = null,
|
||||||
|
parent: tp.pid,
|
||||||
|
logger: log.Logger,
|
||||||
|
stdin_behavior: std.process.Child.StdIo,
|
||||||
|
handlers: Handlers,
|
||||||
|
|
||||||
|
const Receiver = tp.Receiver(*Process);
|
||||||
|
|
||||||
|
pub fn create(allocator: std.mem.Allocator, argv_: tp.message, stdin_behavior: std.process.Child.StdIo, handlers: Handlers) Error!tp.pid {
|
||||||
|
const argv = try argv_.clone(allocator);
|
||||||
|
var arg0_: []const u8 = "";
|
||||||
|
if (!try argv.match(.{ tp.extract(&arg0_), tp.more })) {
|
||||||
|
allocator.free(argv.buf);
|
||||||
|
return error.InvalidShellArg0;
|
||||||
|
}
|
||||||
|
const self = try allocator.create(Process);
|
||||||
|
self.* = .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.argv = argv,
|
||||||
|
.arg0 = try allocator.dupeZ(u8, arg0_),
|
||||||
|
.receiver = Receiver.init(receive, self),
|
||||||
|
.parent = tp.self_pid().clone(),
|
||||||
|
.logger = log.logger(@typeName(Self)),
|
||||||
|
.stdin_behavior = stdin_behavior,
|
||||||
|
.handlers = handlers,
|
||||||
|
};
|
||||||
|
return tp.spawn_link(self.allocator, self, Process.start, self.arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *Process) void {
|
||||||
|
if (self.sp) |*sp| sp.deinit();
|
||||||
|
self.parent.deinit();
|
||||||
|
self.logger.deinit();
|
||||||
|
self.allocator.free(self.arg0);
|
||||||
|
self.allocator.free(self.argv.buf);
|
||||||
|
self.close() catch {};
|
||||||
|
self.allocator.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(self: *Process) tp.result {
|
||||||
|
if (self.sp) |*sp| {
|
||||||
|
defer self.sp = null;
|
||||||
|
try sp.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(self: *Process) tp.result {
|
||||||
|
errdefer self.deinit();
|
||||||
|
_ = tp.set_trap(true);
|
||||||
|
var buf: [1024]u8 = undefined;
|
||||||
|
const json = self.argv.to_json(&buf) catch |e| return tp.exit_error(e, @errorReturnTrace());
|
||||||
|
self.logger.print("shell: execute {s}", .{json});
|
||||||
|
self.sp = tp.subprocess.init(self.allocator, self.argv, module_name, self.stdin_behavior) catch |e| return tp.exit_error(e, @errorReturnTrace());
|
||||||
|
tp.receive(&self.receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive(self: *Process, _: tp.pid_ref, m: tp.message) tp.result {
|
||||||
|
errdefer self.deinit();
|
||||||
|
var bytes: []u8 = "";
|
||||||
|
|
||||||
|
if (try m.match(.{ "input", tp.extract(&bytes) })) {
|
||||||
|
const sp = self.sp orelse return tp.exit_error(error.Closed, null);
|
||||||
|
try sp.send(bytes);
|
||||||
|
} else if (try m.match(.{"close"})) {
|
||||||
|
try self.close();
|
||||||
|
} else if (try m.match(.{ module_name, "stdout", tp.extract(&bytes) })) {
|
||||||
|
self.handlers.out(self.parent.ref(), self.arg0, bytes);
|
||||||
|
} else if (try m.match(.{ module_name, "stderr", tp.extract(&bytes) })) {
|
||||||
|
(self.handlers.err orelse self.handlers.out)(self.parent.ref(), self.arg0, bytes);
|
||||||
|
} else if (try m.match(.{ module_name, "term", tp.more })) {
|
||||||
|
self.handle_terminated(m) catch |e| return tp.exit_error(e, @errorReturnTrace());
|
||||||
|
} else if (try m.match(.{ "exit", "normal" })) {
|
||||||
|
return tp.exit_normal();
|
||||||
|
} else {
|
||||||
|
self.logger.err("receive", tp.unexpected(m));
|
||||||
|
return tp.unexpected(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_terminated(self: *Process, m: tp.message) !void {
|
||||||
|
var err_msg: []const u8 = undefined;
|
||||||
|
var exit_code: i64 = undefined;
|
||||||
|
if (try m.match(.{ tp.any, tp.any, "exited", 0 })) {
|
||||||
|
self.logger.print("'{s}' exited ok", .{self.arg0});
|
||||||
|
} else if (try m.match(.{ tp.any, tp.any, "error.FileNotFound", 1 })) {
|
||||||
|
self.logger.print_err(self.arg0, "'{s}' executable not found", .{self.arg0});
|
||||||
|
} else if (try m.match(.{ tp.any, tp.any, tp.extract(&err_msg), tp.extract(&exit_code) })) {
|
||||||
|
self.logger.print_err(self.arg0, "'{s}' terminated {s} exitcode: {d}", .{ self.arg0, err_msg, exit_code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -3603,6 +3603,9 @@ pub const Editor = struct {
|
||||||
if (ctx.args.match(.{ tp.extract(&file_path), tp.extract(&content) }) catch false) {
|
if (ctx.args.match(.{ tp.extract(&file_path), tp.extract(&content) }) catch false) {
|
||||||
try self.open_scratch(file_path, content);
|
try self.open_scratch(file_path, content);
|
||||||
self.clamp();
|
self.clamp();
|
||||||
|
} else if (ctx.args.match(.{tp.extract(&file_path)}) catch false) {
|
||||||
|
try self.open_scratch(file_path, "");
|
||||||
|
self.clamp();
|
||||||
} else return error.InvalidOpenScratchBufferArgument;
|
} else return error.InvalidOpenScratchBufferArgument;
|
||||||
}
|
}
|
||||||
pub const open_scratch_buffer_meta = .{ .arguments = &.{ .string, .string } };
|
pub const open_scratch_buffer_meta = .{ .arguments = &.{ .string, .string } };
|
||||||
|
|
|
@ -7,6 +7,7 @@ const root = @import("root");
|
||||||
const location_history = @import("location_history");
|
const location_history = @import("location_history");
|
||||||
const project_manager = @import("project_manager");
|
const project_manager = @import("project_manager");
|
||||||
const log = @import("log");
|
const log = @import("log");
|
||||||
|
const shell = @import("shell");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const Plane = @import("renderer").Plane;
|
const Plane = @import("renderer").Plane;
|
||||||
|
@ -382,6 +383,15 @@ const cmds = struct {
|
||||||
}
|
}
|
||||||
pub const open_config_meta = .{ .description = "Edit configuration file" };
|
pub const open_config_meta = .{ .description = "Edit configuration file" };
|
||||||
|
|
||||||
|
pub fn create_scratch_buffer(self: *Self, ctx: Ctx) Result {
|
||||||
|
try self.check_all_not_dirty();
|
||||||
|
tui.reset_drag_context();
|
||||||
|
try self.create_editor();
|
||||||
|
try command.executeName("open_scratch_buffer", ctx);
|
||||||
|
tui.need_render();
|
||||||
|
}
|
||||||
|
pub const create_scratch_buffer_meta = .{ .arguments = &.{ .string, .string } };
|
||||||
|
|
||||||
pub fn restore_session(self: *Self, _: Ctx) Result {
|
pub fn restore_session(self: *Self, _: Ctx) Result {
|
||||||
if (tp.env.get().str("project").len == 0) {
|
if (tp.env.get().str("project").len == 0) {
|
||||||
try open_project_cwd(self, .{});
|
try open_project_cwd(self, .{});
|
||||||
|
@ -592,6 +602,34 @@ const cmds = struct {
|
||||||
defer rg.deinit();
|
defer rg.deinit();
|
||||||
}
|
}
|
||||||
pub const find_in_files_query_meta = .{ .arguments = &.{.string} };
|
pub const find_in_files_query_meta = .{ .arguments = &.{.string} };
|
||||||
|
|
||||||
|
pub fn shell_execute_log(self: *Self, ctx: Ctx) Result {
|
||||||
|
if (!try ctx.args.match(.{ tp.string, tp.more }))
|
||||||
|
return error.InvalidShellArgument;
|
||||||
|
const cmd = ctx.args;
|
||||||
|
const handlers = struct {
|
||||||
|
fn out(_: tp.pid_ref, arg0: []const u8, output: []const u8) void {
|
||||||
|
const logger = log.logger(arg0);
|
||||||
|
var it = std.mem.splitScalar(u8, output, '\n');
|
||||||
|
while (it.next()) |line| logger.print("{s}", .{std.fmt.fmtSliceEscapeLower(line)});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try shell.execute(self.allocator, cmd, .{ .out = handlers.out });
|
||||||
|
}
|
||||||
|
pub const shell_execute_log_meta = .{ .arguments = &.{.string} };
|
||||||
|
|
||||||
|
pub fn shell_execute_insert(self: *Self, ctx: Ctx) Result {
|
||||||
|
if (!try ctx.args.match(.{ tp.string, tp.more }))
|
||||||
|
return error.InvalidShellArgument;
|
||||||
|
const cmd = ctx.args;
|
||||||
|
const handlers = struct {
|
||||||
|
fn out(parent: tp.pid_ref, _: []const u8, output: []const u8) void {
|
||||||
|
parent.send(.{ "cmd", "insert_chars", .{output} }) catch {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try shell.execute(self.allocator, cmd, .{ .out = handlers.out });
|
||||||
|
}
|
||||||
|
pub const shell_execute_insert_meta = .{ .arguments = &.{.string} };
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn handle_editor_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
|
pub fn handle_editor_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
|
||||||
|
|
Loading…
Add table
Reference in a new issue