Skip to content

architecture and data flow

Cody-will edited this page Sep 20, 2026 · 1 revision

Architecture and Data Flow

Related Files:

  • src/main.rs
  • src/cli.rs
  • src/config.rs
  • src/state.rs
  • src/paths.rs
  • src/cmd.rs
  • src/error.rs

Related Pages:

  • Overview and Key Features
  • Installation, Configuration, and Usage
  • Wallpaper, Theme, and Bar Workflow
  • Session Management and Hyprland Integration
Relevant source files

The following files were used as context for generating this wiki page:

Architecture and Data Flow

rice is a small Rust CLI that orchestrates wallpaper changes, theme application, bar restart, and session startup for a Hyprland desktop. The architecture centers around a single binary (rice) that exposes subcommands (wall, bar, session), reads configuration and state from well‑defined filesystem locations, executes external commands, and emits desktop notifications about success or failure.
Sources: README.md:1-124, src/main.rs:1-24, src/cli.rs:1-33

At a high level, the data flow starts from CLI arguments, passes through command dispatch in main, and then fans out into modules that manage configuration (Config), persisted state (State), filesystem paths (paths), command execution (cmd), wallpaper handling (wall and wall::backend), session startup (session), theming (theme), bar restart (bar), and notifications (notifications).
Sources: src/main.rs:1-24, src/config.rs:1-42, src/state.rs:1-49, src/paths.rs:1-67, src/theme.rs:1-42, src/bar.rs:1-30, src/session.rs:1-28, src/wall/mod.rs:1-111, src/wall/backend.rs:1-25, src/notifications.rs:1-53


Top‑Level Architecture

Sources: src/main.rs:1-60, src/cli.rs:1-33, src/config.rs:1-42, src/state.rs:1-49

The top‑level architecture is command‑driven: main parses CLI arguments via clap, maps them to an enum (Command), and then delegates to feature‑specific modules (wall, bar, session). Each operation returns an anyhow::Result<()>, and main centralizes success and error handling, including notifications.

Module Responsibilities

Module Responsibility
main CLI dispatch, global success/error handling, notification trigger
cli CLI shape definition (Command, WallAction, BarAction, SessionAction)
config Load/create ~/.config/rice/config.toml, deserialize into Config
state Load/save ~/.cache/rice/state.json representing last wallpaper/backend
paths Compute XDG dirs, config/state/icon paths, tilde expansion, ensure dirs
cmd Run external commands, validate exit status
wall List images, compute next/prev, apply theme, update state
wall::backend Bridge to wallpaper backends (hyprpaper, swww, none)
theme Apply theme via external command (default wallust), trigger bar restart
bar Restart bar process via pkill + spawn
session Spawn session applications, then restore wallpaper
notifications Show success/error desktop notifications with icon management
error Placeholder for potential future error types

Sources: src/main.rs:1-60, src/cli.rs:1-33, src/config.rs:1-42, src/state.rs:1-49, src/paths.rs:1-67, src/cmd.rs:1-16, src/wall/mod.rs:1-111, src/wall/backend.rs:1-25, src/theme.rs:1-42, src/bar.rs:1-30, src/session.rs:1-28, src/notifications.rs:1-53, src/error.rs:1-2

Global Flow Overview

graph TD
  Main["main()"] --> CliParse["Cli::parse()"]
  CliParse["Cli::parse()"] --> CommandMatch["match Command"]
  CommandMatch["match Command"] --> WallModule["wall::*"]
  CommandMatch["match Command"] --> BarModule["bar::restart"]
  CommandMatch["match Command"] --> SessionModule["session::start"]

  WallModule["wall::*"] --> ConfigLoad["Config::load()"]
  WallModule["wall::*"] --> StateLoad["State::load()"]
  WallModule["wall::*"] --> Paths["paths::expand_tilde()"]
  WallModule["wall::*"] --> BackendSet["backend::set()"]
  WallModule["wall::*"] --> ThemeApply["theme::apply()"]
  WallModule["wall::*"] --> StateSave["State::save()"]

  SessionModule["session::start"] --> ConfigLoad
  SessionModule["session::start"] --> WallStart["wall::start()"]

  BarModule["bar::restart"] --> CmdRun["cmd::run()"]
  BarModule["bar::restart"] --> SpawnBar["spawn bar"]

  ThemeApply["theme::apply()"] --> CmdRun
  ThemeApply["theme::apply()"] --> BarRestart["bar::restart()"]

  Main["main()"] --> Notifications["notifications::success/error"]
Loading

Sources: src/main.rs:1-60, src/cli.rs:1-33, src/wall/mod.rs:1-111, src/wall/backend.rs:1-25, src/theme.rs:1-42, src/bar.rs:1-30, src/session.rs:14-28, src/config.rs:23-42, src/state.rs:22-49, src/paths.rs:46-67, src/cmd.rs:1-16, src/notifications.rs:35-53

This diagram shows how main dispatches to modules, which in turn compose configuration, state, filesystem paths, and external command execution.


Command Dispatch and CLI Layer

Sources: src/main.rs:1-60, src/cli.rs:1-33

CLI Definition

The CLI surface is defined by the Cli struct and nested Command enum, using clap:

#[derive(Parser)]
#[command(name = "rice", about = "Hyprland helpers")]
pub struct Cli {
    #[command(subcommand)]
    pub command: Command,
}

#[derive(Subcommand)]
pub enum Command {
    Wall {
        #[command(subcommand)]
        action: WallAction,
    },
    Bar {
        #[command(subcommand)]
        action: BarAction,
    },
    Session {
        #[command(subcommand)]
        action: SessionAction,
    },
}

Sources: src/cli.rs:1-21

Supported actions:

Command group Action Effect (module call)
wall Next wall::next()
wall Prev wall::prev()
wall Start wall::start()
bar Restart bar::restart(&Config)
session Start session::start()

Sources: src/cli.rs:23-33, src/main.rs:10-23

Main Dispatch and Result Handling

main matches on Cli::parse().command and executes the corresponding function. It then performs uniform success/error handling including notifications:

fn main() -> Result<()> {
    let result = match Cli::parse().command {
        Command::Wall { action } => match action {
            WallAction::Next => wall::next(),
            WallAction::Prev => wall::prev(),
            WallAction::Start => wall::start(),
        },
        Command::Bar { action } => match action {
            BarAction::Restart => bar::restart(&Config::load()?),
        },
        Command::Session { action } => match action {
            SessionAction::Start => session::start(),
        }
    };
    
    match result {
        Ok(()) => {
            if let Ok(cfg) = config::Config::load() {
                notifications::success(&cfg, "Wallpaper updated");
            }
            Ok(())
        },
        Err(e) => {
            if let Ok(cfg) = config::Config::load() {
                notifications::error(&cfg, &format!("Update failed: {e:#}"));
            }
            eprintln!("{e:#}");
            std::process::exit(1);
        }
    }
}

Sources: src/main.rs:10-23, src/main.rs:25-58

On success or error, main attempts to reload Config (independent of the earlier call, if any) and shows a notification via notifications::success or notifications::error. It prints detailed error information using anyhow’s formatting and exits with status 1 on failure.
Sources: src/main.rs:25-58, src/notifications.rs:35-53


Configuration and State Management

Configuration (Config)

Sources: src/config.rs:1-42, src/paths.rs:23-41, examples/config.toml via include_str in src/config.rs:17-18

The Config struct models all user‑configurable aspects:

#[derive(Deserialize, Debug)]
pub struct Config {
    pub wallpaper_dir: PathBuf,
    pub backend: String,
    pub sort: String,
    pub extensions: Vec<String>,
    pub theme: Theme,
    pub bar: Bar,
    pub session: Session,
    pub notifications: Notifications,
}

Sources: src/config.rs:19-28

Key aspects of Config::load:

  1. Compute config file path: ~/.config/rice/config.toml via paths::config_file.
  2. If missing, create parent dir and write the built‑in default from examples/config.toml.
  3. Read and parse TOML into Config.
  4. Expand wallpaper_dir tilde via paths::expand_tilde.
pub fn load() -> Result<Config> {
    let path = paths::config_file()?;

    if !path.exists() {
        let parent = path.parent().context("Config path has no parent")?;
        paths::ensure_dir(parent)?;
        std::fs::write(&path, DEFAULT)
            .with_context(|| format!("failed to write {}", path.display()))?;
    }

    let raw = std::fs::read_to_string(&path)
        .with_context(|| format!("failed to read {}", path.display()))?;

    let mut cfg: Config = toml::from_str::<Config>(&raw)
        .with_context(|| format!("failed to parse {}", path.display()))?;

    cfg.wallpaper_dir = paths::expand_tilde(&cfg.wallpaper_dir);

    Ok(cfg)
}

Sources: src/config.rs:30-42, src/paths.rs:23-41

Config also provides an image extension helper:

pub fn is_valid_img(&self, img_ext: &str) -> bool {
    self.extensions.contains(&img_ext.to_string())
}

Sources: src/config.rs:44-46

Config‑Related Data Flow

graph TD
  Start["First command"] --> GetCfgPath["paths::config_file()"]
  GetCfgPath["paths::config_file()"] --> ExistsCheck["config.toml exists?"]
  ExistsCheck["config.toml exists?"] -->|No| EnsureDir["paths::ensure_dir(parent)"]
  EnsureDir["paths::ensure_dir(parent)"] --> WriteDefault["write DEFAULT config"]
  ExistsCheck["config.toml exists?"] -->|Yes| ReadCfg["read config.toml"]
  WriteDefault["write DEFAULT config"] --> ReadCfg["read config.toml"]
  ReadCfg["read config.toml"] --> ParseTOML["toml::from_str"]
  ParseTOML["toml::from_str"] --> ExpandTilde["paths::expand_tilde(wallpaper_dir)"]
  ExpandTilde["paths::expand_tilde(wallpaper_dir)"] --> ConfigReady["Config in memory"]
Loading

Sources: src/config.rs:30-42, src/paths.rs:23-41

Persistent State (State)

Sources: src/state.rs:1-49, src/paths.rs:33-41

State tracks the last wallpaper and backend to enable cyclic navigation and restore behavior:

#[derive(Serialize, Deserialize, Debug)]
#[serde(default)]
pub struct State {
    pub wallpaper: Option<PathBuf>,
    pub backend: Option<String>,
}

Sources: src/state.rs:7-13

Default values are None for both fields. State::load either returns this default if the file does not exist or reads and deserializes JSON from ~/.cache/rice/state.json:

pub fn load() -> Result<State> {
    let path = paths::state_file()?;

    if !path.exists() {
        return Ok(State::default());
    }
    
    let raw = std::fs::read_to_string(&path)
        .with_context(|| format!("failed to read {}", path.display()))?;

    let state = serde_json::from_str(&raw)
        .with_context(|| format!("failed for parse {}", path.display()))?;

    Ok(state)
}

Sources: src/state.rs:22-38, src/paths.rs:37-41

State::save ensures the parent directory exists, serializes to pretty JSON, and writes to disk:

pub fn save(&self) -> Result<()> {
    let path = paths::state_file()?;
    let parent = path.parent().context("Path does not have a parent")?;
    
    paths::ensure_dir(parent)?;

    let json = serde_json::to_string_pretty(self).context("failed to serialize state")?;
    
    std::fs::write(&path, json)
        .with_context(|| format!("failed to write {}", path.display()))?;

    Ok(())
}

Sources: src/state.rs:40-49

State Lifecycle During Wallpaper Operations

graph TD
  WallCmd["wall::next/prev/start"] --> LoadState["State::load()"]
  LoadState["State::load()"] --> ComputeNew["compute target wallpaper/backend"]
  ComputeNew["compute target wallpaper/backend"] --> ApplyBackend["backend::set()"]
  ApplyBackend["backend::set()"] --> UpdateState["update State struct"]
  UpdateState["update State struct"] --> SaveState["State::save()"]
Loading

Sources: src/wall/mod.rs:30-61, src/wall/mod.rs:78-107, src/state.rs:22-49


Filesystem Paths and Tilde Expansion

Sources: src/paths.rs:1-67

paths centralizes all filesystem locations and directory creation logic:

Function Purpose
project_dirs Obtain XDG ProjectDirs for qualifier rice
config_dir Return ~/.config/rice
cache_dir Return ~/.cache/rice
config_file Return ~/.config/rice/config.toml
icon_file Return ~/.config/rice/icon.png
state_file Return ~/.cache/rice/state.json
ensure_dir mkdir -p-style directory creation
expand_tilde Replace leading ~ or ~/ with $HOME

Sources: src/paths.rs:15-67

expand_tilde logic:

pub fn expand_tilde(path: &Path) -> PathBuf {
    let raw = path.to_string_lossy();

    if raw == "~" {
        return home_dir().unwrap_or_else(|| PathBuf::from(raw.as_ref()));
    }

    if let Some(rest) = raw.strip_prefix("~/") {
        let mut home = home_dir().unwrap_or_else(|| PathBuf::from("~"));
        home.push(rest);
        return home;
    }

    path.to_path_buf()
}

Sources: src/paths.rs:49-67

This is used both for Config.wallpaper_dir and for theme application (to ensure the wallpaper path is absolute/expanded before passing to external tools).
Sources: src/config.rs:38-42, src/theme.rs:27-32


External Command Execution Layer

Generic Command Runner (cmd)

Sources: src/cmd.rs:1-16

cmd::run is the central utility for running external binaries with arguments and validating their exit status:

pub fn run(bin: &str, args: &[&str]) -> Result<()> {
    let status = Command::new(bin)
        .args(args)
        .status()
        .with_context(|| format!("failed to start {bin}"))?;
    
    if !status.success() {
        bail!("{bin} exited with {status}");
    }

    Ok(())
}

Sources: src/cmd.rs:1-16

This is used by:

  • wall::backend::set for hyprctl and swww calls.
  • theme::apply for the theme command (default wallust).
  • bar::restart for pkill against the bar command.

Sources: src/wall/backend.rs:1-20, src/theme.rs:33-41, src/bar.rs:21-26

Desktop Notifications

Sources: src/notifications.rs:1-53

notifications uses notify_rust to show success/error notifications, including managing an embedded PNG icon written to disk on first use.

Key types and functions:

#[derive(Serialize, Deserialize, Debug)]
#[serde(default)]
pub struct Notifications {
    pub on_success: bool,
    pub on_error: bool,
}

Sources: src/notifications.rs:9-16

ensure_icon writes the embedded icon to ~/.config/rice/icon.png if necessary:

fn ensure_icon() -> Result<PathBuf> {
    let path = crate::paths::icon_file()?;
    if !path.exists() {
        if let Some(dir) = path.parent() {
            crate::paths::ensure_dir(dir)?;
        }
        std::fs::write(&path, ICON)
            .with_context(|| format!("failed to write {}", path.display()))?;
    }
    Ok(path)
}

Sources: src/notifications.rs:18-27, src/paths.rs:29-35

show builds and displays the notification:

fn show(body: &str, urgency: Urgency) {
    let Ok(icon) = ensure_icon() else { return };
    let Some(icon) = icon.to_str() else { return };
    let _ = Notification::new()
        .appname("rice")
        .summary("rice")
        .body(body)
        .icon(icon)
        .urgency(urgency)
        .show();
}

Sources: src/notifications.rs:29-37

Public helpers respect the config flags:

pub fn success(cfg: &Config, body: &str) {
    if cfg.notifications.on_success {
        show(body, Urgency::Low);
    }
}

pub fn error(cfg: &Config, body: &str) {
    if cfg.notifications.on_error {
        show(body, Urgency::Normal);
    }
}

Sources: src/notifications.rs:39-47

Notification Flow from main

graph TD
  MainEnd["main() match result"] --> OkBranch["Ok(())"]
  MainEnd["main() match result"] --> ErrBranch["Err(e)"]

  OkBranch["Ok(())"] --> LoadCfgOk["Config::load() (best-effort)"]
  LoadCfgOk["Config::load() (best-effort)"] --> CallSuccess["notifications::success"]

  ErrBranch["Err(e)"] --> LoadCfgErr["Config::load() (best-effort)"]
  LoadCfgErr["Config::load() (best-effort)"] --> CallError["notifications::error"]
  ErrBranch["Err(e)"] --> PrintErr["eprintln!"]
  ErrBranch["Err(e)"] --> Exit1["exit(1)"]
Loading

Sources: src/main.rs:25-58, src/notifications.rs:39-47


Wallpaper Flow and Backend Integration

Image Listing and Sorting

Sources: src/wall/mod.rs:1-29, src/config.rs:44-46

list_images reads the configured wallpaper directory and filters by extension:

fn list_images(cfg: &Config) -> Result<Vec<PathBuf>> {
    let dir = &cfg.wallpaper_dir;
    let mut images: Vec<PathBuf> = fs::read_dir(dir)?
        .filter_map(|res| res.ok())
        .map(|entry| entry.path())
        .filter(|path| {
            path.is_file() && path.extension().map_or(false, |ext| cfg.is_valid_img(&ext.to_string_lossy().to_lowercase()))
        })
        .collect();

    if images.is_empty() {
        bail!("no wallpapers in {}", dir.display());
    }

    if cfg.sort == "name" {
        images.sort();
    }
    
    Ok(images)
}

Sources: src/wall/mod.rs:5-29, src/config.rs:44-46

This function:

  • Reads directory entries.
  • Keeps only files whose extension (lowercased) matches Config.extensions via is_valid_img.
  • Errors if no images are found.
  • Optionally sorts by name if Config.sort == "name".

Next/Prev Calculation and State Update

Sources: src/wall/mod.rs:30-61, src/state.rs:22-49

step(delta) centralizes the cyclic wallpaper stepping logic:

fn step(delta: isize) -> Result<()> {
    let cfg = Config::load()?;
    let mut state = State::load()?;
    let images = list_images(&cfg)?;
    let n = images.len() as isize;

    let idx = state.wallpaper
        .as_ref()
        .and_then(|cur| images.iter().position(|p| p == cur))
        .unwrap_or(0) as isize;

    let next = (idx + delta).rem_euclid(n) as usize;
    let backend = state.backend
        .as_deref()
        .unwrap_or(cfg.backend.as_str());

    backend::set(&images[next], backend)?;
    theme::apply(&images[next], &cfg)?;
    state.wallpaper = Some(images[next].clone());
    state.backend = Some(backend.to_string());
    state.save()?; 
    println!("{}", images[next].display());
    Ok(())
}

Sources: src/wall/mod.rs:30-61

  • If the current wallpaper in state is present in the image list, its index is used; otherwise index 0 is assumed.
  • delta is added and wrapped via rem_euclid to stay within bounds for next/prev movement.
  • Backend is chosen from state if available, otherwise falls back to Config.backend.
  • After setting the backend and applying theme, state is updated and saved.

Public helpers:

pub fn next() -> Result<()> {
    step(1) 
}

pub fn prev() -> Result<()> {
    step(-1) 
}

Sources: src/wall/mod.rs:63-71

start uses the last wallpaper if still present, otherwise falls back to the first image:

pub fn start() -> Result<()> {
    let cfg = Config::load()?;
    let mut state = State::load()?;
    let images = list_images(&cfg)?;

    let path = state.wallpaper
        .as_ref()
        .and_then(|cur| images.iter().find(|p| *p == cur))
        .unwrap_or(&images[0])
        .clone();
    
    let backend = state.backend
        .as_deref()
        .unwrap_or(cfg.backend.as_str())
        .to_string();

    backend::set(&path, &backend)?;
    theme::apply(&path, &cfg)?;
    state.wallpaper = Some(path.clone());
    state.backend = Some(backend);
    state.save()?; 
    println!("{}", path.display());
    Ok(())
}

Sources: src/wall/mod.rs:73-107

Wallpaper Control Flow

graph TD
  WallCmd["wall::next/prev/start"] --> LoadCfgW["Config::load()"]
  WallCmd["wall::next/prev/start"] --> LoadStateW["State::load()"]
  LoadCfgW["Config::load()"] --> ListImgs["list_images(cfg)"]
  ListImgs["list_images(cfg)"] --> ComputeIdx["compute index (current or default)"]
  ComputeIdx["compute index (current or default)"] --> ChoosePath["select wallpaper path"]
  ChoosePath["select wallpaper path"] --> ChooseBackend["state.backend or cfg.backend"]
  ChooseBackend["state.backend or cfg.backend"] --> BackendSetW["backend::set(path, backend)"]
  BackendSetW["backend::set(path, backend)"] --> ThemeApplyW["theme::apply(path, cfg)"]
  ThemeApplyW["theme::apply(path, cfg)"] --> UpdateStateW["update State fields"]
  UpdateStateW["update State fields"] --> SaveStateW["State::save()"]
Loading

Sources: src/wall/mod.rs:30-61, src/wall/mod.rs:73-107, src/wall/backend.rs:1-20, src/theme.rs:27-41, src/state.rs:22-49

Backend Bridge (wall::backend)

Sources: src/wall/backend.rs:1-25

backend::set maps the chosen backend to the appropriate external command:

pub fn set(path: &Path, backend: &str) -> Result<()> {
    let img_path = path.to_str().context("wallpaper path is not valid utf-8")?;
    match backend {
        "hyprpaper" => cmd::run("hyprctl", &["hyprpaper", "wallpaper", &format!("eDP-1, {img_path}")]),
        "swww" => cmd::run("swww", &["img", img_path]),
        "none" => Ok(()),
        _ => bail!("unkown backend {backend}"),
    } 
}

Sources: src/wall/backend.rs:1-20

  • For hyprpaper, it calls hyprctl hyprpaper wallpaper eDP-1, <img_path>.
  • For swww, it calls swww img <img_path>.
  • For none, it is a no‑op.
  • Any other backend string triggers a bail! error.

Theme Application and Bar Restart

Sources: src/theme.rs:1-42, src/bar.rs:1-30

Theme Configuration and Defaults

Theme is a nested config struct:

#[derive(Deserialize, Debug)]
#[serde(default)]
pub struct Theme {
    enable: bool,
    command: String,
    args: Option<Vec<String>>
}

impl Default for Theme {
    fn default() -> Self {
        Self {
            enable: true,
            command: "wallust".into(),
            args: None,
        }
    }
}

Sources: src/theme.rs:7-20, src/config.rs:19-26

The default theme command is wallust, with theming enabled and no extra args.

Bar config:

#[derive(Deserialize, Debug)]
#[serde(default)]
pub struct Bar {
    pub restart_on_theme: bool,
    pub command: String,
}

impl Default for Bar {
    fn default() -> Self {
        Self {
            restart_on_theme: true,
            command: "waybar".into(),
        }
    } 
}

Sources: src/bar.rs:5-18, src/config.rs:19-26

Theme Application Flow

theme::apply is called during wallpaper operations:

pub fn apply(path: &Path, cfg: &Config) -> Result<()> {
    if !cfg.theme.enable {
        return Ok(());
    }
    
    let p = paths::expand_tilde(path);

    let img = p
        .to_str()
        .context("wallpaper path is not valid utf-8")?;

    let mut args: Vec<String> = vec!["run".into(), img.to_string()];
    if let Some(extra) = &cfg.theme.args {
        args.extend(extra.iter().cloned());
    }

    let args_ref: Vec<&str> = args.iter().map(String::as_str).collect();
    cmd::run(&cfg.theme.command, &args_ref)?;
    if cfg.bar.restart_on_theme {
        crate::bar::restart(cfg)?;
    }
    Ok(())
}

Sources: src/theme.rs:22-42

Key behavior:

  • If cfg.theme.enable is false, theming is skipped.
  • The wallpaper path is tilde‑expanded again for safety.
  • Base theme arguments are ["run", <img_path>].
  • Optional cfg.theme.args are appended.
  • The theme command (cfg.theme.command) is executed via cmd::run.
  • If cfg.bar.restart_on_theme is true, bar::restart(cfg) is called afterward.

Bar Restart Logic

pub fn restart(cfg: &Config) -> Result<()> {
    cmd::run("pkill", &[&cfg.bar.command])?;
    std::process::Command::new(&cfg.bar.command)
        .spawn()
        .with_context(|| format!("failed to start {}", cfg.bar.command))?;
    Ok(())
} 

Sources: src/bar.rs:20-28

The bar restart sequence:

  1. Terminate existing bar processes via pkill <bar_command>.
  2. Spawn a new bar process using std::process::Command::new(&cfg.bar.command).spawn().

Theme and Bar Flow

graph TD
  WallFlow["wall::* call"] --> ThemeApply2["theme::apply(path, cfg)"]
  ThemeApply2["theme::apply(path, cfg)"] --> CheckEnable["cfg.theme.enable?"]
  CheckEnable["cfg.theme.enable?"] -->|false| EndTheme["skip theming"]
  CheckEnable["cfg.theme.enable?"] -->|true| ExpandPath["paths::expand_tilde(path)"]
  ExpandPath["paths::expand_tilde(path)"] --> BuildArgs["build args: run + img + extra"]
  BuildArgs["build args: run + img + extra"] --> RunTheme["cmd::run(theme.command, args)"]
  RunTheme["cmd::run(theme.command, args)"] --> CheckBar["cfg.bar.restart_on_theme?"]
  CheckBar["cfg.bar.restart_on_theme?"] -->|false| EndTheme
  CheckBar["cfg.bar.restart_on_theme?"] -->|true| BarRestart2["bar::restart(cfg)"]
  BarRestart2["bar::restart(cfg)"] --> EndTheme
Loading

Sources: src/theme.rs:22-42, src/bar.rs:20-28


Session Startup and Wallpaper Restoration

Sources: src/session.rs:1-28, src/wall/mod.rs:73-107

Session Configuration and Defaults

Session is another nested config struct:

#[derive(Deserialize, Debug)]
#[serde(default)]
pub struct Session {
    pub exec: Vec<String>,
}

impl Default for Session {
    fn default() -> Self {
        Self {
            exec: vec!["waybar".into(), "swaync".into(), "discord".into()],
        }
    }
}

Sources: src/session.rs:5-16, src/config.rs:19-26

Session Start Flow

session::start loads Config, spawns each process in cfg.session.exec, and then starts the wallpaper:

pub fn start() -> Result<()> {
    let cfg = Config::load()?;
    
    for exec in cfg.session.exec {
        std::process::Command::new(&exec)
            .spawn()
            .with_context(|| format!("failed to start {exec}"))?;
    };    

    crate::wall::start()?;
    Ok(()) 
}

Sources: src/session.rs:18-28, src/wall/mod.rs:73-107

  • Each session application is spawned asynchronously; errors are contextualized with the command name.
  • After all commands are spawned, wall::start() is called to restore the last wallpaper or set the first one if none is saved.

Session Data and Control Flow

graph TD
  SessionCmd["Session Start (CLI)"] --> SessionFunc["session::start()"]
  SessionFunc["session::start()"] --> LoadCfgS["Config::load()"]
  LoadCfgS["Config::load()"] --> LoopExec["for each exec in cfg.session.exec"]
  LoopExec["for each exec in cfg.session.exec"] --> SpawnProc["spawn process"]
  SpawnProc["spawn process"] --> LoopExec
  LoopExec["for each exec in cfg.session.exec"] --> AfterLoop["after loop"]
  AfterLoop["after loop"] --> WallStartS["wall::start()"]
  WallStartS["wall::start()"] --> EndSession["session done"]
Loading

Sources: src/session.rs:18-28, src/wall/mod.rs:73-107


Data Structures Summary

Core Configuration Structures

Struct Fields / Description
Config wallpaper_dir: PathBuf, backend: String, sort: String, extensions: Vec<String>, theme: Theme, bar: Bar, session: Session, notifications: Notifications
Theme enable: bool, command: String, args: Option<Vec<String>>
Bar restart_on_theme: bool, command: String
Session exec: Vec<String>
Notifications on_success: bool, on_error: bool
State wallpaper: Option<PathBuf>, backend: Option<String>

Sources: src/config.rs:19-28, src/theme.rs:7-20, src/bar.rs:5-18, src/session.rs:5-16, src/notifications.rs:9-16, src/state.rs:7-13

Persistence and File Layout

File path Producer Consumer(s) Content type
~/.config/rice/config.toml Config::load Config::load TOML config
~/.cache/rice/state.json State::save State::load JSON state
~/.config/rice/icon.png ensure_icon notifications::show PNG icon

Sources: src/paths.rs:23-41, src/config.rs:30-42, src/state.rs:22-49, src/notifications.rs:18-27


Error Handling Architecture

Sources: src/main.rs:25-58, src/cmd.rs:1-16, src/state.rs:22-49, src/config.rs:30-42, src/wall/backend.rs:1-20

The project consistently uses anyhow::Result<()> and anyhow::{bail, Context} to propagate errors with contextual messages across modules. main is the single point where errors are:

  • Translated into error notifications (if configured).
  • Printed with detailed formatting using {e:#}.
  • Mapped to process exit code 1.

Examples:

  • cmd::run wraps process start failures and non‑zero exit codes.
    Sources: src/cmd.rs:1-16
  • Config::load wraps failures to read/write/parse the config file.
    Sources: src/config.rs:30-42
  • State::load and State::save wrap read/parse/write/dir creation failures.
    Sources: src/state.rs:22-49
  • backend::set bails on unknown backend types or non‑UTF‑8 paths.
    Sources: src/wall/backend.rs:1-20

src/error.rs is currently a placeholder indicating planned future specialization of error types.
Sources: src/error.rs:1-2


Conclusion

The rice architecture is a modular, command‑driven Rust CLI organized around clear responsibilities: CLI parsing in cli, orchestration and global error/notification handling in main, configuration and state persistence via config, state, and paths, and feature‑specific modules for wallpapers (wall, wall::backend), theming (theme), bar control (bar), session startup (session), and notifications (notifications). Data flows are straightforward: commands flow from CLI into high‑level functions that combine configuration, persistent state, filesystem paths, and external command execution, with consistent error propagation to the top level. This separation of concerns and explicit data flow makes it easier to extend or adjust behavior in one area (e.g., adding backends, changing theming strategy) without impacting the rest of the system.
Sources: src/main.rs:1-60, src/cli.rs:1-33, src/config.rs:19-42, src/state.rs:7-49, src/paths.rs:15-67, src/wall/mod.rs:5-107, src/wall/backend.rs:1-20, src/theme.rs:7-42, src/bar.rs:5-28, src/session.rs:5-28, src/notifications.rs:9-47, README.md:1-124

Clone this wiki locally