Skip to content

development and contribution guide

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

Development and Contribution Guide

Related Files:

  • README.md
  • rustfmt.toml
  • Cargo.toml
  • src/main.rs
  • src/wall/mod.rs
  • src/wall/backend.rs
  • src/theme.rs
  • src/bar.rs
  • src/session.rs
  • src/notifications.rs

Related Pages:

  • Overview and Key Features
  • Installation, Configuration, and Usage
  • Architecture and Data Flow
Relevant source files

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

Development and Contribution Guide

rice is a small Rust CLI focused on wallpaper management, theme application, bar restart, notifications, and session startup for Hyprland desktops. This guide explains how the codebase is structured, how to develop and test changes, and what to consider when contributing new features or modifications. It focuses on the internal architecture (CLI, configuration, state, wall management, theming, bar control, notifications, sessions, and process execution) to help maintain consistency and robustness across contributions.
Sources: README.md:1-121, src/main.rs:1-26, src/cli.rs:1-37

The guide is based entirely on the existing source code and the top-level README. It covers how commands are wired from the CLI to feature modules, how configuration and runtime state are handled on disk, and how external tools like hyprpaper, swww, wallust, and waybar are invoked from rice.
Sources: README.md:21-88, src/config.rs:1-42, src/wall/mod.rs:1-47, src/theme.rs:1-34, src/bar.rs:1-24, src/notifications.rs:1-44


1. Project Layout and Build Basics

Sources: README.md:40-73, Cargo.toml, rustfmt.toml

The repository is organized as a standard Rust crate with a binary target:

rice/
├── Cargo.toml
├── rustfmt.toml
├── examples/
│   └── config.toml
└── src/
    ├── main.rs
    ├── cli.rs
    ├── paths.rs
    ├── config.rs
    ├── state.rs
    ├── cmd.rs
    ├── theme.rs
    ├── bar.rs
    ├── session.rs
    ├── notifications.rs
    └── wall/
        ├── mod.rs
        └── backend.rs

Sources: README.md:52-68

Development workflow from the README:

git clone https://github.com/Cody-will/rice.git
cd rice
cargo build
cargo run -- wall next
cargo fmt
cargo clippy
cargo test

Sources: README.md:167-181

Key expectations for contributors:

  • Use cargo fmt and cargo clippy before opening a PR.
  • Branch from main.
  • Describe which command(s) you changed in the PR description.
1. Fork the repo
2. Branch from `main`
3. `cargo fmt` and `cargo clippy` before you open the PR
4. Describe the command you changed (`rice wall …`, session, config)

Sources: README.md:183-196

Build and Install

The recommended way to install for actual usage is cargo install, not cargo build:

# From GitHub (no clone)
cargo install --git https://github.com/Cody-will/rice.git

# From a clone
cargo install --path .

Sources: README.md:88-104


2. High-Level Architecture

Sources: src/main.rs:1-32, src/cli.rs:1-37, src/config.rs:1-42, src/state.rs:1-45, src/wall/mod.rs:1-88, src/theme.rs:1-34, src/bar.rs:1-24, src/session.rs:1-27, src/notifications.rs:1-44, src/cmd.rs:1-15

At a high level:

  • main.rs parses CLI arguments and dispatches to feature modules.
  • Each sub-command (wall, bar, session) is implemented in its own module.
  • Configuration is loaded from ~/.config/rice/config.toml.
  • Runtime state is stored in ~/.cache/rice/state.json.
  • Feature modules (wall, theme, bar, session) orchestrate external binaries via cmd::run or std::process::Command.
  • Optional desktop notifications are sent via notify-rust.

Component Overview

Component Responsibility Key Files
CLI Parse commands/subcommands src/cli.rs
Entry point Dispatch commands, global success/error handling src/main.rs
Config Load and default config from TOML src/config.rs, examples/config.toml
Paths Resolve XDG dirs, config/state/icon locations, ~ expand src/paths.rs
State Persist last wallpaper and backend src/state.rs
Wall List images, step next/prev, start last wall src/wall/mod.rs
Backends Talk to hyprpaper or swww src/wall/backend.rs
Theme Run wallust, restart bar if configured src/theme.rs, src/bar.rs
Session Start session apps, then restore wallpaper src/session.rs
Notifications Success/error desktop notifications src/notifications.rs
Command exec Thin wrapper for running external commands src/cmd.rs

Sources: src/main.rs:1-32, src/cli.rs:1-37, src/config.rs:1-42, src/paths.rs:1-64, src/state.rs:1-45, src/wall/mod.rs:1-88, src/wall/backend.rs:1-24, src/theme.rs:1-34, src/bar.rs:1-24, src/session.rs:1-27, src/notifications.rs:1-44, src/cmd.rs:1-15

Architecture Diagram

graph TD
  Main["main.rs"]
  Cli["cli.rs"]
  Config["config.rs"]
  Paths["paths.rs"]
  State["state.rs"]
  Wall["wall/mod.rs"]
  Backend["wall/backend.rs"]
  Theme["theme.rs"]
  Bar["bar.rs"]
  Session["session.rs"]
  Cmd["cmd.rs"]
  Notify["notifications.rs"]

  Main --> Cli
  Main --> Wall
  Main --> Bar
  Main --> Session
  Main --> Config
  Main --> Notify

  Wall --> Config
  Wall --> State
  Wall --> Backend
  Wall --> Theme

  Backend --> Cmd

  Theme --> Config
  Theme --> Paths
  Theme --> Cmd
  Theme --> Bar

  Bar --> Cmd
  Bar --> Config

  Session --> Config
  Session --> Wall

  Config --> Paths
  State --> Paths
  Notify --> Paths
  Notify --> Config
Loading

Sources: src/main.rs:1-32, src/cli.rs:1-37, src/config.rs:1-42, src/paths.rs:1-64, src/state.rs:1-45, src/wall/mod.rs:1-88, src/wall/backend.rs:1-24, src/theme.rs:1-34, src/bar.rs:1-24, src/session.rs:1-27, src/notifications.rs:1-44, src/cmd.rs:1-15


3. Command-Line Interface and Entry Flow

Sources: src/main.rs:1-32, src/cli.rs:1-37

CLI Structure

The CLI is defined using clap with nested subcommands: wall, bar, and session, each with their own action subcommand.
Sources: src/cli.rs:1-37

#[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-20

Wall actions and other actions:

#[derive(Subcommand)]
pub enum WallAction {
    Next,
    Prev,
    Start,
}

#[derive(Subcommand)]
pub enum BarAction {
    Restart,
}

#[derive(Subcommand)]
pub enum SessionAction {
    Start,
}

Sources: src/cli.rs:22-37

Entry Point Logic

main parses the CLI, delegates to modules, and then sends notifications depending on success or failure. The code currently shows a duplicated match arm segment, but the intended structure is clear.
Sources: src/main.rs:1-32

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);
     

     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-47

Note: The above function body currently has duplicated Err(e) arms and an extra Ok(()) branch due to apparent merge/duplication, which should be cleaned up in contributions.

CLI Dispatch Sequence Diagram

sequenceDiagram
  autonumber
  participant U as User
  participant R as rice (main)
  participant C as Cli (clap)
  participant W as wall module
  participant B as bar module
  participant S as session module
  participant CFG as Config
  participant N as Notifications

  U->>+R: Invoke "rice wall next"
  R->>+C: Cli::parse()
  C-->>-R: Command::Wall{Next}
  R->>+W: wall::next()
  W-->>-R: Result<()>
  alt Success
    R->>+CFG: Config::load()
    CFG-->>-R: Ok(Config) / Err
    R->>N: notifications::success(...)
  else Error
    R->>+CFG: Config::load()
    CFG-->>-R: Ok(Config) / Err
    R->>N: notifications::error(...)
  end
Loading

Sources: src/main.rs:10-47, src/cli.rs:1-37, src/notifications.rs:36-44, src/config.rs:20-39


4. Configuration and Paths

Sources: src/config.rs:1-42, src/paths.rs:1-64, README.md:112-161, src/theme.rs:1-20, src/bar.rs:1-17, src/session.rs:1-19, src/notifications.rs:18-28

Config File and Structure

Config::load reads or creates ~/.config/rice/config.toml from the embedded default (examples/config.toml) and then deserializes it. The wallpaper_dir is expanded to handle ~.
Sources: src/config.rs:1-40, src/paths.rs:40-64

#[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:9-18

Config layout in TOML (from README, matching Config and nested defaults):

wallpaper_dir = "~/Pictures/walls"
backend = "hyprpaper"    # hyprpaper | swww | none
sort = "name"
extensions = ["jpg", "jpeg", "png", "webp"]

[theme]
enable = true
command = "wallust"
args = []                 # apply() should call: wallust run <image>

[bar]
restart_on_theme = true
command = "waybar"

[session]
exec = ["waybar", "swaync"]

Sources: README.md:112-141, src/theme.rs:7-20, src/bar.rs:7-17, src/session.rs:5-16, src/notifications.rs:18-28

Config::load:

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:20-39

Config Options Table

Field Type Default (via nested structs or example) Description
wallpaper_dir PathBuf "~/Pictures/walls" Directory containing wallpapers
backend String "hyprpaper" Wallpaper backend: hyprpaper, swww, or none
sort String "name" Sort strategy for wallpapers (name supported)
extensions Vec<String> ["jpg","jpeg","png","webp"] Allowed image file extensions
theme.enable bool true Whether to run theme application after changing wall
theme.command String "wallust" Executable to apply theme
theme.args Option<Vec<String>> None (empty by default) Extra args passed after wallust run <image>
bar.restart_on_theme bool true Whether to restart bar after theme
bar.command String "waybar" Bar process name to restart
session.exec Vec<String> ["waybar", "swaync", "discord"] Binaries to spawn when starting a session
notifications.on_success bool false Show notification on success
notifications.on_error bool true Show notification on error

Sources: README.md:112-141, src/config.rs:9-18, src/theme.rs:7-20, src/bar.rs:7-17, src/session.rs:5-16, src/notifications.rs:18-28

Paths and XDG Handling

paths.rs provides utilities for locating config, cache, and icon files, and for creating directories:

pub fn project_dirs() -> Result<ProjectDirs> {
    ProjectDirs::from("", "", "rice").context("could not determine XDG dirs for rice")
}

/// `~/.config/rice`
pub fn config_dir() -> Result<PathBuf> {
    Ok(project_dirs()?.config_dir().to_path_buf())
}

/// `~/.cache/rice`
pub fn cache_dir() -> Result<PathBuf> {
    Ok(project_dirs()?.cache_dir().to_path_buf())
}

Sources: src/paths.rs:16-31

File paths:

pub fn config_file() -> Result<PathBuf> {
    Ok(config_dir()?.join("config.toml"))
}

pub fn icon_file() -> Result<PathBuf> {
    Ok(config_dir()?.join("icon.png"))
}

pub fn state_file() -> Result<PathBuf> {
    Ok(cache_dir()?.join("state.json"))
}

Sources: src/paths.rs:33-48

Directory creation and ~ expansion:

pub fn ensure_dir(path: &Path) -> Result<()> {
    std::fs::create_dir_all(path)
        .with_context(|| format!("failed to create {}", path.display()))
}

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:50-64


5. State Management

Sources: src/state.rs:1-64, src/wall/mod.rs:49-88, src/paths.rs:40-48, README.md:145-149

Runtime state (State) is stored in JSON at ~/.cache/rice/state.json and holds the last used wallpaper and backend.

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

impl Default for State {
    fn default() -> Self {
        Self {
            wallpaper: None,
            backend: None,
        }
    }
}

Sources: src/state.rs:7-20

Loading state:

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-39

Saving state:

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:41-60

State is updated in wall::step and wall::start after successfully setting the wallpaper and applying the theme.
Sources: src/wall/mod.rs:49-88, README.md:145-149


6. Wallpaper Handling

Sources: src/wall/mod.rs:1-108, src/wall/backend.rs:1-24, src/config.rs:9-18, src/theme.rs:22-34, README.md:27-38

Listing and Sorting Wallpapers

list_images reads the configured wallpaper_dir, filters to files with allowed extensions, and sorts them by name when sort is "name":

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:8-33, src/config.rs:41-44

Config::is_valid_img checks membership in extensions:

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

Sources: src/config.rs:41-44

Stepping Next/Previous Wallpaper

The core stepping logic is in step(delta: isize):

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:35-57

Public wrappers:

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

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

Sources: src/wall/mod.rs:59-66

Unit tests validate the wrap-around indexing behavior:

#[test]
fn wrap_next_from_last() {
    let n: isize = 3;
    let idx: isize = 2;
    assert_eq!((idx + 1).rem_euclid(n) as usize, 0);
}

Sources: src/wall/mod.rs:90-102

Starting from Saved State

start restores the last wallpaper saved in State, or 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:68-88

Backend Invocation

wall/backend.rs chooses the correct backend 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-21

Note: The output string uses "eDP-1, {img_path}" as the target for hyprpaper IPC. This is hard-coded in the backend implementation.
Sources: src/wall/backend.rs:7-13


7. Theme Application and Bar Restart

Sources: src/theme.rs:1-34, src/bar.rs:1-24, src/config.rs:9-18, README.md:29-38

Theme Configuration and Defaults

Theme uses serde(default) and has defaults aligning with the README example:

#[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:5-20, README.md:120-129

Applying Theme

theme::apply runs the theme command and optionally restarts the bar:

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-34

This function is called after setting the wallpaper in both step and start.
Sources: src/wall/mod.rs:49-57, src/wall/mod.rs:80-83

Bar Restart Logic

Bar configuration and defaults:

#[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-17, README.md:131-135

Restart implementation:

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:19-24

This is called from theme::apply whenever cfg.bar.restart_on_theme is true.
Sources: src/theme.rs:31-34


8. Session Management

Sources: src/session.rs:1-27, src/config.rs:9-18, README.md:139-149, src/wall/mod.rs:68-88

Session describes a list of executables to spawn when starting a session:

#[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

session::start loads config, spawns each command, 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-27

The README documents how session start ties into Hyprland startup and wall start.
Sources: README.md:145-161


9. Notifications

Sources: src/notifications.rs:1-58, src/main.rs:10-47, src/paths.rs:33-48, src/config.rs:9-18

Notifications controls whether success or error notifications are shown:

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

impl Default for Notifications {
    fn default() -> Self {
        Self {
            on_success: false,
            on_error: true,
        }
    }
}

Sources: src/notifications.rs:18-28

Icon management embeds an icon and writes it to ~/.config/rice/icon.png on first use:

const ICON: &[u8] = include_bytes!("../assets/rice-notify.png");

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:3-7, src/notifications.rs:30-40, src/paths.rs:33-40

Notification display:

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();
}

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:42-58

main uses these helpers after each command finishes or fails.
Sources: src/main.rs:22-47


10. External Command Execution

Sources: src/cmd.rs:1-15, src/wall/backend.rs:1-21, src/theme.rs:22-34, src/bar.rs:19-24, src/session.rs:18-27

cmd::run is a small helper for running external commands that must succeed:

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-15

This is used by:

  • wall/backend.rs for hyprctl and swww.
  • theme.rs for the theme command (e.g. wallust).
  • bar.rs for pkill.

Sources: src/wall/backend.rs:1-21, src/theme.rs:22-34, src/bar.rs:19-24

session.rs uses std::process::Command::new(...).spawn() directly since it does not require synchronous completion.
Sources: src/session.rs:18-27


11. Development Practices and Extension Points

Coding Style and Formatting

  • Use cargo fmt to adhere to formatting rules; rustfmt.toml defines any custom style, if present.
  • Use cargo clippy to catch lints before submitting changes.

Sources: rustfmt.toml, README.md:167-181

Adding or Modifying Commands

When adding a new CLI action:

  1. Add a variant to the appropriate enum in cli.rs.
  2. Update the match in main.rs to dispatch to your new function.
  3. Implement your logic in a dedicated module (or appropriate existing module).
  4. Decide whether to use cmd::run for synchronous commands or std::process::Command for async spawns.

Sources: src/cli.rs:1-37, src/main.rs:10-26, src/cmd.rs:1-15, src/session.rs:18-27

Working with Config

  • Extend Config with new fields as needed, ensuring they are deserializable.
  • Add appropriate defaults via nested structs using serde(default) or by updating examples/config.toml and README snippets.
  • If the config file is absent, it is auto-created from examples/config.toml through the embedded DEFAULT string.

Sources: src/config.rs:1-18, src/config.rs:20-39, README.md:112-141

Handling State Safely

  • Use State::load() and State::save() rather than managing JSON manually.
  • Preserve unknown values by only overwriting fields you explicitly manage.
  • Update state only after external actions (wallpaper set, theme applied) succeed.

Sources: src/state.rs:22-60, src/wall/mod.rs:35-57, src/wall/mod.rs:68-88

Error Handling and Notifications

  • Return anyhow::Result<()> from your functions to integrate with global error handling.
  • On error, main prints a formatted error and exits with code 1 after sending an error notification (if configured).
  • Consider wrapping external failures with informative context via with_context.

Sources: src/main.rs:10-47, src/cmd.rs:1-15, src/notifications.rs:42-58, src/config.rs:20-39, src/state.rs:22-60

Potential Cleanups / Improvements (As Seen in Code)

These are directly observable inconsistencies that contributors might address:

  • main.rs contains duplicated Err(e) arms and stray Ok(()) text inside the Err branch, indicating a merge or copy-paste error.
  • wall/mod.rs shows duplicated partial fragments for start, next, and prev, though a complete final version is present at the end.
  • wall/backend.rs has a typo: "unkown backend {backend}".
  • error.rs is currently empty and can be used in the future for domain-specific errors.

Sources: src/main.rs:28-47, src/wall/mod.rs:58-88, src/wall/backend.rs:15-21, src/error.rs:1-2


12. Data and Control Flow Overview

Wallpaper Change Flow

graph TD
  Start["User command"]
  LoadCfg["Config::load"]
  LoadState["State::load"]
  ListImgs["list_images"]
  PickIdx["Compute next index"]
  PickBackend["Choose backend"]
  SetWall["backend::set"]
  ApplyTheme["theme::apply"]
  SaveState["State::save"]
  Notify["notifications::success/error"]

  Start --> LoadCfg
  LoadCfg --> LoadState
  LoadState --> ListImgs
  ListImgs --> PickIdx
  PickIdx --> PickBackend
  PickBackend --> SetWall
  SetWall --> ApplyTheme
  ApplyTheme --> SaveState
  SaveState --> Notify
Loading

Sources: src/main.rs:10-47, src/config.rs:20-39, src/state.rs:22-60, src/wall/mod.rs:35-57, src/wall/backend.rs:1-21, src/theme.rs:22-34, src/notifications.rs:42-58

Session Start Flow

graph TD
  SessionCmd["rice session start"]
  LoadCfg2["Config::load"]
  SpawnExecs["Spawn session.exec\nbinaries"]
  StartWall["wall::start"]
  SetWall2["backend::set + theme::apply"]
  SaveState2["State::save"]

  SessionCmd --> LoadCfg2
  LoadCfg2 --> SpawnExecs
  SpawnExecs --> StartWall
  StartWall --> SetWall2
  SetWall2 --> SaveState2
Loading

Sources: src/main.rs:18-26, src/session.rs:18-27, src/wall/mod.rs:68-88, src/wall/backend.rs:1-21, src/theme.rs:22-34, src/state.rs:22-60


13. Summary

The rice project is structured around a clean separation of concerns: configuration and paths, persistent state, wallpaper management and backends, theme and bar integration, session startup, notifications, and external command execution. The CLI in main.rs and cli.rs serves as the front door for these capabilities, and Config and State provide stable, file-based integration points for runtime behavior. When contributing, align with existing patterns: use anyhow::Result, keep feature logic in dedicated modules, respect config/state design, and ensure new behavior is surfaced via the structured CLI.
Sources: src/main.rs:1-32, src/cli.rs:1-37, src/config.rs:1-42, src/state.rs:1-60, src/wall/mod.rs:1-88, src/theme.rs:1-34, src/bar.rs:1-24, src/session.rs:1-27, src/notifications.rs:1-58, README.md:21-41

Clone this wiki locally