-
-
Notifications
You must be signed in to change notification settings - Fork 26
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
feat: add ratatui widget feature #38
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"rust-analyzer.cargo.features": ["ratatui"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
use std::{ | ||
thread, | ||
time::{Duration, Instant}, | ||
}; | ||
|
||
use ratatui::DefaultTerminal; | ||
use spinners::{Spinner, SpinnerWidget, Spinners}; | ||
|
||
fn main() { | ||
let terminal = ratatui::init(); | ||
let result = run(terminal); | ||
ratatui::restore(); | ||
result.expect("error running ratatui"); | ||
} | ||
|
||
fn run(mut terminal: DefaultTerminal) -> std::io::Result<()> { | ||
let spinner_widget = SpinnerWidget::default(); | ||
let mut spinner = Spinner::with_stream( | ||
Spinners::Dots9, | ||
"Waiting for 3 seconds".into(), | ||
spinner_widget.as_steam(), | ||
); | ||
|
||
const FPS: f64 = 60.0; | ||
let frame_duration = Duration::from_secs_f64(1.0 / FPS); | ||
|
||
// Normally a Rataui app would have a loop that interacts with user events, but for this example | ||
// we just want to show the spinner for 3 seconds, then stop it, making sure to render the | ||
// spinner in the terminal each frame | ||
let start = Instant::now(); | ||
while start.elapsed() < Duration::from_secs(3) { | ||
terminal.draw(|frame| frame.render_widget(&spinner_widget, frame.area()))?; | ||
thread::sleep(frame_duration); | ||
} | ||
|
||
spinner.stop_with_message("Finishing waiting for 3 seconds\n".into()); | ||
|
||
// show the finished spinner for 3 more seconds | ||
let start = Instant::now(); | ||
while start.elapsed() < Duration::from_secs(3) { | ||
terminal.draw(|frame| frame.render_widget(&spinner_widget, frame.area()))?; | ||
thread::sleep(frame_duration); | ||
} | ||
Ok(()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
use std::{ | ||
io::{Result, Write}, | ||
sync::{Arc, Mutex}, | ||
}; | ||
|
||
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; | ||
|
||
use crate::Stream; | ||
|
||
/// A ratatui widget that can be used to display a spinner | ||
/// | ||
/// # Examples | ||
/// | ||
/// ```no_run | ||
/// use ratatui::widgets::SpinnerWidget; | ||
/// | ||
/// let spinner_widget = SpinnerWidget::default(); | ||
/// let spinner = Spinner::with_stream( | ||
/// Spinners::Dots9, | ||
/// "Waiting ".into(), | ||
/// spinner_widget.as_steam(), | ||
/// ); | ||
/// # let area = Rect::default(); | ||
/// # let buf = &mut Buffer::empty(area); | ||
/// spinner_widget.render(area, buf); | ||
/// ``` | ||
#[derive(Clone, Default)] | ||
pub struct SpinnerWidget { | ||
value: Arc<Mutex<Vec<u8>>>, | ||
} | ||
|
||
impl SpinnerWidget { | ||
pub(crate) fn clear(&self) { | ||
self.value.lock().unwrap().clear(); | ||
} | ||
|
||
pub fn as_steam(&self) -> Stream { | ||
Stream::Ratatui(self.clone()) | ||
} | ||
} | ||
|
||
impl Write for SpinnerWidget { | ||
fn write(&mut self, buf: &[u8]) -> Result<usize> { | ||
let mut vec = self.value.lock().unwrap(); | ||
vec.write(buf) | ||
} | ||
|
||
fn flush(&mut self) -> Result<()> { | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl Widget for &SpinnerWidget { | ||
fn render(self, area: Rect, buf: &mut Buffer) { | ||
let value = self.value.lock().unwrap(); | ||
let value = std::str::from_utf8(&value).unwrap_or_default(); | ||
value.render(area, buf); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,80 +1,111 @@ | ||
use std::{io::{Write, stdout, stderr, Result}, time::Instant}; | ||
use std::{ | ||
io::{stderr, stdout, Result, Write}, | ||
time::Instant, | ||
}; | ||
|
||
/// Handles the Printing logic for the Spinner | ||
#[derive(Default, Copy, Clone)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is technically a breaking change, but I don't think it's possible to implement this without removing Copy. |
||
#[derive(Default, Clone)] | ||
pub enum Stream { | ||
#[default] | ||
Stderr, | ||
Stdout, | ||
#[cfg(feature = "ratatui")] | ||
Ratatui(crate::SpinnerWidget), | ||
} | ||
|
||
impl Stream { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a lot of churn in this. In summary:
|
||
/// Matches on self and returns the internal writer | ||
fn match_target(&self) -> Box<dyn Write> { | ||
match self { | ||
Self::Stderr => Box::new(stderr()), | ||
Self::Stdout => Box::new(stdout()) | ||
/// Writes the current message and optionally prints the durations | ||
pub fn write( | ||
&mut self, | ||
frame: &str, | ||
message: &str, | ||
start_time: Option<Instant>, | ||
stop_time: Option<Instant>, | ||
) -> Result<()> { | ||
match start_time { | ||
None => self.print(frame, message)?, | ||
Some(start_time) => self.print_with_duration(frame, message, start_time, stop_time)?, | ||
} | ||
Ok(()) | ||
} | ||
|
||
/// Writes the message without duration | ||
fn print_message( | ||
writer: &mut Box<dyn Write>, | ||
frame: &str, | ||
message: &str) -> Result<()> | ||
{ | ||
write!(writer, "\r{} {}", frame, message)?; | ||
fn print(&mut self, frame: &str, message: &str) -> Result<()> { | ||
let mut writer = self.writer(); | ||
self.start_of_line()?; | ||
write!(writer, "{frame} {message}")?; | ||
writer.flush() | ||
} | ||
|
||
/// Writes the message with the duration | ||
fn print_message_with_duration( | ||
writer: &mut Box<dyn Write>, | ||
frame: &str, | ||
message: &str, | ||
start_time: Instant, | ||
stop_time: Option<Instant>) -> Result<()> | ||
{ | ||
fn print_with_duration( | ||
&mut self, | ||
frame: &str, | ||
message: &str, | ||
start_time: Instant, | ||
stop_time: Option<Instant>, | ||
) -> Result<()> { | ||
let mut writer = self.writer(); | ||
let now = stop_time.unwrap_or_else(Instant::now); | ||
let duration = now.duration_since(start_time).as_secs_f64(); | ||
write!(writer, "\r{}{:>10.3} s\t{}", frame, duration, message)?; | ||
write!(writer, "\r{frame}{duration:>10.3} s\t{message}")?; | ||
writer.flush() | ||
} | ||
|
||
/// Writes the current message and optionally prints the durations | ||
pub fn write( | ||
&self, | ||
frame: &str, | ||
message: &str, | ||
start_time: Option<Instant>, | ||
stop_time: Option<Instant>) -> Result<()> | ||
{ | ||
let mut writer = self.match_target(); | ||
match start_time { | ||
None => Self::print_message( | ||
&mut writer, frame, message)?, | ||
Some(start_time) => Self::print_message_with_duration( | ||
&mut writer, frame, message, start_time, stop_time)? | ||
}; | ||
Ok(()) | ||
} | ||
|
||
/// Handles the stopping logic given an optional message and symbol | ||
pub fn stop( | ||
&self, | ||
message: Option<&str>, | ||
symbol: Option<&str>) -> Result<()> | ||
{ | ||
let mut writer = self.match_target(); | ||
pub fn stop(&self, message: Option<&str>, symbol: Option<&str>) -> Result<()> { | ||
let mut writer = self.writer(); | ||
match (message, symbol) { | ||
// persist with symbol and message | ||
(Some(m), Some(s)) => writeln!(writer, "\x1b[2K\r{} {}", s, m), | ||
|
||
// persist with message only | ||
(Some(m), None) => writeln!(writer, "\x1b[2K\r{}", m), | ||
|
||
// simple newline | ||
_ => writeln!(writer) | ||
(Some(message), Some(symbol)) => { | ||
self.erase_line()?; | ||
writeln!(writer, "{symbol} {message}") | ||
} | ||
(Some(message), None) => { | ||
self.erase_line()?; | ||
writeln!(writer, "{message}") | ||
} | ||
_ => writeln!(writer), | ||
}?; | ||
writer.flush() | ||
} | ||
|
||
fn start_of_line(&self) -> Result<()> { | ||
match self { | ||
#[cfg(feature = "ratatui")] | ||
Self::Ratatui(widget) => { | ||
widget.clear(); | ||
Ok(()) | ||
} | ||
_ => { | ||
let mut writer = self.writer(); | ||
write!(writer, "\r")?; | ||
writer.flush() | ||
} | ||
} | ||
} | ||
|
||
fn erase_line(&self) -> Result<()> { | ||
match self { | ||
#[cfg(feature = "ratatui")] | ||
Self::Ratatui(widget) => { | ||
widget.clear(); | ||
Ok(()) | ||
} | ||
_ => { | ||
let mut writer = self.writer(); | ||
write!(writer, "\x1b[2K\r")?; | ||
writer.flush() | ||
} | ||
} | ||
} | ||
|
||
/// Matches on self and returns the internal writer | ||
fn writer(&self) -> Box<dyn Write> { | ||
match self { | ||
Self::Stderr => Box::new(stderr()), | ||
Self::Stdout => Box::new(stdout()), | ||
#[cfg(feature = "ratatui")] | ||
Self::Ratatui(widget) => Box::new(widget.clone()), | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible that this could have been the shared state instead of the entire widget, but I prefer passing around wrapped shared state values instead of raw values. Do you have preferences one way or another?