Running an external program and wait for input?

Hey!

I’m writing a small TUI in Notty where it displays a list of grepped files.

I’d like to be able to allow the user to edit their files in $EDITOR and then return to the program, but I can’t figure out how that works with Notty.

So the idea is, you enter a file name, it opens $EDITOR and waits for you to complete and then you reenter the program, reloading the state.

How would you set that up in OCaml?

Basically you spawn $EDITOR with the filename as an argument and block until the spawn returns. For example here’s (.mli, .ml) an API that does that.

I would really advise you to use Unix.create_process_*, and then Unix.wait_pid on the result. But if you are fond of escaping problems you might even try to use Sys.command with an appropriate dose of Filename.quote_command.

(And I guess there may be a few reset to peform on your notty rendering)

1 Like

Excellent, in my main loop with notty, I did

    let pid = Unix.create_process "vi" [||] Unix.stdin Unix.stdout Unix.stderr in
    let _ = Unix.waitpid [] pid in
    main t (x, y)

to test and it worked wonderfully.

1 Like

Note that create_process won’t work when $EDITOR is the prefix for a shell command, rather than just a path. VS Code, for example, has to be set as EDITOR="code --wait", otherwise it’ll return immediately.

So, to match common usage, you’d have to go with Sys.command, which is admittedly a bit more awkward to get right without quotation bugs/vulnerabilities.

If you go with Sys.command, do not forget to take a look at Filename.quote_command which will take care of much of the difficulty around quoting.

Cheers,
Nicolas

You don’t have to. If you assume $EDITOR is reasonably specified (doesn’t expect shell expansions) you can also simply parse $EDITOR (which is what the API I linked to does with this function).

2 Likes

So the Unix.create_process solution basically works, except that when I resize the window it crashes with this error:

Fatal error: exception Unix.Unix_error(Unix.EINTR, "wait", "")
Raised by primitive operation at Dune__exe__Main.main in file "bin/main.ml", line 69, characters 12-24
Called from Dune__exe__Main in file "bin/main.ml", line 75, characters 9-42

The logic I have for my launching my program is

  | `Key (`ASCII 'e', []) | `Key (`Enter, []) ->
    (* Editor might be set with extra args, in that case we need to separate them from the editor itself. *)
    let[@warning "-8"] (editor :: args) =
      String.split_on_char ' ' (Sys.getenv "EDITOR")
    in
    let selected_file, _ = get_selected_file files y in
    let full_path_file = execution_directory ^ "/" ^ selected_file in
    let full_args = Array.append (Array.of_list args) [| full_path_file |] in
    Term.cursor t (Some (0, 0));
    let _ =
      Unix.create_process_env
        editor
        full_args
        (Unix.environment ())
        Unix.stdin
        Unix.stdout
        Unix.stderr
    in
    let _ = Unix.wait () in
    Term.cursor t None;
    main t ((x, y), scroll)

Currently my method of extracting arguments might be a bit naive, I just split on space.

I do also have this Notty case

  | `Resize _ -> main t (pos, scroll)

Maybe that’s what is causing the issue?

Window resizes trigger the SIGWINCH signal, which interrupts your system call, you need to retry it (example). See the discussion here in the ocamlunix course.

1 Like