Repository navigation
development and contribution guide
Related Files:
README.mdrustfmt.tomlCargo.tomlsrc/main.rssrc/wall/mod.rssrc/wall/backend.rssrc/theme.rssrc/bar.rssrc/session.rssrc/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:
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
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 testSources: README.md:167-181
Key expectations for contributors:
- Use
cargo fmtandcargo clippybefore 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
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
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.rsparses 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 viacmd::runorstd::process::Command. - Optional desktop notifications are sent via
notify-rust.
| 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
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
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
Sources: src/main.rs:1-32, src/cli.rs:1-37
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
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.
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
Sources: src/main.rs:10-47, src/cli.rs:1-37, src/notifications.rs:36-44, src/config.rs:20-39
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::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
| 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.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
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
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
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
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
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
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
Sources: src/theme.rs:1-34, src/bar.rs:1-24, src/config.rs:9-18, README.md:29-38
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
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 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
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
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
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.rsforhyprctlandswww. -
theme.rsfor the theme command (e.g.wallust). -
bar.rsforpkill.
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
- Use
cargo fmtto adhere to formatting rules;rustfmt.tomldefines any custom style, if present. - Use
cargo clippyto catch lints before submitting changes.
Sources: rustfmt.toml, README.md:167-181
When adding a new CLI action:
- Add a variant to the appropriate enum in
cli.rs. - Update the
matchinmain.rsto dispatch to your new function. - Implement your logic in a dedicated module (or appropriate existing module).
- Decide whether to use
cmd::runfor synchronous commands orstd::process::Commandfor async spawns.
Sources: src/cli.rs:1-37, src/main.rs:10-26, src/cmd.rs:1-15, src/session.rs:18-27
- Extend
Configwith new fields as needed, ensuring they are deserializable. - Add appropriate defaults via nested structs using
serde(default)or by updatingexamples/config.tomland README snippets. - If the config file is absent, it is auto-created from
examples/config.tomlthrough the embeddedDEFAULTstring.
Sources: src/config.rs:1-18, src/config.rs:20-39, README.md:112-141
- Use
State::load()andState::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
- Return
anyhow::Result<()>from your functions to integrate with global error handling. - On error,
mainprints 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
These are directly observable inconsistencies that contributors might address:
-
main.rscontains duplicatedErr(e)arms and strayOk(())text inside theErrbranch, indicating a merge or copy-paste error. -
wall/mod.rsshows duplicated partial fragments forstart,next, andprev, though a complete final version is present at the end. -
wall/backend.rshas a typo:"unkown backend {backend}". -
error.rsis 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
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
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
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
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
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