Krolyxon
Commit 7f77734

rmptuisrcevent_handlerhandler.rs

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