codex-wrangler 1.2.1

Linux/X11/i3 tray switcher for live coding-agent TUI sessions
use std::{fs, path::PathBuf};

const FILE: &str = "window-mode";

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Posture {
    #[default]
    Floating,
    Tiled,
}

pub struct Ledger {
    path: Option<PathBuf>,
    remembered: Option<Posture>,
}

impl Posture {
    pub const fn from_floating(floating: bool) -> Self {
        if floating {
            Self::Floating
        } else {
            Self::Tiled
        }
    }

    pub const fn floating(self) -> bool {
        matches!(self, Self::Floating)
    }

    const fn text(self) -> &'static str {
        match self {
            Self::Floating => "floating\n",
            Self::Tiled => "tiled\n",
        }
    }

    fn parse(text: &str) -> Option<Self> {
        match text.trim() {
            "floating" => Some(Self::Floating),
            "tiled" => Some(Self::Tiled),
            _ => None,
        }
    }
}

impl Ledger {
    pub fn restore() -> (Self, Posture) {
        let path = match crate::state::path(FILE) {
            Ok(path) => path,
            Err(error) => {
                eprintln!("codex-wrangler cannot resolve its XDG state: {error:#}");
                return (
                    Self {
                        path: None,
                        remembered: None,
                    },
                    Posture::default(),
                );
            }
        };
        Self::restore_from(path)
    }

    pub fn remember(&mut self, posture: Posture) {
        if self.remembered == Some(posture) {
            return;
        }
        let Some(path) = &self.path else {
            return;
        };
        match crate::state::seal(path, posture.text().as_bytes()) {
            Ok(()) => self.remembered = Some(posture),
            Err(error) => {
                eprintln!("codex-wrangler cannot save its window mode: {error:#}");
            }
        }
    }

    fn restore_from(path: PathBuf) -> (Self, Posture) {
        let restored = match fs::read_to_string(&path) {
            Ok(text) => {
                if let Some(posture) = Posture::parse(&text) {
                    Some(posture)
                } else {
                    eprintln!(
                        "codex-wrangler ignored invalid window mode in `{}`",
                        path.display()
                    );
                    None
                }
            }
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
            Err(error) => {
                eprintln!(
                    "codex-wrangler cannot read window mode from `{}`: {error}",
                    path.display()
                );
                None
            }
        };
        (
            Self {
                path: Some(path),
                remembered: restored,
            },
            restored.unwrap_or_default(),
        )
    }
}

#[cfg(test)]
mod tests {
    use std::os::unix::fs::PermissionsExt as _;

    use super::*;

    #[test]
    fn posture_round_trips_through_private_atomic_state() {
        let root = tempfile::tempdir().expect("temporary state root");
        let path = root.path().join("codex-wrangler").join(FILE);
        let (mut ledger, restored) = Ledger::restore_from(path.clone());
        assert_eq!(restored, Posture::Floating);

        ledger.remember(Posture::Tiled);
        assert_eq!(
            fs::read_to_string(&path).expect("persisted mode"),
            "tiled\n"
        );
        assert_eq!(
            fs::metadata(path.parent().expect("state parent"))
                .expect("state parent metadata")
                .permissions()
                .mode()
                & 0o777,
            0o700
        );
        assert_eq!(
            fs::metadata(&path)
                .expect("state metadata")
                .permissions()
                .mode()
                & 0o777,
            0o600
        );

        let (_ledger, restored) = Ledger::restore_from(path);
        assert_eq!(restored, Posture::Tiled);
    }
}