| use std::borrow::Cow; |
| use std::io; |
| use std::sync::{Arc, Mutex}; |
|
|
| use anyhow::Result; |
| use bstr::ByteSlice; |
| use colored::Colorize; |
| use forge_domain::ConsoleWriter; |
| use forge_markdown_stream::StreamdownRenderer; |
| use forge_spinner::SpinnerManager; |
|
|
| |
| |
| |
| |
| |
| pub struct SharedSpinner<P: ConsoleWriter>(Arc<Mutex<SpinnerManager<P>>>); |
|
|
| impl<P: ConsoleWriter> Clone for SharedSpinner<P> { |
| fn clone(&self) -> Self { |
| Self(Arc::clone(&self.0)) |
| } |
| } |
|
|
| impl<P: ConsoleWriter + 'static> SharedSpinner<P> { |
| |
| pub fn new(spinner: SpinnerManager<P>) -> Self { |
| Self(Arc::new(Mutex::new(spinner))) |
| } |
|
|
| |
| pub fn start(&self, message: Option<&str>) -> Result<()> { |
| self.0 |
| .lock() |
| .unwrap_or_else(|e| e.into_inner()) |
| .start(message) |
| } |
|
|
| |
| pub fn stop(&self, message: Option<String>) -> Result<()> { |
| self.0 |
| .lock() |
| .unwrap_or_else(|e| e.into_inner()) |
| .stop(message) |
| } |
|
|
| |
| pub fn pause(&self) { |
| self.0.lock().unwrap_or_else(|e| e.into_inner()).pause() |
| } |
|
|
| |
| pub fn resume(&self) { |
| self.0.lock().unwrap_or_else(|e| e.into_inner()).resume() |
| } |
|
|
| |
| pub fn reset(&self) { |
| self.0.lock().unwrap_or_else(|e| e.into_inner()).reset() |
| } |
|
|
| |
| pub fn write_ln(&self, message: impl ToString) -> Result<()> { |
| self.0 |
| .lock() |
| .unwrap_or_else(|e| e.into_inner()) |
| .write_ln(message) |
| } |
|
|
| |
| pub fn ewrite_ln(&self, message: impl ToString) -> Result<()> { |
| self.0 |
| .lock() |
| .unwrap_or_else(|e| e.into_inner()) |
| .ewrite_ln(message) |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] |
| pub enum Style { |
| #[default] |
| Normal, |
| Dimmed, |
| } |
|
|
| impl Style { |
| |
| fn apply(self, content: String) -> String { |
| match self { |
| Self::Normal => content, |
| Self::Dimmed => content.dimmed().to_string(), |
| } |
| } |
| } |
|
|
| fn term_width() -> usize { |
| terminal_size::terminal_size() |
| .map(|(w, _)| w.0 as usize) |
| .unwrap_or(80) |
| } |
|
|
| |
| |
| |
| |
| |
| pub struct StreamingWriter<P: ConsoleWriter + 'static> { |
| active: Option<ActiveRenderer<P>>, |
| spinner: SharedSpinner<P>, |
| printer: Arc<P>, |
| } |
|
|
| impl<P: ConsoleWriter + 'static> StreamingWriter<P> { |
| |
| |
| pub fn new(spinner: SharedSpinner<P>, printer: Arc<P>) -> Self { |
| Self { active: None, spinner, printer } |
| } |
|
|
| |
| pub fn write(&mut self, text: &str) -> Result<()> { |
| self.write_styled(text, Style::Normal) |
| } |
|
|
| |
| pub fn write_dimmed(&mut self, text: &str) -> Result<()> { |
| self.write_styled(text, Style::Dimmed) |
| } |
|
|
| |
| pub fn finish(&mut self) -> Result<()> { |
| if let Some(active) = self.active.take() { |
| active.finish()?; |
| } |
| Ok(()) |
| } |
|
|
| fn write_styled(&mut self, text: &str, style: Style) -> Result<()> { |
| self.ensure_renderer(style)?; |
| if let Some(ref mut active) = self.active { |
| active.push(text)?; |
| } |
| Ok(()) |
| } |
|
|
| fn ensure_renderer(&mut self, new_style: Style) -> Result<()> { |
| let needs_switch = self.active.as_ref().is_some_and(|a| a.style != new_style); |
|
|
| if needs_switch && let Some(old) = self.active.take() { |
| old.finish()?; |
| } |
|
|
| if self.active.is_none() { |
| let writer = StreamDirectWriter { |
| spinner: self.spinner.clone(), |
| printer: self.printer.clone(), |
| style: new_style, |
| }; |
| let renderer = StreamdownRenderer::new(writer, term_width()); |
| self.active = Some(ActiveRenderer { renderer, style: new_style }); |
| } |
| Ok(()) |
| } |
| } |
|
|
| |
| struct ActiveRenderer<P: ConsoleWriter + 'static> { |
| renderer: StreamdownRenderer<StreamDirectWriter<P>>, |
| style: Style, |
| } |
|
|
| impl<P: ConsoleWriter + 'static> ActiveRenderer<P> { |
| pub fn push(&mut self, text: &str) -> Result<()> { |
| self.renderer.push(text)?; |
| Ok(()) |
| } |
|
|
| pub fn finish(self) -> Result<()> { |
| self.renderer.finish()?; |
| Ok(()) |
| } |
| } |
|
|
| |
| struct StreamDirectWriter<P: ConsoleWriter> { |
| spinner: SharedSpinner<P>, |
| printer: Arc<P>, |
| style: Style, |
| } |
|
|
| impl<P: ConsoleWriter + 'static> StreamDirectWriter<P> { |
| fn pause_spinner(&self) { |
| self.spinner.pause(); |
| } |
|
|
| fn resume_spinner(&self) { |
| self.spinner.resume(); |
| } |
| } |
|
|
| impl<P: ConsoleWriter> Drop for StreamDirectWriter<P> { |
| fn drop(&mut self) { |
| let _ = self.printer.flush(); |
| let _ = self.printer.flush_err(); |
| } |
| } |
|
|
| impl<P: ConsoleWriter + 'static> io::Write for StreamDirectWriter<P> { |
| fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
| self.pause_spinner(); |
|
|
| let content = match buf.to_str() { |
| Ok(content) => Cow::Borrowed(content), |
| Err(_) => buf.to_str_lossy(), |
| }; |
| let styled = self.style.apply(content.into_owned()); |
| self.printer.write(styled.as_bytes())?; |
| self.printer.flush()?; |
|
|
| |
| if buf.last() == Some(&b'\n') { |
| self.resume_spinner(); |
| } |
|
|
| |
| |
| |
| |
| Ok(buf.len()) |
| } |
|
|
| fn flush(&mut self) -> io::Result<()> { |
| self.printer.flush() |
| } |
| } |
|
|