|
@@ -1,6 +1,7 @@
|
|
|
use crate::{
|
|
|
serve::{ansi_buffer::AnsiStringLine, Builder, ServeUpdate, Watcher, WebServer},
|
|
|
- BuildStage, BuildUpdate, DioxusCrate, Platform, ServeArgs, TraceContent, TraceMsg, TraceSrc,
|
|
|
+ BuildStage, BuildUpdate, DioxusCrate, Platform, RustcDetails, ServeArgs, TraceContent,
|
|
|
+ TraceMsg, TraceSrc,
|
|
|
};
|
|
|
use crossterm::{
|
|
|
cursor::{Hide, Show},
|
|
@@ -28,7 +29,7 @@ use tracing::Level;
|
|
|
const TICK_RATE_MS: u64 = 100;
|
|
|
const VIEWPORT_MAX_WIDTH: u16 = 100;
|
|
|
const VIEWPORT_HEIGHT_SMALL: u16 = 5;
|
|
|
-const VIEWPORT_HEIGHT_BIG: u16 = 12;
|
|
|
+const VIEWPORT_HEIGHT_BIG: u16 = 13;
|
|
|
|
|
|
/// The TUI that drives the console output.
|
|
|
///
|
|
@@ -63,6 +64,8 @@ pub struct Output {
|
|
|
// ! needs to be wrapped in an &mut since `render stateful widget` requires &mut... but our
|
|
|
// "render" method only borrows &self (for no particular reason at all...)
|
|
|
throbber: RefCell<throbber_widgets_tui::ThrobberState>,
|
|
|
+
|
|
|
+ rustc_details: RustcDetails,
|
|
|
}
|
|
|
|
|
|
#[allow(unused)]
|
|
@@ -76,17 +79,9 @@ struct RenderState<'a> {
|
|
|
}
|
|
|
|
|
|
impl Output {
|
|
|
- pub(crate) fn start(cfg: &ServeArgs) -> io::Result<Self> {
|
|
|
+ pub(crate) async fn start(cfg: &ServeArgs) -> crate::Result<Self> {
|
|
|
let mut output = Self {
|
|
|
- term: Rc::new(RefCell::new(
|
|
|
- Terminal::with_options(
|
|
|
- CrosstermBackend::new(stdout()),
|
|
|
- TerminalOptions {
|
|
|
- viewport: Viewport::Inline(VIEWPORT_HEIGHT_SMALL),
|
|
|
- },
|
|
|
- )
|
|
|
- .ok(),
|
|
|
- )),
|
|
|
+ term: Rc::new(RefCell::new(None)),
|
|
|
interactive: cfg.is_interactive_tty(),
|
|
|
dx_version: format!(
|
|
|
"{}-{}",
|
|
@@ -95,7 +90,6 @@ impl Output {
|
|
|
),
|
|
|
platform: cfg.build_arguments.platform.expect("To be resolved by now"),
|
|
|
events: None,
|
|
|
- // messages: Vec::new(),
|
|
|
more_modal_open: false,
|
|
|
pending_logs: VecDeque::new(),
|
|
|
throbber: RefCell::new(throbber_widgets_tui::ThrobberState::default()),
|
|
@@ -107,6 +101,7 @@ impl Output {
|
|
|
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
|
|
interval
|
|
|
},
|
|
|
+ rustc_details: RustcDetails::from_cli().await?,
|
|
|
};
|
|
|
|
|
|
output.startup()?;
|
|
@@ -127,11 +122,24 @@ impl Output {
|
|
|
original_hook(info);
|
|
|
}));
|
|
|
|
|
|
- enable_raw_mode()?;
|
|
|
- stdout()
|
|
|
- .execute(Hide)?
|
|
|
- .execute(EnableFocusChange)?
|
|
|
- .execute(EnableBracketedPaste)?;
|
|
|
+ // Check if writing the terminal is going to block infinitely.
|
|
|
+ // If it does, we should disable interactive mode. This ensures we work with programs like `bg`
|
|
|
+ // which suspend the process and cause us to block when writing output.
|
|
|
+ if Self::enable_raw_mode().is_err() {
|
|
|
+ self.term.take();
|
|
|
+ self.interactive = false;
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
+
|
|
|
+ self.term.replace(
|
|
|
+ Terminal::with_options(
|
|
|
+ CrosstermBackend::new(stdout()),
|
|
|
+ TerminalOptions {
|
|
|
+ viewport: Viewport::Inline(VIEWPORT_HEIGHT_SMALL),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ .ok(),
|
|
|
+ );
|
|
|
|
|
|
// Initialize the event stream here - this is optional because an EvenStream in a non-interactive
|
|
|
// terminal will cause a panic instead of simply doing nothing.
|
|
@@ -142,6 +150,36 @@ impl Output {
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
+ /// Enable raw mode, but don't let it block forever.
|
|
|
+ ///
|
|
|
+ /// This lets us check if writing to tty is going to block forever and then recover, allowing
|
|
|
+ /// interopability with programs like `bg`.
|
|
|
+ fn enable_raw_mode() -> io::Result<()> {
|
|
|
+ #[cfg(unix)]
|
|
|
+ {
|
|
|
+ use tokio::signal::unix::{signal, SignalKind};
|
|
|
+
|
|
|
+ // Ignore SIGTSTP, SIGTTIN, and SIGTTOU
|
|
|
+ _ = signal(SignalKind::from_raw(20))?; // SIGTSTP
|
|
|
+ _ = signal(SignalKind::from_raw(21))?; // SIGTTIN
|
|
|
+ _ = signal(SignalKind::from_raw(22))?; // SIGTTOU
|
|
|
+ }
|
|
|
+
|
|
|
+ use std::io::IsTerminal;
|
|
|
+
|
|
|
+ if !stdout().is_terminal() {
|
|
|
+ return io::Result::Err(io::Error::new(io::ErrorKind::Other, "Not a terminal"));
|
|
|
+ }
|
|
|
+
|
|
|
+ enable_raw_mode()?;
|
|
|
+ stdout()
|
|
|
+ .execute(Hide)?
|
|
|
+ .execute(EnableFocusChange)?
|
|
|
+ .execute(EnableBracketedPaste)?;
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
/// Call the shutdown functions that might mess with the terminal settings - see the related code
|
|
|
/// in "startup" for more details about what we need to unset
|
|
|
pub(crate) fn shutdown(&self) -> io::Result<()> {
|
|
@@ -163,6 +201,10 @@ impl Output {
|
|
|
use futures_util::future::OptionFuture;
|
|
|
use futures_util::StreamExt;
|
|
|
|
|
|
+ if !self.interactive {
|
|
|
+ return std::future::pending().await;
|
|
|
+ }
|
|
|
+
|
|
|
// Wait for the next user event or animation tick
|
|
|
loop {
|
|
|
let next = OptionFuture::from(self.events.as_mut().map(|f| f.next()));
|
|
@@ -226,7 +268,16 @@ impl Output {
|
|
|
stdout()
|
|
|
.execute(Clear(ClearType::All))?
|
|
|
.execute(Clear(ClearType::Purge))?;
|
|
|
- _ = self.term.borrow_mut().as_mut().map(|t| t.clear());
|
|
|
+
|
|
|
+ // Clear the terminal and push the frame to the bottom
|
|
|
+ _ = self.term.borrow_mut().as_mut().map(|t| {
|
|
|
+ let frame_rect = t.get_frame().area();
|
|
|
+ let term_size = t.size().unwrap();
|
|
|
+ let remaining_space = term_size
|
|
|
+ .height
|
|
|
+ .saturating_sub(frame_rect.y + frame_rect.height);
|
|
|
+ t.insert_before(remaining_space, |_| {})
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
// Toggle the more modal by swapping the the terminal with a new one
|
|
@@ -611,7 +662,7 @@ impl Output {
|
|
|
self.render_feature_list(frame, app_features, state);
|
|
|
|
|
|
// todo(jon) should we write https ?
|
|
|
- let address = match state.server.server_address() {
|
|
|
+ let address = match state.server.displayed_address() {
|
|
|
Some(address) => format!("http://{}", address).blue(),
|
|
|
None => "no server address".dark_gray(),
|
|
|
};
|
|
@@ -656,16 +707,20 @@ impl Output {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- fn render_more_modal(&self, frame: &mut Frame<'_>, area: Rect, _state: RenderState) {
|
|
|
+ fn render_more_modal(&self, frame: &mut Frame<'_>, area: Rect, state: RenderState) {
|
|
|
+ let [col1, col2] =
|
|
|
+ Layout::horizontal([Constraint::Length(50), Constraint::Fill(1)]).areas(area);
|
|
|
+
|
|
|
let [top, bottom] = Layout::vertical([Constraint::Fill(1), Constraint::Length(2)])
|
|
|
.horizontal_margin(1)
|
|
|
- .areas(area);
|
|
|
+ .areas(col1);
|
|
|
|
|
|
- let meta_list: [_; 5] = Layout::vertical([
|
|
|
+ let meta_list: [_; 6] = Layout::vertical([
|
|
|
Constraint::Length(1), // spacing
|
|
|
Constraint::Length(1), // item 1
|
|
|
Constraint::Length(1), // item 2
|
|
|
Constraint::Length(1), // item 3
|
|
|
+ Constraint::Length(1), // item 4
|
|
|
Constraint::Length(1), // Spacing
|
|
|
])
|
|
|
.areas(top);
|
|
@@ -680,15 +735,27 @@ impl Output {
|
|
|
frame.render_widget(
|
|
|
Paragraph::new(Line::from(vec![
|
|
|
"rustc: ".gray(),
|
|
|
- "1.79.9 (nightly)".yellow(),
|
|
|
+ self.rustc_details.version.as_str().yellow(),
|
|
|
])),
|
|
|
meta_list[2],
|
|
|
);
|
|
|
frame.render_widget(
|
|
|
- Paragraph::new(Line::from(vec!["Hotreload: ".gray(), "rsx only".yellow()])),
|
|
|
+ Paragraph::new(Line::from(vec![
|
|
|
+ "Hotreload: ".gray(),
|
|
|
+ "rsx and assets".yellow(),
|
|
|
+ ])),
|
|
|
meta_list[3],
|
|
|
);
|
|
|
|
|
|
+ let server_address = match state.server.server_address() {
|
|
|
+ Some(address) => format!("http://{}", address).yellow(),
|
|
|
+ None => "no address".dark_gray(),
|
|
|
+ };
|
|
|
+ frame.render_widget(
|
|
|
+ Paragraph::new(Line::from(vec!["Network: ".gray(), server_address])),
|
|
|
+ meta_list[4],
|
|
|
+ );
|
|
|
+
|
|
|
let links_list: [_; 2] =
|
|
|
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(bottom);
|
|
|
|
|
@@ -707,6 +774,35 @@ impl Output {
|
|
|
])),
|
|
|
links_list[1],
|
|
|
);
|
|
|
+
|
|
|
+ let cmds = [
|
|
|
+ "",
|
|
|
+ "r: rebuild the app",
|
|
|
+ "o: open the app",
|
|
|
+ "p: pause rebuilds",
|
|
|
+ "v: toggle verbose logs",
|
|
|
+ "t: toggle tracing logs ",
|
|
|
+ "c: clear the screen",
|
|
|
+ "/: toggle more commands",
|
|
|
+ ];
|
|
|
+ let layout: [_; 8] = Layout::vertical(cmds.iter().map(|_| Constraint::Length(1)))
|
|
|
+ .horizontal_margin(1)
|
|
|
+ .areas(col2);
|
|
|
+ for (idx, cmd) in cmds.iter().enumerate() {
|
|
|
+ if cmd.is_empty() {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ let (cmd, detail) = cmd.split_once(": ").unwrap_or((cmd, ""));
|
|
|
+ frame.render_widget(
|
|
|
+ Paragraph::new(Line::from(vec![
|
|
|
+ cmd.gray(),
|
|
|
+ ": ".gray(),
|
|
|
+ detail.dark_gray(),
|
|
|
+ ])),
|
|
|
+ layout[idx],
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// Render borders around the terminal, forcing an inner clear while we're at it
|
|
@@ -905,9 +1001,12 @@ impl Output {
|
|
|
line = line.dark_gray();
|
|
|
}
|
|
|
|
|
|
- let line_length: usize = line.spans.iter().map(|f| f.content.len()).sum();
|
|
|
-
|
|
|
- lines.push(AnsiStringLine::new(line_length.max(100) as _).render(&line));
|
|
|
+ // Create the ansi -> raw string line with a width of either the viewport width or the max width
|
|
|
+ let line_length = line.styled_graphemes(Style::default()).count();
|
|
|
+ lines.push(
|
|
|
+ AnsiStringLine::new(line_length.max(VIEWPORT_MAX_WIDTH.into()) as _)
|
|
|
+ .render(&line),
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
|