use serde::{Deserialize, Serialize};
use std::{
fs,
path::{Path, PathBuf},
};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Alias {
pub name: String,
pub expansion: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trigger {
pub id: String,
pub pattern: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub send: Option<String>,
}
impl Trigger {
#[allow(dead_code)]
pub fn new(pattern: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4().to_string(),
pattern: pattern.into(),
color: None,
send: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PanelKind {
Automap,
Notes,
#[doc(hidden)]
CharSheet,
#[doc(hidden)]
Paperdoll,
#[doc(hidden)]
Inventory,
}
impl PanelKind {
#[allow(dead_code)]
pub fn label(&self) -> &'static str {
match self {
PanelKind::Automap => "Automap",
PanelKind::Notes => "Notes",
_ => "Legacy",
}
}
pub fn short_label(&self) -> &'static str {
match self {
PanelKind::Automap => "Map",
PanelKind::Notes => "Notes",
_ => "---",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SidebarSide {
Left,
Right,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PanelConfig {
pub kind: PanelKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub side: Option<SidebarSide>,
#[serde(default = "default_panel_height_pct")]
pub height_pct: u8,
}
fn default_panel_height_pct() -> u8 {
50
}
fn default_right_visible() -> bool {
true
}
fn default_right_width() -> u16 {
26
}
fn default_panels() -> Vec<PanelConfig> {
vec![
PanelConfig {
kind: PanelKind::Automap,
side: Some(SidebarSide::Right),
height_pct: 35,
},
PanelConfig {
kind: PanelKind::Notes,
side: Some(SidebarSide::Right),
height_pct: 65,
},
]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SidebarLayout {
#[serde(default = "default_right_visible")]
pub right_visible: bool,
#[serde(default = "default_right_width")]
pub right_width: u16,
#[serde(default = "default_panels")]
pub panels: Vec<PanelConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<String>,
}
impl Default for SidebarLayout {
fn default() -> Self {
Self {
right_visible: true,
right_width: 26,
panels: default_panels(),
notes: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Server {
pub id: String,
pub name: String,
pub host: String,
pub port: u16,
#[serde(default)]
pub tls: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
impl Server {
pub fn new(name: impl Into<String>, host: impl Into<String>, port: u16) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name: name.into(),
host: host.into(),
port,
tls: false,
notes: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Character {
pub id: String,
pub name: String,
pub server_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub login: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub password_hint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub aliases: Vec<Alias>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub triggers: Vec<Trigger>,
#[serde(default)]
pub sidebar: SidebarLayout,
}
impl Character {
pub fn new(name: impl Into<String>, server_id: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name: name.into(),
server_id: server_id.into(),
login: None,
password_hint: None,
notes: None,
aliases: Vec::new(),
triggers: Vec::new(),
sidebar: SidebarLayout::default(),
}
}
pub fn effective_login(&self) -> &str {
self.login.as_deref().unwrap_or(&self.name)
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub servers: Vec<Server>,
#[serde(default)]
pub characters: Vec<Character>,
}
impl Config {
pub fn default_path() -> PathBuf {
let base = std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").expect("HOME environment variable not set");
PathBuf::from(home).join(".config")
});
base.join("durthang").join("config.toml")
}
pub fn load(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
if !path.exists() {
return Ok(Self::default());
}
let contents = fs::read_to_string(path)?;
Ok(toml::from_str(&contents)?)
}
pub fn save(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let contents = toml::to_string_pretty(self)?;
fs::write(path, contents)?;
Ok(())
}
#[allow(dead_code)]
pub fn server_by_id(&self, id: &str) -> Option<&Server> {
self.servers.iter().find(|s| s.id == id)
}
pub fn characters_for_server(&self, server_id: &str) -> Vec<&Character> {
self.characters
.iter()
.filter(|c| c.server_id == server_id)
.collect()
}
}
const KEYRING_SERVICE: &str = "durthang";
fn keyring_entry(server_id: &str, character_name: &str) -> keyring::Result<keyring::Entry> {
let account = format!("{server_id}/{character_name}");
keyring::Entry::new(KEYRING_SERVICE, &account)
}
pub fn store_password(
server_id: &str,
character_name: &str,
password: &str,
) -> keyring::Result<()> {
keyring_entry(server_id, character_name)?.set_password(password)
}
pub fn get_password(server_id: &str, character_name: &str) -> keyring::Result<Option<String>> {
match keyring_entry(server_id, character_name)?.get_password() {
Ok(pw) => Ok(Some(pw)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(e) => Err(e),
}
}
pub fn delete_password(server_id: &str, character_name: &str) -> keyring::Result<()> {
keyring_entry(server_id, character_name)?.delete_credential()
}