Skip to main content

wallr_core/cli/
mod.rs

1use crate::animation::{Effect, EffectOverrides, apply_effect_overrides, effect_from_name};
2use crate::config::{ScalingMode, ThemeProvider};
3use clap::{Args, Parser, Subcommand};
4use std::path::PathBuf;
5
6/// Transition + customization flags shared by `img` / `set` / `preview`.
7#[derive(Args, Debug, Clone, Default)]
8pub struct EffectArgs {
9    /// Transition effect: simple, fade, blur, wipe, slide, left, right, top,
10    /// bottom, zoom, pixelate, ripple, dissolve, wave, grow, center, outer,
11    /// any, random
12    #[arg(long, value_name = "NAME", value_parser = parse_effect_name)]
13    pub effect: Option<String>,
14
15    /// Transition duration, for example 800ms, 1s, or 1.2s
16    #[arg(long, value_name = "DURATION")]
17    pub duration: Option<String>,
18
19    /// Effect origin: a preset (top_left, top, top_right, left, center, right,
20    /// bottom_left, bottom, bottom_right) or a normalized "x,y" (0..1)
21    #[arg(long, value_name = "PRESET|X,Y")]
22    pub origin: Option<String>,
23
24    /// Wipe/wave travel angle in degrees (0 = right, 90 = up)
25    #[arg(long, value_name = "DEG")]
26    pub angle: Option<f32>,
27
28    /// Wipe/slide direction vector "x,y" (e.g. "1,0" for left-to-right)
29    #[arg(long, value_name = "X,Y")]
30    pub direction: Option<String>,
31
32    /// Easing curve: linear, ease_in, ease_out, ease_in_out
33    #[arg(long, value_enum)]
34    pub easing: Option<crate::animation::Easing>,
35
36    /// Start value (fade opacity, blur radius, zoom scale, pixelate size)
37    #[arg(long, value_name = "VALUE")]
38    pub from: Option<f32>,
39
40    /// End value (fade opacity, blur radius, zoom scale, pixelate size)
41    #[arg(long, value_name = "VALUE")]
42    pub to: Option<f32>,
43
44    /// Wave/ripple frequency
45    #[arg(long, value_name = "HZ")]
46    pub frequency: Option<f32>,
47
48    /// Wave/ripple amplitude
49    #[arg(long, value_name = "VALUE")]
50    pub amplitude: Option<f32>,
51
52    /// Ripple expansion speed
53    #[arg(long, value_name = "VALUE")]
54    pub speed: Option<f32>,
55
56    /// Wipe feather / dissolve edge softness
57    #[arg(long, value_name = "VALUE")]
58    pub softness: Option<f32>,
59
60    /// Dissolve noise scale
61    #[arg(long, value_name = "VALUE")]
62    pub scale: Option<f32>,
63}
64
65impl EffectArgs {
66    /// Build an `Effect` from `--effect <name>` + all override flags.
67    /// Falls back to `fallback` when no effect name is given.
68    pub fn to_effect(&self, fallback: Effect) -> Effect {
69        let mut effect = self
70            .effect
71            .as_deref()
72            .and_then(effect_from_name)
73            .unwrap_or(fallback);
74        apply_effect_overrides(&mut effect, &self.to_overrides());
75        effect
76    }
77
78    pub fn to_overrides(&self) -> EffectOverrides {
79        let origin = self.origin.as_deref().and_then(parse_origin);
80        EffectOverrides {
81            origin,
82            origin_preset: if origin.is_none() {
83                self.origin.clone()
84            } else {
85                None
86            },
87            direction: self.direction.as_deref().and_then(parse_vec2),
88            angle: self.angle,
89            easing: self.easing,
90            from: self.from,
91            to: self.to,
92            frequency: self.frequency,
93            amplitude: self.amplitude,
94            speed: self.speed,
95            softness: self.softness,
96            scale: self.scale,
97        }
98    }
99}
100
101/// Validate `--effect` against the known effect names.
102fn parse_effect_name(s: &str) -> Result<String, String> {
103    if crate::animation::effect_names().contains(&s) {
104        Ok(s.to_string())
105    } else {
106        Err(format!(
107            "unknown effect '{}' — expected one of: {}",
108            s,
109            crate::animation::effect_names().join(", ")
110        ))
111    }
112}
113
114/// Parse "x,y" into a normalized origin, or None if it's not numeric.
115fn parse_origin(s: &str) -> Option<(f32, f32)> {
116    let mut parts = s.split(',');
117    let x = parts.next()?.trim().parse::<f32>().ok()?;
118    let y = parts.next()?.trim().parse::<f32>().ok()?;
119    Some((x.clamp(0.0, 1.0), y.clamp(0.0, 1.0)))
120}
121
122/// Parse "x,y" into a direction vector (not normalized here — shader normalizes).
123fn parse_vec2(s: &str) -> Option<[f32; 2]> {
124    let mut parts = s.split(',');
125    let x = parts.next()?.trim().parse::<f32>().ok()?;
126    let y = parts.next()?.trim().parse::<f32>().ok()?;
127    Some([x, y])
128}
129
130#[derive(Parser, Debug)]
131#[command(
132    name = "wallr",
133    about = "Wayland wallpaper engine with animation and theme pipelines",
134    version
135)]
136pub struct WallrCli {
137    #[command(subcommand)]
138    pub command: Commands,
139
140    /// Custom config file path
141    #[arg(global = true, long)]
142    pub config: Option<PathBuf>,
143
144    /// Increase log verbosity
145    #[arg(global = true, short = 'v', long, action = clap::ArgAction::Count)]
146    pub verbose: u8,
147
148    /// Suppress non-error output
149    #[arg(global = true, short = 'q', long)]
150    pub quiet: bool,
151}
152
153#[derive(Subcommand, Debug)]
154pub enum Commands {
155    /// Set wallpaper with animation + theme pipeline
156    Img {
157        /// Path to the wallpaper image
158        path: PathBuf,
159        /// Disable theme generation
160        #[arg(long)]
161        no_theme: bool,
162        /// Theme provider override for this call (matugen, wallust, pywal)
163        #[arg(long, value_enum)]
164        theme: Option<ThemeProvider>,
165        /// Target monitor
166        #[arg(long)]
167        monitor: Option<String>,
168        /// Animation package to use
169        #[arg(long)]
170        animation: Option<String>,
171        /// Scaling mode
172        #[arg(long, value_enum)]
173        mode: Option<ScalingMode>,
174        /// Transition effect + customization flags
175        #[command(flatten)]
176        effect_args: EffectArgs,
177    },
178    /// Alias/superset of img
179    Set {
180        /// Path to the wallpaper image
181        path: PathBuf,
182        /// Disable theme generation
183        #[arg(long)]
184        no_theme: bool,
185        /// Theme provider override for this call (matugen, wallust, pywal)
186        #[arg(long, value_enum)]
187        theme: Option<ThemeProvider>,
188        /// Target monitor
189        #[arg(long)]
190        monitor: Option<String>,
191        /// Animation package to use
192        #[arg(long)]
193        animation: Option<String>,
194        /// Scaling mode
195        #[arg(long, value_enum)]
196        mode: Option<ScalingMode>,
197        /// Transition effect + customization flags
198        #[command(flatten)]
199        effect_args: EffectArgs,
200    },
201    /// Environment diagnostics
202    Doctor,
203    /// Lint animation YAML
204    Validate {
205        /// Path to animation YAML file
206        path: PathBuf,
207    },
208    /// Configuration management
209    Config {
210        #[command(subcommand)]
211        subcommand: ConfigCommands,
212    },
213    /// Cache management
214    Cache {
215        #[command(subcommand)]
216        subcommand: CacheCommands,
217    },
218    /// Re-run reload list without changing wallpaper
219    Reload,
220    /// Monitor management
221    Monitor {
222        #[command(subcommand)]
223        subcommand: MonitorCommands,
224    },
225
226    // Core Daemon & Package commands
227    /// Start the background wallpaper daemon
228    Daemon {
229        /// Override the target frame rate limit (FPS)
230        #[arg(long)]
231        max_fps: Option<u32>,
232    },
233    /// Create a working animation package starter.
234    New {
235        /// Package name or output directory.
236        name: String,
237        /// Include a raw WGSL shader starter.
238        #[arg(long)]
239        shader: bool,
240    },
241    /// Watch a directory for new images and automatically set them
242    Watch {
243        /// Directory to watch
244        dir: PathBuf,
245    },
246    /// Preview an animation or wallpaper image
247    Preview {
248        /// Path to preview
249        path: PathBuf,
250        /// Watch for changes
251        #[arg(long)]
252        watch: bool,
253        /// Animation package or YAML file to preview
254        #[arg(long, value_name = "PACKAGE|FILE")]
255        animation: Option<String>,
256        /// Transition effect + customization flags
257        #[command(flatten)]
258        effect_args: EffectArgs,
259    },
260    /// Install an animation package from a GitHub repository (`username/repo`)
261    Install {
262        /// Remote package reference, e.g. `username/repo`
263        package: String,
264    },
265    /// Search for installed packages in the local registry
266    Search {
267        /// Query to search for
268        query: String,
269    },
270    /// Control the running daemon via IPC commands
271    Ipc {
272        #[command(subcommand)]
273        subcommand: IpcCommands,
274    },
275    /// Gracefully stop the running daemon (alias for `ipc stop`)
276    Quit,
277}
278
279#[derive(Subcommand, Debug)]
280pub enum ConfigCommands {
281    /// Get a configuration value
282    Get { key: String },
283    /// Set a configuration value
284    Set { key: String, value: String },
285    /// Print the config path
286    Path,
287}
288
289#[derive(Subcommand, Debug)]
290pub enum CacheCommands {
291    /// Clear the cache
292    Clear,
293    /// Show cache info
294    Info,
295}
296
297#[derive(Subcommand, Debug)]
298pub enum MonitorCommands {
299    /// List monitors
300    List,
301    /// Show current monitor
302    Current,
303}
304
305#[derive(Subcommand, Debug)]
306pub enum IpcCommands {
307    /// Pause animations
308    Pause {
309        /// Target specific monitor (default: all)
310        #[arg(long)]
311        monitor: Option<String>,
312    },
313    /// Resume animations
314    Resume {
315        /// Target specific monitor (default: all)
316        #[arg(long)]
317        monitor: Option<String>,
318    },
319    /// Reload wallpaper
320    Reload,
321    /// Preview animation
322    Preview,
323    /// Stop daemon
324    Stop,
325    /// Get daemon status
326    Status,
327    /// Get video decoder and GPU information
328    Info {
329        /// Target specific monitor (default: all outputs)
330        #[arg(long)]
331        monitor: Option<String>,
332    },
333    /// Seek video to timestamp (format: HH:MM:SS or seconds)
334    Seek {
335        /// Timestamp in format HH:MM:SS or seconds
336        timestamp: String,
337        /// Target specific monitor (default: first output)
338        #[arg(long)]
339        monitor: Option<String>,
340    },
341    /// Blank an output without replacing persisted wallpaper
342    Blank {
343        /// Target specific monitor (default: all)
344        #[arg(long)]
345        monitor: Option<String>,
346        #[command(flatten)]
347        effect_args: EffectArgs,
348    },
349    /// Restore a previously blanked output
350    Restore {
351        /// Target specific monitor (default: all)
352        #[arg(long)]
353        monitor: Option<String>,
354        #[command(flatten)]
355        effect_args: EffectArgs,
356    },
357}