♻️ refactor(cli): restructure cli commands for better organization

- rename `label_cli.rs` to `labels_cli.rs`
- rename `message_cli.rs` to `messages_cli.rs`
- move config related commands to `rules config` subcommand
- introduce `rules run` subcommand
This commit is contained in:
Jeremiah Russell
2025-10-15 10:55:32 +01:00
committed by Jeremiah Russell
parent 7c2bcd37b4
commit 3beab7d82d
14 changed files with 101 additions and 77 deletions

View File

@@ -0,0 +1,41 @@
use clap::{Parser, Subcommand};
mod action_cli;
mod label_cli;
mod rules_cli;
use action_cli::ActionCli;
use cull_gmail::{Config, Result};
use label_cli::LabelCli;
use rules_cli::RulesCli;
#[derive(Subcommand, Debug)]
enum SubCmds {
/// Configure end-of-life rules
#[clap(name = "rules")]
Rules(RulesCli),
/// Add or remove Label from rule
#[clap(name = "label")]
Label(LabelCli),
/// Set action on a specific rule
#[clap(name = "action")]
Action(ActionCli),
}
#[derive(Parser, Debug)]
pub struct ConfigCli {
#[clap(flatten)]
logging: clap_verbosity_flag::Verbosity,
#[command(subcommand)]
sub_command: SubCmds,
}
impl ConfigCli {
pub fn run(&self, config: Config) -> Result<()> {
match &self.sub_command {
SubCmds::Rules(rules_cli) => rules_cli.run(config),
SubCmds::Label(label_cli) => label_cli.run(config),
SubCmds::Action(action_cli) => action_cli.run(config),
}
}
}

View File

@@ -0,0 +1,35 @@
use clap::{Parser, ValueEnum};
use cull_gmail::{Config, EolAction, Error, Result};
#[derive(Debug, Clone, Parser, ValueEnum)]
pub enum Action {
/// Set the action to trash
#[clap(name = "trash")]
Trash,
/// Set the action to
#[clap(name = "add")]
Delete,
}
#[derive(Debug, Parser)]
pub struct ActionCli {
/// Id of the rule on which action applies
#[clap(short, long)]
id: usize,
/// Configuration commands
#[command(subcommand)]
action: Action,
}
impl ActionCli {
pub fn run(&self, mut config: Config) -> Result<()> {
if config.get_rule(self.id).is_none() {
return Err(Error::RuleNotFound(self.id));
}
match self.action {
Action::Trash => config.set_action_on_rule(self.id, &EolAction::Trash),
Action::Delete => config.set_action_on_rule(self.id, &EolAction::Trash),
}
}
}

View File

@@ -0,0 +1,40 @@
use clap::{Parser, Subcommand};
use cull_gmail::{Config, Error};
mod add_cli;
mod list_cli;
mod remove_cli;
use add_cli::AddCli;
use list_cli::ListCli;
use remove_cli::RemoveCli;
#[derive(Debug, Subcommand)]
pub enum LabelCommands {
/// List the labels associated with a rule
#[clap(name = "list")]
List(ListCli),
/// Add label to rule
#[clap(name = "add")]
Add(AddCli),
/// Remove a label from a
#[clap(name = "remove", alias = "rm")]
Remove(RemoveCli),
}
#[derive(Debug, Parser)]
pub struct LabelCli {
/// Configuration commands
#[command(subcommand)]
command: LabelCommands,
}
impl LabelCli {
pub fn run(&self, config: Config) -> Result<(), Error> {
match &self.command {
LabelCommands::List(list_cli) => list_cli.run(config),
LabelCommands::Add(add_cli) => add_cli.run(config),
LabelCommands::Remove(rm_cli) => rm_cli.run(config),
}
}
}

View File

@@ -0,0 +1,23 @@
use clap::Parser;
use cull_gmail::{Config, Error, Result};
#[derive(Debug, Parser)]
pub struct AddCli {
/// Id of the rule on which action applies
#[clap(short, long)]
id: usize,
/// Label to add to the rule
#[clap(short, long)]
label: String,
}
impl AddCli {
pub fn run(&self, mut config: Config) -> Result<()> {
if config.get_rule(self.id).is_none() {
return Err(Error::RuleNotFound(self.id));
}
config.add_label_to_rule(self.id, &self.label)
}
}

View File

@@ -0,0 +1,29 @@
use clap::Parser;
use cull_gmail::{Config, Error, Result};
#[derive(Debug, Parser)]
pub struct ListCli {
/// Id of the rule on which action applies
#[clap(short, long)]
id: usize,
}
impl ListCli {
pub fn run(&self, config: Config) -> Result<()> {
let Some(rule) = config.get_rule(self.id) else {
return Err(Error::RuleNotFound(self.id));
};
print!("Labels in rule: ");
for (i, label) in rule.labels().iter().enumerate() {
if i != 0 {
print!(", {label}");
} else {
print!("`{label}");
}
}
println!("`");
Ok(())
}
}

View File

@@ -0,0 +1,23 @@
use clap::Parser;
use cull_gmail::{Config, Error, Result};
#[derive(Debug, Parser)]
pub struct RemoveCli {
/// Id of the rule on which action applies
#[clap(short, long)]
id: usize,
/// Label to remove from the rule
#[clap(short, long)]
label: String,
}
impl RemoveCli {
pub fn run(&self, mut config: Config) -> Result<()> {
if config.get_rule(self.id).is_none() {
return Err(Error::RuleNotFound(self.id));
}
config.remove_label_from_rule(self.id, &self.label)
}
}

View File

@@ -0,0 +1,38 @@
use clap::{Parser, Subcommand};
use cull_gmail::{Config, Error};
mod add_cli;
mod rm_cli;
use add_cli::AddCli;
use rm_cli::RmCli;
#[derive(Debug, Subcommand)]
pub enum RulesCommands {
/// List the rules configured and saved in the config file
#[clap(name = "list")]
List,
/// Add a rules to the config file
#[clap(name = "add")]
Add(AddCli),
/// Remove a rule from the config file
#[clap(name = "remove", alias = "rm")]
Remove(RmCli),
}
#[derive(Debug, Parser)]
pub struct RulesCli {
/// Configuration commands
#[command(subcommand)]
command: RulesCommands,
}
impl RulesCli {
pub fn run(&self, config: Config) -> Result<(), Error> {
match &self.command {
RulesCommands::List => config.list_rules(),
RulesCommands::Add(add_cli) => add_cli.run(config),
RulesCommands::Remove(rm_cli) => rm_cli.run(config),
}
}
}

View File

@@ -0,0 +1,50 @@
use std::fmt;
use clap::{Parser, ValueEnum};
use cull_gmail::{Config, Error, MessageAge, Retention};
#[derive(Debug, Clone, Parser, ValueEnum)]
pub enum Period {
Days,
Weeks,
Months,
Years,
}
impl fmt::Display for Period {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Period::Days => write!(f, "days"),
Period::Weeks => write!(f, "weeks"),
Period::Months => write!(f, "months"),
Period::Years => write!(f, "years"),
}
}
}
#[derive(Debug, Parser)]
pub struct AddCli {
/// Period for the rule
#[arg(short, long)]
period: Period,
/// Count of the period
#[arg(short, long, default_value = "1")]
count: i64,
/// Optional specific label; if not specified one will be generated
#[arg(short, long)]
label: Option<String>,
/// Immediate delete instead of move to trash
#[arg(long)]
delete: bool,
}
impl AddCli {
pub fn run(&self, mut config: Config) -> Result<(), Error> {
let generate = self.label.is_none();
let message_age = MessageAge::new(self.period.to_string().as_str(), self.count);
let retention = Retention::new(message_age, generate);
config.add_rule(retention, self.label.as_ref(), self.delete);
config.save()
}
}

View File

@@ -0,0 +1,32 @@
use clap::Parser;
use cull_gmail::{Config, Error};
#[derive(Debug, Parser)]
pub struct RmCli {
/// Id of the rule to remove
#[clap(short, long)]
id: Option<usize>,
/// Label in the rule to remove (the rule will be removed)
#[clap(short, long)]
label: Option<String>,
}
impl RmCli {
pub fn run(&self, mut config: Config) -> Result<(), Error> {
if self.id.is_none() && self.label.is_none() {
return Err(Error::NoRuleSelector);
}
if let Some(id) = self.id {
config.remove_rule_by_id(id)?;
config.save()?;
}
if let Some(label) = &self.label {
config.remove_rule_by_label(label)?;
config.save()?;
}
Ok(())
}
}

View File

@@ -0,0 +1,60 @@
use clap::Parser;
use cull_gmail::{Config, EolAction, GmailClient, Result, RuleProcessor};
#[derive(Debug, Parser)]
pub struct RunCli {
/// Execute the action
#[clap(short, long, display_order = 1, help_heading = "Action")]
execute: bool,
/// Skip any rules that apply the action `trash`
#[clap(short = 't', long, display_order = 2, help_heading = "Skip Action")]
skip_trash: bool,
/// Skip any rules that apply the action `delete`
#[clap(short = 'd', long, display_order = 3, help_heading = "Skip Action")]
skip_delete: bool,
}
impl RunCli {
pub async fn run(&self, client: &mut GmailClient, config: Config) -> Result<()> {
let rules = config.get_rules_by_label();
for label in config.labels() {
let Some(rule) = rules.get(&label) else {
log::warn!("no rule found for label `{label}`");
continue;
};
log::info!("Executing rule `#{}` for label `{label}`", rule.describe());
client.set_rule(rule.clone());
client.set_execute(self.execute);
client.find_rule_and_messages_for_label(&label).await?;
let Some(action) = client.action() else {
log::warn!("no valid action specified for rule #{}", rule.id());
continue;
};
if self.execute {
match action {
EolAction::Trash => {
log::info!("***executing trash messages***");
if client.batch_trash().await.is_err() {
log::warn!("Move to trash failed for label `{label}`");
continue;
}
}
EolAction::Delete => {
log::info!("***executing final delete messages***");
if client.batch_delete().await.is_err() {
log::warn!("Delete failed for label `{label}`");
continue;
}
}
}
} else {
log::warn!("Execution stopped for dry run");
}
}
Ok(())
}
}