use crate::view::{BrokenView, MatchReason, SavedView};
use crate::widgets::text_input::TextInput;
use ratatui::widgets::TableState;
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub enum ViewModalMode {
#[default]
List,
Create,
Edit,
}
pub struct ViewRow {
pub view: SavedView,
pub score: f64,
pub reason: Option<MatchReason>,
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub enum FormFocus {
#[default]
Name,
Description,
Matching,
ExactPath,
RelativePath,
PathPattern,
FilenamePattern,
SchemaMatch,
}
#[derive(Default)]
pub struct ViewModal {
pub active: bool,
pub mode: ViewModalMode,
pub table_state: TableState,
pub rows: Vec<ViewRow>,
pub broken_views: Vec<BrokenView>, pub form_focus: FormFocus,
pub matching_expanded: bool,
pub name_input: TextInput,
pub description_input: TextInput,
pub exact_path_input: TextInput,
pub relative_path_input: TextInput,
pub path_pattern_input: TextInput,
pub filename_pattern_input: TextInput,
pub schema_match_enabled: bool,
pub table: Option<String>,
pub editing_view_id: Option<String>, pub name_error: Option<String>,
pub status: Option<String>,
pub history_limit: usize,
pub score_details: Option<(String, String)>,
}
impl ViewModal {
pub fn new() -> Self {
Self::default()
}
pub fn selected_view(&self) -> Option<&SavedView> {
self.table_state
.selected()
.and_then(|i| self.rows.get(i))
.map(|row| &row.view)
}
pub fn select_next(&mut self) {
if self.rows.is_empty() {
return;
}
let i = match self.table_state.selected() {
Some(i) if i + 1 < self.rows.len() => i + 1,
Some(_) => 0,
None => 0,
};
self.table_state.select(Some(i));
}
pub fn select_prev(&mut self) {
if self.rows.is_empty() {
return;
}
let i = match self.table_state.selected() {
Some(0) | None => self.rows.len() - 1,
Some(i) => i - 1,
};
self.table_state.select(Some(i));
}
pub fn focus_order(&self) -> &'static [FormFocus] {
if self.matching_expanded {
&[
FormFocus::Name,
FormFocus::Description,
FormFocus::Matching,
FormFocus::ExactPath,
FormFocus::RelativePath,
FormFocus::PathPattern,
FormFocus::FilenamePattern,
FormFocus::SchemaMatch,
]
} else {
&[FormFocus::Name, FormFocus::Description, FormFocus::Matching]
}
}
pub fn toggle_matching(&mut self) {
self.matching_expanded = !self.matching_expanded;
if !self.matching_expanded && !self.focus_order().contains(&self.form_focus) {
self.form_focus = FormFocus::Matching;
}
}
pub fn focused_input_mut(&mut self) -> Option<&mut TextInput> {
match self.form_focus {
FormFocus::Name => Some(&mut self.name_input),
FormFocus::Description => Some(&mut self.description_input),
FormFocus::ExactPath => Some(&mut self.exact_path_input),
FormFocus::RelativePath => Some(&mut self.relative_path_input),
FormFocus::PathPattern => Some(&mut self.path_pattern_input),
FormFocus::FilenamePattern => Some(&mut self.filename_pattern_input),
FormFocus::Matching | FormFocus::SchemaMatch => None,
}
}
pub fn criteria_count(&self) -> usize {
[
&self.exact_path_input,
&self.relative_path_input,
&self.path_pattern_input,
&self.filename_pattern_input,
]
.iter()
.filter(|input| !input.value().trim().is_empty())
.count()
+ usize::from(self.schema_match_enabled)
+ usize::from(self.table.is_some())
}
fn reset_form(&mut self, history_limit: usize, theme: &crate::config::Theme) {
self.form_focus = FormFocus::Name;
self.matching_expanded = false;
self.name_error = None;
self.history_limit = history_limit;
self.name_input = TextInput::new()
.with_history_limit(history_limit)
.with_theme(theme);
self.description_input = TextInput::multiline()
.with_history_limit(history_limit)
.with_theme(theme);
self.exact_path_input = TextInput::new()
.with_history_limit(history_limit)
.with_theme(theme);
self.relative_path_input = TextInput::new()
.with_history_limit(history_limit)
.with_theme(theme);
self.path_pattern_input = TextInput::new()
.with_history_limit(history_limit)
.with_theme(theme);
self.filename_pattern_input = TextInput::new()
.with_history_limit(history_limit)
.with_theme(theme);
self.schema_match_enabled = false;
self.table = None;
}
pub fn enter_create_mode(&mut self, history_limit: usize, theme: &crate::config::Theme) {
self.mode = ViewModalMode::Create;
self.editing_view_id = None;
self.reset_form(history_limit, theme);
}
pub fn enter_edit_mode(
&mut self,
view: &SavedView,
history_limit: usize,
theme: &crate::config::Theme,
) {
self.mode = ViewModalMode::Edit;
self.reset_form(history_limit, theme);
self.editing_view_id = Some(view.id.clone());
self.name_input.set_value(&view.name);
self.description_input
.set_value(view.description.clone().unwrap_or_default());
self.exact_path_input.set_value(
view.match_criteria
.exact_path
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default(),
);
self.relative_path_input.set_value(
view.match_criteria
.relative_path
.clone()
.unwrap_or_default(),
);
self.path_pattern_input
.set_value(view.match_criteria.path_pattern.clone().unwrap_or_default());
self.filename_pattern_input.set_value(
view.match_criteria
.filename_pattern
.clone()
.unwrap_or_default(),
);
self.schema_match_enabled = view.match_criteria.schema_columns.is_some();
self.table = view.match_criteria.table.clone();
}
pub fn exit_form(&mut self) {
self.mode = ViewModalMode::List;
self.editing_view_id = None;
self.name_error = None;
}
pub fn close(&mut self) {
self.exit_form();
self.active = false;
self.score_details = None;
}
}
impl crate::form::Form for ViewModal {
type Field = FormFocus;
fn fields(&self) -> Vec<(FormFocus, crate::form::FieldKind)> {
use crate::form::FieldKind;
self.focus_order()
.iter()
.map(|&row| {
let kind = match row {
FormFocus::Description => FieldKind::MultilineText,
FormFocus::Matching => FieldKind::Button,
FormFocus::SchemaMatch => FieldKind::Checkbox,
_ => FieldKind::Text,
};
(row, kind)
})
.collect()
}
fn focused(&self) -> FormFocus {
self.form_focus
}
fn set_focused(&mut self, field: FormFocus) {
self.form_focus = field;
}
fn text_edge(&self, _field: FormFocus) -> (bool, bool) {
let line = self.description_input.cursor_line();
let last = self.description_input.line_count().saturating_sub(1);
(line == 0, line >= last)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::form::Form;
#[test]
fn tab_walks_the_criteria_only_when_expanded() {
let mut modal = ViewModal::new();
modal.form_focus = FormFocus::Name;
modal.focus_next();
modal.focus_next();
assert_eq!(modal.form_focus, FormFocus::Matching);
modal.focus_next();
assert_eq!(modal.form_focus, FormFocus::Name, "collapsed wraps early");
modal.form_focus = FormFocus::Matching;
modal.toggle_matching();
modal.focus_next();
assert_eq!(modal.form_focus, FormFocus::ExactPath);
modal.focus_prev();
modal.focus_prev();
assert_eq!(modal.form_focus, FormFocus::Description);
}
#[test]
fn collapsing_rescues_focus_from_a_hidden_row() {
let mut modal = ViewModal::new();
modal.matching_expanded = true;
modal.form_focus = FormFocus::PathPattern;
modal.toggle_matching();
assert!(!modal.matching_expanded);
assert_eq!(modal.form_focus, FormFocus::Matching);
}
#[test]
fn criteria_count_counts_set_fields_and_the_schema_toggle() {
let mut modal = ViewModal::new();
assert_eq!(modal.criteria_count(), 0);
modal.exact_path_input.set_value("/data/x.csv");
modal.filename_pattern_input.set_value("x_*.csv");
modal.schema_match_enabled = true;
assert_eq!(modal.criteria_count(), 3);
}
}