From a036e7eb46886ff7fe88255ae26c327f63ec4bfa Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 16:26:21 +0100 Subject: v3.3.0: keep client.db in the XDG data dir, add -d/--data-dir --- src/bin/client/main.rs | 35 ++++++++++++++--- src/bin/client/repl.rs | 12 +++--- src/client/mod.rs | 1 + src/client/paths.rs | 103 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 src/client/paths.rs (limited to 'src') diff --git a/src/bin/client/main.rs b/src/bin/client/main.rs index 9018026..3407380 100644 --- a/src/bin/client/main.rs +++ b/src/bin/client/main.rs @@ -5,10 +5,21 @@ mod repl; mod tls; use repl::ClientState; +use zagan_bet::client::paths::{database_path, default_data_dir, parse_args, USAGE}; fn main() { - let args: Vec = std::env::args().collect(); - let unsafe_mode = args.iter().any(|arg| arg == "--unsafe"); + let args = match parse_args(std::env::args().skip(1)) { + Ok(args) => args, + Err(message) => { + eprintln!("{message}"); + std::process::exit(2); + } + }; + + if args.help { + println!("{USAGE}"); + return; + } // Every game board is drawn in place, so refuse a terminal that cannot fit // them before touching the network or the terminal mode. @@ -25,19 +36,33 @@ fn main() { std::process::exit(1); } - if unsafe_mode { + if args.unsafe_mode { println!("WARNING: Running in UNSAFE mode - certificate verification disabled!"); } - connection::set_unsafe_mode(unsafe_mode); + connection::set_unsafe_mode(args.unsafe_mode); - let mut state = ClientState::new(); + let data_dir = args.data_dir.unwrap_or_else(default_data_dir); + if let Err(error) = std::fs::create_dir_all(&data_dir) { + eprintln!("Cannot create {}: {error}", data_dir.display()); + std::process::exit(1); + } + let db_path = database_path(&data_dir); + + let mut state = match ClientState::new(&db_path) { + Ok(state) => state, + Err(error) => { + eprintln!("{error}"); + std::process::exit(1); + } + }; if let Some(host) = state.addr.clone() { state.connect_to(&host); } println!("Welcome to Zagan-Bet Client!"); println!("Type 'help' for available commands."); + println!("Data: {}", db_path.display()); state.run(); } diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index bf02c8d..1debff1 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs @@ -43,11 +43,9 @@ pub struct ClientState { } impl ClientState { - pub fn new() -> Self { - let conn = rusqlite::Connection::open( - std::env::var("CLIENT_DB").unwrap_or_else(|_| "client.db".to_string()), - ) - .unwrap(); + pub fn new(db_path: &std::path::Path) -> Result { + let conn = rusqlite::Connection::open(db_path) + .map_err(|error| format!("cannot open {}: {error}", db_path.display()))?; database::init_db(&conn); let default_ip = "127.0.0.1".to_string(); let default_port = 11525; @@ -55,7 +53,7 @@ impl ClientState { let default_host = format!("{}:{}", default_ip, default_port); let addr = last_host.or(Some(default_host)); - Self { + Ok(Self { stream: None, conn, addr, @@ -66,7 +64,7 @@ impl ClientState { prompt_text: String::new(), terminal_size: crossterm::terminal::size().unwrap_or((MIN_COLUMNS, MIN_ROWS)), size_warned: None, - } + }) } pub fn connect_to(&mut self, addr: &str) { diff --git a/src/client/mod.rs b/src/client/mod.rs index dc8423b..6841be3 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,4 +1,5 @@ pub mod core; pub mod database; pub mod games; +pub mod paths; pub mod ui; diff --git a/src/client/paths.rs b/src/client/paths.rs new file mode 100644 index 0000000..b6b8d1f --- /dev/null +++ b/src/client/paths.rs @@ -0,0 +1,103 @@ +//! Client data location and command-line arguments. + +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +pub const APP_DIR: &str = "zagan-bet"; +pub const DATABASE_FILE: &str = "client.db"; + +pub const USAGE: &str = "\ +usage: client [-d ] [--unsafe] + + -d, --data-dir where to keep client.db (default: XDG data dir) + --unsafe skip certificate verification + -h, --help show this message + +The database lives in $XDG_DATA_HOME/zagan-bet/client.db, falling back to +~/.local/share/zagan-bet/client.db (or %APPDATA%\\zagan-bet on Windows)."; + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct ClientArgs { + pub unsafe_mode: bool, + pub data_dir: Option, + pub help: bool, +} + +/// Parse arguments, without the program name. +pub fn parse_args(args: impl IntoIterator) -> Result { + let mut parsed = ClientArgs::default(); + let mut args = args.into_iter(); + + while let Some(argument) = args.next() { + match argument.as_str() { + "-d" | "--data-dir" => { + let Some(value) = args.next() else { + return Err(format!("{argument} needs a directory\n{USAGE}")); + }; + if value.is_empty() { + return Err(format!("{argument} needs a directory\n{USAGE}")); + } + parsed.data_dir = Some(PathBuf::from(value)); + } + "--unsafe" => parsed.unsafe_mode = true, + "-h" | "--help" => parsed.help = true, + _ => { + if let Some(value) = argument.strip_prefix("--data-dir=") { + if value.is_empty() { + return Err(format!("--data-dir needs a directory\n{USAGE}")); + } + parsed.data_dir = Some(PathBuf::from(value)); + } else { + return Err(format!("unknown argument: {argument}\n{USAGE}")); + } + } + } + } + + Ok(parsed) +} + +/// `$XDG_DATA_HOME/zagan-bet`, `%APPDATA%\zagan-bet`, or +/// `$HOME/.local/share/zagan-bet`, in that order. +pub fn data_dir_from( + xdg_data_home: Option<&OsStr>, + home: Option<&OsStr>, + appdata: Option<&OsStr>, +) -> PathBuf { + if let Some(xdg) = xdg_data_home { + let path = Path::new(xdg); + // The XDG spec says a relative XDG_DATA_HOME is invalid. + if !xdg.is_empty() && path.is_absolute() { + return path.join(APP_DIR); + } + } + + if let Some(appdata) = appdata { + let path = Path::new(appdata); + if !appdata.is_empty() && path.is_absolute() { + return path.join(APP_DIR); + } + } + + if let Some(home) = home { + let path = Path::new(home); + if !home.is_empty() && path.is_absolute() { + return path.join(".local").join("share").join(APP_DIR); + } + } + + // No usable home directory: keep the old behaviour and stay local. + PathBuf::from(APP_DIR) +} + +pub fn default_data_dir() -> PathBuf { + data_dir_from( + std::env::var_os("XDG_DATA_HOME").as_deref(), + std::env::var_os("HOME").as_deref(), + std::env::var_os("APPDATA").as_deref(), + ) +} + +pub fn database_path(data_dir: &Path) -> PathBuf { + data_dir.join(DATABASE_FILE) +} -- cgit v1.2.3