kimun_notes/components/dialogs/
workspace_switcher.rs1use 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)>, 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}