quinjet 0.0.51

A fast, live, keyboard-first Git source-control interface for the terminal
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub(crate) struct TabId(u64);

impl TabId {
    pub(crate) const fn new(value: u64) -> Self {
        Self(value)
    }

    pub(crate) const fn value(self) -> u64 {
        self.0
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TabInfo {
    pub(crate) id: TabId,
    pub(crate) title: String,
    pub(crate) root: PathBuf,
    pub(crate) machine: Option<String>,
    pub(crate) active: bool,
}

#[derive(Debug)]
struct RepositoryTab<T> {
    id: TabId,
    title: String,
    root: PathBuf,
    pending: bool,
    value: T,
}

#[derive(Debug)]
pub(crate) struct RepositoryTabs<T> {
    tabs: Vec<RepositoryTab<T>>,
    active: Option<TabId>,
    next_id: u64,
}

impl<T> RepositoryTabs<T> {
    #[cfg(test)]
    pub(crate) fn new(title: impl Into<String>, root: impl Into<PathBuf>, value: T) -> Self {
        Self::new_with_id(TabId::new(0), title, root, value)
    }

    pub(crate) fn new_with_id(
        id: TabId,
        title: impl Into<String>,
        root: impl Into<PathBuf>,
        value: T,
    ) -> Self {
        Self::new_with_pending(id, title, root, value, false)
    }

    pub(crate) fn new_pending_with_id(
        id: TabId,
        title: impl Into<String>,
        root: impl Into<PathBuf>,
        value: T,
    ) -> Self {
        Self::new_with_pending(id, title, root, value, true)
    }

    fn new_with_pending(
        id: TabId,
        title: impl Into<String>,
        root: impl Into<PathBuf>,
        value: T,
        pending: bool,
    ) -> Self {
        Self {
            tabs: vec![RepositoryTab {
                id,
                title: title.into(),
                root: root.into(),
                pending,
                value,
            }],
            active: Some(id),
            next_id: id.value().wrapping_add(1),
        }
    }

    pub(crate) const fn active_id(&self) -> Option<TabId> {
        self.active
    }

    pub(crate) fn active(&self) -> Option<&T> {
        let active = self.active?;
        self.tabs
            .iter()
            .find(|tab| tab.id == active)
            .map(|tab| &tab.value)
    }

    pub(crate) fn active_mut(&mut self) -> Option<&mut T> {
        let active = self.active?;
        self.tabs
            .iter_mut()
            .find(|tab| tab.id == active)
            .map(|tab| &mut tab.value)
    }

    pub(crate) fn get(&self, id: TabId) -> Option<&T> {
        self.tabs
            .iter()
            .find(|tab| tab.id == id)
            .map(|tab| &tab.value)
    }

    pub(crate) fn get_mut(&mut self, id: TabId) -> Option<&mut T> {
        self.tabs
            .iter_mut()
            .find(|tab| tab.id == id)
            .map(|tab| &mut tab.value)
    }

    #[cfg(test)]
    pub(crate) fn iter(&self) -> impl DoubleEndedIterator<Item = (TabId, &T)> + ExactSizeIterator {
        self.tabs.iter().map(|tab| (tab.id, &tab.value))
    }

    pub(crate) fn iter_mut(
        &mut self,
    ) -> impl DoubleEndedIterator<Item = (TabId, &mut T)> + ExactSizeIterator {
        self.tabs.iter_mut().map(|tab| (tab.id, &mut tab.value))
    }

    #[cfg(test)]
    pub(crate) const fn len(&self) -> usize {
        self.tabs.len()
    }

    pub(crate) const fn is_empty(&self) -> bool {
        self.tabs.is_empty()
    }

    pub(crate) fn append(
        &mut self,
        title: impl Into<String>,
        root: impl Into<PathBuf>,
        value: T,
    ) -> TabId {
        let id = TabId(self.next_id);
        self.append_with_id(id, title, root, value)
    }

    pub(crate) fn append_with_id(
        &mut self,
        id: TabId,
        title: impl Into<String>,
        root: impl Into<PathBuf>,
        value: T,
    ) -> TabId {
        self.next_id = self.next_id.max(id.value().wrapping_add(1));
        self.tabs.push(RepositoryTab {
            id,
            title: title.into(),
            root: root.into(),
            pending: false,
            value,
        });
        self.active = Some(id);
        id
    }

    pub(crate) fn append_pending_with_id(
        &mut self,
        id: TabId,
        title: impl Into<String>,
        root: impl Into<PathBuf>,
        value: T,
    ) -> TabId {
        self.next_id = self.next_id.max(id.value().wrapping_add(1));
        self.tabs.push(RepositoryTab {
            id,
            title: title.into(),
            root: root.into(),
            pending: true,
            value,
        });
        self.active = Some(id);
        id
    }

    pub(crate) fn append_pending(
        &mut self,
        title: impl Into<String>,
        root: impl Into<PathBuf>,
        value: T,
    ) -> TabId {
        let id = TabId(self.next_id);
        self.append_pending_with_id(id, title, root, value)
    }

    pub(crate) fn is_pending(&self, id: TabId) -> bool {
        self.tabs
            .iter()
            .find(|tab| tab.id == id)
            .is_some_and(|tab| tab.pending)
    }

    pub(crate) fn id_for_root(&self, root: &Path) -> Option<TabId> {
        self.tabs
            .iter()
            .find(|tab| tab.root == root)
            .map(|tab| tab.id)
    }

    pub(crate) fn activate(&mut self, id: TabId) -> bool {
        if self.tabs.iter().any(|tab| tab.id == id) {
            self.active = Some(id);
            true
        } else {
            false
        }
    }

    pub(crate) fn replace(
        &mut self,
        id: TabId,
        title: impl Into<String>,
        root: impl Into<PathBuf>,
        value: T,
    ) -> Option<T> {
        let tab = self.tabs.iter_mut().find(|tab| tab.id == id)?;
        tab.title = title.into();
        tab.root = root.into();
        tab.pending = false;
        Some(std::mem::replace(&mut tab.value, value))
    }

    #[cfg(test)]
    pub(crate) fn cycle_next(&mut self) -> Option<TabId> {
        let next = self.active.map_or_else(
            || self.tabs.first(),
            |active| {
                self.tabs
                    .iter()
                    .skip_while(|tab| tab.id != active)
                    .nth(1)
                    .or_else(|| self.tabs.first())
            },
        )?;
        self.active = Some(next.id);
        Some(next.id)
    }

    #[cfg(test)]
    pub(crate) fn cycle_previous(&mut self) -> Option<TabId> {
        let previous = self.active.map_or_else(
            || self.tabs.last(),
            |active| {
                self.tabs
                    .iter()
                    .position(|tab| tab.id == active)
                    .and_then(|index| index.checked_sub(1))
                    .and_then(|index| self.tabs.get(index))
                    .or_else(|| self.tabs.last())
            },
        )?;
        self.active = Some(previous.id);
        Some(previous.id)
    }

    pub(crate) fn reorder(&mut self, source: TabId, target: TabId) -> bool {
        let Some(source_index) = self.tabs.iter().position(|tab| tab.id == source) else {
            return false;
        };
        let Some(target_index) = self.tabs.iter().position(|tab| tab.id == target) else {
            return false;
        };
        if source_index == target_index {
            return true;
        }
        let tab = self.tabs.remove(source_index);
        self.tabs.insert(target_index, tab);
        true
    }

    pub(crate) fn close(&mut self, id: TabId) -> Option<T> {
        let index = self.tabs.iter().position(|tab| tab.id == id)?;
        let was_active = self.active == Some(id);
        let removed = self.tabs.remove(index);
        if was_active {
            self.active = self
                .tabs
                .get(index)
                .or_else(|| self.tabs.last())
                .map(|tab| tab.id);
        }
        Some(removed.value)
    }

    pub(crate) fn close_others(&mut self, id: TabId) -> Vec<T> {
        if !self.tabs.iter().any(|tab| tab.id == id) {
            return Vec::new();
        }
        let mut retained = Vec::with_capacity(1);
        let mut removed = Vec::with_capacity(self.tabs.len().saturating_sub(1));
        for tab in self.tabs.drain(..) {
            if tab.id == id {
                retained.push(tab);
            } else {
                removed.push(tab.value);
            }
        }
        self.tabs = retained;
        self.active = Some(id);
        removed
    }

    pub(crate) fn close_all(&mut self) -> Vec<T> {
        self.active = None;
        std::mem::take(&mut self.tabs)
            .into_iter()
            .map(|tab| tab.value)
            .collect()
    }

    pub(crate) fn infos(&self) -> Vec<TabInfo> {
        self.tabs
            .iter()
            .map(|tab| TabInfo {
                id: tab.id,
                title: tab.title.clone(),
                root: tab.root.clone(),
                machine: None,
                active: self.active == Some(tab.id),
            })
            .collect()
    }
}

#[cfg(test)]
mod tests;