Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add spawn vim docs #651

Merged
merged 10 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions code/recipes/how-to-spawn-vim/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "how-to-spawn-vim"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
publish.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
ratatui = "0.28.1"
86 changes: 86 additions & 0 deletions code/recipes/how-to-spawn-vim/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// ANCHOR: all
// ANCHOR: imports
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
widgets::Paragraph,
DefaultTerminal, Frame,
};
use std::io::{stdout, Result};
use std::process::Command;

type Terminal = ratatui::Terminal<CrosstermBackend<std::io::Stdout>>;
// ANCHOR_END: imports

// ANCHOR: action_enum
enum Action {
EditFile,
Quit,
None,
}
// ANCHOR_END: action_enum

// ANCHOR: main
fn main() -> Result<()> {
deepanchal marked this conversation as resolved.
Show resolved Hide resolved
let terminal = ratatui::init();
let app_result = run(terminal);
ratatui::restore();
app_result
}
// ANCHOR_END: main

// ANCHOR: run
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(draw)?;
match handle_events()? {
Action::EditFile => run_editor(&mut terminal)?,
Action::Quit => break,
Action::None => {}
}
}
Ok(())
}
// ANCHOR_END: run

// ANCHOR: handle-events
fn handle_events() -> Result<Action> {
if !event::poll(std::time::Duration::from_millis(16))? {
return Ok(Action::None);
}
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char('q') => Ok(Action::Quit),
KeyCode::Char('e') => Ok(Action::EditFile),
_ => Ok(Action::None),
},
_ => Ok(Action::None),
}
}
// ANCHOR_END: handle-events

// ANCHOR: run_editor
fn run_editor(terminal: &mut Terminal) -> Result<()> {
stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?;
Command::new("vim").arg("/tmp/a.txt").status()?;
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
terminal.clear()?;
Ok(())
}
// ANCHOR_END: run_editor

// ANCHOR: draw
fn draw(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello ratatui! (press 'q' to quit, 'e' to edit a file)"),
frame.area(),
);
}
// ANCHOR_END: draw
// ANCHOR_END: all
1 change: 1 addition & 0 deletions src/content/docs/recipes/apps/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ This section covers recipes for developing applications:
- [Setup Panic Hooks](./panic-hooks/)
- [Use Better Panic](./better-panic/)
- [Migrate from TUI-rs](./migrate-from-tui-rs/)
- [Spawn Vim](./spawn-vim/)
117 changes: 117 additions & 0 deletions src/content/docs/recipes/apps/spawn-vim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: Spawn External Editor (Vim)
sidebar:
order: 9
label: Spawn External Editor (Vim)
---

In this recipe, we will explore how to spawn an external editor (Vim) from within the TUI app. This
example demonstrates how to temporarily exit the TUI, run an external command, and then return back
to our TUI app.

Full code:

```rust collapsed title="main.rs (click to expand)"
{{ #include @code/recipes/how-to-spawn-vim/src/main.rs }}
```

## Setup

First, let's look at the main function and the event handling logic:

```rust title="main.rs"
{{ #include @code/recipes/how-to-spawn-vim/src/main.rs:action_enum }}

{{ #include @code/recipes/how-to-spawn-vim/src/main.rs:main }}

{{ #include @code/recipes/how-to-spawn-vim/src/main.rs:run }}

{{ #include @code/recipes/how-to-spawn-vim/src/main.rs:handle-events }}
```

After initializing the terminal in `main` function, we enter a loop in `run` function where we draw
the UI and handle events. The `handle_events` function listens for key events and returns an
`Action` based on the key pressed. Here, we are calling `run_editor` function on `Action::EditFile`
which we will define in next section.

## Spawning Vim

Now, let's define the function `run_editor` function attached to `Action::EditFile` action.

```rust title="main.rs"
{{ #include @code/recipes/how-to-spawn-vim/src/main.rs:run_editor }}
```

To spawn Vim from our TUI app, we first need to relinquish control of input and output, allowing Vim
to have full control over the terminal.

The `run_editor` function handles the logic for spawning vim. First, we leave the alternate screen
and disable raw mode to restore terminal to it's original state. This part is similar to what
[`ratatui::restore`](https://docs.rs/ratatui/latest/ratatui/fn.restore.html) function does in the
`main` function. Next, we spawn a child process with
`Command::new("vim").arg("/tmp/a.txt").status()` which launches `vim` to edit the given file. At
this point, we have given up control of our TUI app to vim. Our TUI app will now wait for the exit
status of the child process. Once the user exits Vim, our TUI app regains control over the terminal
by re-entering alternate screen and enabling raw mode. Lastly, we clear the terminal to ensure the
TUI is displayed correctly.

:::note

Before running another application from your app, you must relinquish control of input and output,
allowing the other app to function correctly.

In the example above, we use a simple event-handling setup. However, if you are using advanced
setups like [component template](https://github.com/ratatui-org/templates), you will need to pause
input events before spawning an external process like Vim. Otherwise, Vim won't have full control
over keybindings and it won't work properly.

Using the
[`tui` module](https://github.com/ratatui-org/templates/blob/5e823efc871107345d59e5deff9284235c1f0bbc/component/template/src/tui.rs)
of the component template, you can do something like this to pause and resume event handlers:

```rust
Action::EditFile => {
tui.exit()?;
let cmd = String::from("vim");
let cmd_args = vec!["/tmp/a.txt".into()];
let status = std::process::Command::new(&command).args(&args).status()?;
if !status.success() {
eprintln!("\nCommand failed with status: {}", status);
}
tui.enter()?;
tui.terminal.clear();
}
```

One more thing to note is that when attempting to start an external process without using the
pattern in the component template, issues can arise such as ANSI RGB values being printed into the
TUI upon returning from the external process. This happens because Vim requests the terminal
background color, and when the terminal responds over stdin, those responses are read by Crossterm
instead. If you encounter such issues, please refer to
[orhun/rattler-build@84ea16a](https://github.com/orhun/rattler-build/commit/84ea16a4f5af33e2703b6330fcb977065263cef6)
and [kdheepak/taskwarrior-tui#46](https://github.com/kdheepak/taskwarrior-tui/issues/46). Using
`select!` + `cancellation_token` + `tokio` as in the component template avoids this problem.

:::

## Running code

Running this program will display "Hello ratatui! (press 'q' to quit, 'e' to edit a file)" in the
terminal. Pressing 'e' will spawn a child process to spawn Vim for editing a temporary file and then
return to the ratatui application after Vim is closed.

Feel free to adapt this example to use other editors like `nvim`, `nano`, etc., by changing the
command in the `Action::EditFile` arm.

:::tip

If you prefer to launch the user-specified `$EDITOR` and retrieve the buffer (edited content) back
into your application, you can use the [`edit`](https://crates.io/crates/edit) crate. This can be
particularly useful if you need to capture the changes made by the user in the editor. There's also
[`editor-command`](https://docs.rs/editor-command/latest/editor_command) crate if you want more
control over launching / overriding editors based on `VISUAL` or `EDITOR` environment variables.

Alternatively, you may use the [`edtui`](https://github.com/preiter93/edtui) crate from ratatui's
ecosystem, which provides text editor widget inspired by vim.

:::