diff --git a/kitty/launch.py b/kitty/launch.py index d599885e1..d5b4203fb 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -943,6 +943,7 @@ class EditCmd: self.file_size = -1 self.version = 0 self.source_window_id = self.editor_window_id = -1 + self.editor_exit_code: int | None = None simple = 'file_inode', 'file_data', 'abort_signaled', 'version' for k, v in parse_message(msg, simple): if k == 'file_inode': @@ -1023,9 +1024,13 @@ class EditCmd: self.send_data(source_window, 'UPDATE', data) editor_window = boss.window_id_map.get(self.editor_window_id) if editor_window is None: + if self.editor_exit_code is None: + # Wait for the PID death callback to provide the editor's exit code. + # It will call check_status() again once the exit code is known. + return edits_in_flight.pop(self.source_window_id, None) if source_window is not None: - self.send_data(source_window, 'DONE') + self.send_data(source_window, 'DONE', str(self.editor_exit_code).encode()) self.abort_signaled = self.abort_signaled or 'closed' else: self.schedule_check() @@ -1142,6 +1147,17 @@ def remote_edit(msg: str, window: Window) -> None: q.abort_signaled = 'replaced' edits_in_flight[window.id] = c w.actions_on_close.append(c.on_edit_window_close) + editor_pid = w.child.pid + if editor_pid: + def on_editor_pid_death(wait_status: int, err: Exception | None) -> None: + c.editor_exit_code = os.waitstatus_to_exitcode(wait_status) if err is None else 1 + c.check_status() + try: + get_boss().monitor_pid(editor_pid, on_editor_pid_death) + except RuntimeError: + c.editor_exit_code = 0 # monitoring table full, assume success + else: + c.editor_exit_code = 0 c.schedule_check() diff --git a/tools/cmd/edit_in_kitty/main.go b/tools/cmd/edit_in_kitty/main.go index 4901f0bb1..02819bb96 100644 --- a/tools/cmd/edit_in_kitty/main.go +++ b/tools/cmd/edit_in_kitty/main.go @@ -29,7 +29,7 @@ func encode(x string) string { type OnDataCallback = func(data_type string, data []byte) error -func edit_loop(data_to_send string, kill_if_signaled bool, on_data OnDataCallback) (err error) { +func edit_loop(data_to_send string, kill_if_signaled bool, on_data OnDataCallback) (exit_code int, err error) { lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking) if err != nil { return @@ -52,7 +52,14 @@ func edit_loop(data_to_send string, kill_if_signaled bool, on_data OnDataCallbac if line == "KITTY_DATA_END" { lp.QueueWriteString(update_type + "\r\n") if update_type == "DONE" { - lp.Quit(0) + if data.Len() > 0 { + if b, err2 := base64.StdEncoding.DecodeString(data.String()); err2 == nil { + if n, err3 := strconv.Atoi(strings.TrimSpace(string(b))); err3 == nil { + exit_code = n + } + } + } + lp.Quit(exit_code) return nil } b, err := base64.StdEncoding.DecodeString(data.String()) @@ -150,7 +157,7 @@ func edit_loop(data_to_send string, kill_if_signaled bool, on_data OnDataCallbac return } if canceled { - return tui.Canceled + return 1, tui.Canceled } ds := lp.DeathSignalName() @@ -160,29 +167,29 @@ func edit_loop(data_to_send string, kill_if_signaled bool, on_data OnDataCallbac lp.KillIfSignalled() return } - return &tui.KilledBySignal{Msg: fmt.Sprint("Killed by signal: ", ds), SignalName: ds} + return 1, &tui.KilledBySignal{Msg: fmt.Sprint("Killed by signal: ", ds), SignalName: ds} } return } -func edit_in_kitty(path string, opts *Options) (err error) { +func edit_in_kitty(path string, opts *Options) (exit_code int, err error) { read_file, err := os.Open(path) if err != nil { - return fmt.Errorf("Failed to open %s for reading with error: %w", path, err) + return 1, fmt.Errorf("Failed to open %s for reading with error: %w", path, err) } defer read_file.Close() var s unix.Stat_t err = unix.Fstat(int(read_file.Fd()), &s) if err != nil { - return fmt.Errorf("Failed to stat %s with error: %w", path, err) + return 1, fmt.Errorf("Failed to stat %s with error: %w", path, err) } if s.Size > int64(opts.MaxFileSize)*1024*1024 { - return fmt.Errorf("File size %s is too large for performant editing", humanize.Bytes(uint64(s.Size))) + return 1, fmt.Errorf("File size %s is too large for performant editing", humanize.Bytes(uint64(s.Size))) } file_data, err := io.ReadAll(read_file) if err != nil { - return fmt.Errorf("Failed to read from %s with error: %w", path, err) + return 1, fmt.Errorf("Failed to read from %s with error: %w", path, err) } read_file.Close() data := strings.Builder{} @@ -199,11 +206,11 @@ func edit_in_kitty(path string, opts *Options) (err error) { add_encoded := func(key, val string) { add(key, encode(val)) } if unix.Access(path, unix.R_OK|unix.W_OK) != nil { - return fmt.Errorf("%s is not readable and writeable", path) + return 1, fmt.Errorf("%s is not readable and writeable", path) } cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("Failed to get the current working directory with error: %w", err) + return 1, fmt.Errorf("Failed to get the current working directory with error: %w", err) } add_encoded("cwd", cwd) for _, arg := range os.Args[2:] { @@ -219,12 +226,12 @@ func edit_in_kitty(path string, opts *Options) (err error) { } return } - err = edit_loop(data.String(), true, write_data) + exit_code, err = edit_loop(data.String(), true, write_data) if err != nil { if err == tui.Canceled { - return err + return 1, err } - return fmt.Errorf("Failed to receive edited file back from terminal with error: %w", err) + return 1, fmt.Errorf("Failed to receive edited file back from terminal with error: %w", err) } return } @@ -265,8 +272,7 @@ func EntryPoint(parent *cli.Command) *cli.Command { if err != nil { return 1, err } - err = edit_in_kitty(file_path, &opts) - return 0, err + return edit_in_kitty(file_path, &opts) }, }) AddCloneSafeOpts(sc) diff --git a/tools/cmd/tool/confirm_and_run_shebang.go b/tools/cmd/tool/confirm_and_run_shebang.go index cdb96be3c..ba2ff3d9b 100644 --- a/tools/cmd/tool/confirm_and_run_shebang.go +++ b/tools/cmd/tool/confirm_and_run_shebang.go @@ -4,6 +4,7 @@ package tool import ( "bufio" + "errors" "fmt" "os" "os/exec" @@ -120,7 +121,13 @@ func confirm_and_run_shebang(args []string, confirm_policy ConfirmPolicy) (rc in editor.Stdin = os.Stdin editor.Stdout = os.Stdout editor.Stderr = os.Stderr - editor.Run() + if err = editor.Run(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return exitErr.ExitCode(), nil + } + return 1, err + } return confirm_and_run_shebang(args, ConfirmIfNeeded) case "y": }