Giter Club home page Giter Club logo

rustyline-async's Introduction

RustyLine Async

Docs

A minimal readline with multiline and async support.

Inspired by rustyline, async-readline, & termion-async-input. Built using crossterm.

Features

  • Works on all platforms supported by crossterm.
  • Full Unicode Support (Including Grapheme Clusters)
  • Multiline Editing
  • In-memory History
  • Ctrl-C, Ctrl-D are returned as Ok(Interrupt) and Ok(Eof) ReadlineEvents.
  • Ctrl-U to clear line before cursor
  • Ctrl-left & right to move to next or previous whitespace
  • Home/Ctrl-A and End/Ctrl-E to jump to the start and end of the input (Ctrl-A & Ctrl-E can be toggled off by disabling the "emacs" feature)
  • Ctrl-L clear screen
  • Ctrl-W delete until previous space
  • Extensible design based on crossterm's event-stream feature

Feel free to PR to add more features!

Example:

cargo run --example readline

rustyline-async

License

This software is licensed under The Unlicense license.

rustyline-async's People

Contributors

arades79 avatar bobo1239 avatar jwodder avatar manforowicz avatar siphalor avatar srtopster avatar zyansheep avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

rustyline-async's Issues

Prompt & input remain on screen after hitting Ctrl-C

When a line of input is terminated by pressing Ctrl-C, the prompt and any text entered so far will remain on the screen even as new lines are printed below them. This is unlike lines terminated by pressing Enter, for which the line and prompt disappear, and it leads to a messy-looking interface.

Program that can be used to reproduce this behavior:

use rustyline_async::{Readline, ReadlineError};
use std::io::Write;
use std::time::Duration;
use tokio::time::sleep;

#[tokio::main]
async fn main() -> Result<(), ReadlineError> {
    let (mut rl, mut stdout) = Readline::new("prompt> ".into())?;
    loop {
        tokio::select! {
            _ = sleep(Duration::from_secs(1)) => {
                writeln!(stdout, "Message received!")?;
            }
            cmd = rl.readline() => match cmd {
                Ok(line) => {
                    writeln!(stdout, "You entered: {line:?}")?;
                    rl.add_history_entry(line.clone());
                    if line == "quit" {
                        break;
                    }
                }
                Err(ReadlineError::Eof) => {
                    writeln!(stdout, "<EOF>")?;
                    break;
                }
                Err(ReadlineError::Interrupted) => {writeln!(stdout, "^C")?; continue; }
                Err(e) => {
                    writeln!(stdout, "Error: {e:?}")?;
                    break;
                }
            }
        }
    }
    rl.flush()?;
    Ok(())
}

Recording of this behavior:

cancel-bug

(Incidentally, if no text is printed in response to the Ctrl-C, things get a bit messed up on the next output; perhaps that should be a separate issue.)

How should history work?

Issue on implementation details for readline history:

Up for debate

  • Should the history really be unchangeable?
  • Should this be hidden behind a feature flag?
  • The implementation (#4) currently uses a Mutex to ensure thread-safety of appending new entries.
    Failing to acquire a lock (which should only be possible, if another thread panicked with the lock), will currently panic.
    Alternatives:
    • Pass lock errors through as a new type of error
    • Use a RwLock instead, currently not really useful as there aren't really any read-only accesses.
    • Remove the thread-safety and let the user handle mutable concurrent accesses to Readline
  • Should there be a way to get (read-only) access to the history from Readline?
  • Should this be separated into its own file? (Should be a general consideration in this library :) )
  • Should the constructor be changed into accepting struct/builder for the history size (and future additional parameters)?
  • The VecDeque used is currently created through default.
    Technically a capacity could be specified based on the max_size.
    This could consume unnecessarily large memory amounts when the max_size is high, but the history's actual length is low.

My Thoughts

Should the history really be unchangeable?

Probably... A history is a history because it happened in the past, and past in unchangeable.

Should this be hidden behind a feature flag?

Nahh

The implementation currently uses a Mutex to ensure thread-safety of appending new entries.
Failing to acquire a lock (which should only be possible, if another thread panicked with the lock), will currently panic.
Alternatives:
Pass lock errors through as a new type of error
Use a RwLock instead, currently not really useful as there aren't really any read-only accesses.
Remove the thread-safety and let the user handle mutable concurrent accesses to Readline
Should there be a way to get (read-only) access to the history from Readline?

My "perfect" history implementation would look something along the lines of :

  1. append-from-anywhere & read-from-anywhere like boxcar
  2. storage-agnostic system that allows for either in-memory history or sync-to-file history
    a. The downside sync-to-file is how its done, do we use Seek to look for newlines between history entries? Or do we manually serialize & deserialize the whole file whenever we want to sync? seeking & appending would be faster, but what happens if we want max history sizes? then the whole file needs to be rewritten which may require loading the entire thing into memory.

Should this be separated into its own file? (Should be a general consideration in this library :) )

Definitely, maybe even its own library to allow for other people to use it.

Should the constructor be changed into accepting struct/builder for the history size (and future additional parameters)?

That is a good idea, and is basically what rustyline does, which goes with the theme of trying to match their api.

Also to consider: do we want to have history searching (which would probably necessitate a BTree of some kind) or history deduplication?
This is how rustyline does it, no async tho: https://docs.rs/rustyline/6.3.0/src/rustyline/history.rs.html#25-30
I'm curious how ion does it, but I cannot grok their codebase...

Cannot clear prompt at end of program if there's nothing to write

As I understand it, the way to erase the readline prompt at the end of a program is to call rl.flush(); however, this only clears the prompt if there are lines written to the SharedWriter that have yet to be output. If a program chooses to, say, just end immediately when the user enters "quit" without printing anything else, the prompt will be left behind on the screen, prepended to the user's shell prompt, which does not look good.

Possible ways to address this:

  • Make rl.flush() always clear the input line even if no lines were output.
  • Make Readline clear the input line on Drop
  • Give Readline a public method for clearing the input line.

Will the unicode panic be fixed?

Note: currently panics when using compound characters

This may prevent production usage of the crate.

Are there any plans for fixing it?

Output to SharedWriter not shown when `break`ing after `writeln`

Environment:

  • macOS 11.6.8
  • Terminal.app (TERM=xterm-256color)
  • Rust 1.65.0
  • rustyline-async commit f9817e7 cloned from GitHub

When running the readline example in this repository, if I hit Ctrl-D, the program exits immediately without printing the "Exiting..." message, like so:

Screen Shot 2022-11-19 at 15 37 18

If I remove the break in the Err(ReadlineError::Eof) branch, the message is printed.

Similarly, if I add a break to the Err(ReadlineError::Interrupted) branch, pressing Ctrl-C results in the "^C" message not being printed — along with an additional prompt being printed before my shell command prompt, like so:

Screen Shot 2022-11-19 at 15 41 39

Adding rl.flush()?; or Write::flush(&mut stdout)?; after the call to writeln doesn't help.

Note that this isn't just limited to Eof and Interrupted, either. When running the following code:

use rustyline_async::{Readline, ReadlineError};
use std::io::Write;
use std::time::Duration;
use tokio::time::sleep;

#[tokio::main]
async fn main() -> Result<(), ReadlineError> {
    let (mut rl, mut stdout) = Readline::new("> ".into())?;
    loop {
        tokio::select! {
            _ = sleep(Duration::from_secs(3)) => {
                writeln!(stdout, "Message received!")?;
            }
            cmd = rl.readline() => match cmd {
                Ok(line) => {
                    writeln!(stdout, "{line:?}")?;
                    rl.add_history_entry(line.clone());
                    if line == "quit" {
                        break;
                    }
                }
                Err(ReadlineError::Eof) => {
                    writeln!(stdout, "<EOF>")?;
                    break;
                }
                Err(ReadlineError::Interrupted) => {
                    writeln!(stdout, "<Ctrl-C>")?;
                    break;
                }
                Err(e) => {
                    writeln!(stdout, "Error: {e:?}")?;
                    break;
                }
            }
        }
    }
    Ok(())
}

typing "quit" will result in the program exiting without outputting "quit", yet it still outputs the next readline prompt (which ends up prepended to my shell prompt), like so:

Screen Shot 2022-11-19 at 16 12 05

Double reading problem in Windows

Minimal reproducible program later, but here's what the symptoms looks like:

> tteesstt  -- I typed test
> tteesstt> -- Then I pressed enter
>
> >
>
>

Add tests

This project needs tests. Possible ways to implement these tests include:

  • Use an expect-style library (e.g., rexpect or expectrl) to create & interact with a test terminal in which rustyline-async is doing its thing. This may require implementing some test binary that wraps rustyline-async for invocation by the expect library.

  • Use vt100 or similar to calculate what the screen looks like while using rustyline-async and assert that it looks correct.

  • Use vhs to record interactions with some test binary that wraps rustyline-async and then assert that the display remains the same after the code changes. ratatui uses this approach for testing its examples.

Line wrapping print glitch

After pressing enter on a wrapped line, it looks as if the line content prints twice (with some visual glitches).

I'm using rustyline-async version 0.4.0 on Linux. Here's the example code I used to demonstrate this issue:

use rustyline_async::{Readline, ReadlineEvent};
use std::io::Write;

#[async_std::main]
async fn main() {
    let (mut readline, mut printer) = Readline::new("> ".to_string()).unwrap();
    
    while let ReadlineEvent::Line(text) = readline.readline().await.unwrap() {
        readline.add_history_entry(text.to_string());
        writeln!(printer, "Hi there!").unwrap();
    }
}
long-line-glitch.mp4

Reconsider the Unlicense

rustyline-async is currently licensed under the Unlicense, which has a number of problems as a license, not least being invalid under German law. The project should consider switching to a better-drafted license, such as 0BSD.

However — and this is very important — because multiple people have contributed code to rustyline-async and one or more of them did so without assenting to a contributor license agreement, the copyright for rustyline-async is currently held by multiple people, all of whom must assent in order for the license to be changed.

Add a license

Currently, this library has no explicit license, resulting in a strong copyright.

As you're welcoming contributions in the README, I suspect this to be an oversight.

You can check https://choosealicense.com/ for a simple overview over some free licenses.

Personally, I prefer using the MIT or Apache 2.0 licenses, but the final choice is up to you (and probably partially me because I contributed without noticing it and therefore probably have copyright on my stuff ^^ - but I'll go with any free license).

Readline visual glitch

When reading input lines without printing anything between them, a text glitch occurs.

I noticed this while running rustyline-async version 0.4.0 in terminal on Linux. Code:

use rustyline_async::{Readline, ReadlineEvent};

#[tokio::main]
async fn main() {
    let (mut readline, _printer) = Readline::new("> ".to_string()).unwrap();
    
    while let ReadlineEvent::Line(text) = readline.readline().await.unwrap() {
        readline.add_history_entry(text.to_string());
    }
}
recording.mp4

Pass focus events down

I can add this, although I'm not sure how far/when I'll get that far in my current project.

Basically it would be useful for the program to know if the terminal gains/loses focus for things like showing notifications. Those are passed in to the line but not exposed to the user currently.

I think it would be a straightforward addition, just adding new focus events to the current readline event enum.

Looking for Maintainers

I'm not planning on using this project for the foreseeable future, but I would hate for it to just stay unmaintained. So, if you are someone who has either meaningfully contributed to this project, or maintains a rust library of similar or higher popularity and you want to maintain this project, comment below and you shall receive commit access. :)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.