summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-01 16:26:21 +0100
committersalmonik <salmonik@tuta.io>2026-11-01 16:26:21 +0100
commita036e7eb46886ff7fe88255ae26c327f63ec4bfa (patch)
tree494911ce0d394041c00eff276632e4f5f48f3fb5 /src
parentv3.2.0: straight and split bets anywhere on the felt; add history command (diff)
downloadzagan-bet-a036e7eb46886ff7fe88255ae26c327f63ec4bfa.tar.gz
zagan-bet-a036e7eb46886ff7fe88255ae26c327f63ec4bfa.zip
v3.3.0: keep client.db in the XDG data dir, add -d/--data-dir
Diffstat (limited to '')
-rw-r--r--src/bin/client/main.rs35
-rw-r--r--src/bin/client/repl.rs12
-rw-r--r--src/client/mod.rs1
-rw-r--r--src/client/paths.rs103
4 files changed, 139 insertions, 12 deletions
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;
5mod tls; 5mod tls;
6 6
7use repl::ClientState; 7use repl::ClientState;
8use zagan_bet::client::paths::{database_path, default_data_dir, parse_args, USAGE};
8 9
9fn main() { 10fn main() {
10 let args: Vec<String> = std::env::args().collect(); 11 let args = match parse_args(std::env::args().skip(1)) {
11 let unsafe_mode = args.iter().any(|arg| arg == "--unsafe"); 12 Ok(args) => args,
13 Err(message) => {
14 eprintln!("{message}");
15 std::process::exit(2);
16 }
17 };
18
19 if args.help {
20 println!("{USAGE}");
21 return;
22 }
12 23
13 // Every game board is drawn in place, so refuse a terminal that cannot fit 24 // Every game board is drawn in place, so refuse a terminal that cannot fit
14 // them before touching the network or the terminal mode. 25 // them before touching the network or the terminal mode.
@@ -25,19 +36,33 @@ fn main() {
25 std::process::exit(1); 36 std::process::exit(1);
26 } 37 }
27 38
28 if unsafe_mode { 39 if args.unsafe_mode {
29 println!("WARNING: Running in UNSAFE mode - certificate verification disabled!"); 40 println!("WARNING: Running in UNSAFE mode - certificate verification disabled!");
30 } 41 }
31 42
32 connection::set_unsafe_mode(unsafe_mode); 43 connection::set_unsafe_mode(args.unsafe_mode);
33 44
34 let mut state = ClientState::new(); 45 let data_dir = args.data_dir.unwrap_or_else(default_data_dir);
46 if let Err(error) = std::fs::create_dir_all(&data_dir) {
47 eprintln!("Cannot create {}: {error}", data_dir.display());
48 std::process::exit(1);
49 }
50 let db_path = database_path(&data_dir);
51
52 let mut state = match ClientState::new(&db_path) {
53 Ok(state) => state,
54 Err(error) => {
55 eprintln!("{error}");
56 std::process::exit(1);
57 }
58 };
35 if let Some(host) = state.addr.clone() { 59 if let Some(host) = state.addr.clone() {
36 state.connect_to(&host); 60 state.connect_to(&host);
37 } 61 }
38 62
39 println!("Welcome to Zagan-Bet Client!"); 63 println!("Welcome to Zagan-Bet Client!");
40 println!("Type 'help' for available commands."); 64 println!("Type 'help' for available commands.");
65 println!("Data: {}", db_path.display());
41 66
42 state.run(); 67 state.run();
43} 68}
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 {
43} 43}
44 44
45impl ClientState { 45impl ClientState {
46 pub fn new() -> Self { 46 pub fn new(db_path: &std::path::Path) -> Result<Self, String> {
47 let conn = rusqlite::Connection::open( 47 let conn = rusqlite::Connection::open(db_path)
48 std::env::var("CLIENT_DB").unwrap_or_else(|_| "client.db".to_string()), 48 .map_err(|error| format!("cannot open {}: {error}", db_path.display()))?;
49 )
50 .unwrap();
51 database::init_db(&conn); 49 database::init_db(&conn);
52 let default_ip = "127.0.0.1".to_string(); 50 let default_ip = "127.0.0.1".to_string();
53 let default_port = 11525; 51 let default_port = 11525;
@@ -55,7 +53,7 @@ impl ClientState {
55 let default_host = format!("{}:{}", default_ip, default_port); 53 let default_host = format!("{}:{}", default_ip, default_port);
56 let addr = last_host.or(Some(default_host)); 54 let addr = last_host.or(Some(default_host));
57 55
58 Self { 56 Ok(Self {
59 stream: None, 57 stream: None,
60 conn, 58 conn,
61 addr, 59 addr,
@@ -66,7 +64,7 @@ impl ClientState {
66 prompt_text: String::new(), 64 prompt_text: String::new(),
67 terminal_size: crossterm::terminal::size().unwrap_or((MIN_COLUMNS, MIN_ROWS)), 65 terminal_size: crossterm::terminal::size().unwrap_or((MIN_COLUMNS, MIN_ROWS)),
68 size_warned: None, 66 size_warned: None,
69 } 67 })
70 } 68 }
71 69
72 pub fn connect_to(&mut self, addr: &str) { 70 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 @@
1pub mod core; 1pub mod core;
2pub mod database; 2pub mod database;
3pub mod games; 3pub mod games;
4pub mod paths;
4pub mod ui; 5pub 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 @@
1//! Client data location and command-line arguments.
2
3use std::ffi::OsStr;
4use std::path::{Path, PathBuf};
5
6pub const APP_DIR: &str = "zagan-bet";
7pub const DATABASE_FILE: &str = "client.db";
8
9pub const USAGE: &str = "\
10usage: client [-d <dir>] [--unsafe]
11
12 -d, --data-dir <dir> where to keep client.db (default: XDG data dir)
13 --unsafe skip certificate verification
14 -h, --help show this message
15
16The database lives in $XDG_DATA_HOME/zagan-bet/client.db, falling back to
17~/.local/share/zagan-bet/client.db (or %APPDATA%\\zagan-bet on Windows).";
18
19#[derive(Debug, Default, PartialEq, Eq)]
20pub struct ClientArgs {
21 pub unsafe_mode: bool,
22 pub data_dir: Option<PathBuf>,
23 pub help: bool,
24}
25
26/// Parse arguments, without the program name.
27pub fn parse_args(args: impl IntoIterator<Item = String>) -> Result<ClientArgs, String> {
28 let mut parsed = ClientArgs::default();
29 let mut args = args.into_iter();
30
31 while let Some(argument) = args.next() {
32 match argument.as_str() {
33 "-d" | "--data-dir" => {
34 let Some(value) = args.next() else {
35 return Err(format!("{argument} needs a directory\n{USAGE}"));
36 };
37 if value.is_empty() {
38 return Err(format!("{argument} needs a directory\n{USAGE}"));
39 }
40 parsed.data_dir = Some(PathBuf::from(value));
41 }
42 "--unsafe" => parsed.unsafe_mode = true,
43 "-h" | "--help" => parsed.help = true,
44 _ => {
45 if let Some(value) = argument.strip_prefix("--data-dir=") {
46 if value.is_empty() {
47 return Err(format!("--data-dir needs a directory\n{USAGE}"));
48 }
49 parsed.data_dir = Some(PathBuf::from(value));
50 } else {
51 return Err(format!("unknown argument: {argument}\n{USAGE}"));
52 }
53 }
54 }
55 }
56
57 Ok(parsed)
58}
59
60/// `$XDG_DATA_HOME/zagan-bet`, `%APPDATA%\zagan-bet`, or
61/// `$HOME/.local/share/zagan-bet`, in that order.
62pub fn data_dir_from(
63 xdg_data_home: Option<&OsStr>,
64 home: Option<&OsStr>,
65 appdata: Option<&OsStr>,
66) -> PathBuf {
67 if let Some(xdg) = xdg_data_home {
68 let path = Path::new(xdg);
69 // The XDG spec says a relative XDG_DATA_HOME is invalid.
70 if !xdg.is_empty() && path.is_absolute() {
71 return path.join(APP_DIR);
72 }
73 }
74
75 if let Some(appdata) = appdata {
76 let path = Path::new(appdata);
77 if !appdata.is_empty() && path.is_absolute() {
78 return path.join(APP_DIR);
79 }
80 }
81
82 if let Some(home) = home {
83 let path = Path::new(home);
84 if !home.is_empty() && path.is_absolute() {
85 return path.join(".local").join("share").join(APP_DIR);
86 }
87 }
88
89 // No usable home directory: keep the old behaviour and stay local.
90 PathBuf::from(APP_DIR)
91}
92
93pub fn default_data_dir() -> PathBuf {
94 data_dir_from(
95 std::env::var_os("XDG_DATA_HOME").as_deref(),
96 std::env::var_os("HOME").as_deref(),
97 std::env::var_os("APPDATA").as_deref(),
98 )
99}
100
101pub fn database_path(data_dir: &Path) -> PathBuf {
102 data_dir.join(DATABASE_FILE)
103}