|
| 1 | +use crate::tui::screens::{ImageScreen, LanguageScreen}; |
| 2 | +use anyhow::Result; |
| 3 | +use initramfs_builder::{BuildResult, Compression, InitramfsBuilder, RegistryAuth}; |
| 4 | +use tokio::sync::mpsc::{self, error::TryRecvError}; |
| 5 | + |
| 6 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 7 | +pub enum Screen { |
| 8 | + Language, |
| 9 | + Image, |
| 10 | + Summary, |
| 11 | + Build, |
| 12 | +} |
| 13 | + |
| 14 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 15 | +pub enum WizardMode { |
| 16 | + #[default] |
| 17 | + Quick, |
| 18 | +} |
| 19 | + |
| 20 | +#[derive(Debug, Clone)] |
| 21 | +pub struct BuildConfig { |
| 22 | + pub image: String, |
| 23 | + pub arch: String, |
| 24 | + pub compression: Compression, |
| 25 | + pub output: String, |
| 26 | +} |
| 27 | + |
| 28 | +impl Default for BuildConfig { |
| 29 | + fn default() -> Self { |
| 30 | + Self { |
| 31 | + image: String::new(), |
| 32 | + arch: "amd64".to_string(), |
| 33 | + compression: Compression::Gzip, |
| 34 | + output: "initramfs.cpio.gz".to_string(), |
| 35 | + } |
| 36 | + } |
| 37 | +} |
| 38 | + |
| 39 | +pub struct App { |
| 40 | + pub screen: Screen, |
| 41 | + pub config: BuildConfig, |
| 42 | + #[allow(dead_code)] |
| 43 | + pub mode: WizardMode, |
| 44 | + pub should_quit: bool, |
| 45 | + pub language_screen: LanguageScreen, |
| 46 | + pub image_screen: ImageScreen, |
| 47 | + pub build_progress: Option<String>, |
| 48 | + pub build_error: Option<String>, |
| 49 | + pub validation_error: Option<String>, |
| 50 | + pub loading_frame: usize, |
| 51 | + pub build_success: bool, |
| 52 | + pub build_receiver: Option<mpsc::Receiver<Result<BuildResult>>>, |
| 53 | +} |
| 54 | + |
| 55 | +impl App { |
| 56 | + pub fn new() -> Self { |
| 57 | + Self { |
| 58 | + screen: Screen::Language, |
| 59 | + config: BuildConfig::default(), |
| 60 | + mode: WizardMode::Quick, |
| 61 | + should_quit: false, |
| 62 | + language_screen: LanguageScreen::new(), |
| 63 | + image_screen: ImageScreen::new(), |
| 64 | + build_progress: None, |
| 65 | + build_error: None, |
| 66 | + validation_error: None, |
| 67 | + loading_frame: 0, |
| 68 | + build_success: false, |
| 69 | + build_receiver: None, |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + pub fn next_screen(&mut self) { |
| 74 | + self.validation_error = None; |
| 75 | + self.sync_screen_on_exit(); |
| 76 | + |
| 77 | + self.screen = match self.screen { |
| 78 | + Screen::Language => { |
| 79 | + self.update_image_from_language(); |
| 80 | + Screen::Image |
| 81 | + } |
| 82 | + Screen::Image => Screen::Summary, |
| 83 | + Screen::Summary => Screen::Build, |
| 84 | + Screen::Build => Screen::Build, |
| 85 | + }; |
| 86 | + |
| 87 | + self.sync_screen_on_enter(); |
| 88 | + } |
| 89 | + |
| 90 | + pub fn prev_screen(&mut self) { |
| 91 | + self.validation_error = None; |
| 92 | + self.screen = match self.screen { |
| 93 | + Screen::Language => Screen::Language, |
| 94 | + Screen::Image => Screen::Language, |
| 95 | + Screen::Summary => Screen::Image, |
| 96 | + Screen::Build => Screen::Summary, |
| 97 | + }; |
| 98 | + self.sync_screen_on_enter(); |
| 99 | + } |
| 100 | + |
| 101 | + pub fn sync_screen_on_enter(&mut self) { |
| 102 | + if let Screen::Image = self.screen { |
| 103 | + self.image_screen.sync_from_config(&self.config.image); |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + pub fn sync_screen_on_exit(&mut self) { |
| 108 | + if let Screen::Image = self.screen { |
| 109 | + self.config.image = self.image_screen.sync_to_config(); |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + fn update_image_from_language(&mut self) { |
| 114 | + let preset = &self.language_screen.presets[self.language_screen.selected]; |
| 115 | + if !preset.versions.is_empty() { |
| 116 | + let version_idx = self |
| 117 | + .language_screen |
| 118 | + .version_selected |
| 119 | + .min(preset.versions.len() - 1); |
| 120 | + self.config.image = preset.versions[version_idx].1.to_string(); |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + pub fn validate_current_screen(&mut self) -> bool { |
| 125 | + self.validation_error = None; |
| 126 | + match self.screen { |
| 127 | + Screen::Image => { |
| 128 | + let image = self.image_screen.input.trim(); |
| 129 | + if image.is_empty() { |
| 130 | + self.validation_error = Some("Image cannot be empty".to_string()); |
| 131 | + return false; |
| 132 | + } |
| 133 | + } |
| 134 | + Screen::Summary => { |
| 135 | + if self.config.image.trim().is_empty() { |
| 136 | + self.validation_error = Some("Image is required".to_string()); |
| 137 | + return false; |
| 138 | + } |
| 139 | + } |
| 140 | + _ => {} |
| 141 | + } |
| 142 | + true |
| 143 | + } |
| 144 | + |
| 145 | + pub fn start_build(&mut self) { |
| 146 | + self.screen = Screen::Build; |
| 147 | + self.build_progress = Some("Building initramfs...".to_string()); |
| 148 | + self.loading_frame = 0; |
| 149 | + |
| 150 | + let image = self.config.image.clone(); |
| 151 | + let arch = self.config.arch.clone(); |
| 152 | + let compression = self.config.compression; |
| 153 | + let output = self.config.output.clone(); |
| 154 | + let (tx, rx) = mpsc::channel::<Result<BuildResult>>(1); |
| 155 | + |
| 156 | + self.build_receiver = Some(rx); |
| 157 | + |
| 158 | + tokio::spawn(async move { |
| 159 | + let builder = InitramfsBuilder::new() |
| 160 | + .image(&image) |
| 161 | + .compression(compression) |
| 162 | + .platform("linux", &arch) |
| 163 | + .auth(RegistryAuth::Anonymous); |
| 164 | + |
| 165 | + let result = builder.build(&output).await; |
| 166 | + let _ = tx.send(result).await; |
| 167 | + }); |
| 168 | + } |
| 169 | + |
| 170 | + pub fn check_build_status(&mut self) { |
| 171 | + if let Some(rx) = &mut self.build_receiver { |
| 172 | + let rx: &mut mpsc::Receiver<Result<BuildResult>> = rx; |
| 173 | + match rx.try_recv() { |
| 174 | + Ok(result) => { |
| 175 | + self.build_receiver = None; |
| 176 | + match result { |
| 177 | + Ok(res) => { |
| 178 | + self.build_progress = Some(format!( |
| 179 | + "Success! Output: {} ({} entries, {:.2} MB)", |
| 180 | + self.config.output, |
| 181 | + res.entries, |
| 182 | + res.compressed_size as f64 / 1_048_576.0 |
| 183 | + )); |
| 184 | + self.build_success = true; |
| 185 | + } |
| 186 | + Err(e) => { |
| 187 | + self.build_error = Some(format!("Build failed: {}", e)); |
| 188 | + self.build_success = false; |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | + Err(TryRecvError::Empty) => {} |
| 193 | + Err(TryRecvError::Disconnected) => { |
| 194 | + self.build_receiver = None; |
| 195 | + self.build_error = |
| 196 | + Some("Build task panicked or disconnected unexpectedly".to_string()); |
| 197 | + self.build_success = false; |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + pub fn on_tick(&mut self) { |
| 204 | + self.loading_frame = self.loading_frame.wrapping_add(1); |
| 205 | + } |
| 206 | + |
| 207 | + pub fn generate_cli_command(&self) -> String { |
| 208 | + format!( |
| 209 | + "initramfs-builder build {} \\\n --platform-arch {} \\\n -c {} \\\n -o {}", |
| 210 | + self.config.image, self.config.arch, self.config.compression, self.config.output |
| 211 | + ) |
| 212 | + } |
| 213 | +} |
0 commit comments