Skip to main content

wallr_common/
cli.rs

1use crate::effect::{Effect, EffectOverrides, apply_effect_overrides, effect_from_name};
2use crate::types::{ScalingMode, ThemeProvider};
3use clap::{Args, Parser, Subcommand};
4use std::path::PathBuf;
5
6/// Transition options for wallpaper changes.
7#[derive(Args, Debug, Clone, Default)]
8pub struct EffectArgs {
9    /// Transition effect: fade, wipe, slide, wave, grow, outer (plus aliases:
10    /// simple, left, right, top, bottom, center, any, random)
11    #[arg(short = 'e', long, value_name = "NAME", value_parser = parse_effect_name)]
12    pub effect: Option<String>,
13
14    /// Transition duration (e.g. 700ms, 1s, 1.2s)
15    #[arg(short = 'd', long, value_name = "TIME")]
16    pub duration: Option<String>,
17
18    /// Effect origin: preset (top_left, top, top_right, left, center, right, bottom_left, bottom, bottom_right) or "x,y" (0..1)
19    #[arg(short = 'o', long, value_name = "PRESET|X,Y")]
20    pub origin: Option<String>,
21
22    /// Angle for wipe/slide in degrees (0 = right-to-left, 90 = top-to-bottom, 270 = bottom-to-top)
23    #[arg(short = 'a', long, value_name = "DEG")]
24    pub angle: Option<f32>,
25
26    /// Direction vector "x,y" (e.g. "1,0" or "-1,0")
27    #[arg(long, value_name = "X,Y")]
28    pub direction: Option<String>,
29
30    /// Easing curve: linear, ease_in, ease_out, ease_in_out, bezier
31    #[arg(long, value_enum)]
32    pub easing: Option<crate::effect::Easing>,
33
34    /// Initial parameter value (fade opacity)
35    #[arg(long, value_name = "VAL")]
36    pub from: Option<f32>,
37
38    /// Target parameter value (fade opacity)
39    #[arg(long, value_name = "VAL")]
40    pub to: Option<f32>,
41
42    /// Wave frequency in Hz
43    #[arg(long, value_name = "HZ")]
44    pub frequency: Option<f32>,
45
46    /// Wave amplitude
47    #[arg(long, value_name = "VAL")]
48    pub amplitude: Option<f32>,
49
50    /// Wipe feather softness (0.01 - 0.5)
51    #[arg(long, value_name = "VAL")]
52    pub softness: Option<f32>,
53}
54
55impl EffectArgs {
56    /// Build an `Effect` from `--effect <name>` + all override flags.
57    /// Falls back to `fallback` when no effect name is given.
58    pub fn to_effect(&self, fallback: Effect) -> Effect {
59        let mut effect = self
60            .effect
61            .as_deref()
62            .and_then(effect_from_name)
63            .unwrap_or(fallback);
64        apply_effect_overrides(&mut effect, &self.to_overrides());
65        effect
66    }
67
68    pub fn to_overrides(&self) -> EffectOverrides {
69        let origin = self.origin.as_deref().and_then(parse_origin);
70        EffectOverrides {
71            origin,
72            origin_preset: if origin.is_none() {
73                self.origin.clone()
74            } else {
75                None
76            },
77            direction: self.direction.as_deref().and_then(parse_vec2),
78            angle: self.angle,
79            easing: self.easing,
80            from: self.from,
81            to: self.to,
82            frequency: self.frequency,
83            amplitude: self.amplitude,
84            softness: self.softness,
85        }
86    }
87}
88
89/// Validate `--effect`. Aliases (left, right, top, bottom, center, any,
90/// random, simple) resolve through the real parser; the error message shows
91/// only the six canonical transitions.
92fn parse_effect_name(s: &str) -> Result<String, String> {
93    if crate::effect::effect_from_name(s).is_some() {
94        Ok(s.to_string())
95    } else {
96        Err(format!(
97            "unknown effect '{}' - expected one of: {}",
98            s,
99            crate::effect::effect_names().join(", ")
100        ))
101    }
102}
103
104/// Parse "x,y" into a normalized origin, or None if it's not numeric.
105fn parse_origin(s: &str) -> Option<(f32, f32)> {
106    let mut parts = s.split(',');
107    let x = parts.next()?.trim().parse::<f32>().ok()?;
108    let y = parts.next()?.trim().parse::<f32>().ok()?;
109    Some((x.clamp(0.0, 1.0), y.clamp(0.0, 1.0)))
110}
111
112/// Parse "x,y" into a direction vector.
113fn parse_vec2(s: &str) -> Option<[f32; 2]> {
114    let mut parts = s.split(',');
115    let x = parts.next()?.trim().parse::<f32>().ok()?;
116    let y = parts.next()?.trim().parse::<f32>().ok()?;
117    Some([x, y])
118}
119
120#[derive(Parser, Debug)]
121#[command(
122    name = "wallr",
123    about = "Wayland wallpaper engine with GPU animations and theme pipelines",
124    version
125)]
126pub struct WallrCli {
127    #[command(subcommand)]
128    pub command: Commands,
129
130    /// Custom config file path
131    #[arg(global = true, short = 'c', long)]
132    pub config: Option<PathBuf>,
133
134    /// Verbose logging (-v, -vv)
135    #[arg(global = true, short = 'v', long, action = clap::ArgAction::Count)]
136    pub verbose: u8,
137
138    /// Quiet mode (suppress non-error output)
139    #[arg(global = true, short = 'q', long)]
140    pub quiet: bool,
141}
142
143#[derive(Subcommand, Debug)]
144pub enum Commands {
145    /// Set wallpaper image or video
146    #[command(alias = "img")]
147    Set {
148        /// Wallpaper file path (image, GIF, or video); `-` reads from stdin
149        path: PathBuf,
150
151        /// Target output/monitor
152        #[arg(short = 'm', long, value_name = "OUTPUT")]
153        monitor: Option<String>,
154
155        /// Scaling mode: fill, fit, stretch, center, tile
156        #[arg(long, value_enum)]
157        mode: Option<ScalingMode>,
158
159        /// Disable theme extraction
160        #[arg(long)]
161        no_theme: bool,
162
163        /// Override theme engine (matugen, wallust, pywal)
164        #[arg(short = 't', long, value_enum)]
165        theme: Option<ThemeProvider>,
166
167        /// Transition overrides
168        #[command(flatten)]
169        effect_args: EffectArgs,
170    },
171
172    /// Run system and dependency diagnostics
173    Doctor,
174
175    /// View and edit configuration
176    Config {
177        #[command(subcommand)]
178        subcommand: ConfigCommands,
179    },
180
181    /// Reload config and restart theme hooks
182    Reload,
183
184    /// Query connected outputs
185    Monitor {
186        #[command(subcommand)]
187        subcommand: MonitorCommands,
188    },
189
190    /// Run background daemon
191    Daemon {
192        /// Max render FPS limit
193        #[arg(long)]
194        max_fps: Option<u32>,
195    },
196
197    /// Daemon IPC controls (playback, blanking, info)
198    Ipc {
199        #[command(subcommand)]
200        subcommand: IpcCommands,
201    },
202
203    /// Stop running daemon
204    Quit,
205}
206
207#[derive(Subcommand, Debug)]
208pub enum ConfigCommands {
209    /// Get config value by key
210    Get { key: String },
211    /// Set config value by key
212    Set { key: String, value: String },
213    /// Print path to active config file
214    Path,
215}
216
217#[derive(Subcommand, Debug)]
218pub enum MonitorCommands {
219    /// List all connected display outputs
220    List,
221    /// Display current focused output
222    Current,
223}
224
225#[derive(Subcommand, Debug)]
226pub enum IpcCommands {
227    /// Pause video/GIF playback
228    Pause {
229        /// Target specific monitor (default: all)
230        #[arg(short = 'm', long)]
231        monitor: Option<String>,
232    },
233    /// Resume video/GIF playback
234    Resume {
235        /// Target specific monitor (default: all)
236        #[arg(short = 'm', long)]
237        monitor: Option<String>,
238    },
239    /// Re-render current wallpaper
240    Reload,
241    /// Trigger preview transition
242    Preview,
243    /// Shut down the daemon
244    Stop,
245    /// Query daemon status
246    Status,
247    /// Query GPU, video decoder, and display info
248    Info {
249        /// Target specific monitor
250        #[arg(short = 'm', long)]
251        monitor: Option<String>,
252    },
253    /// Seek video to position (HH:MM:SS or seconds)
254    Seek {
255        /// Position in HH:MM:SS or seconds
256        timestamp: String,
257        /// Target specific monitor
258        #[arg(short = 'm', long)]
259        monitor: Option<String>,
260    },
261    /// Blank output to black with transition
262    Blank {
263        /// Target specific monitor (default: all)
264        #[arg(short = 'm', long)]
265        monitor: Option<String>,
266        #[command(flatten)]
267        effect_args: EffectArgs,
268    },
269    /// Restore wallpaper on blanked output
270    Restore {
271        /// Target specific monitor (default: all)
272        #[arg(short = 'm', long)]
273        monitor: Option<String>,
274        #[command(flatten)]
275        effect_args: EffectArgs,
276    },
277}