Repository navigation
architecture and data flow
Related Files:
src/main.rssrc/cli.rssrc/config.rssrc/state.rssrc/paths.rssrc/cmd.rssrc/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:
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
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 | 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
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"]
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.
Sources: src/main.rs:1-60, src/cli.rs:1-33
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 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
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:
- Compute config file path:
~/.config/rice/config.tomlviapaths::config_file. - If missing, create parent dir and write the built‑in default from
examples/config.toml. - Read and parse TOML into
Config. - Expand
wallpaper_dirtilde viapaths::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
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"]
Sources: src/config.rs:30-42, src/paths.rs:23-41
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
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()"]
Sources: src/wall/mod.rs:30-61, src/wall/mod.rs:78-107, src/state.rs:22-49
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
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::setforhyprctlandswwwcalls. -
theme::applyfor the theme command (defaultwallust). -
bar::restartforpkillagainst the bar command.
Sources: src/wall/backend.rs:1-20, src/theme.rs:33-41, src/bar.rs:21-26
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
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)"]
Sources: src/main.rs:25-58, src/notifications.rs:39-47
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.extensionsviais_valid_img. - Errors if no images are found.
- Optionally sorts by name if
Config.sort == "name".
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.
-
deltais added and wrapped viarem_euclidto 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
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()"]
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
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 callshyprctl hyprpaper wallpaper eDP-1, <img_path>. - For
swww, it callsswww img <img_path>. - For
none, it is a no‑op. - Any other backend string triggers a
bail!error.
Sources: src/theme.rs:1-42, src/bar.rs:1-30
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::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.enableis false, theming is skipped. - The wallpaper path is tilde‑expanded again for safety.
- Base theme arguments are
["run", <img_path>]. - Optional
cfg.theme.argsare appended. - The theme command (
cfg.theme.command) is executed viacmd::run. - If
cfg.bar.restart_on_themeis true,bar::restart(cfg)is called afterward.
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:
- Terminate existing bar processes via
pkill <bar_command>. - Spawn a new bar process using
std::process::Command::new(&cfg.bar.command).spawn().
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
Sources: src/theme.rs:22-42, src/bar.rs:20-28
Sources: src/session.rs:1-28, src/wall/mod.rs:73-107
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 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.
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"]
Sources: src/session.rs:18-28, src/wall/mod.rs:73-107
| 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
| 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
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::runwraps process start failures and non‑zero exit codes.
Sources: src/cmd.rs:1-16 -
Config::loadwraps failures to read/write/parse the config file.
Sources: src/config.rs:30-42 -
State::loadandState::savewrap read/parse/write/dir creation failures.
Sources: src/state.rs:22-49 -
backend::setbails 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
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