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 registry or repository
261    Install {
262        /// Package to install
263        package: String,
264    },
265    /// Publish an animation package to the registry
266    Publish,
267    /// Search for packages in the registry
268    Search {
269        /// Query to search for
270        query: String,
271    },
272    /// Control the running daemon via IPC commands
273    Ipc {
274        #[command(subcommand)]
275        subcommand: IpcCommands,
276    },
277    /// Gracefully stop the running daemon (alias for `ipc stop`)
278    Quit,
279}
280
281#[derive(Subcommand, Debug)]
282pub enum ConfigCommands {
283    /// Get a configuration value
284    Get { key: String },
285    /// Set a configuration value
286    Set { key: String, value: String },
287    /// Print the config path
288    Path,
289}
290
291#[derive(Subcommand, Debug)]
292pub enum CacheCommands {
293    /// Clear the cache
294    Clear,
295    /// Show cache info
296    Info,
297}
298
299#[derive(Subcommand, Debug)]
300pub enum MonitorCommands {
301    /// List monitors
302    List,
303    /// Show current monitor
304    Current,
305}
306
307#[derive(Subcommand, Debug)]
308pub enum IpcCommands {
309    /// Pause animations
310    Pause {
311        /// Target specific monitor (default: all)
312        #[arg(long)]
313        monitor: Option<String>,
314    },
315    /// Resume animations
316    Resume {
317        /// Target specific monitor (default: all)
318        #[arg(long)]
319        monitor: Option<String>,
320    },
321    /// Reload wallpaper
322    Reload,
323    /// Preview animation
324    Preview,
325    /// Stop daemon
326    Stop,
327    /// Get daemon status
328    Status,
329    /// Get video decoder and GPU information
330    Info {
331        /// Target specific monitor (default: all outputs)
332        #[arg(long)]
333        monitor: Option<String>,
334    },
335    /// Seek video to timestamp (format: HH:MM:SS or seconds)
336    Seek {
337        /// Timestamp in format HH:MM:SS or seconds
338        timestamp: String,
339        /// Target specific monitor (default: first output)
340        #[arg(long)]
341        monitor: Option<String>,
342    },
343    /// Blank an output without replacing persisted wallpaper
344    Blank {
345        /// Target specific monitor (default: all)
346        #[arg(long)]
347        monitor: Option<String>,
348        #[command(flatten)]
349        effect_args: EffectArgs,
350    },
351    /// Restore a previously blanked output
352    Restore {
353        /// Target specific monitor (default: all)
354        #[arg(long)]
355        monitor: Option<String>,
356        #[command(flatten)]
357        effect_args: EffectArgs,
358    },
359}