Skip to content

Instantly share code, notes, and snippets.

@Tosainu
Last active June 21, 2025 08:39
Show Gist options
  • Save Tosainu/639a29bc2de7b0b0dc18e4d92cefdb77 to your computer and use it in GitHub Desktop.
Save Tosainu/639a29bc2de7b0b0dc18e4d92cefdb77 to your computer and use it in GitHub Desktop.
// Cargo.toml:
// ```
// [package]
// name = "crossterm-shell"
// version = "0.1.0"
// edition = "2024"
//
// [dependencies.crossterm]
// version = "0.29.0"
// features = ["event-stream"]
//
// [dependencies.futures]
// version = "0.3.31"
//
// [dependencies.tokio]
// version = "1.45.1"
// features = [
// "macros",
// "rt",
// "time",
// ]
// ```
use std::io::Write;
use crossterm::{
ExecutableCommand, cursor,
event::{EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
style, terminal,
};
use futures::StreamExt;
use tokio::time::{Duration, Instant};
pub struct Shell<W: Write> {
buffer: String,
reader: EventStream,
writer: W,
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> std::io::Result<()> {
terminal::enable_raw_mode()?;
let mut shell = Shell::new(std::io::stdout());
shell.show_prompt()?;
let mut interval = tokio::time::interval_at(
Instant::now() + Duration::from_millis(500),
Duration::from_millis(500),
);
let mut counter = 0u32;
loop {
tokio::select! {
_ = interval.tick() => {
shell.write(format!("(✿╹◡╹)ノ {}", counter).as_bytes())?;
counter += 1;
}
line = shell.read_line() => {
match line {
Ok(Some(line)) => shell.write(format!("input: \"{}\"", line).as_bytes())?,
Ok(None) => break,
Err(e) => shell.write(format!("Error: {:?}", e).as_bytes())?,
}
}
};
}
println!("\r");
println!("bye!\r");
terminal::disable_raw_mode()
}
enum ReadLineResult {
Line(String),
Incomplete,
Quit,
}
impl<W: Write> Shell<W> {
pub fn new(writer: W) -> Self {
Shell {
buffer: String::new(),
reader: EventStream::new(),
writer,
}
}
pub fn write(&mut self, bytes: &[u8]) -> std::io::Result<()> {
self.clear_prompt()?;
for line in bytes.split(|c| *c == b'\n') {
self.writer.write_all(line)?;
self.writer.write_all(b"\r\n".as_slice())?;
}
self.show_prompt()?;
Ok(())
}
pub async fn read_line(&mut self) -> std::io::Result<Option<String>> {
while let Some(e) = self.reader.next().await {
if let Some(e) = e?.as_key_event() {
match self.handle_key_event(e)? {
ReadLineResult::Line(line) => return Ok(Some(line)),
ReadLineResult::Incomplete => (),
ReadLineResult::Quit => break,
}
}
}
Ok(None)
}
fn handle_key_event(&mut self, e: KeyEvent) -> std::io::Result<ReadLineResult> {
if e.kind != KeyEventKind::Press && e.kind != KeyEventKind::Repeat {
return Ok(ReadLineResult::Incomplete);
}
match (e.code, e.modifiers) {
(KeyCode::Enter, _) => {
let line = std::mem::take(&mut self.buffer);
self.clear_prompt()?;
self.show_prompt()?;
return Ok(ReadLineResult::Line(line));
}
(KeyCode::Backspace, _) => {
let _ = self.buffer.pop();
self.clear_prompt()?;
self.show_prompt()?;
}
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
return Ok(ReadLineResult::Quit);
}
(KeyCode::Char('d'), KeyModifiers::CONTROL) => {
if self.buffer.is_empty() {
return Ok(ReadLineResult::Quit);
}
}
(KeyCode::Char('l'), KeyModifiers::CONTROL) => {
self.writer
.execute(terminal::Clear(terminal::ClearType::All))?
.execute(cursor::MoveTo(0, 0))?;
}
(KeyCode::Char('u'), KeyModifiers::CONTROL) => {
self.buffer.clear();
self.clear_prompt()?;
self.show_prompt()?;
}
(KeyCode::Char(c), KeyModifiers::NONE) | (KeyCode::Char(c), KeyModifiers::SHIFT) => {
self.buffer.push(c);
self.writer.execute(style::Print(c))?;
}
_ => (),
}
Ok(ReadLineResult::Incomplete)
}
pub fn clear_prompt(&mut self) -> std::io::Result<()> {
self.writer
.execute(terminal::Clear(terminal::ClearType::CurrentLine))?
.execute(cursor::MoveToColumn(0))?;
Ok(())
}
pub fn show_prompt(&mut self) -> std::io::Result<()> {
self.writer
.execute(style::Print(format!(" {}", self.buffer)))?;
Ok(())
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment