feat: diff of states

Added first subcommand - diff.
It allows to check how diffrent
current state of machine is in
compareson to world file.
This commit is contained in:
2026-05-29 20:06:19 +02:00
parent da24be9a63
commit 25fd7a370e
4 changed files with 267 additions and 2 deletions
+3
View File
@@ -0,0 +1,3 @@
pub type Error = std::boxed::Box<dyn core::error::Error>;
pub type Result<T> = core::result::Result<T, Error>;
+121 -2
View File
@@ -13,6 +13,125 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
fn main() { use std::path::PathBuf;
println!("Hello, world!");
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<PathBuf>,
#[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(())
} }
+30
View File
@@ -0,0 +1,30 @@
use crate::{error::Result, world::Packages};
use std::process::{Command, Stdio};
pub fn get_system_state() -> Result<Packages> {
let command = Command::new("pacman")
.arg("-Qqe")
.stdout(Stdio::piped())
.output()?;
let official: Vec<String> = 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> = String::from_utf8(command.stdout)?
.split_whitespace()
.map(|x| x.to_owned())
.collect();
Ok(Packages {
official,
foreign,
ignore: Vec::new(),
})
}
+113
View File
@@ -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<PathBuf> {
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<String>,
pub foreign: Vec<String>,
pub ignore: Vec<String>,
}
#[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<World> {
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<PathBuf>, 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(())
}
}