Rust · 5842 bytes Raw Blame History
1 //! Welcome menu for workspace selection
2 //!
3 //! Displays when fackr is launched without arguments, allowing the user to:
4 //! - Select the current directory as workspace
5 //! - Choose from recently opened workspaces
6 //! - Browse for a directory
7
8 use anyhow::Result;
9 use crossterm::event::{self, Event};
10 use std::path::PathBuf;
11
12 use crate::input::{Key, Modifiers};
13 use crate::render::Screen;
14 use crate::workspace::{recents_get, Recent};
15
16 /// Result of the welcome menu interaction
17 #[derive(Debug)]
18 pub enum WelcomeResult {
19 /// User selected a workspace
20 Selected(PathBuf),
21 /// User quit without selecting
22 Quit,
23 }
24
25 /// Welcome menu state
26 pub struct WelcomeMenu {
27 /// Current directory option (always shown at top)
28 current_dir: PathBuf,
29 /// Recent workspaces
30 recents: Vec<Recent>,
31 /// Currently selected index (0 = current dir, 1+ = recents)
32 selected: usize,
33 /// Scroll offset for the list
34 scroll: usize,
35 }
36
37 impl WelcomeMenu {
38 pub fn new() -> Self {
39 let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
40 let recents = recents_get();
41
42 Self {
43 current_dir,
44 recents,
45 selected: 0,
46 scroll: 0,
47 }
48 }
49
50 /// Total number of items (current dir + recents)
51 pub fn item_count(&self) -> usize {
52 1 + self.recents.len()
53 }
54
55 /// Get the selected path
56 pub fn selected_path(&self) -> PathBuf {
57 if self.selected == 0 {
58 self.current_dir.clone()
59 } else {
60 self.recents[self.selected - 1].path.clone()
61 }
62 }
63
64 /// Move selection up
65 pub fn move_up(&mut self) {
66 if self.selected > 0 {
67 self.selected -= 1;
68 self.ensure_visible();
69 }
70 }
71
72 /// Move selection down
73 pub fn move_down(&mut self) {
74 if self.selected + 1 < self.item_count() {
75 self.selected += 1;
76 self.ensure_visible();
77 }
78 }
79
80 /// Move selection to top
81 pub fn move_to_top(&mut self) {
82 self.selected = 0;
83 self.scroll = 0;
84 }
85
86 /// Move selection to bottom
87 pub fn move_to_bottom(&mut self) {
88 if self.item_count() > 0 {
89 self.selected = self.item_count() - 1;
90 self.ensure_visible();
91 }
92 }
93
94 /// Ensure selected item is visible
95 fn ensure_visible(&mut self) {
96 // We'll update scroll based on visible area in render
97 }
98
99 /// Update scroll to ensure selection is visible within given visible_rows
100 pub fn update_viewport(&mut self, visible_rows: usize) {
101 if visible_rows == 0 {
102 return;
103 }
104 if self.selected < self.scroll {
105 self.scroll = self.selected;
106 } else if self.selected >= self.scroll + visible_rows {
107 self.scroll = self.selected - visible_rows + 1;
108 }
109 }
110
111 /// Get items to display, returns (label, path_display, is_selected, is_current_dir)
112 pub fn visible_items(&self) -> Vec<(String, String, bool, bool)> {
113 let mut items = Vec::new();
114
115 // Current directory is always first
116 let current_label = self
117 .current_dir
118 .file_name()
119 .map(|s| s.to_string_lossy().to_string())
120 .unwrap_or_else(|| self.current_dir.to_string_lossy().to_string());
121 let current_path = self.current_dir.to_string_lossy().to_string();
122 items.push((
123 format!(" {} (current directory)", current_label),
124 current_path,
125 self.selected == 0,
126 true,
127 ));
128
129 // Recent workspaces
130 for (i, recent) in self.recents.iter().enumerate() {
131 let path_display = recent.path.to_string_lossy().to_string();
132 items.push((
133 format!(" {}", recent.label),
134 path_display,
135 self.selected == i + 1,
136 false,
137 ));
138 }
139
140 items
141 }
142
143 /// Get current scroll offset
144 pub fn scroll(&self) -> usize {
145 self.scroll
146 }
147
148 /// Handle a key press, returns Some(result) if menu should close
149 pub fn handle_key(&mut self, key: Key, _mods: Modifiers) -> Option<WelcomeResult> {
150 match key {
151 Key::Up | Key::Char('k') => {
152 self.move_up();
153 None
154 }
155 Key::Down | Key::Char('j') => {
156 self.move_down();
157 None
158 }
159 Key::Home => {
160 self.move_to_top();
161 None
162 }
163 Key::End => {
164 self.move_to_bottom();
165 None
166 }
167 Key::Enter => Some(WelcomeResult::Selected(self.selected_path())),
168 Key::Escape | Key::Char('q') => Some(WelcomeResult::Quit),
169 _ => None,
170 }
171 }
172
173 /// Run the welcome menu, returns selected path or None if user quit
174 /// Assumes screen is already in raw mode
175 pub fn run(screen: &mut Screen) -> Result<Option<PathBuf>> {
176 let mut menu = WelcomeMenu::new();
177
178 loop {
179 // Update viewport based on visible area
180 let visible_rows = screen.rows.saturating_sub(10) as usize;
181 menu.update_viewport(visible_rows);
182
183 // Render
184 screen.render_welcome(&menu.visible_items(), menu.scroll())?;
185
186 // Wait for input
187 if let Event::Key(key_event) = event::read()? {
188 let (key, mods) = Key::from_crossterm(key_event);
189 if let Some(result) = menu.handle_key(key, mods) {
190 return match result {
191 WelcomeResult::Selected(path) => Ok(Some(path)),
192 WelcomeResult::Quit => Ok(None),
193 };
194 }
195 }
196 }
197 }
198 }
199