#![allow(dead_code)]
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::TabId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum GroupColor {
Red,
Orange,
Yellow,
Green,
Cyan,
#[default]
Blue,
Magenta,
Gray,
}
impl GroupColor {
pub fn short(&self) -> &'static str {
match self {
GroupColor::Red => "Rd",
GroupColor::Orange => "Or",
GroupColor::Yellow => "Yl",
GroupColor::Green => "Gn",
GroupColor::Cyan => "Cy",
GroupColor::Blue => "Bl",
GroupColor::Magenta => "Mg",
GroupColor::Gray => "Gy",
}
}
pub fn next(&self) -> Self {
match self {
GroupColor::Red => GroupColor::Orange,
GroupColor::Orange => GroupColor::Yellow,
GroupColor::Yellow => GroupColor::Green,
GroupColor::Green => GroupColor::Cyan,
GroupColor::Cyan => GroupColor::Blue,
GroupColor::Blue => GroupColor::Magenta,
GroupColor::Magenta => GroupColor::Gray,
GroupColor::Gray => GroupColor::Red,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TabGroup {
pub id: String,
pub name: String,
pub color: GroupColor,
pub tab_ids: Vec<TabId>,
pub created_at: DateTime<Utc>,
}
impl TabGroup {
pub fn new(name: String, color: GroupColor) -> Self {
let id = format!("group_{}", chrono::Utc::now().timestamp_millis());
Self {
id,
name,
color,
tab_ids: Vec::new(),
created_at: Utc::now(),
}
}
pub fn add_tab(&mut self, tab_id: TabId) {
if !self.tab_ids.contains(&tab_id) {
self.tab_ids.push(tab_id);
}
}
pub fn remove_tab(&mut self, tab_id: TabId) -> bool {
if let Some(pos) = self.tab_ids.iter().position(|t| *t == tab_id) {
self.tab_ids.swap_remove(pos);
true
} else {
false
}
}
pub fn len(&self) -> usize {
self.tab_ids.len()
}
pub fn is_empty(&self) -> bool {
self.tab_ids.is_empty()
}
pub fn contains(&self, tab_id: TabId) -> bool {
self.tab_ids.contains(&tab_id)
}
}
pub struct TabGroupManager {
pub(crate) groups: HashMap<String, TabGroup>,
pub(crate) tab_to_group: HashMap<TabId, String>,
next_id: u64,
}
impl TabGroupManager {
pub fn new() -> Self {
Self {
groups: HashMap::new(),
tab_to_group: HashMap::new(),
next_id: 1,
}
}
pub fn create_group(&mut self, name: String, color: GroupColor) -> String {
let id = self.generate_group_id();
let group = TabGroup {
id: id.clone(),
name,
color,
tab_ids: Vec::new(),
created_at: Utc::now(),
};
self.groups.insert(id.clone(), group);
id
}
pub fn delete_group(&mut self, group_id: &str) -> bool {
if let Some(group) = self.groups.remove(group_id) {
for tab_id in &group.tab_ids {
self.tab_to_group.remove(tab_id);
}
true
} else {
false
}
}
pub fn assign_tab(&mut self, tab_id: TabId, group_id: &str) -> bool {
self.unassign_tab(tab_id);
if let Some(group) = self.groups.get_mut(group_id) {
group.add_tab(tab_id);
self.tab_to_group.insert(tab_id, group_id.to_string());
true
} else {
false
}
}
pub fn unassign_tab(&mut self, tab_id: TabId) {
if let Some(prev_group_id) = self.tab_to_group.remove(&tab_id)
&& let Some(group) = self.groups.get_mut(&prev_group_id)
{
group.remove_tab(tab_id);
}
}
pub fn group_of(&self, tab_id: TabId) -> Option<&TabGroup> {
self.tab_to_group
.get(&tab_id)
.and_then(|id| self.groups.get(id))
}
pub fn all_groups(&self) -> Vec<&TabGroup> {
let mut groups: Vec<&TabGroup> = self.groups.values().collect();
groups.sort_by(|a, b| a.name.cmp(&b.name));
groups
}
pub fn tabs_in_group(&self, group_id: &str) -> Option<&Vec<TabId>> {
self.groups.get(group_id).map(|g| &g.tab_ids)
}
pub fn group_count(&self) -> usize {
self.groups.len()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &TabGroup)> {
self.groups.iter()
}
pub fn cycle_tab_group(&mut self, tab_id: TabId) {
let group_ids: Vec<String> = self.all_groups().iter().map(|g| g.id.clone()).collect();
if let Some(current_group_id) = self.tab_to_group.get(&tab_id).cloned() {
if let Some(pos) = group_ids.iter().position(|id| id == ¤t_group_id) {
if pos + 1 < group_ids.len() {
let next = group_ids[pos + 1].clone();
self.assign_tab(tab_id, &next);
} else {
self.unassign_tab(tab_id);
}
}
} else if !group_ids.is_empty() {
let first = group_ids[0].clone();
self.assign_tab(tab_id, &first);
}
}
fn generate_group_id(&mut self) -> String {
let id = self.next_id;
self.next_id += 1;
format!("group_{}", id)
}
pub(crate) fn advance_next_id_past_existing_groups(&mut self) {
let max_seen = self
.groups
.keys()
.filter_map(|id| id.strip_prefix("group_"))
.filter_map(|suffix| suffix.parse::<u64>().ok())
.max()
.unwrap_or(0);
self.next_id = self.next_id.max(max_seen + 1);
}
}
impl Default for TabGroupManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_and_delete_group() {
let mut mgr = TabGroupManager::new();
let id = mgr.create_group("Frontend".to_string(), GroupColor::Blue);
assert_eq!(mgr.group_count(), 1);
assert!(mgr.delete_group(&id));
assert_eq!(mgr.group_count(), 0);
}
#[test]
fn test_assign_tab() {
let mut mgr = TabGroupManager::new();
let group_id = mgr.create_group("Backend".to_string(), GroupColor::Green);
let tab1 = TabId::new(1);
let tab2 = TabId::new(2);
assert!(mgr.assign_tab(tab1, &group_id));
assert!(mgr.assign_tab(tab2, &group_id));
let group = mgr.group_of(tab1).unwrap();
assert_eq!(group.len(), 2);
assert!(group.contains(tab1));
}
#[test]
fn test_unassign_tab() {
let mut mgr = TabGroupManager::new();
let group_id = mgr.create_group("Test".to_string(), GroupColor::Red);
let tab1 = TabId::new(1);
mgr.assign_tab(tab1, &group_id);
mgr.unassign_tab(tab1);
assert!(mgr.group_of(tab1).is_none());
let group = mgr.groups.get(&group_id).unwrap();
assert_eq!(group.len(), 0);
}
#[test]
fn test_reassign_tab() {
let mut mgr = TabGroupManager::new();
let g1 = mgr.create_group("G1".to_string(), GroupColor::Blue);
let g2 = mgr.create_group("G2".to_string(), GroupColor::Red);
let tab1 = TabId::new(1);
mgr.assign_tab(tab1, &g1);
assert_eq!(mgr.group_of(tab1).unwrap().id, g1);
mgr.assign_tab(tab1, &g2);
assert_eq!(mgr.group_of(tab1).unwrap().id, g2);
let g1_ref = mgr.groups.get(&g1).unwrap();
assert_eq!(g1_ref.len(), 0);
}
#[test]
fn test_color_cycle() {
let c = GroupColor::Red;
assert_eq!(c.next(), GroupColor::Orange);
assert_eq!(c.next().next(), GroupColor::Yellow);
let gray = GroupColor::Gray;
assert_eq!(gray.next(), GroupColor::Red);
}
#[test]
fn test_cycle_tab_group() {
let mut mgr = TabGroupManager::new();
let g1 = mgr.create_group("G1".to_string(), GroupColor::Blue);
let g2 = mgr.create_group("G2".to_string(), GroupColor::Red);
let tab1 = TabId::new(1);
mgr.cycle_tab_group(tab1);
assert_eq!(mgr.group_of(tab1).unwrap().id, g1);
mgr.cycle_tab_group(tab1);
assert_eq!(mgr.group_of(tab1).unwrap().id, g2);
mgr.cycle_tab_group(tab1);
assert!(mgr.group_of(tab1).is_none());
}
#[test]
fn test_delete_group_clears_assignments() {
let mut mgr = TabGroupManager::new();
let g1 = mgr.create_group("G1".to_string(), GroupColor::Blue);
let tab1 = TabId::new(1);
mgr.assign_tab(tab1, &g1);
mgr.delete_group(&g1);
assert!(mgr.group_of(tab1).is_none());
assert!(!mgr.tab_to_group.contains_key(&tab1));
}
#[test]
fn test_advance_next_id_after_restore() {
let mut mgr = TabGroupManager::new();
mgr.groups.insert(
"group_7".to_string(),
TabGroup {
id: "group_7".to_string(),
name: "Restored".to_string(),
color: GroupColor::Blue,
tab_ids: Vec::new(),
created_at: Utc::now(),
},
);
mgr.advance_next_id_past_existing_groups();
let new_id = mgr.create_group("Fresh".to_string(), GroupColor::Green);
assert_eq!(new_id, "group_8");
assert!(mgr.groups.contains_key("group_7"));
assert!(mgr.groups.contains_key("group_8"));
}
}