use std::collections::HashMap;
use std::path::PathBuf;
use directories::ProjectDirs;
use tracing::{debug, warn};
#[derive(Debug, Default)]
pub struct GroupStore {
assignments: HashMap<String, String>,
path: Option<PathBuf>,
}
impl GroupStore {
pub fn load() -> Self {
let path = Self::default_path();
let mut assignments = HashMap::new();
if let Some(p) = path.as_ref()
&& let Ok(contents) = std::fs::read_to_string(p)
{
for line in contents.lines() {
if let Some((session, group)) = line.split_once('\t') {
let session = session.trim();
let group = group.trim();
if !session.is_empty() && !group.is_empty() {
assignments.insert(session.to_string(), group.to_string());
}
}
}
debug!("loaded {} group assignment(s)", assignments.len());
}
Self { assignments, path }
}
fn default_path() -> Option<PathBuf> {
let dirs = ProjectDirs::from("dev", "tkcd", "tmux-deck")?;
Some(dirs.config_dir().join("groups.tsv"))
}
pub fn group_of(&self, session: &str) -> Option<String> {
self.assignments.get(session).cloned()
}
pub fn group_names(&self) -> Vec<String> {
let mut names: Vec<String> = self
.assignments
.values()
.cloned()
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
names.sort();
names
}
pub fn set(&mut self, session: &str, group: Option<&str>) {
match group.map(str::trim).filter(|g| !g.is_empty()) {
Some(g) => {
self.assignments.insert(session.to_string(), g.to_string());
}
None => {
self.assignments.remove(session);
}
}
self.save();
}
pub fn rename_session(&mut self, old_name: &str, new_name: &str) {
if let Some(group) = self.assignments.remove(old_name) {
self.assignments.insert(new_name.to_string(), group);
self.save();
}
}
pub fn forget(&mut self, session: &str) {
if self.assignments.remove(session).is_some() {
self.save();
}
}
fn save(&self) {
let Some(path) = self.path.as_ref() else {
return;
};
if let Some(parent) = path.parent()
&& let Err(e) = std::fs::create_dir_all(parent)
{
warn!("failed to create config dir for group store: {e}");
return;
}
let mut out = String::new();
for (session, group) in &self.assignments {
if session.contains(['\t', '\n']) || group.contains(['\t', '\n']) {
continue;
}
out.push_str(session);
out.push('\t');
out.push_str(group);
out.push('\n');
}
if let Err(e) = std::fs::write(path, out) {
warn!("failed to write group store: {e}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_and_query() {
let mut store = GroupStore::default();
assert_eq!(store.group_of("a"), None);
store.set("a", Some("work"));
assert_eq!(store.group_of("a"), Some("work".to_string()));
store.set("a", Some(" "));
assert_eq!(store.group_of("a"), None);
store.set("a", Some("work"));
store.set("a", None);
assert_eq!(store.group_of("a"), None);
}
#[test]
fn rename_preserves_group() {
let mut store = GroupStore::default();
store.set("old", Some("work"));
store.rename_session("old", "new");
assert_eq!(store.group_of("old"), None);
assert_eq!(store.group_of("new"), Some("work".to_string()));
}
#[test]
fn forget_removes() {
let mut store = GroupStore::default();
store.set("a", Some("work"));
store.forget("a");
assert_eq!(store.group_of("a"), None);
}
}