diff --git a/src/handlers/app.rs b/src/handlers/app.rs index 76de975e..2832d286 100644 --- a/src/handlers/app.rs +++ b/src/handlers/app.rs @@ -1,4 +1,9 @@ -use std::{cell::RefCell, collections::VecDeque, rc::Rc}; +use std::{ + cell::RefCell, + collections::VecDeque, + process::{Child, Stdio}, + rc::Rc, +}; use chrono::{DateTime, Local}; use rustyline::line_buffer::LineBuffer; @@ -18,6 +23,7 @@ use crate::{ user_input::events::{Event, Key}, }, terminal::TerminalAction, + twitch::TwitchAction, ui::{ components::{Component, Components}, statics::LINE_BUFFER_CAPACITY, @@ -50,6 +56,10 @@ pub struct App { pub theme: Theme, /// Emotes pub emotes: SharedEmotes, + /// Running stream. + // TODO: + // Review if this needs to be a `Rc`. I haven't bothered to check + pub running_stream: Rc>>, } macro_rules! shared { @@ -93,6 +103,7 @@ impl App { Self { components, config: shared_config.clone(), + running_stream: shared!(None), messages, storage, filters, @@ -143,10 +154,15 @@ impl App { } } - pub async fn event(&mut self, event: &Event) -> Option { + pub async fn event(&mut self, event: &Event) -> Option> { if let Event::Input(key) = event { if self.components.debug.is_focused() { - return self.components.debug.event(event).await; + return self + .components + .debug + .event(event) + .await + .map(|ta| ta.map_enter(|()| TwitchAction::Join("".into()))); } match key { @@ -158,7 +174,12 @@ impl App { return match self.state { State::Dashboard => self.components.dashboard.event(event).await, State::Normal => self.components.chat.event(event).await, - State::Help => self.components.help.event(event).await, + State::Help => self + .components + .help + .event(event) + .await + .map(|ta| ta.map_enter(|()| TwitchAction::Join("".into()))), }; } } @@ -167,7 +188,43 @@ impl App { None } + // TODO: + // Should Properly handle if a stream is not available. + // WARN: + // closes a previous stream if open. This is technically overloading this function, but + // whatever. + pub fn open_stream(&self, channel: &str) { + let mut t = self.running_stream.borrow_mut(); + if let Some(c) = t.as_mut() { + c.kill().unwrap(); + } + *t = Some( + std::process::Command::new("streamlink") + .args([ + (String::from("twitch.tv/") + channel).as_str(), + "--default-stream", + "720p, 720p60, best", + "--player", + "mpv", + ]) + .stdout(Stdio::null()) + .spawn() + .expect("Pog"), + ); + } + + // TODO: + // This probably sucks + pub fn close_stream(&self) { + let mut t = self.running_stream.borrow_mut(); + if let Some(c) = t.as_mut() { + c.kill().unwrap(); + } + *t = None; + } + pub fn cleanup(&self) { + self.close_stream(); self.storage.borrow().dump_data(); self.emotes.unload(); } diff --git a/src/handlers/config.rs b/src/handlers/config.rs index 5707608f..f0154f73 100644 --- a/src/handlers/config.rs +++ b/src/handlers/config.rs @@ -94,6 +94,10 @@ pub struct FiltersConfig { pub struct FrontendConfig { /// If the time and date is to be shown. pub show_datetimes: bool, + /// Play stream with `streamlink` upon join a channel. + pub auto_start_streamlink: bool, + /// Only shows currently streaming channels instead of all following channels + pub only_show_live_channels: bool, /// The format of string that will show up in the terminal. pub datetime_format: String, /// If the username should be shown. @@ -169,6 +173,8 @@ impl Default for FrontendConfig { fn default() -> Self { Self { show_datetimes: true, + only_show_live_channels: true, + auto_start_streamlink: false, datetime_format: "%a %b %e %T %Y".to_string(), username_shown: true, palette: Palette::default(), diff --git a/src/handlers/state.rs b/src/handlers/state.rs index c3b6867a..fedc6814 100644 --- a/src/handlers/state.rs +++ b/src/handlers/state.rs @@ -39,7 +39,7 @@ impl FromStr for NormalMode { } } -#[derive(Debug, PartialEq, Eq, Clone, Serialize, DeserializeFromStr)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, DeserializeFromStr)] pub enum State { Dashboard, Normal, diff --git a/src/terminal.rs b/src/terminal.rs index 4081bdc3..74b25210 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -16,12 +16,24 @@ use crate::{ utils::emotes::emotes_enabled, }; -pub enum TerminalAction { +pub enum TerminalAction { Quit, BackOneLayer, SwitchState(State), ClearMessages, - Enter(TwitchAction), + Enter(T), +} + +impl TerminalAction { + pub fn map_enter B>(&self, a_map: F) -> TerminalAction { + match self { + TerminalAction::Quit => TerminalAction::Quit, + TerminalAction::BackOneLayer => TerminalAction::BackOneLayer, + TerminalAction::SwitchState(s) => TerminalAction::SwitchState(*s), + TerminalAction::ClearMessages => TerminalAction::ClearMessages, + TerminalAction::Enter(a) => TerminalAction::Enter(a_map(a)), + } + } } pub async fn ui_driver( @@ -181,10 +193,16 @@ pub async fn ui_driver( app.emotes.unload(); tx.send(TwitchAction::Join(channel.clone())).unwrap(); + + if config.frontend.auto_start_streamlink { + app.open_stream(channel.as_str()); + } + erx = query_emotes(&config, channel); app.set_state(State::Normal); } + TwitchAction::ClearMessages => {} }, } diff --git a/src/twitch/channels.rs b/src/twitch/channels.rs index 0e74d902..97471d4a 100644 --- a/src/twitch/channels.rs +++ b/src/twitch/channels.rs @@ -5,10 +5,13 @@ use std::{ }; use color_eyre::Result; -use reqwest::Client; +use futures::TryFutureExt; use serde::Deserialize; -use crate::{handlers::config::TwitchConfig, ui::components::utils::SearchItemGetter}; +use crate::{ + handlers::config::TwitchConfig, + ui::components::utils::{SearchItemGetter, ToQueryString}, +}; use super::oauth::{get_twitch_client, get_twitch_client_id}; @@ -23,12 +26,68 @@ pub struct FollowingUser { followed_at: String, } +impl ToQueryString for FollowingUser { + fn to_query_string(&self) -> String { + self.broadcaster_name.clone() + } +} + +// "id": "42170724654", +// "user_id": "132954738", +// "user_login": "aws", +// "user_name": "AWS", +// "game_id": "417752", +// "game_name": "Talk Shows & Podcasts", +// "type": "live", +// "title": "AWS Howdy Partner! Y'all welcome ExtraHop to the show!", +// "viewer_count": 20, +// "started_at": "2021-03-31T20:57:26Z", +// "language": "en", +// "thumbnail_url": "https://static-cdn.jtvnw.net/previews-ttv/live_user_aws-{width}x{height}.jpg", +// "tag_ids": [], +// "tags": ["English"] +#[derive(Deserialize, Debug, Clone, Default)] +#[allow(dead_code)] +pub struct StreamingUser { + pub user_login: String, + pub game_name: String, + pub title: String, +} + +impl Display for StreamingUser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + user_login, + game_name, + title, + } = self; + let fmt_game = format!("[{game_name:.22}]"); + write!(f, "{user_login:<16.16}: {fmt_game:<24} {title}",) + } +} + +impl ToQueryString for StreamingUser { + fn to_query_string(&self) -> String { + self.user_login.clone() + } +} + +#[derive(Deserialize, Debug, Clone, Default)] +#[allow(dead_code)] +pub struct StreamingList { + pub data: Vec, + pagination: Pagination, +} + impl Display for FollowingUser { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.broadcaster_login) } } +// data Object[] The list of streams. +// pagination Object The information used to page through the list of results. The object is empty if there are no more pages left to page through. Read More +// cursor String The cursor used to get the next page of results. Set the request’s after or before query parameter to this value depending on whether you’re paging forwards or backwards. #[derive(Deserialize, Debug, Clone, Default)] #[allow(dead_code)] struct Pagination { @@ -52,8 +111,20 @@ pub struct Following { list: FollowingList, } +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct FollowingStreaming { + // TODO: Don't re-create client on new requests + // client: &Client, + twitch_config: TwitchConfig, + list: StreamingList, +} + // https://dev.twitch.tv/docs/api/reference/#get-followed-channels -pub async fn get_user_following(client: &Client, user_id: &str) -> Result { +pub async fn get_following(twitch_config: &TwitchConfig) -> Result { + let client = get_twitch_client(twitch_config.token.as_deref()).await?; + let user_id = &get_twitch_client_id(None).await?.user_id; + Ok(client .get(format!( "https://api.twitch.tv/helix/channels/followed?user_id={user_id}&first={FOLLOWER_COUNT}", @@ -65,11 +136,36 @@ pub async fn get_user_following(client: &Client, user_id: &str) -> Result Result { +// https://dev.twitch.tv/docs/api/reference/#get-followed-streams +pub async fn get_streams(twitch_config: &TwitchConfig) -> Result { let client = get_twitch_client(twitch_config.token.as_deref()).await?; let user_id = &get_twitch_client_id(None).await?.user_id; - get_user_following(&client, user_id).await + let res = client + .clone() + .get(format!( + "https://api.twitch.tv/helix/streams/followed?user_id={user_id}&first={FOLLOWER_COUNT}", + )) + .send() + .await? + .error_for_status()?; + + Ok(res.json::().await?) +} + +impl FollowingStreaming { + pub fn new(twitch_config: TwitchConfig) -> Self { + Self { + twitch_config, + list: StreamingList::default(), + } + } +} + +impl SearchItemGetter for FollowingStreaming { + async fn get_items(&mut self) -> Result> { + get_streams(&self.twitch_config).await.map(|x| x.data) + } } impl Following { @@ -81,15 +177,8 @@ impl Following { } } -impl SearchItemGetter for Following { - async fn get_items(&mut self) -> Result> { - let following = get_following(&self.twitch_config).await; - - following.map(|v| { - v.data - .iter() - .map(ToString::to_string) - .collect::>() - }) +impl SearchItemGetter for Following { + async fn get_items(&mut self) -> Result> { + get_following(&self.twitch_config).await.map(|v| v.data) } } diff --git a/src/twitch/mod.rs b/src/twitch/mod.rs index f58e59b9..d3e7ebf8 100644 --- a/src/twitch/mod.rs +++ b/src/twitch/mod.rs @@ -5,6 +5,7 @@ pub mod oauth; use std::{collections::HashMap, hash::BuildHasher}; +use channels::StreamingUser; use color_eyre::Result; use futures::StreamExt; use irc::{ @@ -120,6 +121,7 @@ pub async fn twitch_irc( tx.send(data_builder.twitch(err.to_string())).await.unwrap(); } + // Set old channel to new channel config.twitch.channel = channel; } diff --git a/src/ui/components/channel_switcher.rs b/src/ui/components/channel_switcher.rs index 50c8a7bb..ebe6cb2a 100644 --- a/src/ui/components/channel_switcher.rs +++ b/src/ui/components/channel_switcher.rs @@ -144,7 +144,7 @@ impl Display for ChannelSwitcherWidget { } } -impl Component for ChannelSwitcherWidget { +impl Component for ChannelSwitcherWidget { fn draw(&mut self, f: &mut Frame, area: Option) { let mut r = area.map_or_else(|| centered_rect(60, 60, 23, f.area()), |a| a); // Make sure we have space for the input widget, which has a height of 3. @@ -256,7 +256,7 @@ impl Component for ChannelSwitcherWidget { self.search_input.draw(f, Some(input_rect)); } - async fn event(&mut self, event: &Event) -> Option { + async fn event(&mut self, event: &Event) -> Option> { if let Event::Input(key) = event { match key { Key::Esc => { diff --git a/src/ui/components/chat.rs b/src/ui/components/chat.rs index b2294dce..4b4c8611 100644 --- a/src/ui/components/chat.rs +++ b/src/ui/components/chat.rs @@ -24,6 +24,7 @@ use crate::{ }, }, terminal::TerminalAction, + twitch::TwitchAction, ui::components::{ following::FollowingWidget, ChannelSwitcherWidget, ChatInputWidget, Component, MessageSearchWidget, @@ -164,7 +165,7 @@ impl ChatWidget { } } -impl Component for ChatWidget { +impl Component for ChatWidget { fn draw(&mut self, f: &mut Frame, area: Option) { let r = area.map_or_else(|| f.area(), |a| a); @@ -289,7 +290,7 @@ impl Component for ChatWidget { } } - async fn event(&mut self, event: &Event) -> Option { + async fn event(&mut self, event: &Event) -> Option> { if let Event::Input(key) = event { let limit = self.scroll_offset.get_offset() < self.messages.borrow().len().saturating_sub(1); @@ -299,7 +300,13 @@ impl Component for ChatWidget { } else if self.channel_input.is_focused() { self.channel_input.event(event).await } else if self.search_input.is_focused() { - self.search_input.event(event).await + // WARN: + // Currently `search_input` will never return a TwitchAction. + // If for some reason it does, it will be forced to a `Join("")` + self.search_input + .event(event) + .await + .map(|ta| ta.map_enter(|_| TwitchAction::Join("".into()))) } else if self.following.is_focused() { self.following.event(event).await } else { diff --git a/src/ui/components/chat_input.rs b/src/ui/components/chat_input.rs index 9e6d6919..47e39ad4 100644 --- a/src/ui/components/chat_input.rs +++ b/src/ui/components/chat_input.rs @@ -99,7 +99,7 @@ impl Display for ChatInputWidget { } } -impl Component for ChatInputWidget { +impl Component for ChatInputWidget { fn draw(&mut self, f: &mut Frame, area: Option) { self.input.draw(f, area); @@ -108,7 +108,7 @@ impl Component for ChatInputWidget { } } - async fn event(&mut self, event: &Event) -> Option { + async fn event(&mut self, event: &Event) -> Option> { if self.emote_picker.is_focused() { if let Some(TerminalAction::Enter(TwitchAction::Privmsg(emote))) = self.emote_picker.event(event).await diff --git a/src/ui/components/dashboard.rs b/src/ui/components/dashboard.rs index c1e03d50..fb66899a 100644 --- a/src/ui/components/dashboard.rs +++ b/src/ui/components/dashboard.rs @@ -149,7 +149,7 @@ impl DashboardWidget { } } -impl Component for DashboardWidget { +impl Component for DashboardWidget { fn draw(&mut self, f: &mut Frame, area: Option) { let r = area.map_or_else(|| f.area(), |a| a); @@ -216,7 +216,7 @@ impl Component for DashboardWidget { } } - async fn event(&mut self, event: &Event) -> Option { + async fn event(&mut self, event: &Event) -> Option> { if let Event::Input(key) = event { if self.channel_input.is_focused() { return self.channel_input.event(event).await; diff --git a/src/ui/components/debug.rs b/src/ui/components/debug.rs index 9cb83bbf..84aeb0fc 100644 --- a/src/ui/components/debug.rs +++ b/src/ui/components/debug.rs @@ -56,7 +56,7 @@ impl DebugWidget { } } -impl Component for DebugWidget { +impl Component<()> for DebugWidget { fn draw(&mut self, f: &mut Frame, area: Option) { let r = area.map_or_else(|| f.area(), |a| a); @@ -114,7 +114,7 @@ impl Component for DebugWidget { f.render_widget(bottom_block, rect); } - async fn event(&mut self, event: &Event) -> Option { + async fn event(&mut self, event: &Event) -> Option> { if let Event::Input(key) = event { match key { Key::Char('q') => return Some(TerminalAction::Quit), diff --git a/src/ui/components/emote_picker.rs b/src/ui/components/emote_picker.rs index 8cb48b04..3988ea10 100644 --- a/src/ui/components/emote_picker.rs +++ b/src/ui/components/emote_picker.rs @@ -117,7 +117,7 @@ impl EmotePickerWidget { } } -impl Component for EmotePickerWidget { +impl Component for EmotePickerWidget { fn draw(&mut self, f: &mut Frame, area: Option) { let mut r = area.map_or_else(|| centered_rect(60, 60, 23, f.area()), |a| a); // Make sure we have space for the input widget, which has a height of 3. @@ -253,7 +253,7 @@ impl Component for EmotePickerWidget { self.input.draw(f, Some(input_rect)); } - async fn event(&mut self, event: &Event) -> Option { + async fn event(&mut self, event: &Event) -> Option> { if let Event::Input(key) = event { match key { Key::Esc => self.toggle_focus(), diff --git a/src/ui/components/error.rs b/src/ui/components/error.rs index f30a9909..9a73b7cb 100644 --- a/src/ui/components/error.rs +++ b/src/ui/components/error.rs @@ -34,7 +34,7 @@ impl ErrorWidget { } } -impl Component for ErrorWidget { +impl Component<()> for ErrorWidget { fn draw(&mut self, f: &mut Frame, area: Option) { let r = area.map_or_else(|| f.area(), |a| a); diff --git a/src/ui/components/following.rs b/src/ui/components/following.rs index 9a5ad8d1..5f4ee82e 100644 --- a/src/ui/components/following.rs +++ b/src/ui/components/following.rs @@ -5,7 +5,10 @@ use tui::{layout::Rect, Frame}; use crate::{ handlers::{config::SharedCompleteConfig, user_input::events::Event}, terminal::TerminalAction, - twitch::{channels::Following, TwitchAction}, + twitch::{ + channels::{Following, FollowingStreaming, FollowingUser, StreamingUser}, + TwitchAction, + }, ui::components::Component, }; @@ -22,21 +25,36 @@ static INCORRECT_SCOPES_ERROR_MESSAGE: Lazy> = Lazy::new(|| { ] }); +type SearchFollowingStreamingWidget = SearchWidget; +type SearchFollowingWidget = SearchWidget; + +pub enum SearchWidgetType { + All(SearchFollowingWidget), + Live(SearchFollowingStreamingWidget), +} + pub struct FollowingWidget { #[allow(dead_code)] config: SharedCompleteConfig, - pub search_widget: SearchWidget, + pub search_widget: SearchWidgetType, + // pub search_widget: SearchWidget, } impl FollowingWidget { pub fn new(config: SharedCompleteConfig) -> Self { - let item_getter = Following::new(config.borrow().twitch.clone()); - - let search_widget = SearchWidget::new( - config.clone(), - item_getter, - INCORRECT_SCOPES_ERROR_MESSAGE.to_vec(), - ); + let search_widget = if config.borrow().frontend.only_show_live_channels { + SearchWidgetType::Live(SearchWidget::new( + config.clone(), + FollowingStreaming::new(config.borrow().twitch.clone()), + INCORRECT_SCOPES_ERROR_MESSAGE.to_vec(), + )) + } else { + SearchWidgetType::All(SearchWidget::new( + config.clone(), + Following::new(config.borrow().twitch.clone()), + INCORRECT_SCOPES_ERROR_MESSAGE.to_vec(), + )) + }; Self { config, @@ -45,24 +63,50 @@ impl FollowingWidget { } pub const fn is_focused(&self) -> bool { - self.search_widget.is_focused() + match &self.search_widget { + SearchWidgetType::All(w) => w.is_focused(), + SearchWidgetType::Live(w) => w.is_focused(), + } } pub async fn toggle_focus(&mut self) { - self.search_widget.toggle_focus().await; + match &mut self.search_widget { + SearchWidgetType::All(w) => w.toggle_focus().await, + SearchWidgetType::Live(w) => w.toggle_focus().await, + } } } -impl Component for FollowingWidget { +impl Component for FollowingWidget { fn draw(&mut self, f: &mut Frame, area: Option) { - self.search_widget.draw(f, area); + match &mut self.search_widget { + SearchWidgetType::All(w) => w.draw(f, area), + SearchWidgetType::Live(w) => w.draw(f, area), + } } - async fn event(&mut self, event: &Event) -> Option { - let action = self.search_widget.event(event).await; + async fn event(&mut self, event: &Event) -> Option> { + let w_event = match &mut self.search_widget { + SearchWidgetType::All(w) => w + .event(event) + .await + .map(|ta| ta.map_enter(|a| a.broadcaster_name.clone())), + SearchWidgetType::Live(w) => w + .event(event) + .await + .map(|ta| ta.map_enter(|a| a.user_login.clone())), + }; + + let action = w_event.map(|ta| match ta { + TerminalAction::Enter(s) => TerminalAction::Enter(TwitchAction::Join(s)), + TerminalAction::Quit => TerminalAction::Quit, + TerminalAction::BackOneLayer => TerminalAction::BackOneLayer, + TerminalAction::SwitchState(s) => TerminalAction::SwitchState(s), + TerminalAction::ClearMessages => TerminalAction::ClearMessages, + }); if let Some(TerminalAction::Enter(TwitchAction::Join(channel))) = &action { - self.config.borrow_mut().twitch.channel.clone_from(channel); + self.config.borrow_mut().twitch.channel.clone_from(&channel); } action diff --git a/src/ui/components/help.rs b/src/ui/components/help.rs index 835464f9..c195a261 100644 --- a/src/ui/components/help.rs +++ b/src/ui/components/help.rs @@ -28,7 +28,7 @@ impl HelpWidget { } } -impl Component for HelpWidget { +impl Component<()> for HelpWidget { fn draw(&mut self, f: &mut Frame, area: Option) { let r = area.map_or_else(|| f.area(), |a| a); diff --git a/src/ui/components/message_search.rs b/src/ui/components/message_search.rs index fe65846e..6d986b01 100644 --- a/src/ui/components/message_search.rs +++ b/src/ui/components/message_search.rs @@ -57,12 +57,12 @@ impl Display for MessageSearchWidget { } } -impl Component for MessageSearchWidget { +impl Component<()> for MessageSearchWidget { fn draw(&mut self, f: &mut Frame, area: Option) { self.input.draw(f, area); } - async fn event(&mut self, event: &Event) -> Option { + async fn event(&mut self, event: &Event) -> Option> { if let Event::Input(key) = event { match key { Key::Esc => { diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index 877ee710..4181eae3 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -46,14 +46,14 @@ static WINDOW_SIZE_TOO_SMALL_ERROR: Lazy> = Lazy::new(|| { ] }); -pub trait Component { +pub trait Component { #[allow(unused_variables)] fn draw(&mut self, f: &mut Frame, area: Option) { todo!() } #[allow(clippy::unused_async)] - async fn event(&mut self, event: &Event) -> Option { + async fn event(&mut self, event: &Event) -> Option> { if let Event::Input(key) = event { match key { Key::Char('q') => return Some(TerminalAction::Quit), diff --git a/src/ui/components/utils/input_widget.rs b/src/ui/components/utils/input_widget.rs index 2ff73904..92a73a69 100644 --- a/src/ui/components/utils/input_widget.rs +++ b/src/ui/components/utils/input_widget.rs @@ -129,7 +129,7 @@ impl Display for InputWidget { } } -impl Component for InputWidget { +impl Component<()> for InputWidget { fn draw(&mut self, f: &mut Frame, area: Option) { let r = area.map_or_else(|| centered_rect(60, 60, 20, f.area()), |a| a); @@ -227,7 +227,7 @@ impl Component for InputWidget { } } - async fn event(&mut self, event: &Event) -> Option { + async fn event(&mut self, event: &Event) -> Option> { if let Event::Input(key) = event { match key { Key::Ctrl('f') | Key::Right => { diff --git a/src/ui/components/utils/mod.rs b/src/ui/components/utils/mod.rs index f210bfa1..206cbc5d 100644 --- a/src/ui/components/utils/mod.rs +++ b/src/ui/components/utils/mod.rs @@ -8,4 +8,4 @@ pub use input_widget::{ InputWidget, }; pub use popups::centered_rect; -pub use search_widget::{SearchItemGetter, SearchWidget}; +pub use search_widget::{SearchItemGetter, SearchWidget, ToQueryString}; diff --git a/src/ui/components/utils/search_widget.rs b/src/ui/components/utils/search_widget.rs index 574b2e82..1e03af77 100644 --- a/src/ui/components/utils/search_widget.rs +++ b/src/ui/components/utils/search_widget.rs @@ -22,7 +22,6 @@ use crate::{ user_input::events::{Event, Key}, }, terminal::TerminalAction, - twitch::TwitchAction, ui::components::{Component, ErrorWidget}, utils::{ styles::{NO_COLOR, SEARCH_STYLE, TITLE_STYLE}, @@ -41,9 +40,22 @@ where async fn get_items(&mut self) -> Result>; } +pub trait ToQueryString { + fn to_query_string(&self) -> String; +} + +// WARN: +// This prevents other traits from implementing `ToQueryString`, unless we use "specialization". +// See : https://rust-lang.github.io/rfcs/1210-impl-specialization.html +// impl ToQueryString for T { +// fn to_query_string(&self) -> String { +// self.to_string() +// } +// } + pub struct SearchWidget where - T: ToString + Clone, + T: ToString + Clone + ToQueryString, U: SearchItemGetter, { config: SharedCompleteConfig, @@ -63,7 +75,7 @@ where impl SearchWidget where - T: ToString + Clone, + T: ToString + Clone + ToQueryString, U: SearchItemGetter, { pub fn new( @@ -144,9 +156,9 @@ where } } -impl Component for SearchWidget +impl Component for SearchWidget where - T: ToString + Clone, + T: ToString + Clone + ToQueryString, U: SearchItemGetter, { fn draw(&mut self, f: &mut Frame, area: Option) { @@ -179,7 +191,7 @@ where let mut matched = vec![]; for item in current_items.clone() { - let matched_indices = item_filter(item.to_string()); + let matched_indices = item_filter(item.to_query_string()); if matched_indices.is_empty() { continue; @@ -266,7 +278,7 @@ where self.search_input.draw(f, Some(input_rect)); } - async fn event(&mut self, event: &Event) -> Option { + async fn event(&mut self, event: &Event) -> Option> { if self.error_widget.is_focused() && matches!(event, Event::Input(Key::Esc)) { self.error_widget.toggle_focus(); self.toggle_focus().await; @@ -287,22 +299,21 @@ where Key::ScrollUp | Key::Up => self.previous(), Key::Enter => { if let Some(i) = self.list_state.selected() { - let selected_channel = if let Some(v) = self.filtered_items.clone() { + let selected_item = if let Some(v) = self.filtered_items.clone() { if v.is_empty() { return None; } - v.get(i).unwrap().to_string() + v.get(i).unwrap().clone() } else { - self.items.as_ref().unwrap().get(i).unwrap().to_string() - } - .to_lowercase(); + self.items.as_ref().unwrap().get(i).unwrap().clone() + }; self.toggle_focus().await; self.unselect(); - return Some(TerminalAction::Enter(TwitchAction::Join(selected_channel))); + return Some(TerminalAction::Enter(selected_item.clone())); } } _ => {