feat: device hot-reload, tools-relative adb, auto port bind (#3586)

* wip: bind to 0.0.0.0 instead of 127.0.0.1

* dont hang in background - ignore suspend commands when tty is disabled

* use system adb

* pass configs through to android mobile launch

* fix: line sizes + clear pushes frame

* dont resume window state on mobile

* fix screen sizing for ipad

* attempt and fail to implement codesigning

* Clippy and note about safety in android bindings
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-01-20 17:42:13 -08:00
1 parent e199e86dad
commit 8334f7fbaa
22 files changed
+620 -212

No files matched your search

+127 -28
View File
@@ -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),
);
}
}