use std::sync::Arc;
use crate::misc::config::config;
use crate::misc::download::{self, BackgroundDownloaderAndRead};
use crate::tui::Pane;
use crate::tui::popups::download_notif::DownloadNotification;
use crate::tui::popups::sql_query_picker::SqlQueryPicker;
use crate::tui::table::Table;
use crate::tui::toast::Toast;
use crate::tui::{error_popup::ErrorPopup, tabs::Tabs};
use crate::{
handler::message::Message,
tui::{
component::{Component, FocusState},
popups::{
command_palette::CommandPalette, help_modal::Help, importer::Importer,
theme_selector::ThemeSelector,
},
schema::schema::Schema,
},
};
use crossterm::event::KeyCode;
use itertools::Itertools;
use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect};
use url::Url;
pub struct App {
tabs: Tabs,
overlay: Option<Overlay>,
schema: Option<Schema>,
toast: Option<Toast>,
dls: Vec<DownloadNotification>,
running: bool,
}
impl App {
pub fn new(tabs: Tabs) -> Self {
Self {
tabs,
overlay: None,
schema: None,
toast: None,
running: true,
dls: Vec::new(),
}
}
pub fn running(&self) -> bool {
self.running
}
fn show_theme_selector(&mut self) {
self.overlay = Some(Overlay::ThemeSelector(Default::default()));
}
fn show_palette(&mut self) {
self.overlay = Some(Overlay::CommandPicker(CommandPalette::default()));
}
fn show_error(&mut self, message: impl Into<String>) {
self.overlay = Some(Overlay::Error(ErrorPopup::new(message)));
}
fn show_toast(&mut self, message: impl Into<String>) {
self.toast = Some(Toast::new(message));
}
fn show_importer(&mut self) {
self.overlay = Some(Overlay::Import(Importer::default()))
}
fn show_sql_query_picker(&mut self) {
self.overlay = Some(Overlay::SqlQueryPicker(SqlQueryPicker::new(
self.tabs
.selected()
.map(Pane::table)
.map(Table::data_frame)
.cloned(),
)));
}
fn add_download(&mut self, url: &Url, reader: Arc<dyn download::Reader>) {
self.dls.push(DownloadNotification::new(
url.as_str().to_owned(),
BackgroundDownloaderAndRead::new(url.to_owned(), reader),
));
}
fn reload_app_config(&mut self) {
if let Err(err) = config().reload() {
self.show_error(err.to_string());
}
}
fn dismiss_overlay(&mut self) {
self.overlay = None;
}
fn show_schema(&mut self) {
self.schema = Some(Default::default());
}
fn dismiss_schema(&mut self) {
self.schema = None;
}
fn quit(&mut self) {
self.running = false;
}
}
impl Component for App {
fn render(
&mut self,
area: ratatui::prelude::Rect,
buf: &mut ratatui::prelude::Buffer,
_: crate::tui::component::FocusState,
) {
match (self.overlay.as_mut(), self.schema.as_mut()) {
(Some(overlay), Some(schema)) => {
schema.render(area, buf, FocusState::NotFocused);
overlay.responder().render(area, buf, FocusState::Focused);
}
(Some(overlay), None) => {
self.tabs.render(area, buf, FocusState::NotFocused);
overlay.responder().render(area, buf, FocusState::Focused);
}
(None, Some(schema)) => {
schema.render(area, buf, FocusState::Focused);
}
(None, None) => {
self.tabs.render(area, buf, FocusState::Focused);
}
}
let areas = Layout::new(
Direction::Vertical,
self.dls.iter().map(|_| Constraint::Length(3)),
)
.flex(Flex::End)
.split(right_notif_bar(area));
for (dl, area) in self.dls.iter_mut().zip(areas.iter()) {
dl.render(*area, buf, FocusState::NotFocused);
}
if let Some(toast) = self.toast.as_mut() {
toast.render(area, buf, FocusState::NotFocused);
}
}
fn handle(&mut self, event: crossterm::event::KeyEvent) -> bool {
(if let Some(overlay) = self.overlay.as_mut() {
overlay.responder().handle(event)
} else if let Some(schema) = self.schema.as_mut() {
schema.handle(event)
} else {
self.tabs.handle(event)
}) || match event.code {
KeyCode::Char(':') => {
self.show_palette();
true
}
KeyCode::Char('Q') => {
self.quit();
true
}
_ => false,
}
}
fn update(&mut self, action: &Message, _: FocusState) {
match action {
Message::Quit => self.quit(),
Message::AppDismissOverlay => self.dismiss_overlay(),
Message::AppShowError(message) => self.show_error(message),
Message::AppShowToast(message) => self.show_toast(message),
Message::AppShowCommandPicker => self.show_palette(),
Message::AppShowThemeSelector => self.show_theme_selector(),
Message::AppShowSchema => self.show_schema(),
Message::AppShowImporter => self.show_importer(),
Message::AppDismissSchema => self.dismiss_schema(),
Message::AppShowSqlQuery => self.show_sql_query_picker(),
Message::AppReloadConfig => self.reload_app_config(),
Message::AppDownloadDataSource(url, reader) => self.add_download(url, reader.clone()),
_ => (),
};
match (self.overlay.as_mut(), self.schema.as_mut()) {
(Some(overlay), Some(schema)) => {
overlay.responder().update(action, FocusState::Focused);
schema.update(action, FocusState::NotFocused);
self.tabs.update(action, FocusState::NotFocused);
}
(Some(overlay), None) => {
overlay.responder().update(action, FocusState::Focused);
self.tabs.update(action, FocusState::NotFocused);
}
(None, Some(schema)) => {
schema.update(action, FocusState::Focused);
self.tabs.update(action, FocusState::NotFocused);
}
(None, None) => {
self.tabs.update(action, FocusState::Focused);
}
}
}
fn tick(&mut self) {
if let Some(overlay) = self.overlay.as_mut() {
overlay.responder().tick();
}
if let Some(toast) = self.toast.as_mut()
&& toast.is_finished()
{
self.toast.take();
}
self.dls
.iter()
.enumerate()
.filter_map(|(idx, dl)| (!dl.is_running()).then_some(idx))
.collect_vec()
.into_iter()
.rev()
.for_each(|idx| {
let dl = self.dls.remove(idx).into_downloader();
match dl.join() {
Ok(nfs) => {
for (name, df) in nfs {
Message::TabsAddNamePane(df, name).enqueue();
}
}
Err(err) => self.show_error(err.to_string()),
}
});
self.tabs.tick();
}
}
#[derive(Debug)]
pub enum Overlay {
Error(ErrorPopup),
CommandPicker(CommandPalette),
ThemeSelector(ThemeSelector),
SqlQueryPicker(SqlQueryPicker),
Import(Importer),
Help(Help),
}
impl Overlay {
fn responder(&mut self) -> &mut dyn Component {
match self {
Overlay::Error(error) => error,
Overlay::CommandPicker(command_palette) => command_palette,
Overlay::ThemeSelector(theme_selector) => theme_selector,
Overlay::Help(help) => help,
Overlay::Import(step_by_step) => step_by_step,
Overlay::SqlQueryPicker(sql_query_picker) => sql_query_picker,
}
}
}
fn right_notif_bar(area: Rect) -> Rect {
Rect {
x: area.width.saturating_sub(if config().show_table_borders() {
41
} else {
40
}),
y: 1,
width: 40,
height: area.height.saturating_sub(2),
}
}