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