use crate::app::form::ListMove;
use crate::cli::FileFormat;
use crate::widgets::info::FileFacts;
use crate::widgets::info::InfoTab;
use crate::{App, AppEvent};
use crossterm::event::{KeyCode, KeyEvent};
#[derive(Default)]
pub struct InfoState {
pub(crate) file_facts: Option<(u64, FileFacts)>,
pub codebook: Option<std::sync::Arc<crate::home::codebook::Codebook>>,
pub catalog_entry: Option<(String, std::sync::Arc<crate::home::catalog::Dataset>)>,
pub documentation: crate::widgets::documentation::DocState,
pub info_documentation: crate::widgets::documentation::DocState,
pub head_web_rows: bool,
}
impl InfoState {
pub(crate) fn reset_for_dataset(
&mut self,
config: &crate::config::AppConfig,
path: Option<&std::path::Path>,
) {
self.file_facts = None;
self.follow_catalogs(config, path);
}
pub(crate) fn follow_catalogs(
&mut self,
config: &crate::config::AppConfig,
path: Option<&std::path::Path>,
) {
let shown = crate::home::catalogs(config);
self.codebook = path.and_then(|p| crate::home::codebook_for(&shown, p));
self.catalog_entry = path.and_then(|p| crate::home::catalog_entry_for(&shown, p));
}
}
impl App {
pub(crate) fn info_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
let schema_tab = self.info_modal.active_tab == InfoTab::Schema;
let notes_tab = self.info_modal.active_tab == InfoTab::Notes;
let documentation_tab = self.info_modal.active_tab == InfoTab::Documentation
&& self.info.info_documentation.is_open();
let detail_tab = matches!(
self.info_modal.active_tab,
InfoTab::Metadata | InfoTab::Format
);
let tables: Option<usize> = detail_tab
.then(|| self.data_table_state.as_ref()?.format_detail())
.flatten()
.filter(|d| self.info_modal.active_tab == InfoTab::Format && !d.tables.is_empty())
.map(|d| d.list.len());
let notes = self
.data_table_state
.as_ref()
.map(|s| s.notes().len())
.unwrap_or(0);
let total_rows = self
.data_table_state
.as_ref()
.map(|s| s.schema().len())
.unwrap_or(0);
let visible = self.info_modal.schema_visible_height;
if let Some(step) = ListMove::from_key(event) {
let one = matches!(step, ListMove::Up | ListMove::Down);
let modal = &mut self.info_modal;
if schema_tab && one {
if step == ListMove::Down {
modal.schema_table_down(total_rows, visible);
} else {
modal.schema_table_up(total_rows, visible);
}
return None;
} else if documentation_tab && !matches!(step, ListMove::Home | ListMove::End) {
let page = self.info.info_documentation.view_height;
self.info.info_documentation.move_cursor(step.delta(page));
return None;
} else if notes_tab && one {
modal.notes_move(step.delta(1), notes);
return None;
} else if let Some(tables) = tables {
let page = modal.detail_visible;
modal.detail_selected = step.apply(modal.detail_selected, tables, page);
return None;
} else if detail_tab {
modal.detail_scroll_by(step.delta(modal.detail_visible));
return None;
}
}
match event.code {
KeyCode::Esc | KeyCode::Char('i') if event.is_press() => {
self.close_overlay();
}
KeyCode::Char('c') if event.is_press() && self.row_estimate().is_some() => {
self.count_exactly();
}
KeyCode::Char('x') if event.is_press() => {
if let Some(path) = self.hex_target() {
self.close_overlay();
self.open_hex(path, crate::app::hex_view::Origin::Info, false, None);
}
}
KeyCode::Char('H') if event.is_press() && schema_tab => {
if self.header_toggle_offered() {
self.close_overlay();
return self.toggle_header();
}
}
KeyCode::Left | KeyCode::Char('h') | KeyCode::BackTab if event.is_press() => {
let offered = self.info_tabs_on_offer();
self.info_modal.switch_tab_prev(offered);
}
KeyCode::Right | KeyCode::Char('l') | KeyCode::Tab if event.is_press() => {
let offered = self.info_tabs_on_offer();
self.info_modal.switch_tab(offered);
}
KeyCode::Enter | KeyCode::Char(' ') if event.is_press() && documentation_tab => {
self.info.info_documentation.toggle_legend();
}
KeyCode::Char('o') if event.is_press() && documentation_tab => {
let link = self.info.info_documentation.link();
self.ask_to_open_link(link);
}
KeyCode::Char('y') if event.is_press() && documentation_tab => {
match self.info.info_documentation.copy_text() {
Some(text) => self.copy_documentation_text(text),
None => self.flash_note("Nothing to copy on this line".to_string()),
}
}
KeyCode::Enter if event.is_press() && notes_tab => {
self.read_the_selected_note_s_column_as_text();
}
KeyCode::Enter if event.is_press() && schema_tab => {
let column = self.data_table_state.as_ref().and_then(|s| {
s.schema()
.get_at_index(self.info_modal.schema_selected_index)
.map(|(name, _)| name.to_string())
});
if let Some(column) = column {
self.open_retype(&column);
}
}
KeyCode::Enter if event.is_press() && tables.is_some() => {
return self.open_table_from_info();
}
_ => {}
}
None
}
fn open_table_from_info(&mut self) -> Option<AppEvent> {
let detail = self.data_table_state.as_ref()?.format_detail()?;
let (name, _) = detail.list.get(self.info_modal.detail_selected)?;
let name = name.clone();
if !detail.tables.contains(&name) {
self.flash_note("Not a table".to_string());
return None;
}
if detail.table.as_ref() == Some(&name) {
self.flash_note("Already open".to_string());
return None;
}
self.close_overlay();
self.switch_table(Some(name))
}
fn read_the_selected_note_s_column_as_text(&mut self) {
let Some(state) = self.data_table_state.as_mut() else {
return;
};
let notes = state.notes();
let Some(column) = notes
.get(self.info_modal.notes_selected_index)
.and_then(|note| note.read_as_text.clone())
else {
return;
};
if let Ok(true) = state.deferred(|s| s.read_column_as_text(&column)) {
let notes = state.notes().len();
self.info_modal.notes_selected_index = self
.info_modal
.notes_selected_index
.min(notes.saturating_sub(1));
self.info_modal.notes_scroll_offset = 0;
self.spawn_async_collect(Self::LOADING_BUFFER);
}
}
fn info_tabs_on_offer(&self) -> crate::widgets::info::TabsOffered {
let facts_tab = self.info_facts_tab();
self.data_table_state
.as_ref()
.map(|state| crate::widgets::info::TabsOffered {
documentation: self.info.info_documentation.is_open(),
..crate::widgets::info::TabsOffered::of(state, facts_tab)
})
.unwrap_or_default()
}
pub(crate) fn opened_format(&self) -> Option<FileFormat> {
self.source
.opened
.as_ref()
.and_then(|(_, options)| options.format)
.or_else(|| self.path.as_deref().and_then(FileFormat::from_path))
}
pub(crate) fn info_facts_tab(&self) -> Option<&'static str> {
self.info_facts()
.and_then(|(format, _)| format.summary_tab())
}
pub(crate) fn info_facts(&self) -> Option<(FileFormat, crate::formats::readers::Facts)> {
match self.file_facts()? {
FileFacts::Read {
size: None,
detail: None,
..
} => None,
_ => self.facts_of_open(),
}
}
}