Repository navigation
Home
Related Files:
README.mdCargo.toml
Related Pages:
- Installation, Configuration, and Usage
- Architecture and Data Flow
- Wallpaper, Theme, and Bar Workflow
Relevant source files
The following files were used as context for generating this wiki page:
rice is a small Rust CLI designed to orchestrate wallpapers, theming, and basic session helpers for a Hyprland desktop environment. It centralizes common tasks—such as cycling wallpapers, running wallust, restarting waybar, and starting user sessions—behind a single binary and a simple command-line interface that is intended for use in window manager keybindings.
Sources: README.md:1-59
The core of rice is a set of cohesive modules that handle configuration, filesystem paths, wallpaper backends, theming, bar control, session startup, persistent state, notifications, and command execution. The top-level CLI routes user commands into these modules, which then coordinate to update both the visual wallpaper and associated system components.
Sources: src/main.rs:1-26, src/cli.rs:1-39, src/wall/mod.rs:1-35, src/theme.rs:1-33, src/bar.rs:1-23
Sources: src/main.rs:1-64, src/cli.rs:1-39, src/config.rs:1-40, src/wall/mod.rs:1-102, src/wall/backend.rs:1-27, src/session.rs:1-30, src/state.rs:1-53, src/notifications.rs:1-55, src/paths.rs:1-63, src/cmd.rs:1-18, README.md:1-120
At a high level, rice consists of:
- A CLI front-end that exposes subcommands for wallpaper, bar, and session operations.
- A configuration subsystem stored in TOML under
~/.config/rice/config.toml. - A wallpaper subsystem that lists images, steps forward/backward, and applies changes via backends.
- A theme subsystem that runs an external theming command (e.g.,
wallust) and optionally restarts the bar. - A bar subsystem that restarts the configured status bar.
- A session subsystem that spawns configured applications and restores the last wallpaper.
- A state subsystem that maintains the last wallpaper and backend in JSON under
~/.cache/rice/state.json. - A notifications subsystem to display desktop notifications about success/failure.
- A paths helper module to resolve config/cache locations and expand
~. - A generic command runner to execute external binaries.
graph TD
Main["main.rs"]
Cli["cli.rs"]
Config["config.rs"]
Paths["paths.rs"]
Wall["wall/mod.rs"]
Backend["wall/backend.rs"]
Theme["theme.rs"]
Bar["bar.rs"]
Session["session.rs"]
State["state.rs"]
Notify["notifications.rs"]
Cmd["cmd.rs"]
Main --> Cli
Main --> Config
Main --> Wall
Main --> Bar
Main --> Session
Main --> Notify
Config --> Paths
Config --> Theme
Config --> Bar
Config --> Session
Config --> Notify
Wall --> Config
Wall --> State
Wall --> Theme
Wall --> Backend
Backend --> Cmd
Theme --> Paths
Theme --> Cmd
Theme --> Bar
Bar --> Cmd
Session --> Config
Session --> Wall
State --> Paths
Notify --> Paths
This diagram shows how the main entrypoint and configuration sit at the center of the system, coordinating interactions between wallpaper, theme, bar, session, state, notifications, and command execution.
Sources: src/main.rs:1-64, src/config.rs:1-40, src/wall/mod.rs:1-35, src/wall/backend.rs:1-27, src/theme.rs:1-33, src/bar.rs:1-23, src/session.rs:1-30, src/state.rs:1-53, src/notifications.rs:1-55, src/paths.rs:1-63, src/cmd.rs:1-18
Sources: src/cli.rs:1-39, src/main.rs:1-64, README.md:16-33
The CLI exposes three primary top-level commands—wall, bar, and session—each with specific subcommands:
| Top-level command | Subcommand | Effect (high-level) |
|---|---|---|
wall |
next |
Step to next wallpaper and apply theme/state |
wall |
prev |
Step to previous wallpaper and apply theme/state |
wall |
start |
Restore last wallpaper (or first) and apply theme/state |
bar |
restart |
Restart the configured bar (default waybar) |
session |
start |
Spawn configured session apps, then start wallpaper |
Sources: src/cli.rs:1-39, src/wall/mod.rs:59-102, src/bar.rs:18-23, src/session.rs:19-30, README.md:16-33
#[derive(Parser)]
#[command(name = "rice", about = "Hyprland helpers")]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}The Command enum further branches into Wall, Bar, and Session variants, each carrying a more specific action enum (WallAction, BarAction, SessionAction).
Sources: src/cli.rs:1-39
The main function parses the CLI and matches on command to dispatch into module-specific functions:
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);
}
}
}The logic:
- Parse arguments into
Cli. - Invoke the appropriate function in
wall,bar, orsession. - On success, optionally show a success notification (
"Wallpaper updated"). - On error, show an error notification and exit with status 1.
Sources: src/main.rs:8-64
sequenceDiagram
autonumber
participant U as User
participant Shell as Shell
participant Bin as rice (main)
participant Cli as CLI Parser
participant Wall as wall module
participant Bar as bar module
participant Sess as session module
participant Cfg as Config
participant Notif as Notifications
U->>Shell: run "rice wall next"
Shell->>+Bin: exec rice
Bin->>+Cli: parse args
Cli-->>-Bin: Command::Wall(Next)
alt wall command
Bin->>+Wall: next()
Wall-->>-Bin: Result
else bar command
Bin->>+Cfg: Config::load()
Cfg-->>-Bin: Config
Bin->>+Bar: restart(&Config)
Bar-->>-Bin: Result
else session command
Bin->>+Sess: start()
Sess-->>-Bin: Result
end
alt Result Ok
Bin->>+Cfg: Config::load()
Cfg-->>-Bin: Config
Bin->>+Notif: success("Wallpaper updated")
Notif-->>-Bin: (shown/ignored)
Bin-->>Shell: exit 0
else Result Err
Bin->>+Cfg: Config::load()
Cfg-->>-Bin: Config
Bin->>+Notif: error("Update failed: ...")
Notif-->>-Bin: (shown/ignored)
Bin-->>Shell: exit 1
end
This sequence shows how rice processes a user command from the shell, dispatches it to the appropriate module, then uses config and notifications for reporting.
Sources: src/main.rs:8-64, src/cli.rs:1-39, src/notifications.rs:33-55, src/config.rs:18-40
Sources: src/config.rs:1-40, src/paths.rs:1-63, src/state.rs:1-53, src/notifications.rs:1-32, README.md:45-111
paths.rs centralizes all filesystem location logic and ~ expansion:
| Function | Path / Behavior |
|---|---|
project_dirs |
XDG dirs for qualifier rice (config and cache) |
config_dir |
~/.config/rice |
cache_dir |
~/.cache/rice |
config_file |
~/.config/rice/config.toml |
icon_file |
~/.config/rice/icon.png |
state_file |
~/.cache/rice/state.json |
ensure_dir |
mkdir -p style directory creation |
expand_tilde |
Expand leading ~ using $HOME
|
Sources: src/paths.rs:15-63
expand_tilde supports:
-
~→$HOME -
~/something→$HOME/something - Other paths returned unchanged.
Sources: src/paths.rs:43-63
The Config struct is deserialized from TOML and enriched with default values from embedded example config:
const DEFAULT: &str = include_str!("../examples/config.toml");
#[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:1-17
Loading logic:
- Compute config path.
- If missing, create parent dir and write the embedded default TOML content.
- Read file into string.
- Deserialize TOML into
Config. - Expand
wallpaper_dir~usingpaths::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:19-40
The README documents example configuration keys and their meanings:
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:90-116
The State struct holds runtime state across invocations:
#[derive(Serialize, Deserialize, Debug)]
#[serde(default)]
pub struct State {
pub wallpaper: Option<PathBuf>,
pub backend: Option<String>,
}Sources: src/state.rs:7-19
Load and save semantics:
-
load():- If
state.jsondoesn’t exist, return defaultStatewithNonevalues. - Otherwise, read JSON and deserialize.
- If
-
save():- Ensure parent directory exists.
- Serialize to pretty JSON.
- Write to
state.json.
Sources: src/state.rs:21-53
Sources: src/wall/mod.rs:1-102, src/wall/backend.rs:1-27, src/config.rs:1-40, src/state.rs:1-53, src/theme.rs:1-33
The wallpaper subsystem includes:
- Listing candidate images from the configured directory.
- Stepping forward/backward through the list.
- Restoring last wallpaper.
- Applying wall changes via backend commands.
- Triggering theme application and bar restart.
list_images collects valid image files from cfg.wallpaper_dir:
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-35, src/config.rs:42-48
Config::is_valid_img checks whether the image extension belongs to the configured extensions list:
pub fn is_valid_img(&self, img_ext: &str) -> bool {
self.extensions.contains(&img_ext.to_string())
}Sources: src/config.rs:42-48
The step function encapsulates the next/prev 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:37-59
Key behavior:
- Loads configuration and current state.
- Finds the index of the current wallpaper in the sorted list (default 0 if not found).
- Uses
rem_euclidfor wraparound at both ends. - Falls back to
cfg.backendif state’s backend is absent. - Delegates setting the wallpaper to
backend::set. - Applies theming via
theme::apply. - Updates and persists state.
- Prints the selected path to stdout.
next and prev are thin wrappers:
pub fn next() -> Result<()> {
step(1)
}
pub fn prev() -> Result<()> {
step(-1)
}Sources: src/wall/mod.rs:61-69
Two tests verify wraparound behavior:
#[cfg(test)]
mod tests {
#[test]
fn wrap_next_from_last() {
let n: isize = 3;
let idx: isize = 2;
assert_eq!((idx + 1).rem_euclid(n) as usize, 0);
}
#[test]
fn wrap_prev_from_first() {
let n: isize = 3;
let idx: isize = 0;
assert_eq!((idx + -1).rem_euclid(n) as usize, 2);
}
}Sources: src/wall/mod.rs:81-101
The start function restores the last used wallpaper 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:71-79, src/wall/mod.rs:102-120
Behavior:
- Attempts to find
state.wallpaperin the current list. - If missing, selects the first image.
- Chooses backend from state or config.
- Calls backend and theme, then persists state.
wall/backend.rs abstracts over the wallpaper backend:
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-18
Supported backend values (as seen in README and code):
-
"hyprpaper": useshyprctl hyprpaper wallpaper eDP-1, <path> -
"swww": usesswww img <path> -
"none": skip wallpaper change - Other values cause an error.
Sources: src/wall/backend.rs:1-18, README.md:93-97
Sources: src/theme.rs:1-33, src/bar.rs:1-23, src/config.rs:1-17, README.md:93-105
Theme 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:1-18
Default behavior:
- Theming is enabled by default.
- Uses
wallustas command. - No extra arguments unless specified.
Bar struct:
#[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:1-17
theme::apply is called after wallpaper changes:
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:20-33
Key behavior:
- No-op if
theme.enableis false. - Ensures
~expansion on the wallpaper path. - Constructs command arguments starting with
"run"and image path, then appends extra args if configured. - Executes the theme command via
cmd::run. - If
bar.restart_on_themeistrue, callsbar::restart.
bar::restart restarts the configured bar process:
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-23
Steps:
- Use
pkillto terminate existing bar processes by command name. - Spawn a new bar process.
- Propagate errors with context.
graph TD
Step["wall step()"]
ThemeApply["theme::apply()"]
Expand["expand_tilde()"]
BuildArgs["build theme args"]
RunTheme["cmd::run(theme cmd)"]
RestartCheck["bar.restart_on_theme?"]
BarRestart["bar::restart()"]
Pkill["cmd::run(\"pkill\")"]
SpawnBar["spawn bar"]
Step --> ThemeApply
ThemeApply --> Expand
ThemeApply --> BuildArgs
BuildArgs --> RunTheme
RunTheme --> RestartCheck
RestartCheck --> BarRestart
BarRestart --> Pkill
BarRestart --> SpawnBar
This shows how a wallpaper change flows into theme application and optional bar restart.
Sources: src/wall/mod.rs:37-59, src/theme.rs:20-33, src/bar.rs:19-23, src/paths.rs:43-63, src/cmd.rs:1-18
Sources: src/session.rs:1-30, src/config.rs:1-17, src/wall/mod.rs:71-79,102-120
The session feature allows starting a collection of user-defined applications and then restoring the wallpaper.
Session 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:1-15
The README’s example:
[session]
exec = ["waybar", "swaync"]Sources: README.md:104-109
session::start:
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:17-30
Behavior:
- Loads configuration.
- Iterates through
session.execlist, spawning each command as a separate process. - Calls
wall::startto restore the last wallpaper (and theme/bar accordingly).
graph TD
SessCmd["session::start()"]
LoadCfg["Config::load()"]
ForEachExec["for each exec in cfg.session.exec"]
SpawnCmd["spawn process"]
WallStart["wall::start()"]
RestoreWall["restore wallpaper"]
ApplyTheme["apply theme + bar"]
SessCmd --> LoadCfg
LoadCfg --> ForEachExec
ForEachExec --> SpawnCmd
ForEachExec --> WallStart
WallStart --> RestoreWall
WallStart --> ApplyTheme
This flow illustrates how the session command orchestrates application startup followed by wallpaper and theme restoration.
Sources: src/session.rs:17-30, src/wall/mod.rs:71-79,102-120, src/theme.rs:20-33
Sources: src/notifications.rs:1-55, src/main.rs:26-64, src/paths.rs:25-39
Notifications provide feedback on command success or failure.
Notifications struct:
#[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:7-20
By default:
- Success notifications are disabled.
- Error notifications are enabled.
ensure_icon writes a bundled PNG to the config directory if needed:
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:1-6, src/notifications.rs:22-32, src/paths.rs:31-39
show uses notify_rust::Notification to display a desktop notification with the icon, app name, summary, body, and urgency.
Sources: src/notifications.rs:34-43
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:45-55
These are used in main after command execution:
- On success,
success(&cfg, "Wallpaper updated"). - On error,
error(&cfg, &format!("Update failed: {e:#}")).
Sources: src/main.rs:26-64
Sources: src/cmd.rs:1-18, src/wall/backend.rs:1-18, src/theme.rs:20-33, src/bar.rs:19-23
cmd::run provides a common wrapper for running external commands:
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-18
It is used in:
-
wall/backend.rsforhyprpaper,swww, andpkill. -
theme.rsfor the theming command. -
bar.rsto kill existing bar processes.
This centralization ensures consistent error handling and messaging for subprocess execution.
Sources: src/wall/backend.rs:1-18, src/theme.rs:20-33, src/bar.rs:19-23
Sources: README.md:16-59,90-116, src/wall/mod.rs:1-102, src/wall/backend.rs:1-18, src/theme.rs:1-33, src/bar.rs:1-23, src/session.rs:1-30, src/state.rs:1-53, src/notifications.rs:1-55, src/config.rs:1-48
| Feature | Description | Primary Modules |
|---|---|---|
| Wallpaper cycling | Next/previous wallpaper selection with wraparound and persistent state |
wall/mod.rs, state.rs, config.rs
|
| Wallpaper backend abstraction | Support for hyprpaper, swww, or none via a common interface |
wall/backend.rs, cmd.rs
|
| Theming integration | Optional theming command (default wallust) executed after wallpaper changes |
theme.rs, config.rs
|
| Bar restart on theme | Optional bar restart (default waybar) after theme application |
bar.rs, config.rs, theme.rs
|
| Session startup | Launch configured applications and then restore the wallpaper |
session.rs, wall/mod.rs, config.rs
|
| Config auto-generation | Auto-creates ~/.config/rice/config.toml from embedded example if missing |
config.rs, paths.rs, examples/config.toml (embedded) |
| State persistence | Stores last wallpaper and backend in ~/.cache/rice/state.json
|
state.rs, paths.rs
|
| Notifications | Configurable success/error desktop notifications with embedded icon |
notifications.rs, paths.rs, main.rs
|
| CLI entrypoint |
rice wall ..., rice bar restart, rice session start for keybind usage |
main.rs, cli.rs, README.md
|
rice provides a focused, modular CLI for managing wallpapers, theming, bar restarts, and session startup within a Hyprland desktop. Through clear separation of concerns—configuration, filesystem paths, wallpaper backends, theming, bar control, session orchestration, state persistence, notifications, and subprocess execution—it offers a simple yet extensible foundation that can be controlled entirely via a small set of CLI subcommands suitable for window manager keybindings.
Sources: README.md:16-59, src/main.rs:1-64, src/wall/mod.rs:1-102, src/theme.rs:1-33, src/session.rs:1-30, src/bar.rs:1-23, src/state.rs:1-53, src/notifications.rs:1-55