use std::{
fs, io,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use crate::{SortMode, Theme};
mod sort_mode_serde {
use super::*;
use serde::{self, Deserialize, Deserializer, Serializer};
pub fn serialize<S>(value: &Option<SortMode>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(m) => serializer.serialize_some(sort_mode_to_key(*m)),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<SortMode>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
Ok(opt.and_then(|s| sort_mode_from_key(&s)))
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct AppState {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theme: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_dir_right: Option<PathBuf>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "sort_mode_serde"
)]
pub sort_mode: Option<SortMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_hidden: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_sizes: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_dir_sizes: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub use_trash: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_git: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bookmarks: Option<Vec<PathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub single_pane: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cd_on_exit: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub editor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_pane: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pane_dirs: Option<Vec<PathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_pane_idx: Option<usize>,
}
pub(crate) fn config_dir() -> Option<PathBuf> {
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
Some(base.join("tfe"))
}
#[must_use]
pub fn state_path() -> Option<PathBuf> {
config_dir().map(|d| d.join("settings.json"))
}
fn sort_mode_to_key(mode: SortMode) -> &'static str {
match mode {
SortMode::Name => "name",
SortMode::SizeDesc => "size_desc",
SortMode::Extension => "extension",
}
}
fn sort_mode_from_key(s: &str) -> Option<SortMode> {
match s {
"name" => Some(SortMode::Name),
"size_desc" => Some(SortMode::SizeDesc),
"extension" => Some(SortMode::Extension),
_ => None,
}
}
#[must_use]
pub fn load_state_from(path: &Path) -> AppState {
try_load_state_from(path).unwrap_or_default()
}
pub fn try_load_state_from(path: &Path) -> io::Result<AppState> {
let data = fs::read_to_string(path)?;
let mut state = serde_json::from_str::<AppState>(&data)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if let Some(ref p) = state.last_dir
&& !p.is_dir()
{
state.last_dir = None;
}
if let Some(ref p) = state.last_dir_right
&& !p.is_dir()
{
state.last_dir_right = None;
}
if let Some(ref dirs) = state.pane_dirs
&& (dirs.is_empty() || dirs.iter().any(|d| !d.is_dir()))
{
state.pane_dirs = None;
}
Ok(state)
}
pub fn save_state_to(path: &Path, state: &AppState) -> io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(state).map_err(|e| io::Error::other(e.to_string()))?;
let tmp_path = path.with_extension("tmp");
fs::write(&tmp_path, json.as_bytes())?;
fs::rename(&tmp_path, path)?;
Ok(())
}
#[must_use]
pub fn load_state() -> AppState {
let Some(json_path) = state_path() else {
return AppState::default();
};
load_state_from(&json_path)
}
pub fn save_state(state: &AppState) {
let _ = try_save_state(state);
}
pub fn try_save_state(state: &AppState) -> io::Result<()> {
match state_path() {
Some(path) => save_state_to(&path, state),
None => Ok(()),
}
}
#[must_use]
pub fn resolve_theme_idx(name: &str, themes: &[(&str, Theme)]) -> usize {
let key = name.to_lowercase().replace('-', " ");
for (i, (n, _)) in themes.iter().enumerate() {
if n.to_lowercase().replace('-', " ") == key {
return i;
}
}
eprintln!(
"tfe: unknown theme {name:?} — falling back to default. \
Run `tfe --list-themes` to see available options."
);
0
}
#[cfg(test)]
#[path = "persistence_tests.rs"]
mod tests;