use super::{AppState, FocusField, PickerColumn, append_recipient_token};
impl AppState {
pub fn open_picker(&mut self) {
self.open_picker_focused(PickerColumn::Sessions);
}
pub fn open_bundle_picker(&mut self) {
self.open_picker_focused(PickerColumn::Bundles);
}
fn open_picker_focused(&mut self, focus: PickerColumn) {
self.picker_open = true;
self.events_overlay_open = false;
self.help_overlay_open = false;
self.picker_focus = focus;
self.picker_filter.clear();
self.sync_bundle_selection_to_active();
self.apply_recipient_list_update();
}
pub fn close_picker(&mut self) {
self.picker_open = false;
self.picker_filter.clear();
}
pub(crate) fn focus_picker_session_column(&mut self) {
self.picker_focus = PickerColumn::Sessions;
self.picker_filter.clear();
}
pub fn toggle_picker_focus(&mut self) {
self.commit_focused_selection_to_real_index();
self.picker_filter.clear();
self.picker_focus = match self.picker_focus {
PickerColumn::Bundles => PickerColumn::Sessions,
PickerColumn::Sessions => PickerColumn::Bundles,
};
}
pub fn picker_filter_push(&mut self, character: char) {
self.picker_filter.push(character);
self.reselect_focused_after_filter_change();
}
pub fn picker_filter_backspace(&mut self) {
if self.picker_filter.pop().is_some() {
self.reselect_focused_after_filter_change();
}
}
pub fn toggle_events_overlay(&mut self) {
self.events_overlay_open = !self.events_overlay_open;
if self.events_overlay_open {
self.close_picker();
self.help_overlay_open = false;
self.ensure_pending_choice_selection();
}
}
pub fn toggle_help_overlay(&mut self) {
self.help_overlay_open = !self.help_overlay_open;
if self.help_overlay_open {
self.close_picker();
self.events_overlay_open = false;
}
}
pub fn move_picker_selection(&mut self, delta: isize) {
match self.picker_focus {
PickerColumn::Bundles => self.move_bundle_selection(delta),
PickerColumn::Sessions => self.move_session_selection(delta),
}
}
fn move_session_selection(&mut self, delta: isize) {
let visible = self.visible_session_indices();
if visible.is_empty() {
self.picker_session_state.select(None);
return;
}
let current = self.picker_session_state.selected().unwrap_or(0);
let next = super::text_util::wrap_index(current, delta, visible.len());
self.picker_session_state.select(Some(next));
}
fn move_bundle_selection(&mut self, delta: isize) {
let visible = self.visible_bundle_indices();
if visible.is_empty() {
self.picker_bundle_state.select(None);
return;
}
let current = self.picker_bundle_state.selected().unwrap_or(0);
let next = super::text_util::wrap_index(current, delta, visible.len());
self.picker_bundle_state.select(Some(next));
}
pub(crate) fn visible_session_indices(&self) -> Vec<usize> {
let filter = self.column_filter(PickerColumn::Sessions);
filter_matching_indices(
self.recipients.iter().map(|recipient| {
(
recipient.session_name.as_str(),
recipient.display_name.as_deref(),
)
}),
filter,
)
}
pub(crate) fn visible_bundle_indices(&self) -> Vec<usize> {
let filter = self.column_filter(PickerColumn::Bundles);
filter_matching_indices(
self.available_bundles
.iter()
.map(|name| (name.as_str(), None)),
filter,
)
}
fn column_filter(&self, column: PickerColumn) -> &str {
if self.picker_focus == column {
self.picker_filter.as_str()
} else {
""
}
}
pub fn insert_picker_selection(&mut self) {
let Some(session_name) = self.selected_picker_recipient_id() else {
self.push_status(
Some("validation_unknown_target".to_string()),
"picker has no selected recipient",
);
return;
};
match self.focus {
FocusField::To => {
self.to_field = append_recipient_token(&self.to_field, session_name.as_str());
self.move_to_field_cursor_end();
}
FocusField::Message => {
self.push_status(
Some("validation_invalid_arguments".to_string()),
"picker inserts recipients only in To field",
);
return;
}
}
self.last_selected_recipient = Some(session_name.clone());
self.close_picker();
self.push_status(None, format!("Inserted recipient {session_name}."));
}
pub fn apply_recipient_list_update(&mut self) {
if self.recipients.is_empty() {
self.picker_session_state.select(None);
return;
}
let index = self.resolve_last_selected_recipient_index().unwrap_or(0);
self.picker_session_state.select(Some(index));
}
pub(super) fn selected_picker_recipient_id(&self) -> Option<String> {
let real = self.selected_session_real_index()?;
self.recipients
.get(real)
.map(|recipient| recipient.session_name.clone())
}
pub(in crate::tui::state) fn selected_picker_bundle(&self) -> Option<String> {
let real = self.selected_bundle_real_index()?;
self.available_bundles.get(real).cloned()
}
fn selected_session_real_index(&self) -> Option<usize> {
let position = self.picker_session_state.selected()?;
self.visible_session_indices().get(position).copied()
}
fn selected_bundle_real_index(&self) -> Option<usize> {
let position = self.picker_bundle_state.selected()?;
self.visible_bundle_indices().get(position).copied()
}
fn sync_bundle_selection_to_active(&mut self) {
if self.available_bundles.is_empty() {
self.picker_bundle_state.select(None);
return;
}
let active_index = self
.available_bundles
.iter()
.position(|name| name == &self.namespace)
.unwrap_or(0);
self.picker_bundle_state.select(Some(active_index));
}
fn reselect_focused_after_filter_change(&mut self) {
match self.picker_focus {
PickerColumn::Sessions => {
let any = !self.visible_session_indices().is_empty();
self.picker_session_state.select(any.then_some(0));
}
PickerColumn::Bundles => {
let any = !self.visible_bundle_indices().is_empty();
self.picker_bundle_state.select(any.then_some(0));
}
}
}
fn commit_focused_selection_to_real_index(&mut self) {
match self.picker_focus {
PickerColumn::Sessions => {
let real = self.selected_session_real_index();
self.picker_session_state.select(real);
}
PickerColumn::Bundles => {
let real = self.selected_bundle_real_index();
self.picker_bundle_state.select(real);
}
}
}
fn resolve_last_selected_recipient_index(&self) -> Option<usize> {
let name = self.last_selected_recipient.as_deref()?;
self.recipients
.iter()
.position(|recipient| recipient.session_name == name)
}
}
fn filter_matching_indices<'a>(
items: impl Iterator<Item = (&'a str, Option<&'a str>)>,
filter: &str,
) -> Vec<usize> {
if filter.is_empty() {
return items.enumerate().map(|(index, _)| index).collect();
}
let needle = filter.to_ascii_lowercase();
items
.enumerate()
.filter_map(|(index, (primary, secondary))| {
let primary_hit = primary.to_ascii_lowercase().contains(&needle);
let secondary_hit = secondary
.map(|value| value.to_ascii_lowercase().contains(&needle))
.unwrap_or(false);
(primary_hit || secondary_hit).then_some(index)
})
.collect()
}