Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

configurable keybindings #23 #36

Merged
merged 18 commits into from
Jun 30, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(config): restructure
micielski committed Jun 24, 2024
commit c19cd778276abd268f635b81c6b2ffeb8b7b736a
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ fuzzy-matcher = "0.3.7"
clap = { version = "4.5.6", features = ["derive"] }
base64 = "0.22"
xdg = "2.5"
url = "2.5"
url = { version = "2.5", features = ["serde"] }
toml = "0.8"

# Async
19 changes: 13 additions & 6 deletions rm-config/src/keymap.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use std::{collections::HashMap, marker::PhantomData};
use std::{collections::HashMap, marker::PhantomData, path::PathBuf, sync::OnceLock};

use anyhow::Result;
use crossterm::event::{KeyCode, KeyModifiers};
use serde::{
micielski marked this conversation as resolved.
Show resolved Hide resolved
de::{self, SeqAccess, Visitor},
de::{self, Visitor},
Deserialize, Serialize,
};
use toml::Table;

use crate::{utils, KEYMAP_CONFIG_FILENAME};
use crate::utils;
use rm_shared::action::Action;

#[derive(Serialize, Deserialize)]
pub struct Keymap {
pub(crate) struct KeymapConfig {
general: General<GeneralAction>,
torrents_tab: TorrentsTab<TorrentsAction>,
}
@@ -241,11 +241,13 @@ impl Default for KeyModifier {
}
}

impl Keymap {
impl KeymapConfig {
pub const FILENAME: &'static str = "keymap.toml";

pub fn init() -> Result<Self> {
let table = {
// TODO: handle errors or there will be hell to pay
if let Ok(table) = utils::fetch_config_table(KEYMAP_CONFIG_FILENAME) {
if let Ok(table) = utils::fetch_config(Self::FILENAME) {
table
} else {
todo!();
@@ -273,4 +275,9 @@ impl Keymap {
let config = toml::from_str(&config_string)?;
Ok(config)
}

pub fn path() -> &'static PathBuf {
static PATH: OnceLock<PathBuf> = OnceLock::new();
PATH.get_or_init(|| utils::get_config_path(Self::FILENAME))
}
}
154 changes: 24 additions & 130 deletions rm-config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,149 +1,43 @@
mod keymap;
mod main_config;
mod utils;

use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use std::{collections::HashMap, path::PathBuf};

use anyhow::{bail, Context, Result};
use anyhow::Result;
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::style::Color;
use rm_shared::action::Action;
use serde::{Deserialize, Serialize};
use toml::Table;
use xdg::BaseDirectories;
use keymap::KeymapConfig;
use main_config::MainConfig;

use crate::utils::put_config;
use keymap::Keymap;
use rm_shared::action::Action;

#[derive(Serialize, Deserialize)]
pub struct Config {
pub connection: Connection,
pub general: General,
#[serde(skip)]
pub keymap: Option<HashMap<(KeyCode, KeyModifiers), Action>>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct General {
#[serde(default)]
pub auto_hide: bool,
#[serde(default = "default_accent_color")]
pub accent_color: Color,
#[serde(default = "default_beginner_mode")]
pub beginner_mode: bool,
}

fn default_accent_color() -> Color {
Color::LightMagenta
}

fn default_beginner_mode() -> bool {
true
pub general: main_config::General,
pub connection: main_config::Connection,
pub keymap: HashMap<(KeyCode, KeyModifiers), Action>,
pub directories: Directories,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Connection {
pub username: Option<String>,
pub password: Option<String>,
pub url: String,
}

const DEFAULT_CONFIG: &str = include_str!("../defaults/config.toml");
static XDG_DIRS: OnceLock<BaseDirectories> = OnceLock::new();
static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
pub const MAIN_CONFIG_FILENAME: &str = "config.toml";
pub const KEYMAP_CONFIG_FILENAME: &str = "keymap.toml";

pub fn xdg_dirs() -> &'static BaseDirectories {
XDG_DIRS.get_or_init(|| xdg::BaseDirectories::with_prefix("rustmission").unwrap())
}

pub fn get_config_path(filename: &str) -> &'static PathBuf {
CONFIG_PATH.get_or_init(|| xdg_dirs().place_config_file(filename).unwrap())
pub struct Directories {
pub main_path: &'static PathBuf,
pub keymap_path: &'static PathBuf,
}

impl Config {
pub fn init() -> Result<Self> {
let Ok(table) = utils::fetch_config_table(MAIN_CONFIG_FILENAME) else {
put_config(DEFAULT_CONFIG, MAIN_CONFIG_FILENAME)?;
// TODO: check if the user really changed the config.
println!(
"Update {:?} and start rustmission again",
get_config_path(MAIN_CONFIG_FILENAME)
);
std::process::exit(0);
};
let main_config = MainConfig::init()?;
let keybindings = KeymapConfig::init()?;

Self::table_config_verify(&table)?;

let mut config = Self::table_to_config(&table)?;
config.keymap = Some(Keymap::init().unwrap().to_hashmap());
Ok(config)
}

fn table_to_config(table: &Table) -> Result<Self> {
let config_string = table.to_string();
let config: Self = toml::from_str(&config_string)?;
Ok(config)
}

fn table_config_verify(table: &Table) -> Result<()> {
let Some(connection_table) = table.get("connection").unwrap().as_table() else {
bail!("expected connection table")
let directories = Directories {
main_path: MainConfig::path(),
keymap_path: KeymapConfig::path(),
};

let url = connection_table
.get("url")
.and_then(|url| url.as_str())
.with_context(|| {
format!(
"no url given in: {}",
get_config_path(MAIN_CONFIG_FILENAME).to_str().unwrap()
)
})?;

url::Url::parse(url).with_context(|| {
format!(
"invalid url '{url}' in {}",
get_config_path(MAIN_CONFIG_FILENAME).to_str().unwrap()
)
})?;

Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

fn invalid_config() -> Table {
toml::toml! {
[connection]
username = "username"
password = "password"
auto_hide = "dfgoij"
url = "bad_url"
}
}

fn valid_config() -> Table {
toml::toml! {
[connection]
username = "username"
password = "password"
url = "http://192.168.1.1/transmission/rpc"
}
}

#[test]
fn validates_properly() {
let valid_config = valid_config();
assert!(Config::table_config_verify(&valid_config).is_ok());
}

#[test]
fn invalidates_properly() {
let invalid_config = invalid_config();
assert!(Config::table_config_verify(&invalid_config).is_err());
Ok(Self {
general: main_config.general,
connection: main_config.connection,
keymap: keybindings.to_hashmap(),
directories,
})
}
}
60 changes: 60 additions & 0 deletions rm-config/src/main_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use std::{path::PathBuf, sync::OnceLock};

use anyhow::Result;
use ratatui::style::Color;
use serde::{Deserialize, Serialize};
use url::Url;

use crate::utils::{self, put_config};

#[derive(Serialize, Deserialize)]
pub struct MainConfig {
pub general: General,
pub connection: Connection,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct General {
#[serde(default)]
pub auto_hide: bool,
#[serde(default = "default_accent_color")]
pub accent_color: Color,
#[serde(default = "default_beginner_mode")]
pub beginner_mode: bool,
}

fn default_accent_color() -> Color {
Color::LightMagenta
}

fn default_beginner_mode() -> bool {
true
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Connection {
pub username: Option<String>,
pub password: Option<String>,
pub url: Url,
}

impl MainConfig {
pub(crate) const FILENAME: &'static str = "config.toml";
const DEFAULT_CONFIG: &'static str = include_str!("../defaults/config.toml");

pub(crate) fn init() -> Result<Self> {
let Ok(config) = utils::fetch_config(Self::FILENAME) else {
put_config(Self::DEFAULT_CONFIG, Self::FILENAME)?;
// TODO: check if the user really changed the config.
println!("Update {:?} and start rustmission again", Self::path());
std::process::exit(0);
};

Ok(config)
}

pub(crate) fn path() -> &'static PathBuf {
static PATH: OnceLock<PathBuf> = OnceLock::new();
PATH.get_or_init(|| utils::get_config_path(Self::FILENAME))
}
}
20 changes: 15 additions & 5 deletions rm-config/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
sync::OnceLock,
};

use anyhow::Result;
use toml::Table;
use serde::de::DeserializeOwned;
use xdg::BaseDirectories;

use crate::get_config_path;
pub fn xdg_dirs() -> &'static BaseDirectories {
static XDG_DIRS: OnceLock<BaseDirectories> = OnceLock::new();
XDG_DIRS.get_or_init(|| xdg::BaseDirectories::with_prefix("rustmission").unwrap())
}

pub fn get_config_path(filename: &str) -> PathBuf {
xdg_dirs().place_config_file(filename).unwrap()
}

pub fn fetch_config_table(config_name: &str) -> Result<Table> {
let config_path = crate::xdg_dirs()
pub fn fetch_config<T: DeserializeOwned>(config_name: &str) -> Result<T> {
let config_path = xdg_dirs()
.find_config_file(config_name)
.ok_or_else(|| anyhow::anyhow!("{} not found", config_name))?;

@@ -20,7 +30,7 @@ pub fn fetch_config_table(config_name: &str) -> Result<Table> {
Ok(toml::from_str(&config_buf)?)
}

pub fn put_config(content: &str, filename: &str) -> Result<Table> {
pub fn put_config<T: DeserializeOwned>(content: &str, filename: &str) -> Result<T> {
let config_path = get_config_path(filename);
let mut config_file = File::create(config_path)?;
config_file.write_all(content.as_bytes())?;
4 changes: 2 additions & 2 deletions rm-main/src/app.rs
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ impl Ctx {
});
}
Err(e) => {
let config_path = rm_config::get_config_path(rm_config::MAIN_CONFIG_FILENAME);
let config_path = config.directories.main_path;
return Err(Error::msg(format!(
"{e}\nIs the connection info in {:?} correct?",
config_path
@@ -111,7 +111,7 @@ impl App {

tokio::select! {
event = tui_event => {
if let Some(action) = event_to_action(self.mode, event.unwrap(), self.ctx.config.keymap.as_ref().unwrap()) {
if let Some(action) = event_to_action(self.mode, event.unwrap(), &self.ctx.config.keymap) {
if let Some(action) = self.update(action).await {
self.ctx.action_tx.send(action).unwrap();
}
3 changes: 1 addition & 2 deletions rm-main/src/transmission/utils.rs
Original file line number Diff line number Diff line change
@@ -14,9 +14,8 @@ pub fn client_from_config(config: &Config) -> TransClient {
.as_ref()
.unwrap_or(&"".to_string())
.clone();
let url = config.connection.url.parse().unwrap();

let auth = BasicAuth { user, password };

TransClient::with_auth(url, auth)
TransClient::with_auth(config.connection.url.clone(), auth)
}