use std::collections::BTreeMap;
use std::sync::LazyLock;
use std::time::Duration;
use crossterm::event::{KeyCode, KeyEvent};
use crate::config::{self, ALPHAI_KEY_FIELD, Config, KeyField};
use crate::source::{make_source, registry};
use super::{App, NewsLayout};
use crate::theme::Panels;
#[derive(Default)]
pub struct SettingsState {
pub open: bool,
pub first_run: bool,
pub cursor: usize,
pub editing: bool,
pub input: String,
pub source_choice: String,
pub key_values: BTreeMap<&'static str, String>,
pub every_input: String,
pub news_open_choice: String,
pub theme_choice: &'static str,
pub news_layout_choice: NewsLayout,
pub borders_choice: Panels,
pub message: Option<String>,
}
#[derive(Clone, Copy)]
pub enum SettingsRow {
SourceChoice,
Key(&'static KeyField),
PollEvery,
NewsOpen,
NewsLayout,
Borders,
ThemeChoice,
Save,
}
pub fn settings_rows() -> &'static [SettingsRow] {
static ROWS: LazyLock<Vec<SettingsRow>> = LazyLock::new(|| {
let mut rows = vec![SettingsRow::SourceChoice];
rows.extend(
registry::SOURCES
.iter()
.flat_map(|s| s.key_fields)
.map(SettingsRow::Key),
);
rows.push(SettingsRow::Key(&ALPHAI_KEY_FIELD));
rows.push(SettingsRow::PollEvery);
rows.push(SettingsRow::NewsOpen);
rows.push(SettingsRow::NewsLayout);
rows.push(SettingsRow::Borders);
rows.push(SettingsRow::ThemeChoice);
rows.push(SettingsRow::Save);
rows
});
&ROWS
}
impl App {
pub fn open_settings(&mut self) {
let key_values = settings_rows()
.iter()
.filter_map(|row| match row {
SettingsRow::Key(field) => Some((
field.config_name,
self.config
.keys
.get(field.config_name)
.cloned()
.unwrap_or_default(),
)),
_ => None,
})
.collect();
let s = &mut self.settings;
s.open = true;
s.cursor = 0;
s.editing = false;
s.message = None;
s.source_choice = self.source_name.to_string();
s.key_values = key_values;
s.every_input = self.every.read().unwrap().as_secs().to_string();
s.news_open_choice = if self.config.news_open_original() {
"original".to_string()
} else {
"alphai".to_string()
};
s.theme_choice = self.theme_name;
s.news_layout_choice = self.news_layout;
s.borders_choice = self.theme.panels;
}
pub(super) fn handle_settings_key(&mut self, key: KeyEvent) -> bool {
if self.settings.editing {
let s = &mut self.settings;
match key.code {
KeyCode::Enter => {
let value = s.input.trim().to_string();
match settings_rows()[s.cursor] {
SettingsRow::Key(field) => {
s.key_values.insert(field.config_name, value);
}
SettingsRow::PollEvery => s.every_input = value,
_ => {}
}
s.editing = false;
}
KeyCode::Esc => s.editing = false,
KeyCode::Backspace => {
s.input.pop();
}
KeyCode::Char(c) if !c.is_control() && !c.is_whitespace() => s.input.push(c),
_ => {}
}
return false;
}
match key.code {
KeyCode::Char('q') => return true,
KeyCode::Esc => {
self.settings.open = false;
self.settings.first_run = false;
}
KeyCode::Up | KeyCode::Char('k') => {
self.settings.cursor = self.settings.cursor.saturating_sub(1)
}
KeyCode::Down | KeyCode::Char('j') | KeyCode::Tab => {
self.settings.cursor = (self.settings.cursor + 1).min(settings_rows().len() - 1)
}
KeyCode::Left => self.cycle_row(-1),
KeyCode::Right | KeyCode::Char(' ') => self.cycle_row(1),
KeyCode::Enter => match settings_rows()[self.settings.cursor] {
SettingsRow::SourceChoice
| SettingsRow::NewsOpen
| SettingsRow::NewsLayout
| SettingsRow::Borders
| SettingsRow::ThemeChoice => self.cycle_row(1),
SettingsRow::Key(field) => {
let s = &mut self.settings;
s.input = s
.key_values
.get(field.config_name)
.cloned()
.unwrap_or_default();
s.editing = true;
}
SettingsRow::PollEvery => {
let s = &mut self.settings;
s.input = s.every_input.clone();
s.editing = true;
}
SettingsRow::Save => self.settings_save(),
},
_ => {}
}
false
}
fn cycle_row(&mut self, dir: isize) {
match settings_rows()[self.settings.cursor] {
SettingsRow::SourceChoice => {
let s = &mut self.settings;
s.source_choice = step_source(&s.source_choice, dir).to_string();
}
SettingsRow::NewsOpen => self.toggle_news_open_choice(),
SettingsRow::NewsLayout => {
let layout = self.settings.news_layout_choice.step(dir);
self.settings.news_layout_choice = layout;
self.news_layout = layout;
self.card_scroll = 0;
}
SettingsRow::Borders => {
let panels = self.settings.borders_choice.step(dir);
self.settings.borders_choice = panels;
self.theme.panels = panels;
}
SettingsRow::ThemeChoice => self.cycle_theme_choice(dir),
_ => {}
}
}
fn cycle_theme_choice(&mut self, dir: isize) {
self.set_theme(crate::theme::step_preset(self.settings.theme_choice, dir));
self.settings.theme_choice = self.theme_name;
}
fn toggle_news_open_choice(&mut self) {
let s = &mut self.settings;
s.news_open_choice = if s.news_open_choice == "original" {
"alphai".to_string()
} else {
"original".to_string()
};
}
pub(crate) fn settings_merged_config(&self) -> Config {
let mut cfg = self.config.clone();
cfg.source = Some(self.settings.source_choice.clone());
for (name, value) in &self.settings.key_values {
let value = value.trim();
if value.is_empty() {
cfg.keys.remove(*name);
} else {
cfg.keys.insert((*name).to_string(), value.to_string());
}
}
cfg.news_open = Some(self.settings.news_open_choice.clone());
let mut theme = cfg.theme.take().unwrap_or_default();
if self.settings.theme_choice == crate::theme::DEFAULT_PRESET {
theme.remove("preset");
} else {
theme.insert("preset".to_string(), self.settings.theme_choice.to_string());
}
cfg.theme = (!theme.is_empty()).then_some(theme);
let mut ui = cfg.ui.take().unwrap_or_default();
let layout = self.settings.news_layout_choice;
ui.news_layout = (layout != NewsLayout::default()).then(|| layout.name().to_string());
let panels = self.settings.borders_choice;
ui.borders = (panels != Panels::default()).then(|| panels.name().to_string());
cfg.ui = (ui != config::UiConfig::default()).then_some(ui);
if let Some(secs) = parse_every(&self.settings.every_input) {
cfg.every = Some(secs);
}
cfg.watchlist = self.symbols.clone();
cfg
}
fn settings_save(&mut self) {
let Some(every_secs) = parse_every(&self.settings.every_input) else {
self.settings.message = Some("poll interval: whole seconds, 2 or more".to_string());
return;
};
let cfg = self.settings_merged_config();
let keys_changed = registry::find(&self.settings.source_choice).is_some_and(|info| {
info.key_fields
.iter()
.any(|field| cfg.key_value(field) != self.config.key_value(field))
});
let source_changed = !self
.settings
.source_choice
.eq_ignore_ascii_case(self.source_name)
|| keys_changed;
if source_changed {
match make_source(&self.settings.source_choice, &cfg) {
Ok(src) => {
self.set_price_source(src);
self.data.clear();
self.data_window.clear();
self.from_cache.clear();
self.quote_fetched.clear();
self.abandoned.clear();
self.fallback_exhausted = false;
}
Err(e) => {
self.settings.message = Some(format!("{e:#}"));
return;
}
}
}
if cfg.keys != self.config.keys {
self.fallback_exhausted = false;
}
let every = Duration::from_secs(every_secs);
if *self.every.read().unwrap() != every {
*self.every.write().unwrap() = every;
self.refresh.notify_one();
}
if cfg.alphai_key() != self.config.alphai_key() {
let key = cfg.alphai_key();
self.change_alphai_key(key);
}
self.watchlist_saved = true;
match config::save_at(self.config_path.as_deref(), &cfg) {
Ok(()) => {
self.config = cfg;
self.settings.open = false;
self.settings.first_run = false;
}
Err(e) => {
self.config = cfg;
self.settings.message = Some(format!("could not write config: {e:#}"));
}
}
}
}
fn parse_every(input: &str) -> Option<u64> {
input.trim().parse::<u64>().ok().filter(|&v| v >= 2)
}
fn step_source(cur: &str, dir: isize) -> &'static str {
let n = registry::SOURCES.len() as isize;
match registry::SOURCES.iter().position(|s| s.id == cur) {
Some(i) => registry::SOURCES[((i as isize + dir).rem_euclid(n)) as usize].id,
None => registry::SOURCES[0].id,
}
}
#[cfg(test)]
mod tests {
use super::step_source;
#[test]
fn source_cycle_covers_all_and_wraps_both_ways() {
use crate::source::registry::SOURCES;
let mut cur = SOURCES[0].id;
let mut seen = vec![cur];
for _ in 1..SOURCES.len() {
cur = step_source(cur, 1);
seen.push(cur);
}
let mut ids: Vec<&str> = SOURCES.iter().map(|s| s.id).collect();
seen.sort_unstable();
ids.sort_unstable();
assert_eq!(seen, ids);
assert_eq!(step_source(cur, 1), SOURCES[0].id);
assert_eq!(
step_source(SOURCES[0].id, -1),
SOURCES[SOURCES.len() - 1].id
);
assert_eq!(step_source("weird", 1), SOURCES[0].id);
}
}