Krolyxon
Commit 9ff7ca4

rmptuisrcevent_handlerhandler.rs

473 lines · 17.3 KB · Rust

1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
2use rust_fuzzy_search::fuzzy_search_sorted;
3
4use crate::{
5 app::{App, PlaylistFocus, SelectedTab},
6 connection::VolumeStatus,
7 ui::InputMode,
8 AppResult,
9};
10
11pub async fn handle_key_events(key: KeyEvent, app: &mut App) -> AppResult<()> {
12 if app.inputmode != InputMode::Normal {
13 if key
14 .modifiers
15 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
16 && is_bound(app, "quit", key)
17 {
18 app.quit();
19 return Ok(());
20 }
21 return handle_input_key(key, app).await;
22 }
23 if app.playlist_popup {
24 if is_bound(app, "popup_close", key) {
25 app.playlist_popup = false;
26 } else if is_bound(app, "move_down", key) {
27 app.append_list.next();
28 } else if is_bound(app, "move_up", key) {
29 app.append_list.prev();
30 } else if is_bound(app, "popup_select", key) {
31 app.append_selected_to().await?;
32 }
33 return Ok(());
34 }
35 if app.selected_tab == SelectedTab::DirectoryBrowser && !app.browser_search_results.is_empty() {
36 if is_bound(app, "search_next", key) {
37 cycle_browser_search(app, true);
38 return Ok(());
39 } else if is_bound(app, "search_previous", key) {
40 cycle_browser_search(app, false);
41 return Ok(());
42 }
43 }
44
45 if is_bound(app, "quit", key) {
46 app.quit();
47 } else if is_bound(app, "clear_queue", key) {
48 app.conn.clear().await?;
49 app.update_queue().await?;
50 app.conn.update_status().await?;
51 } else if is_bound(app, "toggle_pause", key) {
52 app.conn.toggle_pause().await?;
53 } else if is_bound(app, "pause", key) {
54 app.conn.pause().await?;
55 } else if is_bound(app, "toggle_repeat", key) {
56 app.conn.toggle_repeat().await?;
57 } else if is_bound(app, "toggle_random", key) {
58 app.conn.toggle_random().await?;
59 } else if is_bound(app, "open_playlist_menu", key) {
60 app.playlist_popup = true;
61 } else if is_bound(app, "seek_forward", key) || is_bound(app, "seek_backward", key) {
62 if let Some(position) = app.conn.status.song {
63 let current = app.conn.status.elapsed.as_secs();
64 let target = if is_bound(app, "seek_forward", key) {
65 current.saturating_add(app.config.playback.seek_seconds)
66 } else {
67 current.saturating_sub(app.config.playback.seek_seconds)
68 };
69 app.conn
70 .seek(position.pos, std::time::Duration::from_secs(target))
71 .await?;
72 app.conn.update_status().await?;
73 }
74 } else if is_bound(app, "cycle_tabs", key) {
75 app.cycle_tabs();
76 if app.selected_tab == SelectedTab::Playlists {
77 app.refresh_playlist_view().await?;
78 }
79 } else if is_bound(app, "select_queue", key) {
80 app.selected_tab = SelectedTab::Queue;
81 } else if is_bound(app, "select_browser", key) {
82 app.selected_tab = SelectedTab::DirectoryBrowser;
83 } else if is_bound(app, "select_playlists", key) {
84 app.selected_tab = SelectedTab::Playlists;
85 app.refresh_playlist_view().await?;
86 } else if is_bound(app, "next_track", key) {
87 if !app.queue_list.list.is_empty() {
88 app.conn.next().await?;
89 app.update_queue().await?;
90 app.conn.update_status().await?;
91 }
92 } else if is_bound(app, "previous_track", key) {
93 if !app.queue_list.list.is_empty() {
94 app.conn.previous().await?;
95 app.update_queue().await?;
96 app.conn.update_status().await?;
97 }
98 } else if is_bound(app, "volume_up", key) {
99 app.conn
100 .inc_volume(app.config.playback.volume_step as i8)
101 .await?;
102 } else if is_bound(app, "volume_down", key) {
103 app.conn
104 .dec_volume(app.config.playback.volume_step as i8)
105 .await?;
106 } else if is_bound(app, "toggle_mute", key) {
107 match app.conn.volume_status {
108 VolumeStatus::Muted(volume) => {
109 app.conn.set_volume(volume).await?;
110 app.conn.volume_status = VolumeStatus::Unmuted;
111 }
112 VolumeStatus::Unmuted => {
113 app.conn.set_volume(0).await?;
114 app.conn.volume_status = VolumeStatus::Muted(app.conn.status.volume);
115 }
116 }
117 app.conn.update_status().await?;
118 } else if is_bound(app, "update_database", key) {
119 app.conn.update_database().await?;
120 app.should_update_song_list = true;
121 app.conn.update_status().await?;
122 } else if is_bound(app, "search", key) {
123 app.inputmode = InputMode::Editing;
124 app.search_input.clear();
125 app.search_cursor_pos = 0;
126 app.browser_search_results.clear();
127 app.browser_search_index = 0;
128 } else if is_bound(app, "toggle_selected_item", key) {
129 app.toggle_selected_item().await?;
130 }
131
132 match app.selected_tab {
133 SelectedTab::Queue => handle_queue_key(key, app).await?,
134 SelectedTab::DirectoryBrowser => handle_browser_key(key, app).await?,
135 SelectedTab::Playlists => handle_playlists_key(key, app).await?,
136 }
137 Ok(())
138}
139
140fn is_bound(app: &App, action: &str, key: KeyEvent) -> bool {
141 app.config.keybindings.matches(action, key)
142}
143
144async fn handle_queue_key(key: KeyEvent, app: &mut App) -> AppResult<()> {
145 if is_bound(app, "move_down", key) {
146 app.queue_list.next();
147 } else if is_bound(app, "move_up", key) {
148 app.queue_list.prev();
149 } else if is_bound(app, "activate", key) {
150 if app.queue_list.index < app.queue_list.list.len() {
151 app.conn.play(Some(app.queue_list.index as u32)).await?;
152 app.conn.update_status().await?;
153 }
154 } else if is_bound(app, "queue_delete", key) {
155 if app.queue_list.index < app.queue_list.list.len() {
156 app.conn.delete(app.queue_list.index as u32).await?;
157 app.update_queue().await?;
158 app.conn.update_status().await?;
159 }
160 } else if is_bound(app, "queue_move_down", key) {
161 let position = app.queue_list.index;
162 if position + 1 < app.queue_list.list.len() {
163 app.conn
164 .swap(position as u32, (position + 1) as u32)
165 .await?;
166 app.queue_list.index += 1;
167 app.update_queue().await?;
168 }
169 } else if is_bound(app, "queue_move_up", key) {
170 let position = app.queue_list.index;
171 if position > 0 && position < app.queue_list.list.len() {
172 app.conn
173 .swap(position as u32, (position - 1) as u32)
174 .await?;
175 app.queue_list.index -= 1;
176 app.update_queue().await?;
177 }
178 } else if is_bound(app, "go_to_top", key) {
179 app.queue_list.index = 0;
180 } else if is_bound(app, "go_to_bottom", key) {
181 app.queue_list.index = app.queue_list.list.len().saturating_sub(1);
182 }
183 Ok(())
184}
185
186async fn handle_browser_key(key: KeyEvent, app: &mut App) -> AppResult<()> {
187 if is_bound(app, "move_down", key) {
188 app.browser.next();
189 } else if is_bound(app, "move_up", key) {
190 app.browser.prev();
191 } else if is_bound(app, "activate", key) {
192 let previous_path = app.browser.path.clone();
193 app.handle_enter().await?;
194 if app.browser.path != previous_path {
195 app.browser_search_results.clear();
196 app.search_input.clear();
197 }
198 } else if is_bound(app, "go_back", key) {
199 app.browser.handle_go_back(&mut app.conn).await?;
200 app.browser_search_results.clear();
201 app.search_input.clear();
202 } else if is_bound(app, "go_to_top", key) {
203 app.browser.selected = 0;
204 } else if is_bound(app, "go_to_bottom", key) {
205 app.browser.selected = app.browser.entries.len().saturating_sub(1);
206 }
207 Ok(())
208}
209
210async fn handle_playlists_key(key: KeyEvent, app: &mut App) -> AppResult<()> {
211 if is_bound(app, "playlist_focus_contents", key) {
212 app.playlist_focus = PlaylistFocus::Songs;
213 return Ok(());
214 }
215 if is_bound(app, "playlist_focus_list", key) {
216 app.playlist_focus = PlaylistFocus::List;
217 return Ok(());
218 }
219
220 if app.playlist_focus == PlaylistFocus::Songs {
221 if is_bound(app, "move_down", key) {
222 app.playlist_song_index = app
223 .playlist_song_index
224 .saturating_add(1)
225 .min(app.playlist_contents.len().saturating_sub(1));
226 } else if is_bound(app, "move_up", key) {
227 app.playlist_song_index = app.playlist_song_index.saturating_sub(1);
228 } else if is_bound(app, "go_to_top", key) {
229 app.playlist_song_index = 0;
230 } else if is_bound(app, "go_to_bottom", key) {
231 app.playlist_song_index = app.playlist_contents.len().saturating_sub(1);
232 } else if is_bound(app, "playlist_delete_song", key) {
233 app.delete_selected_playlist_song().await?;
234 } else if is_bound(app, "playlist_move_song_down", key) {
235 app.move_selected_playlist_song(true).await?;
236 } else if is_bound(app, "playlist_move_song_up", key) {
237 app.move_selected_playlist_song(false).await?;
238 } else if is_bound(app, "activate", key) {
239 app.play_selected_playlist_song().await?;
240 } else if is_bound(app, "playlist_delete", key) {
241 app.delete_selected_playlist().await?;
242 }
243 return Ok(());
244 }
245
246 let previous_index = app.pl_list.index;
247 if is_bound(app, "move_down", key) {
248 app.pl_list.next();
249 } else if is_bound(app, "move_up", key) {
250 app.pl_list.prev();
251 } else if is_bound(app, "go_to_top", key) {
252 app.pl_list.index = 0;
253 } else if is_bound(app, "go_to_bottom", key) {
254 app.pl_list.index = app.pl_list.list.len().saturating_sub(1);
255 } else if is_bound(app, "playlist_rename", key) && !app.pl_list.list.is_empty() {
256 app.inputmode = InputMode::PlaylistRename;
257 app.pl_newname_input.clear();
258 app.pl_cursor_pos = 0;
259 } else if is_bound(app, "playlist_delete", key) {
260 app.delete_selected_playlist().await?;
261 } else if is_bound(app, "playlist_load", key) {
262 if let Some(playlist) = app.pl_list.list.get(app.pl_list.index) {
263 app.conn.load_playlist(playlist).await?;
264 app.update_queue().await?;
265 }
266 }
267 if previous_index != app.pl_list.index {
268 app.playlist_song_index = 0;
269 app.refresh_playlist_view().await?;
270 }
271 Ok(())
272}
273
274async fn handle_input_key(key: KeyEvent, app: &mut App) -> AppResult<()> {
275 if is_bound(app, "input_cancel", key) {
276 match app.inputmode {
277 InputMode::Editing => {
278 app.search_input.clear();
279 app.browser_search_results.clear();
280 app.inputmode = InputMode::Normal;
281 }
282 InputMode::PlaylistRename => {
283 app.pl_newname_input.clear();
284 app.inputmode = InputMode::Normal;
285 }
286 InputMode::NewPlaylist => {
287 app.pl_new_pl_input.clear();
288 app.inputmode = InputMode::Normal;
289 }
290 InputMode::Normal => {}
291 }
292 app.reset_cursor();
293 } else if is_bound(app, "input_backspace", key) {
294 app.delete_char();
295 if app.inputmode == InputMode::Editing {
296 update_search_selection(app);
297 }
298 } else if is_bound(app, "input_cursor_left", key) {
299 app.move_cursor_left();
300 } else if is_bound(app, "input_cursor_right", key) {
301 app.move_cursor_right();
302 } else if is_bound(app, "input_submit", key) {
303 match app.inputmode {
304 InputMode::Editing => {
305 update_search_selection(app);
306 if app.selected_tab != SelectedTab::DirectoryBrowser {
307 app.search_input.clear();
308 }
309 if app.selected_tab == SelectedTab::Playlists {
310 app.refresh_playlist_view().await?;
311 }
312 app.reset_cursor();
313 app.inputmode = InputMode::Normal;
314 }
315 InputMode::PlaylistRename => {
316 if let Some(old_name) = app.pl_list.list.get(app.pl_list.index).cloned() {
317 if !app.pl_newname_input.trim().is_empty() {
318 app.conn
319 .rename_playlist(&old_name, &app.pl_newname_input)
320 .await?;
321 app.refresh_playlists().await?;
322 }
323 }
324 app.pl_newname_input.clear();
325 app.reset_cursor();
326 app.inputmode = InputMode::Normal;
327 }
328 InputMode::NewPlaylist => {
329 if !app.pl_new_pl_input.trim().is_empty() {
330 for song in &app.pl_new_pl_songs_buffer {
331 app.conn.add_to_playlist(&app.pl_new_pl_input, song).await?;
332 }
333 app.refresh_playlists().await?;
334 }
335 app.pl_new_pl_input.clear();
336 app.reset_cursor();
337 app.inputmode = InputMode::Normal;
338 }
339 InputMode::Normal => {}
340 }
341 } else if let KeyCode::Char(character) = key.code {
342 if !key
343 .modifiers
344 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
345 {
346 app.enter_char(character);
347 if app.inputmode == InputMode::Editing {
348 update_search_selection(app);
349 }
350 }
351 }
352 Ok(())
353}
354
355fn update_search_selection(app: &mut App) {
356 if app.search_input.is_empty() {
357 return;
358 }
359 match app.selected_tab {
360 SelectedTab::Queue => {
361 let items: Vec<&str> = app
362 .queue_list
363 .list
364 .iter()
365 .map(|song| song.file.as_str())
366 .collect();
367 if let Some((best, _)) = fuzzy_search_sorted(&app.search_input, &items).first() {
368 if let Some(index) = app
369 .queue_list
370 .list
371 .iter()
372 .position(|song| song.file == *best)
373 {
374 app.queue_list.index = index;
375 }
376 }
377 }
378 SelectedTab::DirectoryBrowser => {
379 let items: Vec<String> = app
380 .browser
381 .entries
382 .iter()
383 .map(|entry| entry.name().to_string())
384 .collect();
385 app.browser_search_results = fuzzy_indices(&app.search_input, &items);
386 app.browser_search_index = 0;
387 if let Some(index) = app.browser_search_results.first() {
388 app.browser.selected = *index;
389 }
390 }
391 SelectedTab::Playlists => {
392 let items: Vec<&str> = app.pl_list.list.iter().map(String::as_str).collect();
393 if let Some((best, _)) = fuzzy_search_sorted(&app.search_input, &items).first() {
394 if let Some(index) = app.pl_list.list.iter().position(|name| name == *best) {
395 app.pl_list.index = index;
396 }
397 }
398 }
399 }
400}
401
402fn cycle_browser_search(app: &mut App, forward: bool) {
403 let len = app.browser_search_results.len();
404 if len == 0 {
405 return;
406 }
407 app.browser_search_index = if forward {
408 (app.browser_search_index + 1) % len
409 } else {
410 app.browser_search_index.checked_sub(1).unwrap_or(len - 1)
411 };
412 app.browser.selected = app.browser_search_results[app.browser_search_index];
413}
414
415fn fuzzy_indices(query: &str, items: &[String]) -> Vec<usize> {
416 if query.is_empty() {
417 return Vec::new();
418 }
419 let candidates: Vec<&str> = items.iter().map(String::as_str).collect();
420 let ranked = fuzzy_search_sorted(query, &candidates);
421 let mut used = vec![false; items.len()];
422 ranked
423 .into_iter()
424 .filter(|(_, score)| *score > 0.0)
425 .filter_map(|(name, _)| {
426 let index = items
427 .iter()
428 .enumerate()
429 .find(|(index, item)| !used[*index] && item.as_str() == name)
430 .map(|(index, _)| index)?;
431 used[index] = true;
432 Some(index)
433 })
434 .collect()
435}
436
437pub async fn handle_mouse_events(mouse: MouseEvent, app: &mut App) -> AppResult<()> {
438 let previous_playlist = app.pl_list.index;
439 match mouse.kind {
440 MouseEventKind::ScrollUp => app.handle_scroll_up(),
441 MouseEventKind::ScrollDown => app.handle_scroll_down(),
442 _ => {}
443 }
444 if app.selected_tab == SelectedTab::Playlists && app.pl_list.index != previous_playlist {
445 app.playlist_song_index = 0;
446 app.refresh_playlist_view().await?;
447 }
448 Ok(())
449}
450
451#[cfg(test)]
452mod tests {
453 use super::fuzzy_indices;
454
455 #[test]
456 fn fuzzy_search_returns_all_matches_and_keeps_duplicate_names() {
457 let items = vec![
458 "mr-robot-vol1".to_string(),
459 "mr-robot-vol2".to_string(),
460 "mr-robot-vol3".to_string(),
461 "other-album".to_string(),
462 ];
463 let results = fuzzy_indices("robot", &items);
464 assert_eq!(results.len(), 3);
465 assert_eq!(
466 results
467 .iter()
468 .map(|index| items[*index].as_str())
469 .collect::<Vec<_>>(),
470 vec!["mr-robot-vol1", "mr-robot-vol2", "mr-robot-vol3"]
471 );
472 }
473}