use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HistoryMode {
Persistent { dir: Option<PathBuf> },
Volatile,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum HistoryLocationSource {
Default,
Explicit,
Volatile,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResolvedHistoryLocation {
directory: Option<PathBuf>,
source: HistoryLocationSource,
}
impl ResolvedHistoryLocation {
pub(crate) fn directory(&self) -> Option<&Path> {
self.directory.as_deref()
}
pub(crate) fn source(&self) -> HistoryLocationSource {
self.source
}
pub(crate) fn database_path(&self, filename: &str) -> Option<PathBuf> {
self.directory
.as_ref()
.map(|directory| directory.join(filename))
}
}
pub(crate) fn resolve_history_location(
mode: &HistoryMode,
default_directory: Option<PathBuf>,
) -> ResolvedHistoryLocation {
match mode {
HistoryMode::Persistent {
dir: Some(directory),
} => ResolvedHistoryLocation {
directory: Some(directory.clone()),
source: HistoryLocationSource::Explicit,
},
HistoryMode::Persistent { dir: None } => ResolvedHistoryLocation {
directory: default_directory,
source: HistoryLocationSource::Default,
},
HistoryMode::Volatile => ResolvedHistoryLocation {
directory: None,
source: HistoryLocationSource::Volatile,
},
}
}
#[derive(Debug, Clone)]
pub struct HistoryConfig {
pub menu_max_height: u16,
pub mode: HistoryMode,
}
impl Serialize for HistoryConfig {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Serialize)]
#[serde(untagged)]
enum WireMode<'a> {
Name(&'a str),
Persistent { dir: &'a PathBuf },
}
#[derive(Serialize)]
struct WireHistory<'a> {
menu_max_height: u16,
mode: WireMode<'a>,
}
let mode = match &self.mode {
HistoryMode::Persistent { dir: Some(dir) } => WireMode::Persistent { dir },
HistoryMode::Persistent { dir: None } => WireMode::Name("persistent"),
HistoryMode::Volatile => WireMode::Name("volatile"),
};
WireHistory {
menu_max_height: self.menu_max_height,
mode,
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for HistoryConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum RawMode {
Name(String),
Persistent(RawPersistentMode),
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawPersistentMode {
dir: PathBuf,
}
#[derive(Deserialize)]
struct RawHistoryConfig {
#[serde(default = "default_menu_max_height")]
menu_max_height: u16,
#[serde(default)]
mode: Option<RawMode>,
#[serde(default)]
dir: Option<PathBuf>,
#[serde(default)]
disabled: Option<bool>,
}
let raw = RawHistoryConfig::deserialize(deserializer)?;
let mode = match raw.mode {
Some(RawMode::Name(mode)) => {
if raw.dir.is_some() {
return Err(serde::de::Error::custom(
"history.dir cannot be used when history.mode is set",
));
}
match (mode.as_str(), raw.disabled) {
("persistent", _) => HistoryMode::Persistent { dir: None },
("volatile", _) => HistoryMode::Volatile,
_ => {
return Err(serde::de::Error::unknown_variant(
&mode,
&["persistent", "volatile"],
));
}
}
}
Some(RawMode::Persistent(RawPersistentMode { dir })) => {
if raw.dir.is_some() {
return Err(serde::de::Error::custom(
"history.dir cannot be used when history.mode is set",
));
}
HistoryMode::Persistent { dir: Some(dir) }
}
None => match (raw.disabled, raw.dir) {
(Some(true), _) => HistoryMode::Volatile,
(Some(false), dir) | (None, dir) => HistoryMode::Persistent { dir },
},
};
Ok(Self {
menu_max_height: raw.menu_max_height,
mode,
})
}
}
impl JsonSchema for HistoryConfig {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("HistoryConfig")
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"description": "History configuration.",
"type": "object",
"properties": {
"menu_max_height": {
"description": "Maximum height (rows) for the history search menu (Ctrl+R).",
"type": "integer",
"format": "uint16",
"default": 15,
"maximum": 65535,
"minimum": 0
},
"mode": {
"default": "persistent",
"oneOf": [
{
"type": "string",
"enum": ["persistent", "volatile"]
},
{
"type": "object",
"properties": {
"dir": { "type": "string" }
},
"required": ["dir"],
"additionalProperties": false
}
]
}
}
})
}
}
fn default_menu_max_height() -> u16 {
15
}
impl Default for HistoryConfig {
fn default() -> Self {
HistoryConfig {
menu_max_height: 15,
mode: HistoryMode::Persistent { dir: None },
}
}
}
#[cfg(test)]
mod location_tests {
use super::*;
#[test]
fn resolver_preserves_source_and_never_assigns_volatile_directory() {
let default = PathBuf::from("/xdg/arf/history");
let resolved = resolve_history_location(
&HistoryMode::Persistent { dir: None },
Some(default.clone()),
);
assert_eq!(resolved.directory(), Some(default.as_path()));
assert_eq!(resolved.source(), HistoryLocationSource::Default);
assert_eq!(resolved.database_path("r.db"), Some(default.join("r.db")));
let explicit = PathBuf::from("/custom/history");
let resolved = resolve_history_location(
&HistoryMode::Persistent {
dir: Some(explicit.clone()),
},
Some(default),
);
assert_eq!(resolved.directory(), Some(explicit.as_path()));
assert_eq!(resolved.source(), HistoryLocationSource::Explicit);
let resolved = resolve_history_location(&HistoryMode::Volatile, None);
assert_eq!(resolved.directory(), None);
assert_eq!(resolved.database_path("r.db"), None);
assert_eq!(resolved.source(), HistoryLocationSource::Volatile);
}
}