P2 slice 1: one executable (D-22)
The server and the viewer are libraries with run(prog, args), returning Exit(code) instead of ending the process; ledger-mcp and ledger-gui are one-line shims (the live books keep using them until the cutover). wanderbooks dispatches dev-stdio and gui to them in-process; a mode must come first and owns its flags. The gui feature (default) leaves the viewer out of the server-only build, which just server-only builds, clippies, tests, and checks for window libraries. The viewer's mutation recipe is retired (owner, 2026-10-18); its two musts are tested directly. Two review rounds; mutation on the diff (docs/testing.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
1 parent
18120727eb
commit
cab6e8a334
12 files changed
+832
-378
No files matched your search
@@ -23,6 +23,8 @@ mod widgets;
|
||||
pub use app::App;
|
||||
pub use data::Source;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// The vault key file named on the command line (`--vault-key-file`),
|
||||
/// which wins over a settings file's `[vault] key_file`, as it does for
|
||||
/// the server: books that keep their own settings never name it.
|
||||
@@ -37,3 +39,112 @@ pub fn set_vault_key_file(path: std::path::PathBuf) {
|
||||
pub fn vault_key_file() -> Option<std::path::PathBuf> {
|
||||
VAULT_KEY_FILE.get().cloned()
|
||||
}
|
||||
|
||||
/// How [`run`] stops when it can't go on: the reason is already printed
|
||||
/// on stderr, and the number is the process's exit code (2: a bad argument; 1: the demo books couldn't be built). A
|
||||
/// library never ends the process itself, so the books it opened are
|
||||
/// closed properly on the way out; the binaries turn this into the exit.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Exit(pub i32);
|
||||
|
||||
impl std::fmt::Display for Exit {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "exit code {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Exit {}
|
||||
|
||||
/// Run the viewer with `args` (the command line after the program name);
|
||||
/// `prog` names the program in the usage text (`ledger-gui`, or
|
||||
/// `wanderbooks gui`). It opens the books read-only and returns when the
|
||||
/// window closes. An unknown argument returns [`Exit`] (code 2).
|
||||
///
|
||||
/// `ledger-gui [--data-dir PATH] [--books-toml PATH] [--vault-key-file PATH] [--today YYYY-MM-DD] [--demo]`:
|
||||
/// with no flags it looks for `books-data/` and `wanderbooks.toml` in the
|
||||
/// current directory; `--demo` builds a demo company in a temp directory
|
||||
/// and opens that.
|
||||
pub fn run(
|
||||
prog: &str,
|
||||
args: impl IntoIterator<Item = String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut data_dir: Option<PathBuf> = None;
|
||||
let mut books_toml: Option<PathBuf> = None;
|
||||
let mut today = None;
|
||||
let mut demo = false;
|
||||
let mut args = args.into_iter();
|
||||
while let Some(a) = args.next() {
|
||||
match a.as_str() {
|
||||
"--data-dir" => data_dir = args.next().map(PathBuf::from),
|
||||
"--books-toml" => books_toml = args.next().map(PathBuf::from),
|
||||
"--vault-key-file" => {
|
||||
if let Some(k) = args.next() {
|
||||
crate::set_vault_key_file(PathBuf::from(k));
|
||||
}
|
||||
}
|
||||
"--today" => today = args.next().and_then(|d| d.parse().ok()),
|
||||
"--demo" => demo = true,
|
||||
"-h" | "--help" => {
|
||||
println!(
|
||||
"usage: {prog} [--data-dir PATH] [--books-toml PATH] [--vault-key-file PATH] [--today YYYY-MM-DD] [--demo]"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
other => {
|
||||
eprintln!("unknown argument {other}");
|
||||
return Err(Box::new(Exit(2)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if demo {
|
||||
let d = today.unwrap_or_else(|| chrono::Local::now().date_naive());
|
||||
let dir = std::env::temp_dir().join(format!("wanderbooks-demo-{d}"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
if let Err(e) = crate::demo::build(&dir, d) {
|
||||
eprintln!("building the demo books failed: {e}");
|
||||
return Err(Box::new(Exit(1)));
|
||||
}
|
||||
eprintln!("demo books: {}", dir.display());
|
||||
data_dir = Some(dir.join("books-data"));
|
||||
books_toml = Some(dir.join("wanderbooks.toml"));
|
||||
}
|
||||
let data_dir = data_dir.unwrap_or_else(|| PathBuf::from("books-data"));
|
||||
let books_toml = books_toml.or_else(|| {
|
||||
let guess = data_dir
|
||||
.parent()
|
||||
.unwrap_or(&data_dir)
|
||||
.join("wanderbooks.toml");
|
||||
guess.exists().then_some(guess)
|
||||
});
|
||||
let source = Source {
|
||||
data_dir,
|
||||
books_toml,
|
||||
today,
|
||||
};
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("wanderbooks")
|
||||
.with_app_id("wanderbooks")
|
||||
.with_inner_size([1440.0, 920.0])
|
||||
// Small on purpose: tiling window managers hand out small slots
|
||||
// regardless, and every screen lays itself out to fit.
|
||||
.with_min_inner_size([360.0, 400.0]),
|
||||
..Default::default()
|
||||
};
|
||||
Ok(eframe::run_native(
|
||||
"wanderbooks",
|
||||
options,
|
||||
Box::new(move |cc| Ok(Box::new(App::new(cc, source)))),
|
||||
)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod exit_tests {
|
||||
use super::Exit;
|
||||
|
||||
#[test]
|
||||
fn an_exit_says_its_code() {
|
||||
assert_eq!(Exit(2).to_string(), "exit code 2");
|
||||
assert_eq!(Exit(1).to_string(), "exit code 1");
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,12 @@
|
||||
//! `ledger-gui [--data-dir PATH] [--books-toml PATH] [--vault-key-file PATH] [--today YYYY-MM-DD] [--demo]`
|
||||
//!
|
||||
//! Opens the books read-only and follows them live. With no flags it looks
|
||||
//! for `books-data/` and `wanderbooks.toml` in the current directory.
|
||||
//! `--demo` builds a demo company in a temp directory and opens that.
|
||||
//! `ledger-gui`: the read-only, live viewer. The same viewer is
|
||||
//! `wanderbooks gui`; both call [`ledger_gui::run`].
|
||||
|
||||
use ledger_gui::{App, Source};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
let mut data_dir: Option<PathBuf> = None;
|
||||
let mut books_toml: Option<PathBuf> = None;
|
||||
let mut today = None;
|
||||
let mut demo = false;
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(a) = args.next() {
|
||||
match a.as_str() {
|
||||
"--data-dir" => data_dir = args.next().map(PathBuf::from),
|
||||
"--books-toml" => books_toml = args.next().map(PathBuf::from),
|
||||
"--vault-key-file" => {
|
||||
if let Some(k) = args.next() {
|
||||
ledger_gui::set_vault_key_file(PathBuf::from(k));
|
||||
}
|
||||
}
|
||||
"--today" => today = args.next().and_then(|d| d.parse().ok()),
|
||||
"--demo" => demo = true,
|
||||
"-h" | "--help" => {
|
||||
println!(
|
||||
"usage: ledger-gui [--data-dir PATH] [--books-toml PATH] [--vault-key-file PATH] [--today YYYY-MM-DD] [--demo]"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
other => {
|
||||
eprintln!("unknown argument {other}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
match ledger_gui::run("ledger-gui", std::env::args().skip(1)) {
|
||||
Err(e) => match e.downcast_ref::<ledger_gui::Exit>() {
|
||||
Some(exit) => std::process::exit(exit.0),
|
||||
None => Err(e),
|
||||
},
|
||||
done => done,
|
||||
}
|
||||
if demo {
|
||||
let d = today.unwrap_or_else(|| chrono::Local::now().date_naive());
|
||||
let dir = std::env::temp_dir().join(format!("wanderbooks-demo-{d}"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
if let Err(e) = ledger_gui::demo::build(&dir, d) {
|
||||
eprintln!("building the demo books failed: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
eprintln!("demo books: {}", dir.display());
|
||||
data_dir = Some(dir.join("books-data"));
|
||||
books_toml = Some(dir.join("wanderbooks.toml"));
|
||||
}
|
||||
let data_dir = data_dir.unwrap_or_else(|| PathBuf::from("books-data"));
|
||||
let books_toml = books_toml.or_else(|| {
|
||||
let guess = data_dir
|
||||
.parent()
|
||||
.unwrap_or(&data_dir)
|
||||
.join("wanderbooks.toml");
|
||||
guess.exists().then_some(guess)
|
||||
});
|
||||
let source = Source {
|
||||
data_dir,
|
||||
books_toml,
|
||||
today,
|
||||
};
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("wanderbooks")
|
||||
.with_app_id("wanderbooks")
|
||||
.with_inner_size([1440.0, 920.0])
|
||||
// Small on purpose: tiling window managers hand out small slots
|
||||
// regardless, and every screen lays itself out to fit.
|
||||
.with_min_inner_size([360.0, 400.0]),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"wanderbooks",
|
||||
options,
|
||||
Box::new(move |cc| Ok(Box::new(App::new(cc, source)))),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user