use itertools::Itertools;
+use regex::Regex;
use std::cmp::{min, max};
use std::collections::{HashMap, HashSet};
-use super::activity_stack::UtilityActivity;
+use super::activity_stack::{UtilityActivity, OverlayActivity};
use super::client::{Client, ClientError, FeedId, FeedExtend};
use super::coloured_string::ColouredString;
use super::text::*;
Select(HighlightType, SelectionPurpose),
}
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum SearchDirection { Up, Down }
+
struct File<Type: FileType, Source: FileDataSource> {
contents: FileContents<Type, Source>,
rendered: HashMap<isize, Vec<ColouredString>>,
selection: Option<(isize, usize)>,
select_aux: Option<bool>, // distinguishes fave from unfave, etc
file_desc: Type,
+ search_direction: Option<SearchDirection>,
+ last_search: Option<Regex>,
}
impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
selection: None,
select_aux: None,
file_desc,
+ search_direction: None,
+ last_search: None,
};
Ok(ff)
}
result
}
+
+ fn search(&mut self) -> LogicalAction {
+ if let Some(dir) = self.search_direction {
+ if !self.last_search.is_some() {
+ return LogicalAction::Beep;
+ }
+
+ let next_line = match dir {
+ SearchDirection::Up => |s: &mut Self| s.move_up(1),
+ SearchDirection::Down => |s: &mut Self| s.move_down(1),
+ };
+ loop {
+ let old_pos = self.pos;
+ next_line(self);
+ if self.pos == old_pos {
+ break LogicalAction::Beep;
+ }
+
+ let rendered = self.rendered.get(&self.pos.item)
+ .expect("we should have just rendered it");
+ // self.pos.line indicates the line number just off
+ // the bottom of the screen, so it's never 0 unless
+ // we're at the very top of the file
+ if let Some(lineno) = self.pos.line.checked_sub(1) {
+ if let Some(line) = rendered.get(lineno) {
+ if self.last_search.as_ref()
+ .expect("we just checked it above")
+ .find(line.text()).is_some()
+ {
+ break LogicalAction::Nothing;
+ }
+ }
+ }
+ }
+ } else {
+ LogicalAction::Beep
+ }
+ }
}
impl<Type: FileType, Source: FileDataSource>
LogicalAction::Nothing
}
+ Pr('/') | Pr('\\') => {
+ let search_direction = match key {
+ Pr('/') => SearchDirection::Down,
+ Pr('\\') => SearchDirection::Up,
+ _ => panic!("how are we in this arm anyway?")
+ };
+ self.search_direction = Some(search_direction);
+ LogicalAction::Goto(OverlayActivity::GetSearchExpression(
+ search_direction).into())
+ }
+
+ Pr('n') | Pr('N') => {
+ self.search()
+ }
+
_ => LogicalAction::Nothing,
}
UIMode::ListSubmenu => match key {
{
self.file_desc.save_file_position(self.pos, file_positions);
}
+
+ fn got_search_expression(&mut self, dir: SearchDirection, regex: String)
+ -> LogicalAction
+ {
+ match Regex::new(®ex) {
+ Ok(re) => {
+ self.search_direction = Some(dir);
+ self.last_search = Some(re);
+ self.search()
+ }
+ Err(..) => LogicalAction::Beep,
+ }
+ }
}
pub fn home_timeline(file_positions: &HashMap<FeedId, FilePosition>,
Pop,
PopOverlaySilent,
PopOverlayBeep,
+ GotSearchExpression(SearchDirection, String),
Goto(Activity),
Exit,
Nothing,
_client: &mut Client) {}
fn save_file_position(
&self, _file_positions: &mut HashMap<FeedId, FilePosition>) {}
+ fn got_search_expression(&mut self, _dir: SearchDirection, _regex: String)
+ -> LogicalAction
+ {
+ panic!("a trait returning GetSearchExpression should fill this in");
+ }
}
struct TuiLogicalState {
fn handle_keypress(&mut self, key: OurKey, client: &mut Client) ->
PhysicalAction
{
- let logact = match key {
+ let mut logact = match key {
// Central handling of [ESC]: it _always_ goes to the
// utilities menu, from any UI context at all.
OurKey::Escape => LogicalAction::Goto(
}
};
- match logact {
- LogicalAction::Beep => PhysicalAction::Beep,
- LogicalAction::Exit => PhysicalAction::Exit,
- LogicalAction::Nothing => PhysicalAction::Nothing,
- LogicalAction::Goto(activity) => {
- self.activity_stack.goto(activity);
- self.changed_activity(client, None);
- PhysicalAction::Nothing
- }
- LogicalAction::Pop => {
- self.activity_stack.pop();
- self.changed_activity(client, None);
- PhysicalAction::Nothing
- }
- LogicalAction::PopOverlaySilent => {
- self.pop_overlay_activity();
- PhysicalAction::Nothing
- }
- LogicalAction::PopOverlayBeep => {
- self.pop_overlay_activity();
- PhysicalAction::Beep
- }
- LogicalAction::Error(_) => PhysicalAction::Beep, // FIXME: Error Log
- LogicalAction::PostComposed(post) => {
- let newact = match self.activity_stack.top() {
- Activity::NonUtil(NonUtilityActivity::ComposeToplevel) =>
- NonUtilityActivity::PostComposeMenu.into(),
- Activity::Util(UtilityActivity::ComposeReply(id)) =>
- UtilityActivity::PostReplyMenu(id.clone()).into(),
- act => panic!("can't postcompose {act:?}"),
- };
- self.activity_stack.chain_to(newact);
- self.changed_activity(client, Some(post));
- PhysicalAction::Nothing
- }
- LogicalAction::PostReEdit(post) => {
- let newact = match self.activity_stack.top() {
- Activity::NonUtil(NonUtilityActivity::PostComposeMenu) =>
- NonUtilityActivity::ComposeToplevel.into(),
- Activity::Util(UtilityActivity::PostReplyMenu(id)) =>
- UtilityActivity::ComposeReply(id.clone()).into(),
- act => panic!("can't reedit {act:?}"),
- };
- self.activity_stack.chain_to(newact);
- self.changed_activity(client, Some(post));
- PhysicalAction::Nothing
- }
+ loop {
+ logact = match logact {
+ LogicalAction::Beep => break PhysicalAction::Beep,
+ LogicalAction::Exit => break PhysicalAction::Exit,
+ LogicalAction::Nothing => break PhysicalAction::Nothing,
+ LogicalAction::Goto(activity) => {
+ self.activity_stack.goto(activity);
+ self.changed_activity(client, None);
+ break PhysicalAction::Nothing
+ }
+ LogicalAction::Pop => {
+ self.activity_stack.pop();
+ self.changed_activity(client, None);
+ break PhysicalAction::Nothing
+ }
+ LogicalAction::PopOverlaySilent => {
+ self.pop_overlay_activity();
+ break PhysicalAction::Nothing
+ }
+ LogicalAction::PopOverlayBeep => {
+ self.pop_overlay_activity();
+ break PhysicalAction::Beep
+ }
+ LogicalAction::GotSearchExpression(dir, regex) => {
+ self.pop_overlay_activity();
+ self.activity_state.got_search_expression(dir, regex)
+ }
+ LogicalAction::Error(_) =>
+ break PhysicalAction::Beep, // FIXME: Error Log
+ LogicalAction::PostComposed(post) => {
+ let newact = match self.activity_stack.top() {
+ Activity::NonUtil(
+ NonUtilityActivity::ComposeToplevel) =>
+ NonUtilityActivity::PostComposeMenu.into(),
+ Activity::Util(UtilityActivity::ComposeReply(id)) =>
+ UtilityActivity::PostReplyMenu(id.clone()).into(),
+ act => panic!("can't postcompose {act:?}"),
+ };
+ self.activity_stack.chain_to(newact);
+ self.changed_activity(client, Some(post));
+ break PhysicalAction::Nothing
+ }
+ LogicalAction::PostReEdit(post) => {
+ let newact = match self.activity_stack.top() {
+ Activity::NonUtil(NonUtilityActivity::PostComposeMenu) =>
+ NonUtilityActivity::ComposeToplevel.into(),
+ Activity::Util(UtilityActivity::PostReplyMenu(id)) =>
+ UtilityActivity::ComposeReply(id.clone()).into(),
+ act => panic!("can't reedit {act:?}"),
+ };
+ self.activity_stack.chain_to(newact);
+ self.changed_activity(client, Some(post));
+ break PhysicalAction::Nothing
+ }
+ };
}
}
Ok(get_post_id_to_read()),
Activity::Overlay(OverlayActivity::GetHashtagToRead) =>
Ok(get_hashtag_to_read()),
+ Activity::Overlay(OverlayActivity::GetSearchExpression(dir)) =>
+ Ok(get_search_expression(dir)),
Activity::Util(UtilityActivity::ExamineUser(ref name)) =>
examine_user(client, name),
Activity::Util(UtilityActivity::InfoStatus(ref id)) =>