diff options
Diffstat (limited to '')
| -rw-r--r-- | src/bin/client/main.rs | 35 | ||||
| -rw-r--r-- | src/bin/client/repl.rs | 12 | ||||
| -rw-r--r-- | src/client/mod.rs | 1 | ||||
| -rw-r--r-- | src/client/paths.rs | 103 |
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; | |||
| 5 | mod tls; | 5 | mod tls; |
| 6 | 6 | ||
| 7 | use repl::ClientState; | 7 | use repl::ClientState; |
| 8 | use zagan_bet::client::paths::{database_path, default_data_dir, parse_args, USAGE}; | ||
| 8 | 9 | ||
| 9 | fn main() { | 10 | fn 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 | ||
| 45 | impl ClientState { | 45 | impl 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 @@ | |||
| 1 | pub mod core; | 1 | pub mod core; |
| 2 | pub mod database; | 2 | pub mod database; |
| 3 | pub mod games; | 3 | pub mod games; |
| 4 | pub mod paths; | ||
| 4 | pub mod ui; | 5 | 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 @@ | |||
| 1 | //! Client data location and command-line arguments. | ||
| 2 | |||
| 3 | use std::ffi::OsStr; | ||
| 4 | use std::path::{Path, PathBuf}; | ||
| 5 | |||
| 6 | pub const APP_DIR: &str = "zagan-bet"; | ||
| 7 | pub const DATABASE_FILE: &str = "client.db"; | ||
| 8 | |||
| 9 | pub const USAGE: &str = "\ | ||
| 10 | usage: 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 | |||
| 16 | The 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)] | ||
| 20 | pub 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. | ||
| 27 | pub 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. | ||
| 62 | pub 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 | |||
| 93 | pub 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 | |||
| 101 | pub fn database_path(data_dir: &Path) -> PathBuf { | ||
| 102 | data_dir.join(DATABASE_FILE) | ||
| 103 | } | ||
