use std::path::PathBuf;
use ishou_tokens::{FleetSessionNames, SessionIdentity, SessionName, SessionNameStyle, SessionTheme};
use serde::{Deserialize, Serialize};
use tear_types::id::SessionId;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionState {
Live,
Saved,
Templated,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NameStyle {
#[default]
Emoji,
Glyph,
}
impl From<SessionNameStyle> for NameStyle {
fn from(s: SessionNameStyle) -> Self {
match s {
SessionNameStyle::Emoji => NameStyle::Emoji,
SessionNameStyle::Glyph => NameStyle::Glyph,
}
}
}
impl From<NameStyle> for SessionNameStyle {
fn from(s: NameStyle) -> Self {
match s {
NameStyle::Emoji => SessionNameStyle::Emoji,
NameStyle::Glyph => SessionNameStyle::Glyph,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ThemeMirror {
#[default]
Frost,
Brazil,
}
impl From<SessionTheme> for ThemeMirror {
fn from(t: SessionTheme) -> Self {
match t {
SessionTheme::Frost => ThemeMirror::Frost,
SessionTheme::Brazil => ThemeMirror::Brazil,
}
}
}
impl From<ThemeMirror> for SessionTheme {
fn from(t: ThemeMirror) -> Self {
match t {
ThemeMirror::Frost => SessionTheme::Frost,
ThemeMirror::Brazil => SessionTheme::Brazil,
}
}
}
#[must_use]
pub fn identity_for(name_seed: u64, theme: Option<ThemeMirror>) -> SessionIdentity {
match theme {
Some(t) => FleetSessionNames::in_theme(name_seed, t.into()),
None => FleetSessionNames::identity(name_seed),
}
}
#[must_use]
pub fn display_name_for(
name_seed: u64,
name_style: NameStyle,
theme: Option<ThemeMirror>,
custom_name: Option<&str>,
) -> String {
match custom_name {
Some(n) => n.to_string(),
None => SessionName {
identity: identity_for(name_seed, theme),
style: name_style.into(),
}
.to_string(),
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionRecord {
pub id: SessionId,
pub name_seed: u64,
pub name_style: NameStyle,
pub project_root: PathBuf,
pub cwd: PathBuf,
pub visits: u32,
pub last_seen: u64,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub custom_name: Option<String>,
#[serde(default)]
pub theme: Option<ThemeMirror>,
pub state: SessionState,
}
impl SessionRecord {
#[must_use]
pub fn for_project(
id: SessionId,
project_root: PathBuf,
style: SessionNameStyle,
last_seen: u64,
) -> Self {
let name_seed = ishou_tokens::fleet_session_names::stable_seed(
project_root.to_string_lossy().as_bytes(),
);
Self {
id,
name_seed,
name_style: style.into(),
cwd: project_root.clone(),
project_root,
visits: 1,
last_seen,
tags: Vec::new(),
custom_name: None,
theme: None,
state: SessionState::Live,
}
}
#[must_use]
pub fn for_adhoc(
id: SessionId,
name_seed: u64,
theme: SessionTheme,
cwd: PathBuf,
style: SessionNameStyle,
last_seen: u64,
) -> Self {
Self {
id,
name_seed,
name_style: style.into(),
project_root: cwd.clone(),
cwd,
visits: 1,
last_seen,
tags: Vec::new(),
custom_name: None,
theme: Some(theme.into()),
state: SessionState::Live,
}
}
#[must_use]
pub fn identity(&self) -> SessionIdentity {
identity_for(self.name_seed, self.theme)
}
#[must_use]
pub fn display_name(&self) -> String {
display_name_for(self.name_seed, self.name_style, self.theme, self.custom_name.as_deref())
}
pub fn rename(&mut self, name: impl Into<String>) {
let n = name.into();
self.custom_name = if n.trim().is_empty() { None } else { Some(n) };
}
#[must_use]
pub fn keywords(&self) -> &'static [&'static str] {
self.identity().keywords
}
#[must_use]
pub fn name(&self) -> SessionName {
SessionName { identity: self.identity(), style: self.name_style.into() }
}
#[must_use]
pub fn name_word(&self) -> &'static str {
self.identity().word
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn sid(seed: &str) -> SessionId {
SessionId::from_seed(seed)
}
#[test]
fn style_mirror_round_trips() {
for s in [SessionNameStyle::Emoji, SessionNameStyle::Glyph] {
let mirror: NameStyle = s.into();
let back: SessionNameStyle = mirror.into();
assert_eq!(s, back);
}
}
#[test]
fn for_project_derives_stable_name() {
let root = PathBuf::from("/code/pleme-io/mado");
let a = SessionRecord::for_project(sid("a"), root.clone(), SessionNameStyle::Emoji, 100);
let b = SessionRecord::for_project(sid("b"), root.clone(), SessionNameStyle::Emoji, 200);
assert_eq!(a.name_seed, b.name_seed);
assert_eq!(a.name_word(), b.name_word());
let direct = FleetSessionNames::from_project_path(Path::new("/code/pleme-io/mado"), SessionNameStyle::Emoji);
assert_eq!(a.name().to_string(), direct.to_string());
}
#[test]
fn name_word_is_style_independent() {
let root = PathBuf::from("/x/y/z");
let e = SessionRecord::for_project(sid("e"), root.clone(), SessionNameStyle::Emoji, 0);
let g = SessionRecord::for_project(sid("g"), root, SessionNameStyle::Glyph, 0);
assert_eq!(e.name_word(), g.name_word());
assert_ne!(e.name().to_string(), g.name().to_string());
}
#[test]
fn record_serde_round_trips() {
let rec = SessionRecord {
id: sid("rt"),
name_seed: 42,
name_style: NameStyle::Glyph,
project_root: PathBuf::from("/a/b"),
cwd: PathBuf::from("/a/b/c"),
visits: 7,
last_seen: 1234,
tags: vec!["infra".into(), "deploy".into()],
custom_name: Some("billing-stack".into()),
theme: Some(ThemeMirror::Brazil),
state: SessionState::Saved,
};
let json = serde_json::to_string(&rec).unwrap();
let back: SessionRecord = serde_json::from_str(&json).unwrap();
assert_eq!(rec, back);
assert_eq!(rec.name().to_string(), back.name().to_string());
}
#[test]
fn rename_overrides_display_then_clears() {
let mut r =
SessionRecord::for_project(sid("r"), PathBuf::from("/x"), SessionNameStyle::Emoji, 1);
let emoji_word = r.name_word();
r.rename("billing-stack");
assert_eq!(r.display_name(), "billing-stack");
assert_eq!(r.custom_name.as_deref(), Some("billing-stack"));
r.rename(" ");
assert!(r.custom_name.is_none());
assert!(r.display_name().contains(emoji_word));
}
#[test]
fn adhoc_draws_a_deterministic_themed_name() {
let r = SessionRecord::for_adhoc(
sid("a"),
5,
SessionTheme::Brazil,
PathBuf::from("/tmp/scratch"),
SessionNameStyle::Emoji,
1,
);
assert_eq!(r.identity().theme, SessionTheme::Brazil, "ad-hoc name stays in theme");
assert!(!r.name_word().is_empty());
let again = SessionRecord::for_adhoc(
sid("a"),
5,
SessionTheme::Brazil,
PathBuf::from("/tmp/scratch"),
SessionNameStyle::Emoji,
1,
);
assert_eq!(r.name_word(), again.name_word());
}
}