Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 168 additions & 17 deletions src/tui/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::tui::screens::{ImageScreen, LanguageScreen};
use crate::tui::screens::init::InitMode;
use crate::tui::screens::inject::Injection;
use crate::tui::screens::{
ArchScreen, CompressScreen, ImageScreen, InitScreen, InjectScreen, LanguageScreen,
};
use anyhow::Result;
use initramfs_builder::{BuildResult, Compression, InitramfsBuilder, RegistryAuth};
use tokio::sync::mpsc::{self, error::TryRecvError};
Expand All @@ -7,6 +11,10 @@ use tokio::sync::mpsc::{self, error::TryRecvError};
pub enum Screen {
Language,
Image,
Architecture,
Inject,
Init,
Compression,
Summary,
Build,
}
Expand All @@ -15,35 +23,78 @@ pub enum Screen {
pub enum WizardMode {
#[default]
Quick,
Advanced,
}

#[derive(Debug, Clone)]
pub struct BuildConfig {
pub image: String,
pub arch: String,
pub injections: Vec<Injection>,
pub init_mode: InitMode,
pub compression: Compression,
pub output: String,
}

impl Default for BuildConfig {
fn default() -> Self {
let compression = Compression::Gzip;
Self {
image: String::new(),
arch: "amd64".to_string(),
compression: Compression::Gzip,
output: "initramfs.cpio.gz".to_string(),
arch: detect_host_arch().to_string(),
injections: Vec::new(),
init_mode: InitMode::Default,
compression,
output: format!("initramfs{}", output_extension(compression)),
}
}
}

fn detect_host_arch() -> &'static str {
match std::env::consts::ARCH {
"x86_64" => "amd64",
"aarch64" => "arm64",
_ => "amd64",
}
}

fn output_extension(compression: Compression) -> &'static str {
match compression {
Compression::Gzip => ".cpio.gz",
Compression::Zstd => ".cpio.zst",
Compression::None => ".cpio",
}
}

fn sync_output_extension(output: &str, compression: Compression) -> String {
let base = if let Some(base) = output.strip_suffix(".cpio.gz") {
base
} else if let Some(base) = output.strip_suffix(".cpio.zst") {
base
} else if let Some(base) = output.strip_suffix(".cpio") {
base
} else {
output
};

format!("{}{}", base, output_extension(compression))
}

fn shell_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\\''"))
}

pub struct App {
pub screen: Screen,
pub config: BuildConfig,
#[allow(dead_code)]
pub mode: WizardMode,
pub should_quit: bool,
pub language_screen: LanguageScreen,
pub image_screen: ImageScreen,
pub arch_screen: ArchScreen,
pub inject_screen: InjectScreen,
pub init_screen: InitScreen,
pub compress_screen: CompressScreen,
pub build_progress: Option<String>,
pub build_error: Option<String>,
pub validation_error: Option<String>,
Expand All @@ -54,13 +105,19 @@ pub struct App {

impl App {
pub fn new() -> Self {
let config = BuildConfig::default();
let arch_screen = ArchScreen::new_with_default(&config.arch);
Self {
screen: Screen::Language,
config: BuildConfig::default(),
config,
mode: WizardMode::Quick,
should_quit: false,
language_screen: LanguageScreen::new(),
image_screen: ImageScreen::new(),
arch_screen,
inject_screen: InjectScreen::new(),
init_screen: InitScreen::new(),
compress_screen: CompressScreen::new(),
build_progress: None,
build_error: None,
validation_error: None,
Expand All @@ -79,7 +136,14 @@ impl App {
self.update_image_from_language();
Screen::Image
}
Screen::Image => Screen::Summary,
Screen::Image => match self.mode {
WizardMode::Quick => Screen::Summary,
WizardMode::Advanced => Screen::Architecture,
},
Screen::Architecture => Screen::Inject,
Screen::Inject => Screen::Init,
Screen::Init => Screen::Compression,
Screen::Compression => Screen::Summary,
Screen::Summary => Screen::Build,
Screen::Build => Screen::Build,
};
Expand All @@ -92,21 +156,50 @@ impl App {
self.screen = match self.screen {
Screen::Language => Screen::Language,
Screen::Image => Screen::Language,
Screen::Summary => Screen::Image,
Screen::Architecture => Screen::Image,
Screen::Inject => Screen::Architecture,
Screen::Init => Screen::Inject,
Screen::Compression => Screen::Init,
Screen::Summary => match self.mode {
WizardMode::Quick => Screen::Image,
WizardMode::Advanced => Screen::Compression,
},
Screen::Build => Screen::Summary,
};
self.sync_screen_on_enter();
}

pub fn enter_advanced_mode(&mut self) {
self.sync_screen_on_exit();
self.mode = WizardMode::Advanced;
self.screen = Screen::Architecture;
self.sync_screen_on_enter();
}

pub fn sync_screen_on_enter(&mut self) {
if let Screen::Image = self.screen {
self.image_screen.sync_from_config(&self.config.image);
match self.screen {
Screen::Image => self.image_screen.sync_from_config(&self.config.image),
Screen::Architecture => self.arch_screen.sync_from_config(&self.config.arch),
_ => {}
}
}

pub fn sync_screen_on_exit(&mut self) {
if let Screen::Image = self.screen {
self.config.image = self.image_screen.sync_to_config();
match self.screen {
Screen::Image => self.config.image = self.image_screen.sync_to_config(),
Screen::Architecture => self.config.arch = self.arch_screen.get_selected().to_string(),
Screen::Inject => self.config.injections = self.inject_screen.get_injections(),
Screen::Init => {
if let Some(init_mode) = self.init_screen.get_init_mode() {
self.config.init_mode = init_mode;
}
}
Screen::Compression => {
let compression = self.compress_screen.get_selected();
self.config.compression = compression;
self.config.output = sync_output_extension(&self.config.output, compression);
}
_ => {}
}
}

Expand All @@ -131,6 +224,12 @@ impl App {
return false;
}
}
Screen::Init => {
if self.init_screen.get_init_mode().is_none() {
self.validation_error = Some("Custom init path cannot be empty".to_string());
return false;
}
}
Screen::Summary => {
if self.config.image.trim().is_empty() {
self.validation_error = Some("Image is required".to_string());
Expand All @@ -143,30 +242,67 @@ impl App {
}

pub fn start_build(&mut self) {
self.validation_error = None;
if let Err(err) = self.validate_init_script_path() {
self.screen = Screen::Init;
self.build_progress = None;
self.build_error = None;
self.validation_error = Some(err);
self.sync_screen_on_enter();
return;
}

self.screen = Screen::Build;
self.build_progress = Some("Building initramfs...".to_string());
self.build_error = None;
self.loading_frame = 0;

let image = self.config.image.clone();
let arch = self.config.arch.clone();
let compression = self.config.compression;
let output = self.config.output.clone();
let injections = self.config.injections.clone();
let init_mode = self.config.init_mode.clone();
let (tx, rx) = mpsc::channel::<Result<BuildResult>>(1);

self.build_receiver = Some(rx);

tokio::spawn(async move {
let builder = InitramfsBuilder::new()
let mut builder = InitramfsBuilder::new()
.image(&image)
.compression(compression)
.platform("linux", &arch)
.auth(RegistryAuth::Anonymous);

for inj in &injections {
builder = builder.inject(&inj.src, &inj.dest);
}

if let InitMode::CustomFile(path) = init_mode {
builder = builder.init_script(path);
}

let result = builder.build(&output).await;
let _ = tx.send(result).await;
});
}

fn validate_init_script_path(&self) -> std::result::Result<(), String> {
match &self.config.init_mode {
InitMode::Default => Ok(()),
InitMode::CustomFile(path) => {
if path.exists() && path.is_file() {
Ok(())
} else {
Err(format!(
"Custom init path must be an existing file: {}",
path.display()
))
}
}
}
}

pub fn check_build_status(&mut self) {
if let Some(rx) = &mut self.build_receiver {
let rx: &mut mpsc::Receiver<Result<BuildResult>> = rx;
Expand Down Expand Up @@ -205,9 +341,24 @@ impl App {
}

pub fn generate_cli_command(&self) -> String {
format!(
"initramfs-builder build {} \\\n --platform-arch {} \\\n -c {} \\\n -o {}",
self.config.image, self.config.arch, self.config.compression, self.config.output
)
let mut cmd = format!("initramfs-builder build {}", self.config.image);

for inj in &self.config.injections {
let inject = format!("{}:{}", inj.src, inj.dest);
cmd.push_str(&format!(" \\\n --inject {}", shell_quote(&inject)));
}

if let InitMode::CustomFile(path) = &self.config.init_mode {
cmd.push_str(&format!(
" \\\n --init {}",
shell_quote(&path.display().to_string())
));
}

cmd.push_str(&format!(" \\\n --platform-arch {}", self.config.arch));
cmd.push_str(&format!(" \\\n -c {}", self.config.compression));
cmd.push_str(&format!(" \\\n -o {}", shell_quote(&self.config.output)));

cmd
}
}
Loading
Loading