fission_command_ui/
lib.rs1mod actions;
2mod app;
3mod commands;
4mod components;
5mod density;
6mod routes;
7mod screens;
8mod state;
9mod theme;
10
11use anyhow::Result;
12use fission::terminal::TerminalRunOptions;
13use std::path::PathBuf;
14use theme::UiThemeMode;
15
16pub use app::CliUiApp;
17pub use state::UiState;
18
19#[derive(Clone, Debug)]
20pub struct UiOptions {
21 pub project_dir: PathBuf,
22 pub screenshot: Option<PathBuf>,
23 pub exit_after_render: bool,
24 pub width: Option<u16>,
25 pub height: Option<u16>,
26}
27
28pub fn run_ui(options: UiOptions) -> Result<()> {
29 let state = UiState::load(options.project_dir.clone());
30 let run_options = TerminalRunOptions {
31 width: options.width,
32 height: options.height,
33 screenshot: options.screenshot,
34 exit_after_render: options.exit_after_render,
35 ..TerminalRunOptions::default()
36 };
37 fission::terminal::TerminalApp::with_state(CliUiApp, state)
38 .with_title("Fission command")
39 .with_env(|env| {
40 env.theme = fission::theme::Theme::dark();
41 })
42 .with_sync_env(|state, env| {
43 env.theme = match state.theme_mode {
44 UiThemeMode::Dark => fission::theme::Theme::dark(),
45 UiThemeMode::Light => fission::theme::Theme::default(),
46 };
47 })
48 .with_state_update(|state, runtime, env| state.poll_command_status(runtime, env))
49 .with_exit_request(|state, _runtime, _env| {
50 state.request_exit_confirmation();
51 false
52 })
53 .with_should_exit(|state, _runtime, _env| state.exit_confirmed)
54 .run_with_options(run_options)
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use crate::routes::UiRoute;
61 use crate::state::all_targets;
62 use fission_command_core::Target;
63 use std::path::PathBuf;
64
65 #[test]
66 fn cli_ui_renders_every_route_in_terminal_shell() {
67 for route in UiRoute::ALL {
68 let mut state = UiState {
69 project_dir: PathBuf::from("."),
70 project_name: "test-app".to_string(),
71 app_id: "com.example.test".to_string(),
72 project_status: "Project loaded".to_string(),
73 targets: all_targets().to_vec(),
74 selected_target: Some(Target::Web),
75 route,
76 host: "127.0.0.1".to_string(),
77 port: "8123".to_string(),
78 theme_mode: UiThemeMode::Dark,
79 ..Default::default()
80 };
81 state.devices = vec![crate::state::UiDevice {
82 id: "chrome".to_string(),
83 name: "Chrome/Chromium".to_string(),
84 target: Target::Web,
85 kind: "browser".to_string(),
86 status: "available".to_string(),
87 detail: String::new(),
88 available: true,
89 }];
90
91 let mut app = fission::terminal::TerminalApp::with_state(CliUiApp, state)
92 .with_sync_env(|state, env| {
93 env.theme = match state.theme_mode {
94 UiThemeMode::Dark => fission::theme::Theme::dark(),
95 UiThemeMode::Light => fission::theme::Theme::default(),
96 };
97 });
98 app.render_frame(120, 40).expect("route should render");
99 }
100 }
101
102 #[test]
103 fn cli_ui_renders_confirmation_dialog() {
104 let mut state = UiState {
105 project_dir: PathBuf::from("."),
106 project_name: "test-app".to_string(),
107 app_id: "com.example.test".to_string(),
108 project_status: "Project loaded".to_string(),
109 targets: all_targets().to_vec(),
110 selected_target: Some(Target::Web),
111 host: "127.0.0.1".to_string(),
112 port: "8123".to_string(),
113 theme_mode: UiThemeMode::Dark,
114 ..Default::default()
115 };
116 state.request_command_confirmation(crate::commands::UiCommand::RunSelected);
117
118 let mut app = fission::terminal::TerminalApp::with_state(CliUiApp, state).with_sync_env(
119 |state, env| {
120 env.theme = match state.theme_mode {
121 UiThemeMode::Dark => fission::theme::Theme::dark(),
122 UiThemeMode::Light => fission::theme::Theme::default(),
123 };
124 },
125 );
126 let frame = app.render_frame(120, 40).expect("dialog should render");
127 assert!(frame
128 .as_plain_text()
129 .contains("Confirm: run the selected target"));
130 assert!(frame.as_plain_text().contains("Cancel"));
131 }
132}