Skip to main content

kimun_notes/components/dialogs/
workspace_switcher.rs

1use ratatui::Frame;
2use ratatui::crossterm::event::{KeyCode, KeyEvent};
3use ratatui::layout::{Constraint, Direction, Layout, Rect};
4use ratatui::style::{Modifier, Style};
5use ratatui::widgets::{List, ListItem, ListState, Paragraph};
6
7use crate::components::event_state::EventState;
8use crate::components::events::{AppEvent, AppTx};
9use crate::components::panel::{ModalSpec, modal_chrome};
10use crate::settings::AppSettings;
11use crate::settings::themes::Theme;
12
13pub struct WorkspaceSwitcherModal {
14    workspaces: Vec<(String, bool)>, // (name, is_current)
15    list_state: ListState,
16}
17
18impl WorkspaceSwitcherModal {
19    pub fn new(settings: &AppSettings) -> Self {
20        let mut workspaces: Vec<(String, bool)> = Vec::new();
21        if let Some(ref wc) = settings.workspace_config {
22            let current = &wc.global.current_workspace;
23            let mut names: Vec<&String> = wc.workspaces.keys().collect();
24            names.sort();
25            for name in names {
26                workspaces.push((name.clone(), name == current));
27            }
28        }
29        let mut list_state = ListState::default();
30        if !workspaces.is_empty() {
31            let current_idx = workspaces
32                .iter()
33                .position(|(_, is_cur)| *is_cur)
34                .unwrap_or(0);
35            list_state.select(Some(current_idx));
36        }
37        Self {
38            workspaces,
39            list_state,
40        }
41    }
42
43    pub fn handle_key(&mut self, key: KeyEvent, tx: &AppTx) -> EventState {
44        match key.code {
45            KeyCode::Up => {
46                if !self.workspaces.is_empty() {
47                    let cur = self.list_state.selected().unwrap_or(0);
48                    let next = if cur == 0 {
49                        self.workspaces.len() - 1
50                    } else {
51                        cur - 1
52                    };
53                    self.list_state.select(Some(next));
54                }
55                EventState::Consumed
56            }
57            KeyCode::Down => {
58                if !self.workspaces.is_empty() {
59                    let cur = self.list_state.selected().unwrap_or(0);
60                    let next = (cur + 1) % self.workspaces.len();
61                    self.list_state.select(Some(next));
62                }
63                EventState::Consumed
64            }
65            KeyCode::Enter => {
66                if let Some(idx) = self.list_state.selected()
67                    && let Some((name, is_current)) = self.workspaces.get(idx)
68                    && !is_current
69                {
70                    tx.send(AppEvent::WorkspaceSwitched(name.clone())).ok();
71                }
72                tx.send(AppEvent::CloseOverlay).ok();
73                EventState::Consumed
74            }
75            KeyCode::Esc => {
76                tx.send(AppEvent::CloseOverlay).ok();
77                EventState::Consumed
78            }
79            _ => EventState::NotConsumed,
80        }
81    }
82
83    pub fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, _focused: bool) {
84        let fg = theme.fg.to_ratatui();
85        let gray = theme.gray.to_ratatui();
86        let bg = theme.bg_panel.to_ratatui();
87
88        let height = (self.workspaces.len() as u16 + 5).min(rect.height.saturating_sub(4));
89        let width = 50u16.min(rect.width.saturating_sub(4));
90        let popup = super::fixed_centered_rect(width, height, rect);
91
92        let inner = modal_chrome(
93            f,
94            popup,
95            theme,
96            ModalSpec {
97                title: Some(" Switch Workspace "),
98                border: Some(Style::default().fg(theme.focus_border.to_ratatui())),
99                ..Default::default()
100            },
101        );
102
103        if self.workspaces.is_empty() {
104            f.render_widget(
105                Paragraph::new("  No workspaces configured.\n  Use Preferences to create one.")
106                    .style(Style::default().fg(gray).bg(bg)),
107                inner,
108            );
109            return;
110        }
111
112        let rows = Layout::default()
113            .direction(Direction::Vertical)
114            .constraints([Constraint::Min(0), Constraint::Length(1)])
115            .split(inner);
116
117        let items: Vec<ListItem> = self
118            .workspaces
119            .iter()
120            .map(|(name, is_current)| {
121                let marker = if *is_current { "\u{25CF} " } else { "  " };
122                let style = if *is_current {
123                    Style::default()
124                        .fg(theme.accent.to_ratatui())
125                        .bg(bg)
126                        .add_modifier(Modifier::BOLD)
127                } else {
128                    Style::default().fg(fg).bg(bg)
129                };
130                ListItem::new(format!("{}{}", marker, name)).style(style)
131            })
132            .collect();
133
134        let list = List::new(items)
135            .style(Style::default().bg(bg))
136            .highlight_style(Style::default().bg(theme.selection_bg.to_ratatui()));
137
138        f.render_stateful_widget(list, rows[0], &mut self.list_state);
139
140        f.render_widget(
141            Paragraph::new("  [Enter] Switch  [Esc] Cancel")
142                .style(Style::default().fg(gray).bg(bg)),
143            rows[1],
144        );
145    }
146}