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

Add support for cancelling input with Escape #607

Open
PaulJuliusMartinez opened this issue Mar 3, 2022 · 9 comments · May be fixed by #802
Open

Add support for cancelling input with Escape #607

PaulJuliusMartinez opened this issue Mar 3, 2022 · 9 comments · May be fixed by #802
Labels

Comments

@PaulJuliusMartinez
Copy link

I think in certain contexts it make sense for Escape to cancel entering an input, similar to Ctrl-C or Ctrl-D. When entering a : command in vim, for example, hitting escape will return you to normal mode.

@gwenn
Copy link
Collaborator

gwenn commented Mar 3, 2022

But rustyline has only normal and insert mode, no command line mode.
And on unix, a single escape is always ambiguous: escape sequence versus single escape depending on how fast you type.
Do you try to rebind this key ?

@schungx
Copy link

schungx commented Mar 4, 2022

Rebinding the Escape key works fine (you essentially make it the same as Ctrl-U) and behaves similarly to the command prompt on Windows -- you're probably running Windows if you want Escape to clear the line.

@gwenn
Copy link
Collaborator

gwenn commented Mar 4, 2022

But with Windows Terminal, you may have the same issue as on unix if we activate ENABLE_VIRTUAL_TERMINAL_INPUT...

@schungx
Copy link

schungx commented Mar 4, 2022

But with Windows Terminal, you may have the same issue as on unix if we activate ENABLE_VIRTUAL_TERMINAL_INPUT...

Probably yes, but without ENABLE_VIRTUAL_TERMINAL_INPUT, I have mapped Esc to clear-line like this and it works just fine:

    // On Windows, Esc clears the input buffer
    #[cfg(target_family = "windows")]
    rl.bind_sequence(
        Event::KeySeq(smallvec![KeyEvent(KeyCode::Esc, Modifiers::empty())]),
        EventHandler::Simple(Cmd::Kill(Movement::WholeBuffer)),
    );

So, as long as you're not requiring bracketed paste, mapping the Esc should be quite safe.

With ENABLE_VIRTUAL_TERMINAL_INPUT, you can use the following trick: If it is an escape sequence, Windows Terminal always sends the whole stream at once. Therefore, it will be KeyDown(Escape), KeyDown('[') ... etc.

If it is just the Esc key, it will be KeyDown(Escape), KeyUp(Escape)) or just a lone KeyDown(Escape) without anything following (if the user holds on to the key). It is very easy to distinguish between the two.

@PaulJuliusMartinez
Copy link
Author

But rustyline has only normal and insert mode, no command line mode.

I wasn't referring to vim-mode in Rustyline, just using vim as an example of a program that allows using Escape to cancel entering input in readline-like contexts. You can also use Escape to cancel entering a search pattern after pressing / in vim.

I understand that escapes are fundamentally ambiguous, but there are workarounds for that -- lots of programs allow configuring a timeout where, if no data is supplied after reading an Escape byte, it'll register as an Escape press. And those programs also often support setting that timeout to 0. I think the rough assumption is that you'll basically never read half an escape sequence, so if you ask to read 64 bytes, but only get a single escape byte back, it's pretty likely that's an actual Escape press.

I tried binding a sequence, but it didn't seem to work:

rustyline_editor.bind_sequence(
    KeyEvent::new('\x1B', Modifiers::empty()),
    Cmd::Interrupt,
);

Using a different character -- just 'a', for example -- does work. (Also, what's the difference between Cmd::Abort and Cmd::Interrupt? I would expect Cmd::Abort to stop editing and return an Err, but it didn't seem to do anything.)

@schungx
Copy link

schungx commented Mar 5, 2022

I think Escape is treated differently. You have to bind via:

KeyEvent::KeySeq(smallvec![KeyEvent(KeyCode::Esc, Modifiers::empty())])

KeyEvent::new is only for ASCII.

@gwenn
Copy link
Collaborator

gwenn commented Mar 5, 2022

I understand that escapes are fundamentally ambiguous, but there are workarounds for that -- lots of programs allow configuring a timeout where, if no data is supplied after reading an Escape byte, it'll register as an Escape press. And those programs also often support setting that timeout to 0. I think the rough assumption is that you'll basically never read half an escape sequence, so if you ask to read 64 bytes, but only get a single escape byte back, it's pretty likely that's an actual Escape press.

See https://docs.rs/rustyline/latest/rustyline/config/struct.Config.html#method.keyseq_timeout
"By default, no timeout (-1) or 500ms if EditMode::Vi is activated."
So you can use Escape as the Meta key.

I tried binding a sequence, but it didn't seem to work:

rustyline_editor.bind_sequence(
    KeyEvent::new('\x1B', Modifiers::empty()),
    Cmd::Interrupt,
);

Using a different character -- just 'a', for example -- does work. (Also, what's the difference between Cmd::Abort and Cmd::Interrupt? I would expect Cmd::Abort to stop editing and return an Err, but it didn't seem to do anything.)

This should work by either pressing Escape twice or modifying the default timeout.
And Cmd::Abort is currently used only to abort completion or history search.

@PaulJuliusMartinez
Copy link
Author

PaulJuliusMartinez commented Jan 6, 2024

This does not work for me when using Behavior::PreferTerm, running on macOS 13.6.2, using rustyline 13.0.0:

use rustyline::history::MemHistory;
use rustyline::Editor;

fn main() {
    let editor_config = rustyline::config::Config::builder()
        .behavior(rustyline::config::Behavior::PreferTerm)
        .keyseq_timeout(0)
        .build();

    let mut editor = Editor::<(), MemHistory>::with_history(editor_config, MemHistory::default()).unwrap();

    editor.bind_sequence(
        rustyline::KeyEvent::new('\x1B', rustyline::Modifiers::empty()),
        rustyline::Cmd::Interrupt,
    );

    let result = editor.readline("Enter first command: ");
    println!("Initial input: {:?}", result);
}

If I delete the .behavior(rustyline::config::Behavior::PreferTerm) line, then it does work.

I am unsure what the difference could be here (the code difference seems minuscule for PreferTerm..., but I know that /dev/tty definitely receives escape key presses, because when I open it myself I can read (single) escape values.

@PaulJuliusMartinez
Copy link
Author

I've dug more into this, and discovered the root cause. To detect single-escapes vs escape sequences, after reading a single \x1B byte, rustyline polls the input input fd to see if it has more input to read. When using Behavior::PreferTerm, the input fd is /dev/tty, and calling poll does not work on devices on MacOS.

Here are some blog posts that explain potential workarounds.

I'm not sure how feasible it is to use one of these workarounds, but there may be a more straightforward solution when keyseq_timeout is set to 0. Since the input is buffered, we can just check if there is any additional buffered input, and assume that it is a single escape if there is no buffered input, and it is an escape sequence if there is buffered input, and not bother ever calling poll at all.

It seems like this scenario is already checked for debugging purposes?

if key == E::ESC {
    if !self.tty_in.buffer().is_empty() {
        debug!(target: "rustyline", "read buffer {:?}", self.tty_in.buffer());
    }
    ...
}

I'm not sure if there's any simple workaround when keyseq_timeout is some non-zero value however.

I've made a PR to fix this here: #802

@gwenn gwenn added the bug label Sep 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
3 participants