1 extern crate unicode_width;
2 
3 use super::super::app::{ActiveBlock, App, RouteId};
4 use crate::event::Key;
5 use crate::network::IoEvent;
6 use std::convert::TryInto;
7 use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
8 
9 // Handle event when the search input block is active
handler(key: Key, app: &mut App)10 pub fn handler(key: Key, app: &mut App) {
11   match key {
12     Key::Ctrl('k') => {
13       app.input.drain(app.input_idx..app.input.len());
14     }
15     Key::Ctrl('u') => {
16       app.input.drain(..app.input_idx);
17       app.input_idx = 0;
18       app.input_cursor_position = 0;
19     }
20     Key::Ctrl('l') => {
21       app.input = vec![];
22       app.input_idx = 0;
23       app.input_cursor_position = 0;
24     }
25     Key::Ctrl('w') => {
26       if app.input_cursor_position == 0 {
27         return;
28       }
29       let word_end = match app.input[..app.input_idx].iter().rposition(|&x| x != ' ') {
30         Some(index) => index + 1,
31         None => 0,
32       };
33       let word_start = match app.input[..word_end].iter().rposition(|&x| x == ' ') {
34         Some(index) => index + 1,
35         None => 0,
36       };
37       let deleted: String = app.input[word_start..app.input_idx].iter().collect();
38       let deleted_len: u16 = UnicodeWidthStr::width(deleted.as_str()).try_into().unwrap();
39       app.input.drain(word_start..app.input_idx);
40       app.input_idx = word_start;
41       app.input_cursor_position -= deleted_len;
42     }
43     Key::End | Key::Ctrl('e') => {
44       app.input_idx = app.input.len();
45       let input_string: String = app.input.iter().collect();
46       app.input_cursor_position = UnicodeWidthStr::width(input_string.as_str())
47         .try_into()
48         .unwrap();
49     }
50     Key::Home | Key::Ctrl('a') => {
51       app.input_idx = 0;
52       app.input_cursor_position = 0;
53     }
54     Key::Left | Key::Ctrl('b') => {
55       if !app.input.is_empty() && app.input_idx > 0 {
56         let last_c = app.input[app.input_idx - 1];
57         app.input_idx -= 1;
58         app.input_cursor_position -= compute_character_width(last_c);
59       }
60     }
61     Key::Right | Key::Ctrl('f') => {
62       if app.input_idx < app.input.len() {
63         let next_c = app.input[app.input_idx];
64         app.input_idx += 1;
65         app.input_cursor_position += compute_character_width(next_c);
66       }
67     }
68     Key::Esc => {
69       app.set_current_route_state(Some(ActiveBlock::Empty), Some(ActiveBlock::Library));
70     }
71     Key::Enter => {
72       let input_str: String = app.input.iter().collect();
73 
74       process_input(app, input_str);
75     }
76     Key::Char(c) => {
77       app.input.insert(app.input_idx, c);
78       app.input_idx += 1;
79       app.input_cursor_position += compute_character_width(c);
80     }
81     Key::Backspace | Key::Ctrl('h') => {
82       if !app.input.is_empty() && app.input_idx > 0 {
83         let last_c = app.input.remove(app.input_idx - 1);
84         app.input_idx -= 1;
85         app.input_cursor_position -= compute_character_width(last_c);
86       }
87     }
88     Key::Delete | Key::Ctrl('d') => {
89       if !app.input.is_empty() && app.input_idx < app.input.len() {
90         app.input.remove(app.input_idx);
91       }
92     }
93     _ => {}
94   }
95 }
96 
process_input(app: &mut App, input: String)97 fn process_input(app: &mut App, input: String) {
98   // Don't do anything if there is no input
99   if input.is_empty() {
100     return;
101   }
102 
103   // On searching for a track, clear the playlist selection
104   app.selected_playlist_index = Some(0);
105 
106   if attempt_process_uri(app, &input, "https://open.spotify.com/", "/")
107     || attempt_process_uri(app, &input, "spotify:", ":")
108   {
109     return;
110   }
111 
112   // Default fallback behavior: treat the input as a raw search phrase.
113   app.dispatch(IoEvent::GetSearchResults(input, app.get_user_country()));
114   app.push_navigation_stack(RouteId::Search, ActiveBlock::SearchResultBlock);
115 }
116 
spotify_resource_id(base: &str, uri: &str, sep: &str, resource_type: &str) -> (String, bool)117 fn spotify_resource_id(base: &str, uri: &str, sep: &str, resource_type: &str) -> (String, bool) {
118   let uri_prefix = format!("{}{}{}", base, resource_type, sep);
119   let id_string_with_query_params = uri.trim_start_matches(&uri_prefix);
120   let query_idx = id_string_with_query_params
121     .find('?')
122     .unwrap_or_else(|| id_string_with_query_params.len());
123   let id_string = id_string_with_query_params[0..query_idx].to_string();
124   // If the lengths aren't equal, we must have found a match.
125   let matched = id_string_with_query_params.len() != uri.len() && id_string.len() != uri.len();
126   (id_string, matched)
127 }
128 
129 // Returns true if the input was successfully processed as a Spotify URI.
attempt_process_uri(app: &mut App, input: &str, base: &str, sep: &str) -> bool130 fn attempt_process_uri(app: &mut App, input: &str, base: &str, sep: &str) -> bool {
131   let (album_id, matched) = spotify_resource_id(base, input, sep, "album");
132   if matched {
133     app.dispatch(IoEvent::GetAlbum(album_id));
134     return true;
135   }
136 
137   let (artist_id, matched) = spotify_resource_id(base, input, sep, "artist");
138   if matched {
139     app.get_artist(artist_id, "".to_string());
140     app.push_navigation_stack(RouteId::Artist, ActiveBlock::ArtistBlock);
141     return true;
142   }
143 
144   let (track_id, matched) = spotify_resource_id(base, input, sep, "track");
145   if matched {
146     app.dispatch(IoEvent::GetAlbumForTrack(track_id));
147     return true;
148   }
149 
150   let (playlist_id, matched) = spotify_resource_id(base, input, sep, "playlist");
151   if matched {
152     app.dispatch(IoEvent::GetPlaylistTracks(playlist_id, 0));
153     return true;
154   }
155 
156   let (show_id, matched) = spotify_resource_id(base, input, sep, "show");
157   if matched {
158     app.dispatch(IoEvent::GetShow(show_id));
159     return true;
160   }
161 
162   false
163 }
164 
compute_character_width(character: char) -> u16165 fn compute_character_width(character: char) -> u16 {
166   UnicodeWidthChar::width(character)
167     .unwrap()
168     .try_into()
169     .unwrap()
170 }
171 
172 #[cfg(test)]
173 mod tests {
174   use super::*;
175 
str_to_vec_char(s: &str) -> Vec<char>176   fn str_to_vec_char(s: &str) -> Vec<char> {
177     String::from(s).chars().collect()
178   }
179 
180   #[test]
test_compute_character_width_with_multiple_characters()181   fn test_compute_character_width_with_multiple_characters() {
182     assert_eq!(1, compute_character_width('a'));
183     assert_eq!(1, compute_character_width('ß'));
184     assert_eq!(1, compute_character_width('ç'));
185   }
186 
187   #[test]
test_input_handler_clear_input_on_ctrl_l()188   fn test_input_handler_clear_input_on_ctrl_l() {
189     let mut app = App::default();
190 
191     app.input = str_to_vec_char("My text");
192 
193     handler(Key::Ctrl('l'), &mut app);
194 
195     assert_eq!(app.input, str_to_vec_char(""));
196   }
197 
198   #[test]
test_input_handler_ctrl_u()199   fn test_input_handler_ctrl_u() {
200     let mut app = App::default();
201 
202     app.input = str_to_vec_char("My text");
203 
204     handler(Key::Ctrl('u'), &mut app);
205     assert_eq!(app.input, str_to_vec_char("My text"));
206 
207     app.input_cursor_position = 3;
208     app.input_idx = 3;
209     handler(Key::Ctrl('u'), &mut app);
210     assert_eq!(app.input, str_to_vec_char("text"));
211   }
212 
213   #[test]
test_input_handler_ctrl_k()214   fn test_input_handler_ctrl_k() {
215     let mut app = App::default();
216 
217     app.input = str_to_vec_char("My text");
218 
219     handler(Key::Ctrl('k'), &mut app);
220     assert_eq!(app.input, str_to_vec_char(""));
221 
222     app.input = str_to_vec_char("My text");
223     app.input_cursor_position = 2;
224     app.input_idx = 2;
225     handler(Key::Ctrl('k'), &mut app);
226     assert_eq!(app.input, str_to_vec_char("My"));
227 
228     handler(Key::Ctrl('k'), &mut app);
229     assert_eq!(app.input, str_to_vec_char("My"));
230   }
231 
232   #[test]
test_input_handler_ctrl_w()233   fn test_input_handler_ctrl_w() {
234     let mut app = App::default();
235 
236     app.input = str_to_vec_char("My text");
237 
238     handler(Key::Ctrl('w'), &mut app);
239     assert_eq!(app.input, str_to_vec_char("My text"));
240 
241     app.input_cursor_position = 3;
242     app.input_idx = 3;
243     handler(Key::Ctrl('w'), &mut app);
244     assert_eq!(app.input, str_to_vec_char("text"));
245     assert_eq!(app.input_cursor_position, 0);
246     assert_eq!(app.input_idx, 0);
247 
248     app.input = str_to_vec_char("    ");
249     app.input_cursor_position = 3;
250     app.input_idx = 3;
251     handler(Key::Ctrl('w'), &mut app);
252     assert_eq!(app.input, str_to_vec_char(" "));
253     assert_eq!(app.input_cursor_position, 0);
254     assert_eq!(app.input_idx, 0);
255     app.input_cursor_position = 1;
256     app.input_idx = 1;
257     handler(Key::Ctrl('w'), &mut app);
258     assert_eq!(app.input, str_to_vec_char(""));
259     assert_eq!(app.input_cursor_position, 0);
260     assert_eq!(app.input_idx, 0);
261 
262     app.input = str_to_vec_char("Hello there  ");
263     app.input_cursor_position = 13;
264     app.input_idx = 13;
265     handler(Key::Ctrl('w'), &mut app);
266     assert_eq!(app.input, str_to_vec_char("Hello "));
267     assert_eq!(app.input_cursor_position, 6);
268     assert_eq!(app.input_idx, 6);
269   }
270 
271   #[test]
test_input_handler_esc_back_to_playlist()272   fn test_input_handler_esc_back_to_playlist() {
273     let mut app = App::default();
274 
275     app.set_current_route_state(Some(ActiveBlock::MyPlaylists), None);
276     handler(Key::Esc, &mut app);
277 
278     let current_route = app.get_current_route();
279     assert_eq!(current_route.active_block, ActiveBlock::Empty);
280   }
281 
282   #[test]
test_input_handler_on_enter_text()283   fn test_input_handler_on_enter_text() {
284     let mut app = App::default();
285 
286     app.input = str_to_vec_char("My tex");
287     app.input_cursor_position = app.input.len().try_into().unwrap();
288     app.input_idx = app.input.len();
289 
290     handler(Key::Char('t'), &mut app);
291 
292     assert_eq!(app.input, str_to_vec_char("My text"));
293   }
294 
295   #[test]
test_input_handler_backspace()296   fn test_input_handler_backspace() {
297     let mut app = App::default();
298 
299     app.input = str_to_vec_char("My text");
300     app.input_cursor_position = app.input.len().try_into().unwrap();
301     app.input_idx = app.input.len();
302 
303     handler(Key::Backspace, &mut app);
304     assert_eq!(app.input, str_to_vec_char("My tex"));
305 
306     // Test that backspace deletes from the cursor position
307     app.input_idx = 2;
308     app.input_cursor_position = 2;
309 
310     handler(Key::Backspace, &mut app);
311     assert_eq!(app.input, str_to_vec_char("M tex"));
312 
313     app.input_idx = 1;
314     app.input_cursor_position = 1;
315 
316     handler(Key::Ctrl('h'), &mut app);
317     assert_eq!(app.input, str_to_vec_char(" tex"));
318   }
319 
320   #[test]
test_input_handler_delete()321   fn test_input_handler_delete() {
322     let mut app = App::default();
323 
324     app.input = str_to_vec_char("My text");
325     app.input_idx = 3;
326     app.input_cursor_position = 3;
327 
328     handler(Key::Delete, &mut app);
329     assert_eq!(app.input, str_to_vec_char("My ext"));
330 
331     app.input = str_to_vec_char("ラスト");
332     app.input_idx = 1;
333     app.input_cursor_position = 1;
334 
335     handler(Key::Delete, &mut app);
336     assert_eq!(app.input, str_to_vec_char("ラト"));
337 
338     app.input = str_to_vec_char("Rust");
339     app.input_idx = 2;
340     app.input_cursor_position = 2;
341 
342     handler(Key::Ctrl('d'), &mut app);
343     assert_eq!(app.input, str_to_vec_char("Rut"));
344   }
345 
346   #[test]
test_input_handler_left_event()347   fn test_input_handler_left_event() {
348     let mut app = App::default();
349 
350     app.input = str_to_vec_char("My text");
351     let input_len = app.input.len().try_into().unwrap();
352     app.input_idx = app.input.len();
353     app.input_cursor_position = input_len;
354 
355     handler(Key::Left, &mut app);
356     assert_eq!(app.input_cursor_position, input_len - 1);
357     handler(Key::Left, &mut app);
358     assert_eq!(app.input_cursor_position, input_len - 2);
359     handler(Key::Left, &mut app);
360     assert_eq!(app.input_cursor_position, input_len - 3);
361     handler(Key::Ctrl('b'), &mut app);
362     assert_eq!(app.input_cursor_position, input_len - 4);
363     handler(Key::Ctrl('b'), &mut app);
364     assert_eq!(app.input_cursor_position, input_len - 5);
365 
366     // Pretend to smash the left event to test the we have no out-of-bounds crash
367     for _ in 0..20 {
368       handler(Key::Left, &mut app);
369     }
370 
371     assert_eq!(app.input_cursor_position, 0);
372   }
373 
374   #[test]
test_input_handler_on_enter_text_non_english_char()375   fn test_input_handler_on_enter_text_non_english_char() {
376     let mut app = App::default();
377 
378     app.input = str_to_vec_char("ыа");
379     app.input_cursor_position = app.input.len().try_into().unwrap();
380     app.input_idx = app.input.len();
381 
382     handler(Key::Char('ы'), &mut app);
383 
384     assert_eq!(app.input, str_to_vec_char("ыаы"));
385   }
386 
387   #[test]
test_input_handler_on_enter_text_wide_char()388   fn test_input_handler_on_enter_text_wide_char() {
389     let mut app = App::default();
390 
391     app.input = str_to_vec_char("你");
392     app.input_cursor_position = 2; // 你 is 2 char wide
393     app.input_idx = 1; // 1 char
394 
395     handler(Key::Char('好'), &mut app);
396 
397     assert_eq!(app.input, str_to_vec_char("你好"));
398     assert_eq!(app.input_idx, 2);
399     assert_eq!(app.input_cursor_position, 4);
400   }
401 
402   mod test_uri_parsing {
403     use super::*;
404 
405     const URI_BASE: &str = "spotify:";
406     const URL_BASE: &str = "https://open.spotify.com/";
407 
check_uri_parse(expected_id: &str, parsed: (String, bool))408     fn check_uri_parse(expected_id: &str, parsed: (String, bool)) {
409       assert_eq!(parsed.1, true);
410       assert_eq!(parsed.0, expected_id);
411     }
412 
run_test_for_id_and_resource_type(id: &str, resource_type: &str)413     fn run_test_for_id_and_resource_type(id: &str, resource_type: &str) {
414       check_uri_parse(
415         id,
416         spotify_resource_id(
417           URI_BASE,
418           &format!("spotify:{}:{}", resource_type, id),
419           ":",
420           resource_type,
421         ),
422       );
423       check_uri_parse(
424         id,
425         spotify_resource_id(
426           URL_BASE,
427           &format!("https://open.spotify.com/{}/{}", resource_type, id),
428           "/",
429           resource_type,
430         ),
431       )
432     }
433 
434     #[test]
artist()435     fn artist() {
436       let expected_artist_id = "2ye2Wgw4gimLv2eAKyk1NB";
437       run_test_for_id_and_resource_type(expected_artist_id, "artist");
438     }
439 
440     #[test]
album()441     fn album() {
442       let expected_album_id = "5gzLOflH95LkKYE6XSXE9k";
443       run_test_for_id_and_resource_type(expected_album_id, "album");
444     }
445 
446     #[test]
playlist()447     fn playlist() {
448       let expected_playlist_id = "1cJ6lPBYj2fscs0kqBHsVV";
449       run_test_for_id_and_resource_type(expected_playlist_id, "playlist");
450     }
451 
452     #[test]
show()453     fn show() {
454       let expected_show_id = "3aNsrV6lkzmcU1w8u8kA7N";
455       run_test_for_id_and_resource_type(expected_show_id, "show");
456     }
457 
458     #[test]
track()459     fn track() {
460       let expected_track_id = "10igKaIKsSB6ZnWxPxPvKO";
461       run_test_for_id_and_resource_type(expected_track_id, "track");
462     }
463 
464     #[test]
invalid_format_doesnt_match()465     fn invalid_format_doesnt_match() {
466       let swapped = "show:spotify:3aNsrV6lkzmcU1w8u8kA7N";
467       let totally_wrong = "hehe-haha-3aNsrV6lkzmcU1w8u8kA7N";
468       let random = "random string";
469       let (_, matched) = spotify_resource_id(URI_BASE, swapped, ":", "track");
470       assert_eq!(matched, false);
471       let (_, matched) = spotify_resource_id(URI_BASE, totally_wrong, ":", "track");
472       assert_eq!(matched, false);
473       let (_, matched) = spotify_resource_id(URL_BASE, totally_wrong, "/", "track");
474       assert_eq!(matched, false);
475       let (_, matched) = spotify_resource_id(URL_BASE, random, "/", "track");
476       assert_eq!(matched, false);
477     }
478 
479     #[test]
parse_with_query_parameters()480     fn parse_with_query_parameters() {
481       // If this test ever fails due to some change to the parsing logic, it is likely a sign we
482       // should just integrate the url crate instead of trying to do things ourselves.
483       let playlist_url_with_query =
484         "https://open.spotify.com/playlist/1cJ6lPBYj2fscs0kqBHsVV?si=OdwuJsbsSeuUAOadehng3A";
485       let playlist_url = "https://open.spotify.com/playlist/1cJ6lPBYj2fscs0kqBHsVV";
486       let expected_id = "1cJ6lPBYj2fscs0kqBHsVV";
487 
488       let (actual_id, matched) = spotify_resource_id(URL_BASE, playlist_url, "/", "playlist");
489       assert_eq!(matched, true);
490       assert_eq!(actual_id, expected_id);
491 
492       let (actual_id, matched) =
493         spotify_resource_id(URL_BASE, playlist_url_with_query, "/", "playlist");
494       assert_eq!(matched, true);
495       assert_eq!(actual_id, expected_id);
496     }
497 
498     #[test]
mismatched_resource_types_do_not_match()499     fn mismatched_resource_types_do_not_match() {
500       let playlist_url =
501         "https://open.spotify.com/playlist/1cJ6lPBYj2fscs0kqBHsVV?si=OdwuJsbsSeuUAOadehng3A";
502       let (_, matched) = spotify_resource_id(URL_BASE, playlist_url, "/", "album");
503       assert_eq!(matched, false);
504     }
505   }
506 }
507