Skip to content
Open
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
47 changes: 46 additions & 1 deletion crates/atuin/src/command/client/search/interactive.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::{
fs,
io::{Write, stdout},
process::Command,
time::Duration,
};

use atuin_common::utils::{self, Escapable as _};
use eyre::Result;
use futures_util::FutureExt;
use semver::Version;
use tempfile::NamedTempFile;
use time::OffsetDateTime;
use unicode_width::UnicodeWidthStr;

Expand Down Expand Up @@ -50,6 +53,7 @@ const TAB_TITLES: [&str; 2] = ["Search", "Inspect"];

pub enum InputAction {
Accept(usize),
EditAccept(usize),
Copy(usize),
Delete(usize),
ReturnOriginal,
Expand Down Expand Up @@ -84,6 +88,36 @@ struct StyleState {
inner_width: usize,
}

fn get_visual_editor() -> String {
std::env::var("FCEDIT")
.or_else(|_| std::env::var("EDITOR"))
.or_else(|_| std::env::var("VISUAL"))
.unwrap_or_else(|_| "vim".to_string())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a maintainer here, just procrastinating on other work I should be doing, so take this suggestion with a grain of salt.

You might consider a different fallback here. Some minimal Fedora derivatives come without EDITOR set and with vim-minimal installed which doesn't provide /usr/bin/vim, just /usr/bin/vi. Debian looks like it relies on the alternatives system so calling editor will get you the default.

You could probe to see if there's a /usr/bin/editor and then fall back to vi, or you could turn it into an error instructing the user to set the correct environment variables. That'd look something like this (which I've not tested at all):

        .or_else(|_| std::env::var("VISUAL"))
        .ok_or_else(|| eyre::eyre!("To edit commands, set the EDITOR environment variable to your preferred editor"))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using vi instead of vim sounds right. In systems with vim, vi is usually just another name for vim. A thing I was thinking, related to this, is that perhaps config.toml could optionally specify an editor, and that would be something to check for first. Reporting an error seems like user-friendly behavior. Is that the general way atuin handles situations like this? With respect to the exact editors used, I feel like I would want to probe for vi and issue the error only after that. That way I maintain the same sequence used by fc (modulo any change in config.toml). But maybe my desire to emulate fc is not appropriate.

When you say /usr/bin/editor, you're just using editor as a stand-in, right? Just double-checking. You're asking that I make sure no matter what they have specified, even in one of those variables, that their choice actually exists?

Finally, it occurs to me that this is a feature you might want to turn on/off in config.toml. It seems unlikely, at least to me, but it's a thought. I feel like "off" means "just don't touch Ctrl-v". Hmm. And also I should look to see any place else that mentions keys and consequences, e.g., help text, documentation, etc. A PR that adds a key should update such things as well.

It wasn't immediately obvious to me how to write a test (or a couple of tests) in the test section of interactive.rs since running a visual editor requires interactive input. This is on my mind.

Copy link
Author

@wolf wolf Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, editor is not just a place-holder. On some OSs, that's an actual thing. Actually probing (as you suggest) for the existence of /usr/bin/editor sounds like good behavior. I wonder if we also need to check the actual OS, so that we know editor is appropriate. My initial thought is no.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I know how to do this as your are suggesting. Making sure specific editors exist feels like the code will be bulky compared to what is currently in this PR. Looking specifically for /usr/bin/editor; and maybe adding a key to config.toml. But I also think these are good ideas, regardless of whether you are a maintainer or not.

I will explore implementing this, and update this PR for further review.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I wouldn't enumerate nvim, emacs, etc. As long as the probe for /usr/bin/editor happens after checking the EDITOR environment variable I don't think you need to worry about checking the OS, either, although that might be necessary if some distribution ships something else as /usr/bin/editor and also don't set an EDITOR environment variable.

Personally, I feel like we can get away without a config.toml setting for the editor since the EDITOR environment variable is so widely used and I have a hard time imagining someone who wants to use nvim for git commit messages, but emacs for editing commands in Atuin.

For whatever it's worth, I lean slightly towards presenting the user with an error over defaulting to vi if nothing is set. I expect nearly every Atuin user knows how to use vi, but for any that don't it would be a shocking experience to accidentally end up there. I don't have a great sense of whether or not that matches the general Atuin style though.

There's a table of shortcuts, but I'm not sure where the source for that document is.

Finally, for testing maybe you could set EDITOR to a shell script that writes a string to the given tempfile so it's not interactive? I've not thought hard about that, though.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! A script instead of an actual editor is a great idea! I don't know why I didn't think of that. That's exactly how I test all my dotfile shell functions.

}

fn visual_edit_command(original_command: &str) -> Result<String> {
// 1. Create temp file with command
let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "{original_command}")?;
let temp_path = temp_file.path();

// 2. Get editor
let editor = get_visual_editor();

// 3. Run editor
let status = Command::new(&editor).arg(temp_path).status()?;

if !status.success() {
return Ok(original_command.to_string());
}

// 4. Read edited command
let edited_command = fs::read_to_string(temp_path)?.trim_end().to_string();

// 5. Return with execution prefix
Ok(format!("__atuin_accept__:{edited_command}"))
}

impl State {
async fn query_results(
&mut self,
Expand Down Expand Up @@ -383,6 +417,9 @@ impl State {

match input.code {
KeyCode::Enter => return self.handle_search_accept(settings),
KeyCode::Char('v') if ctrl => {
return InputAction::EditAccept(self.results_state.selected());
}
KeyCode::Char('m') if ctrl => return self.handle_search_accept(settings),
KeyCode::Char('y') if ctrl => {
return InputAction::Copy(self.results_state.selected());
Expand Down Expand Up @@ -1251,6 +1288,11 @@ pub async fn history(
// index is in bounds so we return that entry
Ok(command)
}
InputAction::EditAccept(index) if index < results.len() => {
let original_command = &results[index].command;
let edited_command = visual_edit_command(original_command)?;
Ok(edited_command)
}
InputAction::ReturnOriginal => Ok(String::new()),
InputAction::Copy(index) => {
let cmd = results.swap_remove(index).command;
Expand All @@ -1263,7 +1305,10 @@ pub async fn history(
// * out of bounds -> usually implies no selected entry so we return the input
Ok(app.search.input.into_inner())
}
InputAction::Continue | InputAction::Redraw | InputAction::Delete(_) => {
InputAction::Continue
| InputAction::Redraw
| InputAction::Delete(_)
| InputAction::EditAccept(_) => {
unreachable!("should have been handled!")
}
}
Expand Down