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
		Add a link
		
	
		Reference in a new issue