output.rs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  1. use crate::{
  2. builder::{BuildMessage, MessageType, Stage, UpdateBuildProgress},
  3. dioxus_crate::DioxusCrate,
  4. };
  5. use crate::{
  6. builder::{BuildResult, UpdateStage},
  7. serve::Serve,
  8. };
  9. use core::panic;
  10. use crossterm::{
  11. event::{Event, EventStream, KeyCode, KeyModifiers, MouseEventKind},
  12. terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
  13. tty::IsTty,
  14. ExecutableCommand,
  15. };
  16. use dioxus_cli_config::{AddressArguments, Platform};
  17. use dioxus_hot_reload::ClientMsg;
  18. use futures_util::{future::select_all, Future, StreamExt};
  19. use ratatui::{prelude::*, widgets::*, TerminalOptions, Viewport};
  20. use std::{
  21. cell::RefCell,
  22. collections::{HashMap, HashSet},
  23. io::{self, stdout},
  24. pin::Pin,
  25. rc::Rc,
  26. time::{Duration, Instant},
  27. };
  28. use tokio::{
  29. io::{AsyncBufReadExt, BufReader, Lines},
  30. process::{ChildStderr, ChildStdout},
  31. };
  32. use tracing::Level;
  33. use super::{Builder, Server, Watcher};
  34. #[derive(Default)]
  35. pub struct BuildProgress {
  36. build_logs: HashMap<Platform, ActiveBuild>,
  37. }
  38. impl BuildProgress {
  39. pub fn progress(&self) -> f64 {
  40. self.build_logs
  41. .values()
  42. .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
  43. .map(|build| match build.stage {
  44. Stage::Initializing => 0.0,
  45. Stage::InstallingWasmTooling => 0.0,
  46. Stage::Compiling => build.progress,
  47. Stage::OptimizingWasm | Stage::OptimizingAssets | Stage::Finished => 1.0,
  48. })
  49. .unwrap_or_default()
  50. }
  51. }
  52. pub struct Output {
  53. term: Rc<RefCell<Option<TerminalBackend>>>,
  54. // optional since when there's no tty there's no eventstream to read from - just stdin
  55. events: Option<EventStream>,
  56. _rustc_version: String,
  57. _rustc_nightly: bool,
  58. _dx_version: String,
  59. interactive: bool,
  60. pub(crate) build_progress: BuildProgress,
  61. running_apps: HashMap<Platform, RunningApp>,
  62. is_cli_release: bool,
  63. platform: Platform,
  64. num_lines_with_wrapping: u16,
  65. term_height: u16,
  66. scroll: u16,
  67. fly_modal_open: bool,
  68. anim_start: Instant,
  69. tab: Tab,
  70. addr: AddressArguments,
  71. }
  72. #[derive(PartialEq, Eq, Clone, Copy)]
  73. enum Tab {
  74. Console,
  75. BuildLog,
  76. }
  77. type TerminalBackend = Terminal<CrosstermBackend<io::Stdout>>;
  78. impl Output {
  79. pub fn start(cfg: &Serve) -> io::Result<Self> {
  80. let interactive = std::io::stdout().is_tty() && cfg.interactive.unwrap_or(true);
  81. let mut events = None;
  82. if interactive {
  83. enable_raw_mode()?;
  84. stdout().execute(EnterAlternateScreen)?;
  85. // workaround for ci where the terminal is not fully initialized
  86. // this stupid bug
  87. // https://github.com/crossterm-rs/crossterm/issues/659
  88. events = Some(EventStream::new());
  89. };
  90. // set the panic hook to fix the terminal
  91. set_fix_term_hook();
  92. let term: Option<TerminalBackend> = Terminal::with_options(
  93. CrosstermBackend::new(stdout()),
  94. TerminalOptions {
  95. viewport: Viewport::Fullscreen,
  96. },
  97. )
  98. .ok();
  99. // todo: re-enable rustc version
  100. // let rustc_version = rustc_version().await;
  101. // let rustc_nightly = rustc_version.contains("nightly") || cfg.target_args.nightly;
  102. let _rustc_version = String::from("1.0.0");
  103. let _rustc_nightly = false;
  104. let mut dx_version = String::new();
  105. dx_version.push_str(env!("CARGO_PKG_VERSION"));
  106. let is_cli_release = crate::dx_build_info::PROFILE == "release";
  107. if !is_cli_release {
  108. if let Some(hash) = crate::dx_build_info::GIT_COMMIT_HASH_SHORT {
  109. let hash = &hash.trim_start_matches('g')[..4];
  110. dx_version.push('-');
  111. dx_version.push_str(hash);
  112. }
  113. }
  114. let platform = cfg.build_arguments.platform.expect("To be resolved by now");
  115. Ok(Self {
  116. term: Rc::new(RefCell::new(term)),
  117. events,
  118. _rustc_version,
  119. _rustc_nightly,
  120. _dx_version: dx_version,
  121. interactive,
  122. is_cli_release,
  123. platform,
  124. fly_modal_open: false,
  125. build_progress: Default::default(),
  126. running_apps: HashMap::new(),
  127. scroll: 0,
  128. term_height: 0,
  129. num_lines_with_wrapping: 0,
  130. anim_start: Instant::now(),
  131. tab: Tab::BuildLog,
  132. addr: cfg.server_arguments.address.clone(),
  133. })
  134. }
  135. /// Wait for either the ctrl_c handler or the next event
  136. ///
  137. /// Why is the ctrl_c handler here?
  138. ///
  139. /// Also tick animations every few ms
  140. pub async fn wait(&mut self) -> io::Result<bool> {
  141. // sorry lord
  142. let user_input = match self.events.as_mut() {
  143. Some(events) => {
  144. let pinned: Pin<Box<dyn Future<Output = Option<Result<Event, _>>>>> =
  145. Box::pin(events.next());
  146. pinned
  147. }
  148. None => Box::pin(futures_util::future::pending()) as Pin<Box<dyn Future<Output = _>>>,
  149. };
  150. let has_running_apps = !self.running_apps.is_empty();
  151. let next_stdout = self.running_apps.values_mut().map(|app| {
  152. let future = async move {
  153. let (stdout, stderr) = match &mut app.stdout {
  154. Some(stdout) => (stdout.stdout.next_line(), stdout.stderr.next_line()),
  155. None => return futures_util::future::pending().await,
  156. };
  157. tokio::select! {
  158. Ok(Some(line)) = stdout => (app.result.platform, Some(line), None),
  159. Ok(Some(line)) = stderr => (app.result.platform, None, Some(line)),
  160. else => futures_util::future::pending().await,
  161. }
  162. };
  163. Box::pin(future)
  164. });
  165. let next_stdout = async {
  166. if has_running_apps {
  167. select_all(next_stdout).await.0
  168. } else {
  169. futures_util::future::pending().await
  170. }
  171. };
  172. let animation_timeout = tokio::time::sleep(Duration::from_millis(300));
  173. tokio::select! {
  174. (platform, stdout, stderr) = next_stdout => {
  175. if let Some(stdout) = stdout {
  176. self.running_apps.get_mut(&platform).unwrap().stdout.as_mut().unwrap().stdout_line.push_str(&stdout);
  177. self.push_log(platform, BuildMessage {
  178. level: Level::INFO,
  179. message: MessageType::Text(stdout),
  180. source: Some("app".to_string()),
  181. })
  182. }
  183. if let Some(stderr) = stderr {
  184. self.set_tab(Tab::BuildLog);
  185. self.running_apps.get_mut(&platform).unwrap().stdout.as_mut().unwrap().stderr_line.push_str(&stderr);
  186. self.build_progress.build_logs.get_mut(&platform).unwrap().messages.push(BuildMessage {
  187. level: Level::ERROR,
  188. message: MessageType::Text(stderr),
  189. source: Some("app".to_string()),
  190. });
  191. }
  192. },
  193. event = user_input => {
  194. if self.handle_events(event.unwrap().unwrap()).await? {
  195. return Ok(true)
  196. }
  197. // self.handle_input(event.unwrap().unwrap())?;
  198. }
  199. _ = animation_timeout => {}
  200. }
  201. Ok(false)
  202. }
  203. pub fn shutdown(&mut self) -> io::Result<()> {
  204. // if we're a tty then we need to disable the raw mode
  205. if self.interactive {
  206. disable_raw_mode()?;
  207. stdout().execute(LeaveAlternateScreen)?;
  208. self.drain_print_logs();
  209. }
  210. Ok(())
  211. }
  212. /// Emit the build logs as println! statements such that the terminal has the same output as cargo
  213. ///
  214. /// This is used when the terminal is shutdown and we want the build logs in the terminal. Old
  215. /// versions of the cli would just eat build logs making debugging issues harder than they needed
  216. /// to be.
  217. fn drain_print_logs(&mut self) {
  218. // todo: print the build info here for the most recent build, and then the logs of the most recent build
  219. for (platform, build) in self.build_progress.build_logs.iter_mut() {
  220. if build.messages.is_empty() {
  221. continue;
  222. }
  223. let messages = build.messages.drain(0..);
  224. for message in messages {
  225. match &message.message {
  226. MessageType::Cargo(diagnostic) => {
  227. println!(
  228. "{platform}: {}",
  229. diagnostic.rendered.as_deref().unwrap_or_default()
  230. )
  231. }
  232. MessageType::Text(t) => println!("{platform}: {t}"),
  233. }
  234. }
  235. }
  236. }
  237. /// Handle an input event, returning `true` if the event should cause the program to restart.
  238. pub fn handle_input(&mut self, input: Event) -> io::Result<bool> {
  239. // handle ctrlc
  240. if let Event::Key(key) = input {
  241. if let KeyCode::Char('c') = key.code {
  242. if key.modifiers.contains(KeyModifiers::CONTROL) {
  243. return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl-C"));
  244. }
  245. }
  246. }
  247. if let Event::Key(key) = input {
  248. if let KeyCode::Char('/') = key.code {
  249. self.fly_modal_open = !self.fly_modal_open;
  250. }
  251. }
  252. match input {
  253. Event::Mouse(mouse) if mouse.kind == MouseEventKind::ScrollUp => {
  254. self.scroll = self.scroll.saturating_sub(1);
  255. }
  256. Event::Mouse(mouse) if mouse.kind == MouseEventKind::ScrollDown => {
  257. self.scroll += 1;
  258. }
  259. Event::Key(key) if key.code == KeyCode::Up => {
  260. self.scroll = self.scroll.saturating_sub(1);
  261. }
  262. Event::Key(key) if key.code == KeyCode::Down => {
  263. self.scroll += 1;
  264. }
  265. Event::Key(key) if key.code == KeyCode::Char('r') => {
  266. // todo: reload the app
  267. return Ok(true);
  268. }
  269. Event::Key(key) if key.code == KeyCode::Char('o') => {
  270. // Open the running app.
  271. open::that(format!("http://{}:{}", self.addr.addr, self.addr.port))?;
  272. }
  273. Event::Key(key) if key.code == KeyCode::Char('c') => {
  274. // Clear the currently selected build logs.
  275. let build = self
  276. .build_progress
  277. .build_logs
  278. .get_mut(&self.platform)
  279. .unwrap();
  280. let msgs = match self.tab {
  281. Tab::Console => &mut build.stdout_logs,
  282. Tab::BuildLog => &mut build.messages,
  283. };
  284. msgs.clear();
  285. }
  286. Event::Key(key) if key.code == KeyCode::Char('1') => self.set_tab(Tab::Console),
  287. Event::Key(key) if key.code == KeyCode::Char('2') => self.set_tab(Tab::BuildLog),
  288. Event::Resize(_width, _height) => {
  289. // nothing, it should take care of itself
  290. }
  291. _ => {}
  292. }
  293. if self.scroll
  294. > self
  295. .num_lines_with_wrapping
  296. .saturating_sub(self.term_height + 1)
  297. {
  298. self.scroll = self
  299. .num_lines_with_wrapping
  300. .saturating_sub(self.term_height + 1);
  301. }
  302. Ok(false)
  303. }
  304. pub fn new_ws_message(&mut self, platform: Platform, message: axum::extract::ws::Message) {
  305. if let axum::extract::ws::Message::Text(text) = message {
  306. let msg = serde_json::from_str::<ClientMsg>(text.as_str());
  307. match msg {
  308. Ok(ClientMsg::Log { level, messages }) => {
  309. self.push_log(
  310. platform,
  311. BuildMessage {
  312. level: match level.as_str() {
  313. "info" => Level::INFO,
  314. "warn" => Level::WARN,
  315. "error" => Level::ERROR,
  316. "debug" => Level::DEBUG,
  317. _ => Level::INFO,
  318. },
  319. message: MessageType::Text(
  320. // todo: the js console is giving us a list of params, not formatted text
  321. // we need to translate its styling into our own
  322. messages.first().unwrap_or(&String::new()).clone(),
  323. ),
  324. source: Some("app".to_string()),
  325. },
  326. );
  327. }
  328. Err(err) => {
  329. self.push_log(
  330. platform,
  331. BuildMessage {
  332. level: Level::ERROR,
  333. source: Some("app".to_string()),
  334. message: MessageType::Text(format!("Error parsing message: {err}")),
  335. },
  336. );
  337. }
  338. }
  339. }
  340. }
  341. // todo: re-enable
  342. #[allow(unused)]
  343. fn is_snapped(&self, _platform: Platform) -> bool {
  344. true
  345. // let prev_scrol = self
  346. // .num_lines_with_wrapping
  347. // .saturating_sub(self.term_height);
  348. // prev_scrol == self.scroll
  349. }
  350. pub fn scroll_to_bottom(&mut self) {
  351. self.scroll = (self.num_lines_with_wrapping).saturating_sub(self.term_height);
  352. }
  353. pub fn push_log(&mut self, platform: Platform, message: BuildMessage) {
  354. let snapped = self.is_snapped(platform);
  355. if let Some(build) = self.build_progress.build_logs.get_mut(&platform) {
  356. build.stdout_logs.push(message);
  357. }
  358. if snapped {
  359. self.scroll_to_bottom();
  360. }
  361. }
  362. pub fn new_build_logs(&mut self, platform: Platform, update: UpdateBuildProgress) {
  363. let snapped = self.is_snapped(platform);
  364. // when the build is finished, switch to the console
  365. if update.stage == Stage::Finished {
  366. self.tab = Tab::Console;
  367. }
  368. self.build_progress
  369. .build_logs
  370. .entry(platform)
  371. .or_default()
  372. .update(update);
  373. if snapped {
  374. self.scroll_to_bottom();
  375. }
  376. }
  377. pub fn new_ready_app(&mut self, build_engine: &mut Builder, results: Vec<BuildResult>) {
  378. for result in results {
  379. let out = build_engine
  380. .children
  381. .iter_mut()
  382. .find_map(|(platform, child)| {
  383. if platform == &result.platform {
  384. let stdout = child.stdout.take().unwrap();
  385. let stderr = child.stderr.take().unwrap();
  386. Some((stdout, stderr))
  387. } else {
  388. None
  389. }
  390. });
  391. let platform = result.platform;
  392. let stdout = out.map(|(stdout, stderr)| RunningAppOutput {
  393. stdout: BufReader::new(stdout).lines(),
  394. stderr: BufReader::new(stderr).lines(),
  395. stdout_line: String::new(),
  396. stderr_line: String::new(),
  397. });
  398. let app = RunningApp { result, stdout };
  399. self.running_apps.insert(platform, app);
  400. // Finish the build progress for the platform that just finished building
  401. if let Some(build) = self.build_progress.build_logs.get_mut(&platform) {
  402. build.stage = Stage::Finished;
  403. }
  404. }
  405. }
  406. pub fn render(
  407. &mut self,
  408. _opts: &Serve,
  409. _config: &DioxusCrate,
  410. _build_engine: &Builder,
  411. server: &Server,
  412. _watcher: &Watcher,
  413. ) {
  414. // just drain the build logs
  415. if !self.interactive {
  416. self.drain_print_logs();
  417. return;
  418. }
  419. // Keep the animation track in terms of 100ms frames - the frame should be a number between 0 and 10
  420. // todo: we want to use this somehow to animate things...
  421. let elapsed = self.anim_start.elapsed().as_millis() as f32;
  422. let num_frames = elapsed / 100.0;
  423. let _frame_step = (num_frames % 10.0) as usize;
  424. _ = self
  425. .term
  426. .clone()
  427. .borrow_mut()
  428. .as_mut()
  429. .unwrap()
  430. .draw(|frame| {
  431. // a layout that has a title with stats about the program and then the actual console itself
  432. let body = Layout::default()
  433. .direction(Direction::Vertical)
  434. .constraints(
  435. [
  436. // Title
  437. Constraint::Length(1),
  438. // Body
  439. Constraint::Min(0),
  440. ]
  441. .as_ref(),
  442. )
  443. .split(frame.size());
  444. // Split the body into a left and a right
  445. let console = Layout::default()
  446. .direction(Direction::Horizontal)
  447. .constraints([Constraint::Fill(1), Constraint::Length(14)].as_ref())
  448. .split(body[1]);
  449. let addr = format!("http://{}:{}", self.addr.addr, self.addr.port);
  450. let listening_len = format!("listening at {addr}").len() + 3;
  451. let listening_len = if listening_len > body[0].width as usize {
  452. 0
  453. } else {
  454. listening_len
  455. };
  456. let header = Layout::default()
  457. .direction(Direction::Horizontal)
  458. .constraints(
  459. [
  460. Constraint::Fill(1),
  461. Constraint::Length(listening_len as u16),
  462. ]
  463. .as_ref(),
  464. )
  465. .split(body[0]);
  466. // // Render a border for the header
  467. // frame.render_widget(Block::default().borders(Borders::BOTTOM), body[0]);
  468. // Render the metadata
  469. let mut spans = vec![
  470. Span::from(if self.is_cli_release { "dx" } else { "dx-dev" }).green(),
  471. Span::from(" ").green(),
  472. Span::from("serve").green(),
  473. Span::from(" | ").white(),
  474. Span::from(self.platform.to_string()).green(),
  475. Span::from(" | ").white(),
  476. ];
  477. // If there is build progress, display that next to the platform
  478. if !self.build_progress.build_logs.is_empty() {
  479. if self
  480. .build_progress
  481. .build_logs
  482. .values()
  483. .any(|b| b.failed.is_some())
  484. {
  485. spans.push(Span::from("build failed ❌").red());
  486. } else {
  487. spans.push(Span::from("status: ").green());
  488. let build = self
  489. .build_progress
  490. .build_logs
  491. .values()
  492. .min_by(|a, b| a.partial_cmp(b).unwrap())
  493. .unwrap();
  494. spans.extend_from_slice(&build.spans(Rect::new(
  495. 0,
  496. 0,
  497. build.max_layout_size(),
  498. 1,
  499. )));
  500. }
  501. }
  502. frame.render_widget(Paragraph::new(Line::from(spans)).left_aligned(), header[0]);
  503. // Split apart the body into a center and a right side
  504. // We only want to show the sidebar if there's enough space
  505. if listening_len > 0 {
  506. frame.render_widget(
  507. Paragraph::new(Line::from(vec![
  508. Span::from("listening at ").dark_gray(),
  509. Span::from(format!("http://{}", server.ip).as_str()).gray(),
  510. ])),
  511. header[1],
  512. );
  513. }
  514. // Draw the tabs in the right region of the console
  515. // First draw the left border
  516. frame.render_widget(
  517. Paragraph::new(vec![
  518. {
  519. let mut line = Line::from(" [1] console").dark_gray();
  520. if self.tab == Tab::Console {
  521. line.style = Style::default().fg(Color::LightYellow);
  522. }
  523. line
  524. },
  525. {
  526. let mut line = Line::from(" [2] build").dark_gray();
  527. if self.tab == Tab::BuildLog {
  528. line.style = Style::default().fg(Color::LightYellow);
  529. }
  530. line
  531. },
  532. Line::from(" ").gray(),
  533. Line::from(" [/] more").gray(),
  534. Line::from(" [r] reload").gray(),
  535. Line::from(" [c] clear").gray(),
  536. Line::from(" [o] open").gray(),
  537. Line::from(" [h] hide").gray(),
  538. ])
  539. .left_aligned()
  540. .block(
  541. Block::default()
  542. .borders(Borders::LEFT | Borders::TOP)
  543. .border_set(symbols::border::Set {
  544. top_left: symbols::line::NORMAL.horizontal_down,
  545. ..symbols::border::PLAIN
  546. }),
  547. ),
  548. console[1],
  549. );
  550. // We're going to assemble a text buffer directly and then let the paragraph widgets
  551. // handle the wrapping and scrolling
  552. let mut paragraph_text: Text<'_> = Text::default();
  553. for platform in self.build_progress.build_logs.keys() {
  554. let build = self.build_progress.build_logs.get(platform).unwrap();
  555. let msgs = match self.tab {
  556. Tab::Console => &build.stdout_logs,
  557. Tab::BuildLog => &build.messages,
  558. };
  559. for span in msgs.iter() {
  560. use ansi_to_tui::IntoText;
  561. match &span.message {
  562. MessageType::Text(line) => {
  563. for line in line.lines() {
  564. let text = line.into_text().unwrap_or_default();
  565. for line in text.lines {
  566. let mut out_line = vec![Span::from("[app] ").dark_gray()];
  567. for span in line.spans {
  568. out_line.push(span);
  569. }
  570. let newline = Line::from(out_line);
  571. paragraph_text.push_line(newline);
  572. }
  573. }
  574. }
  575. MessageType::Cargo(diagnostic) => {
  576. let diagnostic = diagnostic.rendered.as_deref().unwrap_or_default();
  577. for line in diagnostic.lines() {
  578. paragraph_text.extend(line.into_text().unwrap_or_default());
  579. }
  580. }
  581. };
  582. }
  583. }
  584. let paragraph = Paragraph::new(paragraph_text)
  585. .left_aligned()
  586. .wrap(Wrap { trim: false });
  587. self.term_height = console[0].height;
  588. self.num_lines_with_wrapping = paragraph.line_count(console[0].width) as u16;
  589. let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
  590. .begin_symbol(None)
  591. .end_symbol(None)
  592. .track_symbol(None)
  593. .thumb_symbol("▐");
  594. let mut scrollbar_state = ScrollbarState::new(
  595. self.num_lines_with_wrapping
  596. .saturating_sub(self.term_height) as usize,
  597. )
  598. .position(self.scroll as usize);
  599. let paragraph = paragraph.scroll((self.scroll, 0));
  600. paragraph
  601. .block(Block::new().borders(Borders::TOP))
  602. .render(console[0], frame.buffer_mut());
  603. // and the scrollbar, those are separate widgets
  604. frame.render_stateful_widget(
  605. scrollbar,
  606. console[0].inner(Margin {
  607. // todo: dont use margin - just push down the body based on its top border
  608. // using an inner vertical margin of 1 unit makes the scrollbar inside the block
  609. vertical: 1,
  610. horizontal: 0,
  611. }),
  612. &mut scrollbar_state,
  613. );
  614. // render the fly modal
  615. self.render_fly_modal(frame, console[0]);
  616. });
  617. }
  618. async fn handle_events(&mut self, event: Event) -> io::Result<bool> {
  619. let mut events = vec![event];
  620. // Collect all the events within the next 10ms in one stream
  621. loop {
  622. let next = self.events.as_mut().unwrap().next();
  623. tokio::select! {
  624. msg = next => events.push(msg.unwrap().unwrap()),
  625. _ = tokio::time::sleep(Duration::from_millis(1)) => break
  626. }
  627. }
  628. // Debounce events within the same frame
  629. let mut handled = HashSet::new();
  630. for event in events {
  631. if !handled.contains(&event) {
  632. if self.handle_input(event.clone())? {
  633. // Restart the running app.
  634. return Ok(true);
  635. }
  636. handled.insert(event);
  637. }
  638. }
  639. Ok(false)
  640. }
  641. fn render_fly_modal(&mut self, frame: &mut Frame, area: Rect) {
  642. if !self.fly_modal_open {
  643. return;
  644. }
  645. // Create a frame slightly smaller than the area
  646. let panel = Layout::default()
  647. .direction(Direction::Vertical)
  648. .constraints([Constraint::Fill(1)].as_ref())
  649. .split(area)[0];
  650. // Wipe the panel
  651. frame.render_widget(Clear, panel);
  652. frame.render_widget(Block::default().borders(Borders::ALL), panel);
  653. let modal = Paragraph::new("Under construction, please check back at a later date!\n")
  654. .alignment(Alignment::Center);
  655. frame.render_widget(modal, panel);
  656. }
  657. fn set_tab(&mut self, tab: Tab) {
  658. self.tab = tab;
  659. self.scroll = 0;
  660. }
  661. }
  662. #[derive(Default, Debug, PartialEq)]
  663. pub struct ActiveBuild {
  664. stage: Stage,
  665. messages: Vec<BuildMessage>,
  666. stdout_logs: Vec<BuildMessage>,
  667. progress: f64,
  668. failed: Option<String>,
  669. }
  670. impl ActiveBuild {
  671. fn update(&mut self, update: UpdateBuildProgress) {
  672. match update.update {
  673. UpdateStage::Start => {
  674. self.stage = update.stage;
  675. self.progress = 0.0;
  676. self.failed = None;
  677. }
  678. UpdateStage::AddMessage(message) => {
  679. self.messages.push(message);
  680. }
  681. UpdateStage::SetProgress(progress) => {
  682. self.progress = progress;
  683. }
  684. UpdateStage::Failed(failed) => {
  685. self.stage = Stage::Finished;
  686. self.failed = Some(failed.clone());
  687. }
  688. }
  689. }
  690. fn spans(&self, area: Rect) -> Vec<Span> {
  691. let mut spans = Vec::new();
  692. let message = match self.stage {
  693. Stage::Initializing => "initializing... ",
  694. Stage::InstallingWasmTooling => "installing wasm tools... ",
  695. Stage::Compiling => "compiling... ",
  696. Stage::OptimizingWasm => "optimizing wasm... ",
  697. Stage::OptimizingAssets => "optimizing assets... ",
  698. Stage::Finished => "finished! 🎉 ",
  699. };
  700. let progress = format!("{}%", (self.progress * 100.0) as u8);
  701. if area.width >= self.max_layout_size() {
  702. spans.push(Span::from(message).light_yellow());
  703. if self.stage != Stage::Finished {
  704. spans.push(Span::from(progress).white());
  705. }
  706. } else {
  707. spans.push(Span::from(progress).white());
  708. }
  709. spans
  710. }
  711. fn max_layout_size(&self) -> u16 {
  712. let progress_size = 4;
  713. let stage_size = self.stage.to_string().len() as u16;
  714. let brace_size = 2;
  715. progress_size + stage_size + brace_size
  716. }
  717. }
  718. impl PartialOrd for ActiveBuild {
  719. fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
  720. Some(
  721. self.stage
  722. .cmp(&other.stage)
  723. .then(self.progress.partial_cmp(&other.progress).unwrap()),
  724. )
  725. }
  726. }
  727. fn set_fix_term_hook() {
  728. let original_hook = std::panic::take_hook();
  729. std::panic::set_hook(Box::new(move |info| {
  730. _ = disable_raw_mode();
  731. _ = stdout().execute(LeaveAlternateScreen);
  732. original_hook(info);
  733. }));
  734. }
  735. // todo: re-enable
  736. #[allow(unused)]
  737. async fn rustc_version() -> String {
  738. tokio::process::Command::new("rustc")
  739. .arg("--version")
  740. .output()
  741. .await
  742. .ok()
  743. .map(|o| o.stdout)
  744. .and_then(|o| {
  745. let out = String::from_utf8(o).unwrap();
  746. out.split_ascii_whitespace().nth(1).map(|v| v.to_string())
  747. })
  748. .unwrap_or_else(|| "<unknown>".to_string())
  749. }
  750. pub struct RunningApp {
  751. result: BuildResult,
  752. stdout: Option<RunningAppOutput>,
  753. }
  754. struct RunningAppOutput {
  755. stdout: Lines<BufReader<ChildStdout>>,
  756. stderr: Lines<BufReader<ChildStderr>>,
  757. stdout_line: String,
  758. stderr_line: String,
  759. }