Skip to content

Commit

Permalink
Select channel popup (#203)
Browse files Browse the repository at this point in the history
  • Loading branch information
boxdot authored Dec 2, 2022
1 parent 022f66c commit d8c50c7
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 288 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

## Unreleased

### Changed

- Replace search box by channel selection popup (Ctrl+p) ([#203])

### Fixed

- Do not create log file when logging is disabled ([#204])

[#203]: https://github.com/boxdot/gurk-rs/pull/203
[#204]: https://github.com/boxdot/gurk-rs/pull/204

## 0.3.0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ libraries that are not available on crates.io.

* App navigation
* `f1` Toggle help panel.
* `alt+tab` Switch between message input box and search bar.
* Message input
* `tab` Send emoji from input line as reaction on selected message.
* `alt+enter` Switch between multi-line and singl-line input modes.
Expand All @@ -103,6 +102,7 @@ libraries that are not available on crates.io.
* `alt+Down / PgDown` Select next message.
* `ctrl+j / Up` Select previous channel.
* `ctrl+k / Down` Select next channel.
* `ctrl+p` Open / close channel selection popup.

## License

Expand Down
113 changes: 57 additions & 56 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::channels::SelectChannel;
use crate::config::Config;
use crate::data::{Channel, ChannelId, Message, TypingAction, TypingSet};
use crate::input::Input;
Expand All @@ -6,9 +7,7 @@ use crate::signal::{
Attachment, GroupIdentifierBytes, GroupMasterKeyBytes, ProfileKey, ResolvedGroup, SignalManager,
};
use crate::storage::{MessageId, Storage};
use crate::util::{
self, FilteredStatefulList, LazyRegex, StatefulList, ATTACHMENT_REGEX, URL_REGEX,
};
use crate::util::{self, LazyRegex, StatefulList, ATTACHMENT_REGEX, URL_REGEX};

use anyhow::{anyhow, bail, Context as _};
use chrono::{Duration, Utc};
Expand Down Expand Up @@ -43,19 +42,17 @@ pub struct App {
pub config: Config,
signal_manager: Box<dyn SignalManager>,
pub storage: Box<dyn Storage>,
pub channels: FilteredStatefulList<ChannelId>,
pub channels: StatefulList<ChannelId>,
pub messages: BTreeMap<ChannelId, StatefulList<u64 /* arrived at*/>>,
pub user_id: Uuid,
pub should_quit: bool,
url_regex: LazyRegex,
attachment_regex: LazyRegex,
display_help: bool,
pub is_searching: bool,
pub channel_text_width: usize,
receipt_handler: ReceiptHandler,
pub input: Input,
pub search_box: Input,
pub is_multiline_input: bool,
pub(crate) select_channel: SelectChannel,
}

impl App {
Expand All @@ -67,7 +64,7 @@ impl App {
let user_id = signal_manager.user_id();

// build index of channels and messages for using them as lists content
let mut channels: FilteredStatefulList<ChannelId> = Default::default();
let mut channels: StatefulList<ChannelId> = Default::default();
let mut messages: BTreeMap<_, StatefulList<_>> = BTreeMap::new();
for channel in storage.channels() {
channels.items.push(channel.id);
Expand All @@ -87,6 +84,7 @@ impl App {
.map(|channel| channel.name.clone());
(Reverse(last_message_arrived_at), channel_name)
});
channels.next();

Ok(Self {
config,
Expand All @@ -99,18 +97,16 @@ impl App {
url_regex: LazyRegex::new(URL_REGEX),
attachment_regex: LazyRegex::new(ATTACHMENT_REGEX),
display_help: false,
is_searching: false,
channel_text_width: 0,
receipt_handler: ReceiptHandler::new(),
input: Default::default(),
search_box: Default::default(),
is_multiline_input: false,
select_channel: Default::default(),
})
}

pub fn get_input(&mut self) -> &mut Input {
if self.is_searching {
&mut self.search_box
if self.select_channel.is_shown {
&mut self.select_channel.input
} else {
&mut self.input
}
Expand Down Expand Up @@ -179,20 +175,33 @@ impl App {
pub fn on_key(&mut self, key: KeyEvent) -> anyhow::Result<()> {
match key.code {
KeyCode::Char('\r') => self.get_input().put_char('\n'),
KeyCode::Enter if key.modifiers.contains(KeyModifiers::ALT) && !self.is_searching => {
self.is_multiline_input = !self.is_multiline_input;
}
KeyCode::Enter if self.is_multiline_input && !self.is_searching => {
self.get_input().new_line();
}
KeyCode::Enter if !self.get_input().data.is_empty() && !self.is_searching => {
if let Some(idx) = self.channels.state.selected() {
self.send_input(self.channels.filtered_items[idx])?;
}
}
KeyCode::Enter => {
// input is empty
self.try_open_url();
if !self.select_channel.is_shown {
if key.modifiers.contains(KeyModifiers::ALT) {
self.is_multiline_input = !self.is_multiline_input;
} else if self.is_multiline_input {
self.get_input().new_line();
} else if !self.input.data.is_empty() {
if let Some(idx) = self.channels.state.selected() {
self.send_input(idx)?;
}
} else {
// input is empty
self.try_open_url();
}
} else if self.select_channel.is_shown {
if let Some(channel_id) = self.select_channel.selected_channel_id().copied() {
self.select_channel.is_shown = false;
let (idx, _) = self
.channels
.items
.iter()
.enumerate()
.find(|(_, &id)| id == channel_id)
.context("channel disappeared during channel select popup")?;
self.channels.state.select(Some(idx));
}
}
}
KeyCode::Home => {
self.get_input().on_home();
Expand All @@ -209,7 +218,19 @@ impl App {
KeyCode::Backspace => {
self.get_input().on_backspace();
}
KeyCode::Esc => self.reset_message_selection(),
KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if !self.select_channel.is_shown {
self.select_channel.reset(&*self.storage);
}
self.select_channel.is_shown = !self.select_channel.is_shown;
}
KeyCode::Esc => {
if self.select_channel.is_shown {
self.select_channel.is_shown = false;
} else {
self.reset_message_selection();
}
}
KeyCode::Char(c) => self.get_input().put_char(c),
KeyCode::Tab => {
if let Some(idx) = self.channels.state.selected() {
Expand Down Expand Up @@ -1241,10 +1262,6 @@ impl App {
self.display_help = !self.display_help;
}

pub fn toggle_search(&mut self) {
self.is_searching = !self.is_searching;
}

pub fn is_help(&self) -> bool {
self.display_help
}
Expand All @@ -1266,32 +1283,16 @@ impl App {
Ok(())
}

/// Filters visible channel based on the provided `pattern`
///
/// `pattern` is compared to channel name or channel member contact names, case insensitively.
pub(crate) fn filter_channels(&mut self, pattern: &str) {
let pattern = pattern.to_lowercase();
pub fn is_select_channel_shown(&self) -> bool {
self.select_channel.is_shown
}

// move out `channels` temporarily to make borrow checker happy
let mut channels = std::mem::take(&mut self.channels);
channels.filter(|channel_id: &ChannelId| {
let channel = self
.storage
.channel(*channel_id)
.expect("non-existent channel");
match pattern.chars().next() {
None => true,
Some('@') => match channel.group_data.as_ref() {
Some(group_data) => group_data
.members
.iter()
.any(|&id| self.name_by_id(id).to_lowercase().contains(&pattern[1..])),
None => channel.name.to_lowercase().contains(&pattern[1..]),
},
_ => channel.name.to_lowercase().contains(&pattern),
}
});
self.channels = channels;
pub fn select_channel_prev(&mut self) {
self.select_channel.prev();
}

pub fn select_channel_next(&mut self) {
self.select_channel.next();
}
}

Expand Down
89 changes: 89 additions & 0 deletions src/channels.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use std::cmp::Reverse;

use tui::widgets::ListState;

use crate::data::ChannelId;
use crate::input::Input;
use crate::storage::Storage;

#[derive(Default)]
pub(crate) struct SelectChannel {
pub is_shown: bool,
pub input: Input,
pub state: ListState,
items: Vec<ItemData>,
filtered_index: Vec<usize /* index into items */>,
}

pub(crate) struct ItemData {
pub channel_id: ChannelId,
pub name: String,
}

impl SelectChannel {
pub fn reset(&mut self, storage: &dyn Storage) {
self.input.take();
self.state = Default::default();

let items = storage.channels().map(|channel| ItemData {
channel_id: channel.id,
name: channel.name.clone(),
});
self.items.clear();
self.items.extend(items);

self.items.sort_unstable_by_key(|item| {
let last_message_arrived_at = storage
.messages(item.channel_id)
.last()
.map(|message| message.arrived_at);
(Reverse(last_message_arrived_at), item.name.clone())
});

self.filtered_index.clear();
}

pub fn prev(&mut self) {
let selected = self
.state
.selected()
.map(|idx| idx.saturating_sub(1))
.unwrap_or(0);
self.state.select(Some(selected));
}

pub fn next(&mut self) {
let selected = self.state.selected().map(|idx| idx + 1).unwrap_or(0);
self.state.select(Some(selected));
}

fn filter_by_input(&mut self) {
let index = self.items.iter().enumerate().filter_map(|(idx, item)| {
if item
.name
.to_ascii_lowercase()
.contains(&self.input.data.to_ascii_lowercase())
{
Some(idx)
} else {
None
}
});
self.filtered_index.clear();
self.filtered_index.extend(index);
}

pub fn filtered_names(&mut self) -> impl Iterator<Item = String> + '_ {
self.filter_by_input();
self.filtered_index
.iter()
.map(|&idx| self.items[idx].name.clone())
}

pub fn selected_channel_id(&self) -> Option<&ChannelId> {
let idx = self.state.selected()?;
let item_idx = self.filtered_index[idx];
let item = &self.items[item_idx];
Some(&item.channel_id)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Signal Messenger client for terminal
pub mod app;
mod channels;
pub mod config;
pub mod cursor;
pub mod data;
Expand Down
19 changes: 13 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ async fn run_single_threaded(relink: bool) -> anyhow::Result<()> {
let col = event.column;
let row = event.row;
if let Some(channel_idx) =
ui::coords_within_channels_view(&terminal.get_frame(), &app, col, row)
ui::coords_within_channels_view(terminal.get_frame().size(), col, row)
.map(|(_, row)| row as usize)
.filter(|&idx| idx < app.channels.items.len())
{
Expand Down Expand Up @@ -278,7 +278,6 @@ async fn run_single_threaded(relink: bool) -> anyhow::Result<()> {
KeyCode::Down if event.modifiers.contains(KeyModifiers::ALT) => app.on_pgdn(),
KeyCode::PageUp => app.on_pgup(),
KeyCode::PageDown => app.on_pgdn(),
KeyCode::Tab if event.modifiers.contains(KeyModifiers::ALT) => app.toggle_search(),
KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::ALT) => {
app.get_input().move_forward_word();
}
Expand All @@ -289,28 +288,36 @@ async fn run_single_threaded(relink: bool) -> anyhow::Result<()> {
app.get_input().on_delete_word();
}
KeyCode::Down => {
if app.is_multiline_input {
if app.is_select_channel_shown() {
app.select_channel_next()
} else if app.is_multiline_input {
app.input.move_line_down();
} else {
app.select_next_channel();
}
}
KeyCode::Char('j') if event.modifiers.contains(KeyModifiers::CONTROL) => {
if app.is_multiline_input {
if app.is_select_channel_shown() {
app.select_channel_next()
} else if app.is_multiline_input {
app.input.move_line_down();
} else {
app.select_next_channel();
}
}
KeyCode::Up => {
if app.is_multiline_input {
if app.is_select_channel_shown() {
app.select_channel_prev()
} else if app.is_multiline_input {
app.input.move_line_up();
} else {
app.select_previous_channel();
}
}
KeyCode::Char('k') if event.modifiers.contains(KeyModifiers::CONTROL) => {
if app.is_multiline_input {
if app.is_select_channel_shown() {
app.select_channel_prev()
} else if app.is_multiline_input {
app.input.move_line_up();
} else {
app.select_previous_channel();
Expand Down
Loading

0 comments on commit d8c50c7

Please sign in to comment.