feat: add command-line interface and package management functionality
- Implemented a new CLI using Clap for managing Fedora configurations and packages. - Added commands for configuring dnf, adding repositories, installing RPM Fusion, adding users to groups, and managing packages. - Created data files for various package lists (AMD, Intel, common, firmware, gnome extra) and user groups. - Introduced Zsh completion script for improved command-line usability. - Removed the old CLI implementation and integrated the new structure into the main application logic.
This commit is contained in:
60
src/cli.rs
60
src/cli.rs
@@ -1,60 +0,0 @@
|
||||
use clap::{Parser, Args, Subcommand, crate_authors, crate_description, crate_name, crate_version};
|
||||
use clap_complete::Shell;
|
||||
|
||||
#[derive(Parser, Debug, PartialEq)]
|
||||
#[command(name = crate_name!(), author = crate_authors!(), version = crate_version!(), about = crate_description!())]
|
||||
pub struct Cli {
|
||||
#[arg(long = "generate", hide = true, value_enum)]
|
||||
pub generator: Option<Shell>,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, PartialEq)]
|
||||
pub enum Commands {
|
||||
#[command(about = "A set of commands to configure Fedora")]
|
||||
Config(ConfigCommand),
|
||||
|
||||
#[command(about = "A set of commands to manage packages")]
|
||||
Package(PackageCommand),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, PartialEq)]
|
||||
pub struct ConfigCommand {
|
||||
#[arg(long, value_name = "CONFIG_FILE", default_value = "config.toml")]
|
||||
pub config_file: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub subcommand: ConfigSubcommand,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, PartialEq)]
|
||||
pub struct PackageCommand {
|
||||
#[arg(long, value_name = "PACKAGE_FILE", default_value = "packages.toml")]
|
||||
pub package_file: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub subcommand: PackageSubcommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, PartialEq)]
|
||||
pub enum ConfigSubcommand {
|
||||
#[command(about = "Generate a configuration file")]
|
||||
Generate,
|
||||
|
||||
#[command(about = "Validate a configuration file")]
|
||||
Validate,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, PartialEq)]
|
||||
pub enum PackageSubcommand {
|
||||
#[command(about = "Install packages")]
|
||||
Install,
|
||||
|
||||
#[command(about = "Remove packages")]
|
||||
Remove,
|
||||
|
||||
#[command(about = "List installed packages")]
|
||||
List,
|
||||
}
|
||||
152
src/lib.rs
Normal file
152
src/lib.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use clap::{
|
||||
Args, Command, Parser, Subcommand, crate_authors, crate_description, crate_name, crate_version,
|
||||
};
|
||||
use clap_complete::{Generator, Shell, generate};
|
||||
use core::str;
|
||||
use env_logger::fmt::style;
|
||||
use std::{error::Error, io::Write};
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
#[derive(Parser, Debug, PartialEq)]
|
||||
#[command(name = crate_name!(), author = crate_authors!(), version = crate_version!(), about = crate_description!())]
|
||||
pub struct Cli {
|
||||
#[arg(long = "generate", hide = true, value_enum)]
|
||||
pub generator: Option<Shell>,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, PartialEq)]
|
||||
pub enum Commands {
|
||||
#[command(about = "Configure dnf")]
|
||||
ConfigureDnf,
|
||||
|
||||
#[command(about = "Add a repository")]
|
||||
AddRepo(AddRepoCommand),
|
||||
|
||||
#[command(about = "Install RPM fusion")]
|
||||
InstallRpmFusion,
|
||||
|
||||
#[command(about = "Add user to groups")]
|
||||
AddUserToGroups(AddUserToGroupsCommand),
|
||||
|
||||
#[command(about = "Manage packages")]
|
||||
Package(PackageCommand),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, PartialEq)]
|
||||
pub struct AddUserToGroupsCommand {
|
||||
#[arg(short, long)]
|
||||
pub user: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, PartialEq)]
|
||||
pub struct AddRepoCommand {
|
||||
#[command(subcommand)]
|
||||
pub command: AddRepoSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, PartialEq)]
|
||||
pub enum AddRepoSubCommand {
|
||||
#[command(about = "Add VSCode repository")]
|
||||
Vscode,
|
||||
|
||||
#[command(about = "Add Mullvad repository")]
|
||||
Mullvad,
|
||||
|
||||
#[command(about = "Add Vivaldi repository")]
|
||||
Vivaldi,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, PartialEq, Clone)]
|
||||
pub struct PackageCommand {
|
||||
#[arg(short, long)]
|
||||
pub remove: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: PackageSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, PartialEq, Clone)]
|
||||
pub enum PackageSubCommand {
|
||||
#[command(about = "Install/Remove the common list")]
|
||||
CommonList,
|
||||
|
||||
#[command(about = "Install/Remove the AMD list")]
|
||||
AmdList,
|
||||
|
||||
#[command(about = "Install/Remove the Intel list")]
|
||||
IntelList,
|
||||
|
||||
#[command(about = "Install/Remove the Gnome extra list")]
|
||||
GnomeExtraList,
|
||||
|
||||
#[command(about = "Install/Remove the firmware list")]
|
||||
FirmwareList,
|
||||
|
||||
#[command(about = "Install/Remove a custom list")]
|
||||
CustomList(CustomListCommand),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, PartialEq, Clone)]
|
||||
pub struct CustomListCommand {
|
||||
#[arg(short, long)]
|
||||
pub file: String,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! success {
|
||||
() => {
|
||||
log::info!();
|
||||
};
|
||||
($($arg:tt)*) => {{
|
||||
let style = env_logger::fmt::style::AnsiColor::Green.on_default().bold();
|
||||
log::info!("{style}{}{style:#}", format!($($arg)*)
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! read_file {
|
||||
($file:expr) => {
|
||||
std::fs::read_to_string($file).map_err(|e| {
|
||||
log::error!("Error reading file {}: {}", $file, e);
|
||||
e
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
pub fn configure_logger() {
|
||||
let log_level = std::env::var("RUST_LOG").unwrap_or("info".to_string());
|
||||
env_logger::Builder::new()
|
||||
.format(|buf, record| {
|
||||
let level = record.level();
|
||||
let style = buf.default_level_style(level);
|
||||
let dimmed = style::AnsiColor::White.on_default().dimmed();
|
||||
let message_style = match level {
|
||||
log::Level::Error => style::AnsiColor::Red.on_default().bold(),
|
||||
log::Level::Warn => style::AnsiColor::Yellow.on_default().bold(),
|
||||
log::Level::Info => style::AnsiColor::White.on_default(),
|
||||
log::Level::Debug => dimmed,
|
||||
log::Level::Trace => dimmed,
|
||||
};
|
||||
writeln!(
|
||||
buf,
|
||||
"{style}{level:<5}{style:#} {dimmed}>{dimmed:#} {message_style}{}{message_style:#}",
|
||||
record.args()
|
||||
)
|
||||
})
|
||||
.parse_filters(log_level.as_str())
|
||||
.init();
|
||||
}
|
||||
|
||||
pub fn print_completions<G: Generator>(generator: G, cmd: &mut Command) {
|
||||
generate(
|
||||
generator,
|
||||
cmd,
|
||||
cmd.get_name().to_string(),
|
||||
&mut std::io::stdout(),
|
||||
);
|
||||
}
|
||||
323
src/main.rs
323
src/main.rs
@@ -1,95 +1,270 @@
|
||||
mod cli;
|
||||
use std::thread::available_parallelism;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::Cli;
|
||||
use env_logger::fmt::style;
|
||||
|
||||
use std::io::Write;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use feddy::*;
|
||||
use ini::Ini;
|
||||
|
||||
fn main() {
|
||||
let log_level = std::env::var("RUST_LOG").unwrap_or("info".to_string());
|
||||
env_logger::Builder::new()
|
||||
.format(|buf, record| {
|
||||
let level = record.level();
|
||||
let style = buf.default_level_style(level);
|
||||
let dimmed = style::AnsiColor::White.on_default().dimmed();
|
||||
let message_style = match level {
|
||||
log::Level::Error => style::AnsiColor::Red.on_default().bold(),
|
||||
log::Level::Warn => style::AnsiColor::Yellow.on_default().bold(),
|
||||
log::Level::Info => style::AnsiColor::White.on_default(),
|
||||
log::Level::Debug => dimmed,
|
||||
log::Level::Trace => dimmed,
|
||||
};
|
||||
writeln!(
|
||||
buf,
|
||||
"{style}{level:<5}{style:#} {dimmed}>{dimmed:#} {message_style}{}{message_style:#}",
|
||||
record.args()
|
||||
)
|
||||
})
|
||||
.parse_filters(log_level.as_str())
|
||||
.init();
|
||||
configure_logger();
|
||||
|
||||
let cli = Cli::parse();
|
||||
log::debug!("Parsed CLI arguments: {:?}", cli);
|
||||
match cli.command {
|
||||
cli::Commands::Config(config) => {
|
||||
log::info!("Config command selected with config file: {}", config.config_file);
|
||||
handle_config_command(config);
|
||||
}
|
||||
cli::Commands::Package(package) => {
|
||||
log::info!("Package command selected with package file: {}", package.package_file);
|
||||
handle_package_command(package);
|
||||
}
|
||||
let mut cmd = Cli::command();
|
||||
|
||||
if let Some(generator) = cli.generator {
|
||||
log::info!("Generating completion file for {generator:?}...");
|
||||
print_completions(generator, &mut cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(command) = cli.command {
|
||||
match command {
|
||||
Commands::ConfigureDnf => {
|
||||
if let Err(e) = configure_dnf() {
|
||||
log::error!("Error configuring dnf: {}", e);
|
||||
}
|
||||
}
|
||||
Commands::AddRepo(repo_command) => {
|
||||
if let Err(e) = add_repo(repo_command) {
|
||||
log::error!("Error adding repository: {}", e);
|
||||
}
|
||||
}
|
||||
Commands::InstallRpmFusion => {
|
||||
if let Err(e) = install_rpm_fusion() {
|
||||
log::error!("Error installing RPM Fusion: {}", e);
|
||||
}
|
||||
}
|
||||
Commands::AddUserToGroups(add_user_command) => {
|
||||
if let Err(e) = add_user_to_groups(add_user_command) {
|
||||
log::error!("Error adding user to groups: {}", e);
|
||||
}
|
||||
}
|
||||
Commands::Package(package_command) => {
|
||||
if let Err(e) = manage_package(package_command) {
|
||||
log::error!("Error executing package command: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cmd.print_help().unwrap();
|
||||
}
|
||||
|
||||
success!("Bye :)");
|
||||
}
|
||||
|
||||
fn handle_config_command(config: cli::ConfigCommand) {
|
||||
match config.subcommand {
|
||||
cli::ConfigSubcommand::Generate => {
|
||||
log::info!("Generating configuration file: {}", config.config_file);
|
||||
handle_generate_config(&config.config_file);
|
||||
}
|
||||
cli::ConfigSubcommand::Validate => {
|
||||
log::info!("Validating configuration file: {}", config.config_file);
|
||||
handle_validate_config(&config.config_file);
|
||||
}
|
||||
fn add_user_to_groups(add_user_command: AddUserToGroupsCommand) -> Result<()> {
|
||||
let user = add_user_command.user.unwrap_or_else(|| {
|
||||
log::info!("No user specified, using current user");
|
||||
whoami::username()
|
||||
});
|
||||
log::info!("Adding user {} to groups...", user);
|
||||
let groups = include_str!("../data/user_groups").to_string();
|
||||
|
||||
let groups = groups
|
||||
.lines()
|
||||
.map(|line| line.trim())
|
||||
.filter(|line| !line.is_empty())
|
||||
.filter(|group| {
|
||||
let group_exists = std::process::Command::new("getent")
|
||||
.arg("group")
|
||||
.arg(group)
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false);
|
||||
if !group_exists {
|
||||
log::warn!("Group {} does not exist.", group);
|
||||
}
|
||||
group_exists
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if groups.is_empty() {
|
||||
log::warn!("No valid groups found to add user to.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let groups = groups.join(",");
|
||||
|
||||
log::info!("Adding user {} to groups: {}", user, groups);
|
||||
|
||||
let mut cmd = std::process::Command::new("usermod");
|
||||
cmd.arg("-aG");
|
||||
cmd.arg(groups);
|
||||
cmd.arg(&user);
|
||||
|
||||
log::debug!("Executing command: {:?}", cmd);
|
||||
|
||||
cmd.status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_package_command(package: cli::PackageCommand) {
|
||||
match package.subcommand {
|
||||
cli::PackageSubcommand::Install => {
|
||||
log::info!("Installing packages from file: {}", package.package_file);
|
||||
handle_install_packages(&package.package_file);
|
||||
fn add_repo(repo_command: AddRepoCommand) -> Result<()> {
|
||||
match repo_command.command {
|
||||
AddRepoSubCommand::Vscode => {
|
||||
add_repo_common(
|
||||
"vscode",
|
||||
"Visual Studio Code",
|
||||
"https://packages.microsoft.com/keys/microsoft.asc",
|
||||
"https://packages.microsoft.com/yumrepos/vscode",
|
||||
"/etc/yum.repos.d/vscode.repo",
|
||||
)?;
|
||||
}
|
||||
cli::PackageSubcommand::Remove => {
|
||||
log::info!("Removing packages from file: {}", package.package_file);
|
||||
handle_remove_packages(&package.package_file);
|
||||
AddRepoSubCommand::Mullvad => {
|
||||
add_repo_common(
|
||||
"mullvad-stable",
|
||||
"Mullvad VPN",
|
||||
"https://repository.mullvad.net/rpm/mullvad-keyring.asc",
|
||||
"https://repository.mullvad.net/rpm/stable/$basearch",
|
||||
"/etc/yum.repos.d/mullvad.repo",
|
||||
)?;
|
||||
}
|
||||
cli::PackageSubcommand::List => {
|
||||
log::info!("Listing installed packages from file: {}", package.package_file);
|
||||
handle_list_packages(&package.package_file);
|
||||
AddRepoSubCommand::Vivaldi => {
|
||||
add_repo_common(
|
||||
"vivaldi",
|
||||
"Vivaldi",
|
||||
"https://repo.vivaldi.com/archive/linux_signing_key.pub",
|
||||
"https://repo.vivaldi.com/archive/rpm/x86_64",
|
||||
"/etc/yum.repos.d/vivaldi.repo",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_generate_config(config_file: &str) {
|
||||
log::info!("Generating configuration file: {}", config_file);
|
||||
// Implement the logic to generate a configuration file
|
||||
fn add_repo_common(
|
||||
name: &str,
|
||||
display_name: &str,
|
||||
gpg_key_url: &str,
|
||||
baseurl: &str,
|
||||
repo_file_path: &str,
|
||||
) -> Result<()> {
|
||||
log::info!("Importing {} GPG key...", display_name);
|
||||
std::process::Command::new("rpm")
|
||||
.arg("--import")
|
||||
.arg(gpg_key_url)
|
||||
.status()?;
|
||||
|
||||
let mut conf = Ini::new();
|
||||
conf.with_section(Some(name))
|
||||
.set("name", display_name)
|
||||
.set("enabled", "1")
|
||||
.set("gpgcheck", "1")
|
||||
.set("autorefresh", "1")
|
||||
.set("baseurl", baseurl)
|
||||
.set("gpgkey", gpg_key_url);
|
||||
|
||||
log::info!("Adding {} repository...", display_name);
|
||||
conf.write_to_file(repo_file_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn handle_validate_config(config_file: &str) {
|
||||
log::info!("Validating configuration file: {}", config_file);
|
||||
// Implement the logic to validate a configuration file
|
||||
|
||||
fn install_rpm_fusion() -> Result<()> {
|
||||
let fedora_version_vec = std::process::Command::new("rpm")
|
||||
.arg("-E")
|
||||
.arg("%fedora")
|
||||
.output()?
|
||||
.stdout;
|
||||
let fedora_version = String::from_utf8_lossy(&fedora_version_vec);
|
||||
let fedora_version = fedora_version.trim();
|
||||
log::info!("Installing RPM Fusion for Fedora {}", fedora_version);
|
||||
|
||||
log::info!("Enabling the openh264 library...");
|
||||
std::process::Command::new("dnf")
|
||||
.arg("config-manager")
|
||||
.arg("setopt")
|
||||
.arg("fedora-cisco-openh264.enabled=1")
|
||||
.status()?;
|
||||
|
||||
log::info!("Installing RPM Fusion free and non-free repositories...");
|
||||
std::process::Command::new("dnf")
|
||||
.arg("install")
|
||||
.arg(format!(
|
||||
"https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-{}.noarch.rpm",
|
||||
fedora_version
|
||||
))
|
||||
.arg(format!(
|
||||
"https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-{}.noarch.rpm",
|
||||
fedora_version
|
||||
))
|
||||
.status()?;
|
||||
|
||||
log::info!("Installing RPM Fusion additional packages...");
|
||||
std::process::Command::new("dnf")
|
||||
.arg("install")
|
||||
.arg("rpmfusion-free-appstream-data")
|
||||
.arg("rpmfusion-nonfree-appstream-data")
|
||||
.arg("rpmfusion-free-release-tainted")
|
||||
.arg("rpmfusion-nonfree-release-tainted")
|
||||
.status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn handle_install_packages(package_file: &str) {
|
||||
log::info!("Installing packages from file: {}", package_file);
|
||||
// Implement the logic to install packages
|
||||
|
||||
fn manage_package(package_command: PackageCommand) -> Result<()> {
|
||||
let list = match &package_command.command {
|
||||
PackageSubCommand::CommonList => include_str!("../data/common_list").to_string(),
|
||||
PackageSubCommand::AmdList => include_str!("../data/amd_list").to_string(),
|
||||
PackageSubCommand::IntelList => include_str!("../data/intel_list").to_string(),
|
||||
PackageSubCommand::GnomeExtraList => include_str!("../data/gnome_extra_list").to_string(),
|
||||
PackageSubCommand::FirmwareList => include_str!("../data/firmware_list").to_string(),
|
||||
PackageSubCommand::CustomList(custom_list_command) => {
|
||||
log::info!("Using custom list from {}", custom_list_command.file);
|
||||
read_file!(&custom_list_command.file)?
|
||||
}
|
||||
};
|
||||
|
||||
manage_list(list, package_command.remove)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn handle_remove_packages(package_file: &str) {
|
||||
log::info!("Removing packages from file: {}", package_file);
|
||||
// Implement the logic to remove packages
|
||||
|
||||
fn manage_list(list: String, remove: bool) -> Result<()> {
|
||||
let packages = list
|
||||
.lines()
|
||||
.map(|line| line.trim())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut dnf_cmd = std::process::Command::new("dnf");
|
||||
if remove {
|
||||
log::info!("Removing common list...");
|
||||
dnf_cmd.arg("remove");
|
||||
} else {
|
||||
log::info!("Installing common list...");
|
||||
dnf_cmd.arg("install").arg("--allowerasing");
|
||||
}
|
||||
|
||||
packages.iter().for_each(|package| {
|
||||
dnf_cmd.arg(package);
|
||||
});
|
||||
|
||||
dnf_cmd.status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_dnf() -> Result<()> {
|
||||
log::info!("Tweaking dnf configuration...");
|
||||
let mut conf = Ini::load_from_file("/etc/dnf/dnf.conf")?;
|
||||
|
||||
let max_parallel_downloads =
|
||||
std::cmp::min(20, std::cmp::max(available_parallelism()?.get() / 2, 3));
|
||||
|
||||
log::info!(
|
||||
"Setting max_parallel_downloads to {}",
|
||||
max_parallel_downloads
|
||||
);
|
||||
log::info!("Setting defaultyes to True");
|
||||
|
||||
conf.with_section(Some("main"))
|
||||
.set("defaultyes", "True")
|
||||
.set("max_parallel_downloads", max_parallel_downloads.to_string());
|
||||
|
||||
// Write the changes back to the file
|
||||
log::info!("Writing changes to /etc/dnf/dnf.conf");
|
||||
conf.write_to_file("/etc/dnf/dnf.conf")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn handle_list_packages(package_file: &str) {
|
||||
log::info!("Listing installed packages from file: {}", package_file);
|
||||
// Implement the logic to list installed packages
|
||||
}
|
||||
Reference in New Issue
Block a user