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::{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)>, 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}