Last active
June 21, 2025 08:39
-
-
Save Tosainu/639a29bc2de7b0b0dc18e4d92cefdb77 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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