diff options
| author | Gustav Sörnäs <gustav@sornas.net> | 2020-10-21 01:01:33 +0200 |
|---|---|---|
| committer | Gustav Sörnäs <gustav@sornas.net> | 2020-10-21 01:01:33 +0200 |
| commit | d58c2aad6844789c24b93387f9b61e4ab8d2a2d3 (patch) | |
| tree | a6f43da526bf84f96f79f96199f0bca33deb66f8 | |
| parent | ec323df881c3aad82ed963fbfbdd9ade9f96e830 (diff) | |
| parent | 6a136ac842dd601ce7f68566c27b5262d221872c (diff) | |
| download | mum-d58c2aad6844789c24b93387f9b61e4ab8d2a2d3.tar.gz | |
Merge branch 'config-file' into 'main'
Config file
Closes #1 and #27
See merge request gustav/mum!16
| -rw-r--r-- | mumctl/src/main.rs | 311 | ||||
| -rw-r--r-- | mumd/Cargo.toml | 1 | ||||
| -rw-r--r-- | mumd/src/state.rs | 30 | ||||
| -rw-r--r-- | mumlib/Cargo.toml | 1 | ||||
| -rw-r--r-- | mumlib/src/command.rs | 1 | ||||
| -rw-r--r-- | mumlib/src/config.rs | 99 | ||||
| -rw-r--r-- | mumlib/src/lib.rs | 1 | ||||
| -rw-r--r-- | usage.org | 30 |
8 files changed, 391 insertions, 83 deletions
diff --git a/mumctl/src/main.rs b/mumctl/src/main.rs index f4b8139..6e97296 100644 --- a/mumctl/src/main.rs +++ b/mumctl/src/main.rs @@ -2,6 +2,8 @@ use clap::{App, AppSettings, Arg, Shell, SubCommand}; use colored::Colorize; use ipc_channel::ipc::{self, IpcSender}; use mumlib::command::{Command, CommandResponse}; +use mumlib::config; +use mumlib::config::ServerConfig; use mumlib::setup_logger; use mumlib::state::Channel; use std::{fs, io, iter}; @@ -18,6 +20,10 @@ macro_rules! err_print { fn main() { setup_logger(io::stderr(), true); + let mut config = config::read_default_cfg(); + if config.is_none() { + println!("{} unable to find config file", "error:".red()); + } let mut app = App::new("mumctl") .setting(AppSettings::ArgRequiredElseHelp) @@ -26,59 +32,90 @@ fn main() { .setting(AppSettings::ArgRequiredElseHelp) .subcommand( SubCommand::with_name("connect") - .setting(AppSettings::ArgRequiredElseHelp) - .arg(Arg::with_name("host").required(true).index(1)) - .arg(Arg::with_name("username").required(true).index(2)) - .arg(Arg::with_name("port").short("p").long("port").takes_value(true)), - ) - .subcommand(SubCommand::with_name("disconnect")), - ) + .arg(Arg::with_name("host").required(true)) + .arg(Arg::with_name("username").required(true)) + .arg(Arg::with_name("port") + .long("port") + .short("p") + .takes_value(true))) + .subcommand( + SubCommand::with_name("disconnect")) + .subcommand( + SubCommand::with_name("config") + .arg(Arg::with_name("server_name").required(true)) + .arg(Arg::with_name("var_name")) + .arg(Arg::with_name("var_value"))) + .subcommand( + SubCommand::with_name("rename") + .arg(Arg::with_name("prev_name").required(true)) + .arg(Arg::with_name("next_name").required(true))) + .subcommand( + SubCommand::with_name("add") + .arg(Arg::with_name("name").required(true)) + .arg(Arg::with_name("host").required(true)) + .arg(Arg::with_name("port") + .long("port") + .takes_value(true) + .default_value("64738")) + .arg(Arg::with_name("username") + .long("username") + .takes_value(true)) + .arg(Arg::with_name("password") + .long("password") + .takes_value(true))) + .subcommand( + SubCommand::with_name("remove") + .arg(Arg::with_name("name").required(true)))) .subcommand( SubCommand::with_name("channel") .setting(AppSettings::ArgRequiredElseHelp) .subcommand( SubCommand::with_name("list") - .arg(Arg::with_name("short").short("s").long("short")), - ) + .arg(Arg::with_name("short") + .long("short") + .short("s"))) .subcommand( - SubCommand::with_name("connect").arg(Arg::with_name("channel").required(true)), - ), - ) - .subcommand(SubCommand::with_name("status")) - .subcommand(SubCommand::with_name("config") - .arg(Arg::with_name("name") - .required(true)) - .arg(Arg::with_name("value") - .required(true))) + SubCommand::with_name("connect") + .arg(Arg::with_name("channel").required(true)))) + .subcommand( + SubCommand::with_name("status")) + .subcommand( + SubCommand::with_name("config") + .arg(Arg::with_name("name").required(true)) + .arg(Arg::with_name("value").required(true))) + .subcommand( + SubCommand::with_name("config-reload")) .subcommand(SubCommand::with_name("completions") - .arg(Arg::with_name("zsh") - .long("zsh")) - .arg(Arg::with_name("bash") - .long("bash")) - .arg(Arg::with_name("fish") - .long("fish"))); + .arg(Arg::with_name("zsh") + .long("zsh")) + .arg(Arg::with_name("bash") + .long("bash")) + .arg(Arg::with_name("fish") + .long("fish"))); let matches = app.clone().get_matches(); if let Some(matches) = matches.subcommand_matches("server") { if let Some(matches) = matches.subcommand_matches("connect") { - let host = matches.value_of("host").unwrap(); - let username = matches.value_of("username").unwrap(); - let port = match matches.value_of("port").map(|e| e.parse()) { - None => Some(64738), - Some(Err(_)) => None, - Some(Ok(v)) => Some(v), - }; - if let Some(port) = port { - err_print!(send_command(Command::ServerConnect { - host: host.to_string(), - port, - username: username.to_string(), - accept_invalid_cert: true, //TODO - })); - } + match_server_connect(matches); } else if let Some(_) = matches.subcommand_matches("disconnect") { err_print!(send_command(Command::ServerDisconnect)); + } else if let Some(matches) = matches.subcommand_matches("config") { + if let Some(config) = &mut config { + match_server_config(matches, config); + } + } else if let Some(matches) = matches.subcommand_matches("rename") { + if let Some(config) = &mut config { + match_server_rename(matches, config); + } + } else if let Some(matches) = matches.subcommand_matches("remove") { + if let Some(config) = &mut config { + match_server_remove(matches, config); + } + } else if let Some(matches) = matches.subcommand_matches("add") { + if let Some(config) = &mut config { + match_server_add(matches, config); + } } } else if let Some(matches) = matches.subcommand_matches("channel") { if let Some(_matches) = matches.subcommand_matches("list") { @@ -96,34 +133,12 @@ fn main() { channel_identifier: matches.value_of("channel").unwrap().to_string() })); } - } else if let Some(_matches) = matches.subcommand_matches("status") { + } else if let Some(_) = matches.subcommand_matches("status") { match send_command(Command::Status) { Ok(res) => match res { Some(CommandResponse::Status { server_state }) => { - println!( - "Connected to {} as {}", - server_state.host, server_state.username - ); - let own_channel = server_state - .channels - .iter() - .find(|e| e.users.iter().any(|e| e.name == server_state.username)) - .unwrap(); - println!( - "Currently in {} with {} other client{}:", - own_channel.name, - own_channel.users.len() - 1, - if own_channel.users.len() == 2 { - "" - } else { - "s" - } - ); - println!("{}{}", INDENTATION, own_channel.name); - for user in &own_channel.users { - println!("{}{}{}", INDENTATION, INDENTATION, user); - } - } + parse_status(&server_state); + }, _ => unreachable!(), }, Err(e) => println!("{} {}", "error:".red(), e), @@ -141,6 +156,8 @@ fn main() { println!("{} Unknown config value {}", "error:".red(), name); } } + } else if matches.subcommand_matches("config-reload").is_some() { + send_command(Command::ConfigReload).unwrap(); } else if let Some(matches) = matches.subcommand_matches("completions") { app.gen_completions_to( "mumctl", @@ -153,6 +170,172 @@ fn main() { ); return; }; + + if let Some(config) = config { + config.write_default_cfg(); + } +} + +fn match_server_connect(matches : &clap::ArgMatches<>) { + let host = matches.value_of("host").unwrap(); + let username = matches.value_of("username").unwrap(); + let port = match matches.value_of("port").map(|e| e.parse()) { + None => Some(64738), + Some(Err(_)) => None, + Some(Ok(v)) => Some(v), + }; + if let Some(port) = port { + err_print!(send_command(Command::ServerConnect { + host: host.to_string(), + port, + username: username.to_string(), + accept_invalid_cert: true, //TODO + })); + } +} + +fn match_server_config(matches: &clap::ArgMatches<>, config: &mut mumlib::config::Config) { + let server_name = matches.value_of("server_name").unwrap(); + if let Some(servers) = &mut config.servers { + let server = servers + .iter_mut() + .find(|s| s.name == server_name); + if let Some(server) = server { + if let Some(var_name) = matches.value_of("var_name") { + if let Some(var_value) = matches.value_of("var_value") { + // save var_value in var_name (if it is valid) + match var_name { + "name" => { + println!("{} use mumctl server rename instead!", "error:".red()); + }, + "host" => { + server.host = var_value.to_string(); + }, + "port" => { + server.port = Some(var_value.parse().unwrap()); + }, + "username" => { + server.username = Some(var_value.to_string()); + }, + "password" => { + server.password = Some(var_value.to_string()); //TODO ask stdin if empty + }, + _ => { + println!("{} variable {} not found", "error:".red(), var_name); + }, + }; + } else { // var_value is None + // print value of var_name + println!("{}", match var_name { + "name" => { server.name.to_string() }, + "host" => { server.host.to_string() }, + "port" => { server.port.map(|s| s.to_string()).unwrap_or(format!("{} not set", "error:".red())) }, + "username" => { server.username.as_ref().map(|s| s.to_string()).unwrap_or(format!("{} not set", "error:".red())) }, + "password" => { server.password.as_ref().map(|s| s.to_string()).unwrap_or(format!("{} not set", "error:".red())) }, + _ => { format!("{} unknown variable", "error:".red()) }, + }); + } + } else { // var_name is None + // print server config + print!("{}{}{}{}", + format!("host: {}\n", server.host.to_string()), + server.port.map(|s| format!("port: {}\n", s)).unwrap_or("".to_string()), + server.username.as_ref().map(|s| format!("username: {}\n", s)).unwrap_or("".to_string()), + server.password.as_ref().map(|s| format!("password: {}\n", s)).unwrap_or("".to_string()), + ) + } + } else { // server is None + println!("{} server {} not found", "error:".red(), server_name); + } + } else { // servers is None + println!("{} no servers found in configuration", "error:".red()); + } +} + +fn match_server_rename(matches: &clap::ArgMatches<>, config: &mut mumlib::config::Config) { + if let Some(servers) = &mut config.servers { + let prev_name = matches.value_of("prev_name").unwrap(); + let next_name = matches.value_of("next_name").unwrap(); + if let Some(server) = servers + .iter_mut() + .find(|s| s.name == prev_name) { + server.name = next_name.to_string(); + } else { + println!("{} server {} not found", "error:".red(), prev_name); + } + } +} + +fn match_server_remove(matches: &clap::ArgMatches<>, config: &mut mumlib::config::Config) { + let name = matches.value_of("name").unwrap(); + if let Some(servers) = &mut config.servers { + match servers.iter().position(|server| server.name == name) { + Some(idx) => { + servers.remove(idx); + }, + None => { + println!("{} server {} not found", "error:".red(), name); + } + }; + } else { + println!("{} no servers found in configuration", "error:".red()); + } +} + +fn match_server_add(matches: &clap::ArgMatches<>, config: &mut mumlib::config::Config) { + let name = matches.value_of("name").unwrap().to_string(); + let host = matches.value_of("host").unwrap().to_string(); + // optional arguments map None to None + let port = matches.value_of("port").map(|s| s.parse().unwrap()); + let username = matches.value_of("username").map(|s| s.to_string()); + let password = matches.value_of("password").map(|s| s.to_string()); + if let Some(servers) = &mut config.servers { + if servers.iter().any(|s| s.name == name) { + println!("{} a server named {} already exists", "error:".red(), name); + } else { + servers.push(ServerConfig { + name, + host, + port, + username, + password, + }); + } + } else { + config.servers = Some(vec![ServerConfig { + name, + host, + port, + username, + password, + }]); + } +} + +fn parse_status(server_state: &mumlib::state::Server) { + println!( + "Connected to {} as {}", + server_state.host, server_state.username + ); + let own_channel = server_state + .channels + .iter() + .find(|e| e.users.iter().any(|e| e.name == server_state.username)) + .unwrap(); + println!( + "Currently in {} with {} other client{}:", + own_channel.name, + own_channel.users.len() - 1, + if own_channel.users.len() == 2 { + "" + } else { + "s" + } + ); + println!("{}{}", INDENTATION, own_channel.name); + for user in &own_channel.users { + println!("{}{}{}", INDENTATION, INDENTATION, user); + } } fn send_command(command: Command) -> mumlib::error::Result<Option<CommandResponse>> { diff --git a/mumd/Cargo.toml b/mumd/Cargo.toml index 9101b43..ffb463a 100644 --- a/mumd/Cargo.toml +++ b/mumd/Cargo.toml @@ -27,6 +27,5 @@ tokio = { version = "0.2", features = ["full"] } tokio-tls = "0.3" tokio-util = { version = "0.3", features = ["codec", "udp"] } -#clap = "2.33" #compressor = "0.3" #daemonize = "0.4" diff --git a/mumd/src/state.rs b/mumd/src/state.rs index 55fd8ae..0dbf9c5 100644 --- a/mumd/src/state.rs +++ b/mumd/src/state.rs @@ -6,6 +6,7 @@ use mumble_protocol::control::msgs; use mumble_protocol::control::ControlPacket; use mumble_protocol::voice::Serverbound; use mumlib::command::{Command, CommandResponse}; +use mumlib::config::Config; use mumlib::error::{ChannelIdentifierError, Error}; use serde::{Deserialize, Serialize}; use std::collections::hash_map::Entry; @@ -21,6 +22,7 @@ pub enum StatePhase { } pub struct State { + config: Option<Config>, server: Option<Server>, audio: Audio, @@ -35,13 +37,17 @@ impl State { packet_sender: mpsc::UnboundedSender<ControlPacket<Serverbound>>, connection_info_sender: watch::Sender<Option<ConnectionInfo>>, ) -> Self { - Self { + let audio = Audio::new(); + let mut state = Self { + config: mumlib::config::read_default_cfg(), server: None, - audio: Audio::new(), + audio, packet_sender, connection_info_sender, phase_watcher: watch::channel(StatePhase::Disconnected), - } + }; + state.reload_config(); + state } //TODO? move bool inside Result @@ -165,6 +171,10 @@ impl State { self.audio.set_input_volume(volume); (false, Ok(None)) } + Command::ConfigReload => { + self.reload_config(); + (false, Ok(None)) + } } } @@ -197,6 +207,20 @@ impl State { self.server.as_mut().unwrap().parse_user_state(msg); } + pub fn reload_config(&mut self) { + if let Some(config) = mumlib::config::read_default_cfg() { + self.config = Some(config); + let config = &self.config.as_ref().unwrap(); + if let Some(audio_config) = &config.audio { + if let Some(input_volume) = audio_config.input_volume { + self.audio.set_input_volume(input_volume); + } + } + } else { + warn!("config file not found"); + } + } + pub fn initialized(&self) { self.phase_watcher .0 diff --git a/mumlib/Cargo.toml b/mumlib/Cargo.toml index a2627d4..471a1fe 100644 --- a/mumlib/Cargo.toml +++ b/mumlib/Cargo.toml @@ -13,3 +13,4 @@ fern = "0.5" log = "0.4" mumble-protocol = "0.3" serde = { version = "1.0", features = ["derive"] } +toml = "0.5" diff --git a/mumlib/src/command.rs b/mumlib/src/command.rs index b4ab07a..05702f0 100644 --- a/mumlib/src/command.rs +++ b/mumlib/src/command.rs @@ -8,6 +8,7 @@ pub enum Command { channel_identifier: String, }, ChannelList, + ConfigReload, InputVolumeSet(f32), ServerConnect { host: String, diff --git a/mumlib/src/config.rs b/mumlib/src/config.rs new file mode 100644 index 0000000..aa8a8ed --- /dev/null +++ b/mumlib/src/config.rs @@ -0,0 +1,99 @@ +use log::*; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use std::fs; +use toml::Value; +use toml::value::Array; + +#[derive(Debug, Deserialize, Serialize)] +struct TOMLConfig { + audio: Option<AudioConfig>, + servers: Option<Array>, +} + +#[derive(Clone, Debug)] +pub struct Config { + pub audio: Option<AudioConfig>, + pub servers: Option<Vec<ServerConfig>>, +} + +impl Config { + pub fn write_default_cfg(&self) -> Result<(), std::io::Error> { + let path = get_cfg_path(); + let path = std::path::Path::new(&path); + // Possible race here. It's fine since it shows when: + // 1) the file doesn't exist when checked and is then created + // 2) the file exists when checked but is then removed + // If 1) we don't do anything anyway so it's fine, and if 2) we + // immediately re-create the file which, while not perfect, at least + // should work. Unless the file is removed AND the permissions + // change, but then we don't have permissions so we can't + // do anything anyways. + if !path.exists() { + warn!("config file {} does not exist, ignoring", path.display()); + Ok(()) + } else { + fs::write(path, toml::to_string(&TOMLConfig::from(self.clone())).unwrap()) + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AudioConfig { + pub input_volume: Option<f32>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ServerConfig { + pub name: String, + pub host: String, + pub port: Option<u16>, + pub username: Option<String>, + pub password: Option<String>, +} + +fn get_cfg_path() -> String { + ".mumdrc".to_string() //TODO XDG_CONFIG and whatever +} + +impl TryFrom<TOMLConfig> for Config { + type Error = toml::de::Error; + + fn try_from(config: TOMLConfig) -> Result<Self, Self::Error> { + Ok(Config { + audio: config.audio, + servers: config.servers.map(|servers| servers + .into_iter() + .map(|s| s.try_into::<ServerConfig>()) + .collect()) + .transpose()?, + }) + } +} + +impl From<Config> for TOMLConfig { + fn from(config: Config) -> Self { + TOMLConfig { + audio: config.audio, + servers: config.servers.map(|servers| servers + .into_iter() + .map(|s| Value::try_from::<ServerConfig>(s).unwrap()) + .collect()), + } + } +} + +pub fn read_default_cfg() -> Option<Config> { + Some(Config::try_from( + toml::from_str::<TOMLConfig>( + &match fs::read_to_string(get_cfg_path()) { + Ok(f) => { + f.to_string() + }, + Err(_) => { + return None + } + } + ).expect("invalid TOML in config file") //TODO + ).expect("invalid config in TOML")) //TODO +} diff --git a/mumlib/src/lib.rs b/mumlib/src/lib.rs index b26db13..93b7682 100644 --- a/mumlib/src/lib.rs +++ b/mumlib/src/lib.rs @@ -1,4 +1,5 @@ pub mod command; +pub mod config; pub mod error; pub mod state; @@ -16,7 +16,7 @@ We want to support / explain how to do the following at some point: The daemon doesn't do anything by itself. Interfacing with it is done through `mumctl`. -* Basic commands +* 0.1 The basic commands are the smallest subset of commands that allow the user to actually use mum for something. In this case it means connecting to a server, listing channels and connecting to channels. @@ -85,17 +85,17 @@ your_name@localhost:65387/root (3) your_name #+END_SRC -* More commands +* 0.2 ** server -*** add -Add a server with a name: +*** TODO add +**** DONE With name #+BEGIN_SRC bash -$ mumctl server add 127.0.0.1 loopback +$ mumctl server add loopback 127.0.0.1 username: *** password: *** #+END_SRC -Add a server without a name: +**** TODO Without name #+BEGIN_SRC bash $ mumctl server add 127.0.0.1 username: *** @@ -110,22 +110,22 @@ loopback [4 / 100] 127.0.0.1 [4 / 100] 127.0.0.3 [OFFLINE] #+END_SRC -*** config -**** username +*** TODO config +**** DONE username #+BEGIN_SRC bash -$ mumctl server config loopback set username xX_gamerboy_Xx +$ mumctl server config loopback username xX_gamerboy_Xx #+END_SRC -**** password +**** TODO password #+BEGIN_SRC bash -$ mumctl server config loopback set password *** +$ mumctl server config loopback password *** #+END_SRC Optionally ask stdin #+BEGIN_SRC bash -$ mumctl server config loopback set password +$ mumctl server config loopback password enter password: *** #+END_SRC -*** connect: handle invalid keys +*** TODO connect: handle invalid keys #+BEGIN_SRC bash server offered invalid key. what do you want to do? [I]nspect, [A]ccept, [D]eny, [C]ompare, [T]emporarily trust (default D): @@ -135,11 +135,11 @@ server offered invalid key. what do you want to do? - Deny: Abort the connection. Do not trust the key. - Compare: Compare the key to a file to confirm legitimacy and ask again. - Temporarily trust: Accept the key and connect, but do not trust the key. -*** rename +*** DONE rename #+BEGIN_SRC bash $ mumctl server rename loopback my_server #+END_SRC -** config +** TODO config #+BEGIN_SRC bash $ mumctl config audio.input_volume 1.1 $ mumctl config audio.input_volume |
