use std::collections::HashMap;
use std::time::Duration;
use std::{io, panic, thread};
use api::model::CrumbData;
use crossbeam_channel::{bounded, select, unbounded, Receiver, Sender};
use crossterm::event::{Event, MouseEvent, MouseEventKind};
use crossterm::{cursor, execute, terminal};
use lazy_static::lazy_static;
use parking_lot::{Mutex, RwLock};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use rclite::Arc;
use service::default_timestamps::DefaultTimestampService;
use tickrs_api as api;
use crate::app::DebugInfo;
use crate::common::{ChartType, TimeFrame};
mod app;
mod common;
mod draw;
mod event;
mod opts;
mod portfolio;
mod service;
mod task;
mod theme;
mod widget;
lazy_static! {
static ref CLIENT: api::Client = api::Client::new();
static ref DEBUG_LEVEL: app::EnvConfig = app::EnvConfig::load();
pub static ref OPTS: opts::Opts = opts::resolve_opts();
pub static ref UPDATE_INTERVAL: u64 = OPTS.update_interval.unwrap_or(1);
pub static ref TIME_FRAME: TimeFrame = OPTS.time_frame.unwrap_or(TimeFrame::Day1);
pub static ref HIDE_TOGGLE: bool = OPTS.hide_toggle;
pub static ref HIDE_PREV_CLOSE: bool = OPTS.hide_prev_close;
pub static ref REDRAW_REQUEST: (Sender<()>, Receiver<()>) = bounded(1);
pub static ref DATA_RECEIVED: (Sender<()>, Receiver<()>) = bounded(1);
pub static ref SHOW_X_LABELS: RwLock<bool> = RwLock::new(OPTS.show_x_labels);
pub static ref ENABLE_PRE_POST: RwLock<bool> = RwLock::new(OPTS.enable_pre_post);
pub static ref TRUNC_PRE: bool = OPTS.trunc_pre;
pub static ref SHOW_VOLUMES: RwLock<bool> = RwLock::new(OPTS.show_volumes);
pub static ref DEFAULT_TIMESTAMPS: RwLock<HashMap<TimeFrame, Vec<i64>>> = Default::default();
pub static ref THEME: theme::Theme = OPTS.theme.unwrap_or_default();
pub static ref YAHOO_CRUMB: async_std::sync::RwLock<Option<CrumbData>> = Default::default();
}
fn main() {
better_panic::install();
let opts = OPTS.clone();
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend).unwrap();
setup_panic_hook();
setup_terminal();
set_crumb();
let request_redraw = REDRAW_REQUEST.0.clone();
let data_received = DATA_RECEIVED.1.clone();
let ui_events = setup_ui_events();
let starting_chart_type = opts.chart_type.unwrap_or(ChartType::Line);
let starting_stocks: Vec<_> = opts
.symbols
.unwrap_or_default()
.into_iter()
.map(|symbol| widget::StockState::new(symbol, starting_chart_type))
.collect();
let starting_mode = if starting_stocks.is_empty() {
app::Mode::AddStock
} else if opts.summary {
app::Mode::DisplaySummary
} else {
app::Mode::DisplayStock
};
let default_timestamp_service = DefaultTimestampService::new();
let app = Arc::new(Mutex::new(app::App {
mode: starting_mode,
stocks: starting_stocks,
add_stock: widget::AddStockState::new(),
help: widget::HelpWidget {},
current_tab: 0,
hide_help: opts.hide_help,
debug: DebugInfo {
enabled: DEBUG_LEVEL.show_debug,
dimensions: (0, 0),
cursor_location: None,
last_event: None,
mode: starting_mode,
},
previous_mode: if opts.summary {
app::Mode::DisplaySummary
} else {
app::Mode::DisplayStock
},
summary_time_frame: opts.time_frame.unwrap_or(TimeFrame::Day1),
default_timestamp_service,
summary_scroll_state: Default::default(),
chart_type: starting_chart_type,
}));
let move_app = app.clone();
thread::spawn(move || {
let app = move_app;
let redraw_requested = REDRAW_REQUEST.1.clone();
loop {
select! {
recv(redraw_requested) -> _ => {
let mut app = app.lock();
draw::draw(&mut terminal, &mut app);
}
default(Duration::from_millis(500)) => {
let mut app = app.lock();
for stock in app.stocks.iter_mut() {
stock.loading_tick();
}
draw::draw(&mut terminal, &mut app);
}
}
}
});
loop {
select! {
recv(data_received) -> _ => {
let mut app = app.lock();
app.update();
for stock in app.stocks.iter_mut() {
stock.update();
if let Some(options) = stock.options.as_mut() {
options.update();
}
}
}
recv(ui_events) -> message => {
let mut app = app.lock();
if app.debug.enabled {
if let Ok(ref event) = message {
app.debug.last_event = Some(event.clone());
}
}
match message {
Ok(Event::Key(key_event)) => {
event::handle_key_bindings(app.mode, key_event, &mut app, &request_redraw);
}
Ok(Event::Mouse(MouseEvent { kind, row, column,.. })) => {
if app.debug.enabled {
match kind {
MouseEventKind::Down(_) => app.debug.cursor_location = Some((row, column)),
MouseEventKind::Up(_) => app.debug.cursor_location = Some((row, column)),
MouseEventKind::Drag(_) => app.debug.cursor_location = Some((row, column)),
_ => {}
}
}
}
Ok(Event::Resize(..)) => {
let _ = request_redraw.try_send(());
}
_ => {}
}
}
}
}
}
fn setup_terminal() {
let mut stdout = io::stdout();
execute!(stdout, cursor::Hide).unwrap();
execute!(stdout, terminal::EnterAlternateScreen).unwrap();
execute!(stdout, terminal::Clear(terminal::ClearType::All)).unwrap();
if DEBUG_LEVEL.debug_mouse {
execute!(stdout, crossterm::event::EnableMouseCapture).unwrap();
}
terminal::enable_raw_mode().unwrap();
}
fn cleanup_terminal() {
let mut stdout = io::stdout();
if DEBUG_LEVEL.debug_mouse {
execute!(stdout, crossterm::event::DisableMouseCapture).unwrap();
}
execute!(stdout, cursor::MoveTo(0, 0)).unwrap();
execute!(stdout, terminal::Clear(terminal::ClearType::All)).unwrap();
execute!(stdout, terminal::LeaveAlternateScreen).unwrap();
execute!(stdout, cursor::Show).unwrap();
terminal::disable_raw_mode().unwrap();
}
fn setup_ui_events() -> Receiver<Event> {
let (sender, receiver) = unbounded();
std::thread::spawn(move || loop {
sender.send(crossterm::event::read().unwrap()).unwrap();
});
receiver
}
fn setup_panic_hook() {
panic::set_hook(Box::new(|panic_info| {
cleanup_terminal();
better_panic::Settings::auto().create_panic_handler()(panic_info);
}));
}
fn set_crumb() {
async_std::task::spawn(async move {
let mut _guard = YAHOO_CRUMB.write().await;
if let Ok(crumb) = CLIENT.get_crumb().await {
*_guard = Some(crumb);
}
});
}