From 25fd7a370e475bb88e03ed012cad0d595175520c Mon Sep 17 00:00:00 2001 From: theMZet Date: Fri, 29 May 2026 20:06:19 +0200 Subject: [PATCH] feat: diff of states Added first subcommand - diff. It allows to check how diffrent current state of machine is in compareson to world file. --- src/error.rs | 3 + src/main.rs | 123 ++++++++++++++++++++++++++++++++++++++++- src/package_manager.rs | 30 ++++++++++ src/world.rs | 113 +++++++++++++++++++++++++++++++++++++ 4 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 src/error.rs create mode 100644 src/package_manager.rs create mode 100644 src/world.rs diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..4d8ebb1 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,3 @@ +pub type Error = std::boxed::Box; + +pub type Result = core::result::Result; diff --git a/src/main.rs b/src/main.rs index 742358c..1a7a71b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,125 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -fn main() { - println!("Hello, world!"); +use std::path::PathBuf; + +use clap::Parser; +use clap_derive::{Parser, Subcommand}; + +mod error; +mod package_manager; +mod world; + +use error::*; + +use crate::{ + package_manager::get_system_state, + world::{World, get_world_location}, +}; + +#[cfg(all(feature = "yay", feature = "paru"))] +compile_error!("'yay' and 'paru' are mutually exclusive and cannot be enabled together."); + +#[cfg(not(target_os = "linux"))] +compile_error!("Only (Arch) linux is supported!"); + +#[derive(Parser)] +struct Args { + #[arg(long, global = true)] + /// Custom path to the world file + world: Option, + + #[arg(long, global = true)] + /// Don't run any commands that mutates state, only write them to stdout - for debug purposes + dry_run: bool, + + #[arg(short, long, global = true)] + yes: bool, + + #[arg(short, long, global = true)] + quiet: bool, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Debug, Clone, Copy, Subcommand)] +enum Commands { + /// Synchronizes state of machine with the world file and update everythink + Sync, + /// Export currently installed packages to the world file + Export { + #[arg(short, long)] + /// Do you want to overwrite the world file + overwrite: bool, + + #[arg(short, long)] + /// When specified, the new world file content will be writen to stdout + stdout: bool, + }, + /// Writes diffrents between the world file and the system + Diff, + /// Delete packages that aren't specified in the world file and abandond ones + Pure { + #[arg(short, long)] + /// Deletes packages with there's configs - same as -Rns instead of -Rs + all: bool, + }, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + let world_path = match args.world { + Some(x) => x, + None => get_world_location()?, + }; + + match args.command { + Commands::Sync => todo!(), + Commands::Export { overwrite, stdout } => todo!(), + Commands::Diff => { + let world = World::load_from(world_path).unwrap_or(World::new_empty()); + let world_state = world.get_packages(); + let state = get_system_state()?; + + let mut added = state.exclude(world_state); + let mut removed = world_state.exclude(&state); + + added.ignore = world_state.ignore.clone(); + removed.ignore = world_state.ignore.clone(); + + added.exclude_ignored(); + removed.exclude_ignored(); + + let mut s = "Official\n".to_string(); + + // official added + added + .official + .iter() + .for_each(|x| s += &format!("[+] {}\n", x)); + // official removed + removed + .official + .iter() + .for_each(|x| s += &format!("[-] {}\n", x)); + + s += "\nForeign\n"; + // foreign added + added + .foreign + .iter() + .for_each(|x| s += &format!("[+] {}\n", x)); + // foreign removed + removed + .foreign + .iter() + .for_each(|x| s += &format!("[-] {}\n", x)); + + println!("{}", s); + } + Commands::Pure { all } => todo!(), + } + Ok(()) } diff --git a/src/package_manager.rs b/src/package_manager.rs new file mode 100644 index 0000000..cdaed81 --- /dev/null +++ b/src/package_manager.rs @@ -0,0 +1,30 @@ +use crate::{error::Result, world::Packages}; +use std::process::{Command, Stdio}; + +pub fn get_system_state() -> Result { + let command = Command::new("pacman") + .arg("-Qqe") + .stdout(Stdio::piped()) + .output()?; + + let official: Vec = String::from_utf8(command.stdout)? + .split_whitespace() + .map(|x| x.to_owned()) + .collect(); + + let command = Command::new("pacman") + .arg("-Qqm") + .stdout(Stdio::piped()) + .output()?; + + let foreign: Vec = String::from_utf8(command.stdout)? + .split_whitespace() + .map(|x| x.to_owned()) + .collect(); + + Ok(Packages { + official, + foreign, + ignore: Vec::new(), + }) +} diff --git a/src/world.rs b/src/world.rs new file mode 100644 index 0000000..0fd0427 --- /dev/null +++ b/src/world.rs @@ -0,0 +1,113 @@ +use std::env; +use std::error::Error; +use std::fs::OpenOptions; +use std::io::{Read, Write}; +use std::{fs::File, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::Result; + +pub fn get_world_location() -> Result { + match env::var("TOPAZ_WORLD") { + Ok(x) => Ok(x.into()), + Err(env::VarError::NotPresent) => Ok("/etc/topaz/world.toml".into()), + Err(env::VarError::NotUnicode(_)) => Err("World location in env is invalid!".into()), + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Packages { + pub official: Vec, + pub foreign: Vec, + pub ignore: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct World { + packages: Packages, +} + +impl Packages { + pub fn new() -> Packages { + Packages { + official: Vec::new(), + foreign: Vec::new(), + ignore: Vec::new(), + } + } +} + +impl Packages { + pub fn exclude(&self, other: &Self) -> Self { + let official = self + .official + .iter() + .filter(|x| !other.official.contains(x)) + .cloned() + .collect(); + let foreign = self + .foreign + .iter() + .filter(|x| !other.foreign.contains(x)) + .cloned() + .collect(); + + Packages { + official, + foreign, + ignore: Vec::new(), + } + } + + pub fn exclude_ignored(&mut self) { + self.official.retain(|x| !self.ignore.contains(x)); + self.foreign.retain(|x| !self.ignore.contains(x)); + } +} + +impl World { + pub fn new_empty() -> World { + World { + packages: Packages::new(), + } + } + + pub fn get_packages(&self) -> &Packages { + &self.packages + } + + pub fn get_mut_packages(&mut self) -> &mut Packages { + &mut self.packages + } + + pub fn load_from(world_path: PathBuf) -> Result { + let mut file = File::open(world_path)?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + + let world: World = toml::from_str(&text)?; + + Ok(world) + } + + pub fn save(&self, world_path: Option, do_print: bool) -> Result<()> { + let text = toml::to_string_pretty(self)?; + + if let Some(world_path) = world_path { + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(world_path)?; + + file.write_all(text.as_bytes())?; + } + + if do_print { + println!("{}", text); + } + + Ok(()) + } +}