meow
This commit is contained in:
parent
b5dd2df0d3
commit
7424aba439
14 changed files with 1092 additions and 1110 deletions
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod pkgs;
|
||||||
|
pub mod tui;
|
||||||
54
src/main.rs
54
src/main.rs
|
|
@ -1,58 +1,6 @@
|
||||||
// src/main.rs - Initialize logging
|
|
||||||
use tracing::{error, info};
|
|
||||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
|
||||||
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
|
||||||
|
|
||||||
mod tui;
|
mod tui;
|
||||||
mod wget_list;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Initialize logging
|
tui::disk_manager::DiskManager::run_tui()?;
|
||||||
init_logging()?;
|
|
||||||
|
|
||||||
info!("Starting lpkg package manager");
|
|
||||||
info!("Version: 0.1.0");
|
|
||||||
|
|
||||||
// Run the TUI
|
|
||||||
if let Err(e) = tui::tui_menu() {
|
|
||||||
error!("TUI error: {}", e);
|
|
||||||
eprintln!("Error: {}", e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("lpkg exiting normally");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_logging() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
// Create log directory if it doesn't exist
|
|
||||||
std::fs::create_dir_all("logs")?;
|
|
||||||
|
|
||||||
// File appender - rotates daily
|
|
||||||
let file_appender = RollingFileAppender::new(Rotation::DAILY, "logs", "lpkg.log");
|
|
||||||
|
|
||||||
// Console layer - only shows info and above
|
|
||||||
let console_layer = fmt::layer()
|
|
||||||
.with_target(true)
|
|
||||||
.with_thread_ids(false)
|
|
||||||
.with_file(true)
|
|
||||||
.with_line_number(true)
|
|
||||||
.with_filter(EnvFilter::new("info"));
|
|
||||||
|
|
||||||
// File layer - shows debug and above
|
|
||||||
let file_layer = fmt::layer()
|
|
||||||
.with_writer(file_appender)
|
|
||||||
.with_ansi(false)
|
|
||||||
.with_target(true)
|
|
||||||
.with_file(true)
|
|
||||||
.with_line_number(true)
|
|
||||||
.with_filter(EnvFilter::new("debug"));
|
|
||||||
|
|
||||||
// Build the subscriber
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(console_layer)
|
|
||||||
.with(file_layer)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
285
src/pkgs/by_name/bi/binutils/cross_toolchain.rs
Normal file
285
src/pkgs/by_name/bi/binutils/cross_toolchain.rs
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
// async cross-toolchain runner that uses parser.rs info (no hardcoding)
|
||||||
|
use crate::pkgs::by_name::bi::binutils::parser::{BinutilsInfo, fetch_page, parse_binutils};
|
||||||
|
use reqwest::Client;
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
ffi::OsStr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
/// Configuration object - uses environment if values omitted.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BinutilsConfig {
|
||||||
|
pub lfs_root: PathBuf, // where the LFS tree will be (used for $LFS)
|
||||||
|
pub target: String, // LFS_TGT (e.g. x86_64-lfs-linux-gnu)
|
||||||
|
pub info: BinutilsInfo, // parsed page info
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BinutilsConfig {
|
||||||
|
/// create from env or params. If target is None, tries $LFS_TGT env var.
|
||||||
|
pub fn new(lfs_root: impl AsRef<Path>, target: Option<String>, info: BinutilsInfo) -> Self {
|
||||||
|
let lfs_root = lfs_root.as_ref().to_path_buf();
|
||||||
|
let target = target
|
||||||
|
.or_else(|| std::env::var("LFS_TGT").ok())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
// fallback best-effort
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
"x86_64-lfs-linux-gnu".to_string()
|
||||||
|
} else {
|
||||||
|
"x86_64-lfs-linux-gnu".to_string()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
lfs_root,
|
||||||
|
target,
|
||||||
|
info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// default places (non-hardcoded) where sources live.
|
||||||
|
/// If env `BINUTILS_SRC_DIR` is set, use that; else try LFS layout:
|
||||||
|
/// - $LFS/src/pkgs/by-name/bi/binutils
|
||||||
|
pub fn source_base_dir(&self) -> PathBuf {
|
||||||
|
if let Ok(s) = std::env::var("BINUTILS_SRC_DIR") {
|
||||||
|
PathBuf::from(s)
|
||||||
|
} else {
|
||||||
|
self.lfs_root
|
||||||
|
.join("src")
|
||||||
|
.join("pkgs")
|
||||||
|
.join("by-name")
|
||||||
|
.join("bi")
|
||||||
|
.join("binutils")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// build directory inside LFS tree (following LFS style)
|
||||||
|
pub fn build_dir(&self) -> PathBuf {
|
||||||
|
self.lfs_root.join("build").join("binutils-pass1")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// install dir (tools)
|
||||||
|
pub fn install_dir(&self) -> PathBuf {
|
||||||
|
self.lfs_root.join("tools")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// High-level orchestration. Async.
|
||||||
|
pub async fn build_binutils_from_page(
|
||||||
|
page_url: &str,
|
||||||
|
lfs_root: impl AsRef<std::path::Path>,
|
||||||
|
target: Option<String>,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
// 1) fetch page
|
||||||
|
info!("Fetching page: {}", page_url);
|
||||||
|
let html = fetch_page(page_url).await?;
|
||||||
|
let info = parse_binutils(&html)?;
|
||||||
|
info!("Parsed info: {:?}", info);
|
||||||
|
|
||||||
|
// 2) build config
|
||||||
|
let cfg = BinutilsConfig::new(lfs_root, target, info.clone());
|
||||||
|
|
||||||
|
// 3) ensure source base dir exists
|
||||||
|
let src_base = cfg.source_base_dir();
|
||||||
|
if !src_base.exists() {
|
||||||
|
info!("Creating source base dir: {:?}", src_base);
|
||||||
|
tokio::fs::create_dir_all(&src_base).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) find extracted source directory (binutils-*)
|
||||||
|
let mut source_dir: Option<PathBuf> = None;
|
||||||
|
if let Ok(mut rd) = tokio::fs::read_dir(&src_base).await {
|
||||||
|
while let Some(entry) = rd.next_entry().await? {
|
||||||
|
let ft = entry.file_type().await?;
|
||||||
|
if ft.is_dir() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if name.to_lowercase().contains("binutils") {
|
||||||
|
source_dir = Some(entry.path());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) if not found, attempt to download & extract
|
||||||
|
if source_dir.is_none() {
|
||||||
|
if let Some(dl) = &cfg.info.download_url {
|
||||||
|
info!("No extracted source found; will download {}", dl);
|
||||||
|
|
||||||
|
// download file into src_base
|
||||||
|
let client = Client::new();
|
||||||
|
let resp = client.get(dl).send().await?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("Download failed: {}", resp.status()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick a filename from URL
|
||||||
|
let url_path = url::Url::parse(dl)?;
|
||||||
|
let filename = url_path
|
||||||
|
.path_segments()
|
||||||
|
.and_then(|seg| seg.last())
|
||||||
|
.and_then(|s| {
|
||||||
|
if !s.is_empty() {
|
||||||
|
Some(s.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or("Cannot determine filename from URL")?;
|
||||||
|
|
||||||
|
let outpath = src_base.join(&filename);
|
||||||
|
info!("Saving archive to {:?}", outpath);
|
||||||
|
let bytes = resp.bytes().await?;
|
||||||
|
tokio::fs::write(&outpath, &bytes).await?;
|
||||||
|
|
||||||
|
// extract using tar (async spawn). Use absolute path to src_base
|
||||||
|
info!("Extracting archive {:?}", outpath);
|
||||||
|
let tar_path = outpath.clone();
|
||||||
|
let mut tar_cmd = Command::new("tar");
|
||||||
|
tar_cmd.arg("-xf").arg(&tar_path).arg("-C").arg(&src_base);
|
||||||
|
let status = tar_cmd.status().await?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err("tar extraction failed".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for extracted dir again
|
||||||
|
if let Ok(mut rd) = tokio::fs::read_dir(&src_base).await {
|
||||||
|
while let Some(entry) = rd.next_entry().await? {
|
||||||
|
let ft = entry.file_type().await?;
|
||||||
|
if ft.is_dir() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if name.to_lowercase().contains("binutils") {
|
||||||
|
source_dir = Some(entry.path());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("No download URL found on the page and no unpacked source present.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let source_dir = match source_dir {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return Err("Could not locate or download/extract Binutils source".into()),
|
||||||
|
};
|
||||||
|
info!("Using source dir: {:?}", source_dir);
|
||||||
|
|
||||||
|
// 6) prepare build dir
|
||||||
|
let build_dir = cfg.build_dir();
|
||||||
|
if !build_dir.exists() {
|
||||||
|
info!("Creating build dir {:?}", build_dir);
|
||||||
|
tokio::fs::create_dir_all(&build_dir).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) run configure: use absolute configure script path in source_dir
|
||||||
|
let configure_path = source_dir.join("configure");
|
||||||
|
if !configure_path.exists() {
|
||||||
|
return Err(format!("configure script not found at {:?}", configure_path).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If parser produced configure args tokens, use them; otherwise fallback to common flags
|
||||||
|
let args = if !cfg.info.configure_args.is_empty() {
|
||||||
|
cfg.info.configure_args.clone()
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
format!("--prefix={}", cfg.install_dir().display()),
|
||||||
|
format!("--with-sysroot={}", cfg.lfs_root.display()),
|
||||||
|
format!("--target={}", cfg.target),
|
||||||
|
"--disable-nls".to_string(),
|
||||||
|
"--disable-werror".to_string(),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// replace $LFS and $LFS_TGT in args
|
||||||
|
let args: Vec<String> = args
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| {
|
||||||
|
a.replace("$LFS", &cfg.lfs_root.to_string_lossy())
|
||||||
|
.replace("$LFS_TGT", &cfg.target)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("Configuring with args: {:?}", args);
|
||||||
|
|
||||||
|
// spawn configure
|
||||||
|
let mut conf_cmd = Command::new(&configure_path);
|
||||||
|
conf_cmd.current_dir(&build_dir);
|
||||||
|
for a in &args {
|
||||||
|
conf_cmd.arg(a);
|
||||||
|
}
|
||||||
|
conf_cmd.stdout(std::process::Stdio::inherit());
|
||||||
|
conf_cmd.stderr(std::process::Stdio::inherit());
|
||||||
|
let status = conf_cmd.status().await?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err("configure step failed".into());
|
||||||
|
}
|
||||||
|
info!("configure completed");
|
||||||
|
|
||||||
|
// 8) run build commands (make-like)
|
||||||
|
if !cfg.info.build_cmds.is_empty() {
|
||||||
|
for b in &cfg.info.build_cmds {
|
||||||
|
// split into program + args
|
||||||
|
let mut parts = shell_words::split(b).unwrap_or_else(|_| vec![b.clone()]);
|
||||||
|
let prog = parts.remove(0);
|
||||||
|
let mut cmd = Command::new(prog);
|
||||||
|
if !parts.is_empty() {
|
||||||
|
cmd.args(parts);
|
||||||
|
}
|
||||||
|
cmd.current_dir(&build_dir);
|
||||||
|
cmd.stdout(std::process::Stdio::inherit());
|
||||||
|
cmd.stderr(std::process::Stdio::inherit());
|
||||||
|
let status = cmd.status().await?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(format!("build step failed: {:?}", b).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// fallback to running `make`
|
||||||
|
let mut m = Command::new("make");
|
||||||
|
m.current_dir(&build_dir);
|
||||||
|
m.stdout(std::process::Stdio::inherit());
|
||||||
|
m.stderr(std::process::Stdio::inherit());
|
||||||
|
let status = m.status().await?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err("make failed".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("build completed");
|
||||||
|
|
||||||
|
// 9) run install commands (make install)
|
||||||
|
if !cfg.info.install_cmds.is_empty() {
|
||||||
|
for inst in &cfg.info.install_cmds {
|
||||||
|
let mut parts = shell_words::split(inst).unwrap_or_else(|_| vec![inst.clone()]);
|
||||||
|
let prog = parts.remove(0);
|
||||||
|
let mut cmd = Command::new(prog);
|
||||||
|
if !parts.is_empty() {
|
||||||
|
cmd.args(parts);
|
||||||
|
}
|
||||||
|
cmd.current_dir(&build_dir);
|
||||||
|
cmd.stdout(std::process::Stdio::inherit());
|
||||||
|
cmd.stderr(std::process::Stdio::inherit());
|
||||||
|
let status = cmd.status().await?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(format!("install step failed: {:?}", inst).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// fallback `make install`
|
||||||
|
let mut mi = Command::new("make");
|
||||||
|
mi.arg("install");
|
||||||
|
mi.current_dir(&build_dir);
|
||||||
|
mi.stdout(std::process::Stdio::inherit());
|
||||||
|
mi.stderr(std::process::Stdio::inherit());
|
||||||
|
let status = mi.status().await?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err("make install failed".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("install completed. Binutils Pass 1 done.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
2
src/pkgs/by_name/bi/binutils/mod.rs
Normal file
2
src/pkgs/by_name/bi/binutils/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod cross_toolchain;
|
||||||
|
pub mod parser;
|
||||||
220
src/pkgs/by_name/bi/binutils/parser.rs
Normal file
220
src/pkgs/by_name/bi/binutils/parser.rs
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
// async parser for Binutils Pass 1 page
|
||||||
|
use reqwest::Client;
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BinutilsInfo {
|
||||||
|
/// "2.45" or derived version text
|
||||||
|
pub version: Option<String>,
|
||||||
|
/// first archive download URL found (.tar.xz or .tar.gz)
|
||||||
|
pub download_url: Option<String>,
|
||||||
|
/// tokens for configure flags (everything after ../configure)
|
||||||
|
pub configure_args: Vec<String>,
|
||||||
|
/// build commands discovered (e.g. ["make"])
|
||||||
|
pub build_cmds: Vec<String>,
|
||||||
|
/// install commands discovered (e.g. ["make install"])
|
||||||
|
pub install_cmds: Vec<String>,
|
||||||
|
/// optional SBU, disk space
|
||||||
|
pub sbu: Option<String>,
|
||||||
|
pub disk_space: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BinutilsInfo {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
version: None,
|
||||||
|
download_url: None,
|
||||||
|
configure_args: Vec::new(),
|
||||||
|
build_cmds: Vec::new(),
|
||||||
|
install_cmds: Vec::new(),
|
||||||
|
sbu: None,
|
||||||
|
disk_space: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch page content (async)
|
||||||
|
pub async fn fetch_page(url: &str) -> Result<String, Box<dyn Error>> {
|
||||||
|
let client = Client::new();
|
||||||
|
let res = client.get(url).send().await?;
|
||||||
|
let status = res.status();
|
||||||
|
if !status.is_success() {
|
||||||
|
return Err(format!("Failed to fetch {}: {}", url, status).into());
|
||||||
|
}
|
||||||
|
let text = res.text().await?;
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the LFS Binutils pass1 page; robust to small formatting changes.
|
||||||
|
/// - extracts version (from <h1> text like "Binutils-2.45 - Pass 1")
|
||||||
|
/// - finds a download URL ending with .tar.xz/.tar.gz
|
||||||
|
/// - finds configure pre block(s), builds token list
|
||||||
|
/// - finds `make` / `make install` pre blocks
|
||||||
|
pub fn parse_binutils(html: &str) -> Result<BinutilsInfo, Box<dyn Error>> {
|
||||||
|
let document = Html::parse_document(html);
|
||||||
|
|
||||||
|
let mut info = BinutilsInfo::default();
|
||||||
|
|
||||||
|
// 1) Version from h1.sect1 (contains "Binutils-2.45 - Pass 1")
|
||||||
|
if let Ok(h1_sel) = Selector::parse("h1.sect1") {
|
||||||
|
if let Some(h1) = document.select(&h1_sel).next() {
|
||||||
|
let txt = h1.text().collect::<Vec<_>>().join(" ");
|
||||||
|
// try to pick the token containing "Binutils-" or "binutils-"
|
||||||
|
if let Some(tok) = txt
|
||||||
|
.split_whitespace()
|
||||||
|
.find(|s| s.to_lowercase().contains("binutils"))
|
||||||
|
{
|
||||||
|
// extract digits from token, e.g. "Binutils-2.45"
|
||||||
|
if let Some(pos) = tok.find('-') {
|
||||||
|
let ver = tok[pos + 1..]
|
||||||
|
.trim()
|
||||||
|
.trim_matches(|c: char| !c.is_ascii() && c != '.')
|
||||||
|
.to_string();
|
||||||
|
if !ver.is_empty() {
|
||||||
|
info.version = Some(ver);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// fallback: try to find "2.45" somewhere in the h1 string
|
||||||
|
for part in txt.split_whitespace() {
|
||||||
|
if part.chars().next().map(|c| c.is_digit(10)).unwrap_or(false) {
|
||||||
|
info.version = Some(part.trim().to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Download URL: look for anchors with href ending .tar.xz/.tar.gz
|
||||||
|
if let Ok(a_sel) = Selector::parse("a[href]") {
|
||||||
|
for a in document.select(&a_sel) {
|
||||||
|
if let Some(href) = a.value().attr("href") {
|
||||||
|
let href = href.trim();
|
||||||
|
if href.ends_with(".tar.xz") || href.ends_with(".tar.gz") || href.ends_with(".tgz")
|
||||||
|
{
|
||||||
|
// Make absolute if relative to page; the typical LFS pages use relative links like ../../... or ../..
|
||||||
|
// If it's already absolute (starts with http), keep it.
|
||||||
|
let url = href.to_string();
|
||||||
|
info.download_url = Some(url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Parse "segmentedlist" entries for SBU and disk space
|
||||||
|
if let Ok(segtitle_sel) =
|
||||||
|
Selector::parse("div.package .segmentedlist .seglistitem .seg strong.segtitle")
|
||||||
|
{
|
||||||
|
if let Ok(segbody_sel) =
|
||||||
|
Selector::parse("div.package .segmentedlist .seglistitem .seg span.segbody")
|
||||||
|
{
|
||||||
|
for (t, b) in document
|
||||||
|
.select(&segtitle_sel)
|
||||||
|
.zip(document.select(&segbody_sel))
|
||||||
|
{
|
||||||
|
let title = t.text().collect::<String>().to_lowercase();
|
||||||
|
let body = b.text().collect::<String>().trim().to_string();
|
||||||
|
if title.contains("approximate build time") {
|
||||||
|
info.sbu = Some(body.clone());
|
||||||
|
} else if title.contains("required disk space") {
|
||||||
|
info.disk_space = Some(body.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) `pre.kbd.command` blocks for configure & make lines
|
||||||
|
if let Ok(pre_sel) = Selector::parse("div.installation pre.kbd.command, pre.kbd.command") {
|
||||||
|
for pre in document.select(&pre_sel) {
|
||||||
|
let text = pre.text().collect::<Vec<_>>().join("\n");
|
||||||
|
let trimmed = text.trim();
|
||||||
|
|
||||||
|
// handle configure block (starts with ../configure or ./configure)
|
||||||
|
if trimmed.starts_with("../configure")
|
||||||
|
|| trimmed.starts_with("./configure")
|
||||||
|
|| trimmed.starts_with(".. /configure")
|
||||||
|
{
|
||||||
|
// normalize: remove trailing backslashes and join lines
|
||||||
|
let mut joined = String::new();
|
||||||
|
for line in trimmed.lines() {
|
||||||
|
let line = line.trim_end();
|
||||||
|
if line.ends_with('\\') {
|
||||||
|
joined.push_str(line.trim_end_matches('\\').trim());
|
||||||
|
joined.push(' ');
|
||||||
|
} else {
|
||||||
|
joined.push_str(line.trim());
|
||||||
|
joined.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove leading "../configure" token and split into args
|
||||||
|
let pieces: Vec<&str> = joined.split_whitespace().collect();
|
||||||
|
let mut args = Vec::new();
|
||||||
|
let mut started = false;
|
||||||
|
for p in pieces {
|
||||||
|
if !started {
|
||||||
|
if p.ends_with("configure")
|
||||||
|
|| p.ends_with("configure")
|
||||||
|
|| p.contains("configure")
|
||||||
|
{
|
||||||
|
started = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// skip until configure found
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
args.push(p.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallback: if no tokens parsed, try chopping first token
|
||||||
|
if args.is_empty() {
|
||||||
|
// attempt to remove the first token (../configure) by index
|
||||||
|
if let Some(pos) = joined.find("configure") {
|
||||||
|
let after = &joined[pos + "configure".len()..];
|
||||||
|
for t in after.split_whitespace() {
|
||||||
|
args.push(t.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.configure_args = args
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle make / make install lines
|
||||||
|
// consider blocks that are exactly "make" or "make install" or lines containing them
|
||||||
|
for line in trimmed.lines().map(|l| l.trim()) {
|
||||||
|
if line == "make" {
|
||||||
|
if !info.build_cmds.contains(&"make".to_string()) {
|
||||||
|
info.build_cmds.push("make".to_string());
|
||||||
|
}
|
||||||
|
} else if line == "make install" {
|
||||||
|
if !info.install_cmds.contains(&"make install".to_string()) {
|
||||||
|
info.install_cmds.push("make install".to_string());
|
||||||
|
}
|
||||||
|
} else if line.starts_with("make ") {
|
||||||
|
// e.g., "make -j2"
|
||||||
|
let t = line.to_string();
|
||||||
|
if !info.build_cmds.contains(&t) {
|
||||||
|
info.build_cmds.push(t);
|
||||||
|
}
|
||||||
|
} else if line.starts_with("time {") && line.contains("make") {
|
||||||
|
// handle the time wrapper line in the note; ignore
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// final sanity: if build_cmds empty but install_cmds contains "make install", add "make"
|
||||||
|
if info.build_cmds.is_empty() && !info.install_cmds.is_empty() {
|
||||||
|
info.build_cmds.push("make".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
1
src/pkgs/by_name/bi/mod.rs
Normal file
1
src/pkgs/by_name/bi/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod binutils;
|
||||||
1
src/pkgs/by_name/mod.rs
Normal file
1
src/pkgs/by_name/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod bi;
|
||||||
1
src/pkgs/mod.rs
Normal file
1
src/pkgs/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod by_name;
|
||||||
1057
src/tui.rs
1057
src/tui.rs
File diff suppressed because it is too large
Load diff
437
src/tui/disk_manager.rs
Normal file
437
src/tui/disk_manager.rs
Normal file
|
|
@ -0,0 +1,437 @@
|
||||||
|
// src/tui/disk_manager.rs
|
||||||
|
use std::{
|
||||||
|
fs::{File, read_dir},
|
||||||
|
io::{self, Seek, SeekFrom, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crossterm::event::{self, Event, KeyCode};
|
||||||
|
use crossterm::execute;
|
||||||
|
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||||
|
use gptman::{GPT, GPTPartitionEntry, PartitionName};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
use tui::{
|
||||||
|
Terminal,
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
text::Span,
|
||||||
|
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||||
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// DiskManager: interactive TUI to view and create GPT partitions on Linux.
|
||||||
|
///
|
||||||
|
/// Requirements (add to Cargo.toml):
|
||||||
|
/// tui = "0.19"
|
||||||
|
/// crossterm = "0.26"
|
||||||
|
/// gptman = "2.0"
|
||||||
|
/// uuid = { version = "1", features = ["v4"] }
|
||||||
|
/// tracing = "0.1"
|
||||||
|
pub struct DiskManager;
|
||||||
|
|
||||||
|
impl DiskManager {
|
||||||
|
/// Entrypoint: run the disk manager UI. This initializes the terminal and starts the loop.
|
||||||
|
pub fn run_tui() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// init terminal
|
||||||
|
let mut stdout = std::io::stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut term = Terminal::new(backend)?;
|
||||||
|
term.clear()?;
|
||||||
|
|
||||||
|
// collect devices (linux-focused: sd*, nvme*, vd*)
|
||||||
|
let mut devices: Vec<PathBuf> = Vec::new();
|
||||||
|
if let Ok(entries) = read_dir("/dev/") {
|
||||||
|
for e in entries.flatten() {
|
||||||
|
let path = e.path();
|
||||||
|
if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
|
||||||
|
if name.starts_with("sd")
|
||||||
|
|| name.starts_with("nvme")
|
||||||
|
|| name.starts_with("vd")
|
||||||
|
|| name.starts_with("mmcblk")
|
||||||
|
{
|
||||||
|
devices.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if devices.is_empty() {
|
||||||
|
// restore terminal before printing
|
||||||
|
execute!(term.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
println!("No block devices found under /dev (sd*, nvme*, vd*, mmcblk*).");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut selected_idx = 0usize;
|
||||||
|
let mut status_msg =
|
||||||
|
String::from("Select disk. ↑/↓ to navigate, Enter=view, C=create, Q=quit.");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
term.draw(|f| {
|
||||||
|
let size = f.size();
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(1)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(6),
|
||||||
|
Constraint::Length(3),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(size);
|
||||||
|
|
||||||
|
// header
|
||||||
|
let header = Paragraph::new(Span::styled(
|
||||||
|
"🔧 Disk Manager — Linux GPT (use carefully!)",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
))
|
||||||
|
.block(Block::default().borders(Borders::ALL));
|
||||||
|
f.render_widget(header, chunks[0]);
|
||||||
|
|
||||||
|
// device list + selection
|
||||||
|
let items: Vec<ListItem> = devices
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, d)| {
|
||||||
|
let label = format!(
|
||||||
|
"{} {}",
|
||||||
|
if i == selected_idx { "▶" } else { " " },
|
||||||
|
d.display()
|
||||||
|
);
|
||||||
|
let mut li = ListItem::new(label);
|
||||||
|
if i == selected_idx {
|
||||||
|
li = li.style(Style::default().fg(Color::Yellow));
|
||||||
|
}
|
||||||
|
li
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list =
|
||||||
|
List::new(items).block(Block::default().borders(Borders::ALL).title("Disks"));
|
||||||
|
f.render_widget(list, chunks[1]);
|
||||||
|
|
||||||
|
// status/footer
|
||||||
|
let footer = Paragraph::new(status_msg.as_str())
|
||||||
|
.style(Style::default().fg(Color::Green))
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("Status"));
|
||||||
|
f.render_widget(footer, chunks[2]);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Input handling
|
||||||
|
if event::poll(std::time::Duration::from_millis(100))? {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Char('Q') => break,
|
||||||
|
KeyCode::Up => {
|
||||||
|
if selected_idx > 0 {
|
||||||
|
selected_idx -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
if selected_idx + 1 < devices.len() {
|
||||||
|
selected_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
let path = devices[selected_idx].clone();
|
||||||
|
match Self::view_partitions_tui(&path, &mut term) {
|
||||||
|
Ok(m) => status_msg = m,
|
||||||
|
Err(e) => status_msg = format!("Error reading partitions: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('c') | KeyCode::Char('C') => {
|
||||||
|
let path = devices[selected_idx].clone();
|
||||||
|
match Self::create_partition_tui(&path, &mut term) {
|
||||||
|
Ok(m) => {
|
||||||
|
info!(target: "disk_manager", "{}", m);
|
||||||
|
status_msg = m;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(target: "disk_manager", "create partition error: {:?}", e);
|
||||||
|
status_msg = format!("Create failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore terminal
|
||||||
|
execute!(term.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
term.show_cursor()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show GPT partitions for the chosen disk in a paged TUI view.
|
||||||
|
fn view_partitions_tui(
|
||||||
|
disk: &PathBuf,
|
||||||
|
term: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
// try to open & read GPT (512 sector size)
|
||||||
|
let mut file = File::open(disk)?;
|
||||||
|
let gpt = match GPT::read_from(&mut file, 512) {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(_) => match GPT::find_from(&mut file) {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(_) => {
|
||||||
|
return Ok(format!("No GPT found on {}", disk.display()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create list of lines to display:
|
||||||
|
let mut lines: Vec<String> = Vec::new();
|
||||||
|
lines.push(format!("Partitions on {}:", disk.display()));
|
||||||
|
for (i, entry_opt) in gpt.partitions.iter().enumerate() {
|
||||||
|
if let Some(entry) = entry_opt {
|
||||||
|
let name = entry.partition_name.to_string();
|
||||||
|
lines.push(format!(
|
||||||
|
"{}: {} -> {} (type: {})",
|
||||||
|
i,
|
||||||
|
entry.starting_lba,
|
||||||
|
entry.ending_lba,
|
||||||
|
// show a short GUID hex for partition type
|
||||||
|
hex::encode_upper(&entry.partition_type_guid)
|
||||||
|
));
|
||||||
|
lines.push(format!(" name: {}", name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lines.len() == 1 {
|
||||||
|
lines.push("No partitions found.".into());
|
||||||
|
}
|
||||||
|
// paged view loop
|
||||||
|
let mut top = 0usize;
|
||||||
|
loop {
|
||||||
|
term.draw(|f| {
|
||||||
|
let size = f.size();
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(1)
|
||||||
|
.constraints([Constraint::Min(1), Constraint::Length(1)].as_ref())
|
||||||
|
.split(size);
|
||||||
|
|
||||||
|
let page_lines: Vec<ListItem> = lines
|
||||||
|
.iter()
|
||||||
|
.skip(top)
|
||||||
|
.take((chunks[0].height as usize).saturating_sub(2))
|
||||||
|
.map(|l| ListItem::new(l.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(page_lines).block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(format!("Partitions: {}", disk.display())),
|
||||||
|
);
|
||||||
|
f.render_widget(list, chunks[0]);
|
||||||
|
|
||||||
|
let footer = Paragraph::new("↑/↓ scroll • q to go back")
|
||||||
|
.block(Block::default().borders(Borders::ALL));
|
||||||
|
f.render_widget(footer, chunks[1]);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if event::poll(std::time::Duration::from_millis(100))? {
|
||||||
|
if let Event::Key(k) = event::read()? {
|
||||||
|
match k.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => break,
|
||||||
|
KeyCode::Up => {
|
||||||
|
if top > 0 {
|
||||||
|
top = top.saturating_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
if top + 1 < lines.len() {
|
||||||
|
top = top.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(format!("Viewed partitions on {}", disk.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fully-TUI flow to enter partition name, size (MB), and choose partition type.
|
||||||
|
/// Writes GPT changes to disk.
|
||||||
|
fn create_partition_tui(
|
||||||
|
disk: &PathBuf,
|
||||||
|
term: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
// open file read+write
|
||||||
|
let mut file = File::options().read(true).write(true).open(disk)?;
|
||||||
|
|
||||||
|
// Read or create GPT
|
||||||
|
let mut gpt = match GPT::read_from(&mut file, 512) {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(_) => match GPT::find_from(&mut file) {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(_) => {
|
||||||
|
// If there's no GPT, create one with a random disk GUID
|
||||||
|
let disk_guid_raw: [u8; 16] = *Uuid::new_v4().as_bytes();
|
||||||
|
// new_from requires a Seek+Read; create new GPT structure on-disk
|
||||||
|
GPT::new_from(&mut file, 512, disk_guid_raw)?
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// interactive fields
|
||||||
|
let mut name = String::from("new_partition");
|
||||||
|
let mut size_mb: u64 = 100; // default 100 MB
|
||||||
|
let mut type_choice = 1usize; // 0 = EFI, 1 = Linux filesystem
|
||||||
|
|
||||||
|
// known GUIDs (string repr) -> will be parsed to raw bytes as required
|
||||||
|
let efi_guid = Uuid::parse_str("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")?; // EFI System
|
||||||
|
let linux_fs_guid = Uuid::parse_str("0FC63DAF-8483-4772-8E79-3D69D8477DE4")?; // Linux filesystem
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Render UI
|
||||||
|
term.draw(|f| {
|
||||||
|
let size = f.size();
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(1)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(3),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(size);
|
||||||
|
|
||||||
|
let title = Paragraph::new(Span::styled(
|
||||||
|
format!("Create partition on {}", disk.display()),
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
))
|
||||||
|
.block(Block::default().borders(Borders::ALL));
|
||||||
|
f.render_widget(title, chunks[0]);
|
||||||
|
|
||||||
|
let name_widget = Paragraph::new(format!("Name: {}", name)).block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title("Partition Name"),
|
||||||
|
);
|
||||||
|
f.render_widget(name_widget, chunks[1]);
|
||||||
|
|
||||||
|
let size_widget = Paragraph::new(format!("Size (MB): {}", size_mb))
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("Size"));
|
||||||
|
f.render_widget(size_widget, chunks[2]);
|
||||||
|
|
||||||
|
let types = vec![
|
||||||
|
format!(
|
||||||
|
"{} EFI System Partition",
|
||||||
|
if type_choice == 0 { "▶" } else { " " }
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"{} Linux filesystem",
|
||||||
|
if type_choice == 1 { "▶" } else { " " }
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let type_items: Vec<ListItem> = types.into_iter().map(ListItem::new).collect();
|
||||||
|
let type_list = List::new(type_items).block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title("Partition Type (use ←/→)"),
|
||||||
|
);
|
||||||
|
f.render_widget(type_list, chunks[3]);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Input
|
||||||
|
if event::poll(std::time::Duration::from_millis(100))? {
|
||||||
|
if let Event::Key(k) = event::read()? {
|
||||||
|
match k.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => {
|
||||||
|
return Ok("Creation cancelled".to_string());
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
type_choice = type_choice.saturating_sub(1);
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
type_choice = (type_choice + 1) % 2;
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
// typing to name: accept visible characters; digits typed also append
|
||||||
|
if !c.is_control() {
|
||||||
|
name.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
name.pop();
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
// increase size by 10MB
|
||||||
|
size_mb = size_mb.saturating_add(10);
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
size_mb = size_mb.saturating_sub(10);
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert MB -> sectors (512 bytes per sector)
|
||||||
|
let sectors = (size_mb as u128 * 1024 * 1024 / 512) as u64;
|
||||||
|
// choose starting LBA: find max ending_lba among existing partitions; align to 2048
|
||||||
|
let last_end = gpt
|
||||||
|
.partitions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| p.as_ref().map(|e| e.ending_lba))
|
||||||
|
.max()
|
||||||
|
.unwrap_or(2048);
|
||||||
|
let start = ((last_end + 2048) / 2048) * 2048 + 1;
|
||||||
|
let end = start + sectors.saturating_sub(1);
|
||||||
|
|
||||||
|
// build partition entry
|
||||||
|
let mut new_entry = GPTPartitionEntry::empty();
|
||||||
|
new_entry.starting_lba = start;
|
||||||
|
new_entry.ending_lba = end;
|
||||||
|
new_entry.partition_name = PartitionName::from(name.as_str());
|
||||||
|
|
||||||
|
// set partition type GUID
|
||||||
|
let type_guid = if type_choice == 0 {
|
||||||
|
*efi_guid.as_bytes()
|
||||||
|
} else {
|
||||||
|
*linux_fs_guid.as_bytes()
|
||||||
|
};
|
||||||
|
new_entry.partition_type_guid = type_guid;
|
||||||
|
|
||||||
|
// find first empty partition slot
|
||||||
|
let idx_opt = gpt.partitions.iter().position(|p| p.is_none());
|
||||||
|
let idx = match idx_opt {
|
||||||
|
Some(i) => i,
|
||||||
|
None => return Err("No free GPT partition entries (maxed out)".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// assign and write
|
||||||
|
gpt.partitions[idx] = Some(new_entry);
|
||||||
|
|
||||||
|
// Seek to start (important)
|
||||||
|
file.seek(SeekFrom::Start(0))?;
|
||||||
|
gpt.write_into(&mut file)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"Created partition '{}' on {} ({} MB, sectors {}..{})",
|
||||||
|
name,
|
||||||
|
disk.display(),
|
||||||
|
size_mb,
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/tui/downloader.rs
Normal file
61
src/tui/downloader.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
use std::io::Stdout;
|
||||||
|
use tracing::instrument;
|
||||||
|
use tui::{
|
||||||
|
Terminal,
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
style::Style,
|
||||||
|
text::Spans,
|
||||||
|
widgets::{Block, Borders, Gauge, List, ListItem},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::tui::settings::Settings;
|
||||||
|
|
||||||
|
pub struct Downloader;
|
||||||
|
|
||||||
|
impl Downloader {
|
||||||
|
#[instrument(skip(terminal, settings))]
|
||||||
|
pub fn show_downloader(
|
||||||
|
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||||
|
settings: &Settings,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let files = vec!["file1.tar.gz", "file2.tar.gz", "file3.tar.gz"];
|
||||||
|
let progress = vec![0.3, 0.5, 0.9];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| {
|
||||||
|
let size = f.size();
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(2)
|
||||||
|
.constraints([Constraint::Percentage(70), Constraint::Percentage(30)].as_ref())
|
||||||
|
.split(size);
|
||||||
|
|
||||||
|
let items: Vec<ListItem> = files
|
||||||
|
.iter()
|
||||||
|
.map(|f| ListItem::new(Spans::from(*f)))
|
||||||
|
.collect();
|
||||||
|
let list = List::new(items).block(
|
||||||
|
Block::default()
|
||||||
|
.title(Spans::from("Downloads"))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(settings.theme.secondary_color())),
|
||||||
|
);
|
||||||
|
f.render_widget(list, chunks[0]);
|
||||||
|
|
||||||
|
for (i, prog) in progress.iter().enumerate() {
|
||||||
|
let gauge = Gauge::default()
|
||||||
|
.block(Block::default().title(files[i]))
|
||||||
|
.gauge_style(Style::default().fg(settings.theme.primary_color()))
|
||||||
|
.ratio(*prog as f64);
|
||||||
|
f.render_widget(gauge, chunks[1]);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
break; // remove in real async loop
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/tui/main_menu.rs
Normal file
49
src/tui/main_menu.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
use crate::tui::disk_manager::DiskManager;
|
||||||
|
use crossterm::event::{self, Event, KeyCode};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io::Stdout;
|
||||||
|
use tui::{
|
||||||
|
Terminal,
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
style::{Color, Style},
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn show_main_menu() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut stdout = std::io::stdout();
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| {
|
||||||
|
let size = f.size();
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(2)
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Length(3)].as_ref())
|
||||||
|
.split(size);
|
||||||
|
|
||||||
|
let menu = Paragraph::new("1) Disk Manager\n0) Exit")
|
||||||
|
.block(Block::default().borders(Borders::ALL));
|
||||||
|
f.render_widget(menu, chunks[0]);
|
||||||
|
|
||||||
|
let status = Paragraph::new("Use number keys to select an option")
|
||||||
|
.style(Style::default().fg(Color::Yellow))
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("Status"));
|
||||||
|
f.render_widget(status, chunks[1]);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if event::poll(std::time::Duration::from_millis(100))? {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('1') => DiskManager::show_disk_manager(&mut terminal)?,
|
||||||
|
KeyCode::Char('0') => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
4
src/tui/mod.rs
Normal file
4
src/tui/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod disk_manager;
|
||||||
|
pub mod downloader;
|
||||||
|
pub mod main_menu;
|
||||||
|
pub mod settings;
|
||||||
28
src/tui/settings.rs
Normal file
28
src/tui/settings.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
use std::io::Stdout;
|
||||||
|
use tracing::instrument;
|
||||||
|
use tui::{Terminal, backend::CrosstermBackend};
|
||||||
|
|
||||||
|
pub struct Settings {
|
||||||
|
pub theme: Theme,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Theme;
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
pub fn primary_color(&self) -> tui::style::Color {
|
||||||
|
tui::style::Color::Cyan
|
||||||
|
}
|
||||||
|
pub fn secondary_color(&self) -> tui::style::Color {
|
||||||
|
tui::style::Color::White
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
#[instrument(skip(terminal))]
|
||||||
|
pub fn show_settings(
|
||||||
|
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Render settings UI here
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue