use crossterm::event::KeyCode;
use super::super::super::config::{current_user_config, SplineConfig};
use super::super::super::i18n::strings;
use super::super::super::math::{ui_size_to_num_base_lots, MAX_UI_ORDER_SIZE_UNITS};
use super::super::super::state::{TuiState, TwapBot, TwapBotConfirm, TwapDraft};
use super::super::super::trading::{InputMode, OrderKind, TradingSide};
use super::super::twap_scheduler;
use super::super::KeyAction;
pub(in crate::tui::runtime) fn handle_editing_twap_key(
code: KeyCode,
state: &mut TuiState,
configs: &std::collections::HashMap<String, SplineConfig>,
) -> KeyAction {
if state.trading.twap_draft.pending_confirm {
return handle_twap_confirm_key(code, state, configs);
}
match code {
KeyCode::Esc => {
state.trading.input_mode = InputMode::Normal;
KeyAction::Redraw
}
KeyCode::Up => {
state.trading.twap_draft.move_field_up();
state.trading.twap_draft.error = None;
KeyAction::Redraw
}
KeyCode::Down => {
state.trading.twap_draft.move_field_down();
state.trading.twap_draft.error = None;
KeyAction::Redraw
}
KeyCode::Tab => {
state.trading.twap_draft.side = state.trading.twap_draft.side.toggle();
state.trading.twap_draft.error = None;
KeyAction::Redraw
}
KeyCode::Left if state.trading.twap_draft.selected_field == 0 => {
let symbols: Vec<String> = state
.market_selector
.markets
.iter()
.map(|m| m.symbol.clone())
.collect();
state.trading.twap_draft.cycle_market(&symbols, -1);
KeyAction::Redraw
}
KeyCode::Right if state.trading.twap_draft.selected_field == 0 => {
let symbols: Vec<String> = state
.market_selector
.markets
.iter()
.map(|m| m.symbol.clone())
.collect();
state.trading.twap_draft.cycle_market(&symbols, 1);
KeyAction::Redraw
}
KeyCode::Enter => {
if state.trading.twap_draft.selected_field + 1 < TwapDraft::FIELD_COUNT {
state.trading.twap_draft.move_field_down();
state.trading.twap_draft.error = None;
return KeyAction::Redraw;
}
if !state.trading.wallet_loaded {
state.trading.twap_draft.error = Some(strings().twap_err_no_wallet.to_string());
return KeyAction::Redraw;
}
if let Err(msg) = build_bot_from_draft(state, configs) {
state.trading.twap_draft.error = Some(msg);
return KeyAction::Redraw;
}
if current_user_config().skip_order_confirmation {
submit_pending_twap(state, configs);
} else {
state.trading.twap_draft.pending_confirm = true;
}
KeyAction::Redraw
}
KeyCode::Backspace => {
if let Some(buf) = field_buffer_mut(state) {
buf.pop();
}
state.trading.twap_draft.error = None;
KeyAction::Redraw
}
KeyCode::Char(c) if c.is_ascii_digit() || c == '.' => {
let allow = match state.trading.twap_draft.selected_field {
3..=5 => c != '.',
_ => true,
};
if allow {
if let Some(buf) = field_buffer_mut(state) {
buf.push(c);
state.trading.twap_draft.error = None;
}
}
KeyAction::Redraw
}
_ => KeyAction::Nothing,
}
}
fn handle_twap_confirm_key(
code: KeyCode,
state: &mut TuiState,
configs: &std::collections::HashMap<String, SplineConfig>,
) -> KeyAction {
match code {
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
submit_pending_twap(state, configs);
KeyAction::Redraw
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
state.trading.twap_draft.pending_confirm = false;
state.trading.twap_draft.error = None;
KeyAction::Redraw
}
_ => KeyAction::Nothing,
}
}
fn submit_pending_twap(
state: &mut TuiState,
configs: &std::collections::HashMap<String, SplineConfig>,
) {
let s = strings();
match build_bot_from_draft(state, configs) {
Ok(bot) => {
let total = bot.total_size;
let slices = bot.slice_count;
let symbol = bot.symbol.clone();
let side = bot.side;
state.twaps_view.push(bot);
state.trading.twap_draft.pending_confirm = false;
state.trading.input_mode = InputMode::Normal;
state.trading.order_kind = OrderKind::Market;
let side_lbl = match side {
TradingSide::Long => s.long_label,
TradingSide::Short => s.short_label,
};
state.trading.set_status_title(format!(
"{}: {} {} {} \u{2014} {} {}",
s.twap_started, side_lbl, total, symbol, slices, s.twap_unit_slices
));
}
Err(msg) => {
state.trading.twap_draft.pending_confirm = false;
state.trading.twap_draft.error = Some(msg);
}
}
}
fn field_buffer_mut(state: &mut TuiState) -> Option<&mut String> {
let d = &mut state.trading.twap_draft;
match d.selected_field {
2 => Some(&mut d.size_buffer),
3 => Some(&mut d.duration_hour_buffer),
4 => Some(&mut d.duration_min_buffer),
5 => Some(&mut d.duration_sec_buffer),
_ => None,
}
}
fn build_bot_from_draft(
state: &TuiState,
configs: &std::collections::HashMap<String, SplineConfig>,
) -> Result<TwapBot, String> {
let s = strings();
let draft = &state.trading.twap_draft;
let market_cfg = configs
.get(&draft.market)
.ok_or_else(|| format!("{} {}", s.st_market_switch_failed, draft.market))?;
let authority: solana_pubkey::Pubkey = match state.trading.keypair.as_ref() {
Some(kp) => {
use solana_signer::Signer;
kp.pubkey()
}
None => return Err(s.twap_err_no_wallet.to_string()),
};
let size: f64 = draft
.size_buffer
.parse::<f64>()
.ok()
.filter(|v| v.is_finite() && *v > 0.0 && *v <= MAX_UI_ORDER_SIZE_UNITS)
.ok_or_else(|| s.twap_err_size.to_string())?;
let hours: u32 = if draft.duration_hour_buffer.is_empty() {
0
} else {
draft
.duration_hour_buffer
.parse::<u32>()
.map_err(|_| s.twap_err_duration.to_string())?
};
let mins: u32 = if draft.duration_min_buffer.is_empty() {
0
} else {
draft
.duration_min_buffer
.parse::<u32>()
.map_err(|_| s.twap_err_duration.to_string())?
};
let secs: u32 = if draft.duration_sec_buffer.is_empty() {
0
} else {
draft
.duration_sec_buffer
.parse::<u32>()
.map_err(|_| s.twap_err_duration.to_string())?
};
if hours == 0 && mins == 0 && secs == 0 {
return Err(s.twap_err_duration.to_string());
}
let total_seconds: u64 = (hours as u64)
.checked_mul(3600)
.and_then(|h| h.checked_add((mins as u64).checked_mul(60)?))
.and_then(|hm| hm.checked_add(secs as u64))
.ok_or_else(|| s.twap_err_duration.to_string())?;
if total_seconds < 1 {
return Err(s.twap_err_too_short.to_string());
}
let total_minutes = hours
.checked_mul(60)
.and_then(|h| h.checked_add(mins))
.unwrap_or(u32::MAX);
let slice_count: u32 = if secs > 0 || total_minutes == 0 {
(total_seconds as u32).max(1)
} else {
total_minutes
};
let slice_size = size / slice_count as f64;
if ui_size_to_num_base_lots(slice_size, market_cfg.base_lot_decimals)
.map(|n| n == 0)
.unwrap_or(true)
{
return Err(s.twap_err_size_too_small.to_string());
}
Ok(TwapBot::new(
market_cfg.symbol.clone(),
draft.side,
size,
slice_count,
total_seconds.max(1),
authority,
))
}
pub(in crate::tui::runtime) fn handle_bots_view_key(
code: KeyCode,
state: &mut TuiState,
cfg: &SplineConfig,
pending_market_switch: &mut Option<String>,
) -> KeyAction {
if let Some(pending) = state.twaps_view.pending_confirm {
return handle_bots_confirm_key(code, state, pending);
}
match code {
KeyCode::Up => {
state.twaps_view.move_up();
KeyAction::Redraw
}
KeyCode::Down => {
state.twaps_view.move_down();
KeyAction::Redraw
}
KeyCode::Enter => {
let target = state
.twaps_view
.bots
.get(state.twaps_view.selected_index)
.map(|b| b.symbol.clone());
if let Some(sym) = target {
if sym != cfg.symbol {
state.trading.input_mode = InputMode::Normal;
state.trading.set_status_title(format!(
"{} {}\u{2026}",
strings().st_switching_to,
sym
));
*pending_market_switch = Some(sym);
return KeyAction::BreakInner;
}
}
state.trading.input_mode = InputMode::Normal;
KeyAction::Redraw
}
KeyCode::Char('p') => {
let result: Option<(&'static str, Option<String>)> =
state.twaps_view.selected_mut().map(|bot| {
use super::super::super::state::TwapStatus;
let s = strings();
match bot.status {
TwapStatus::Running => {
bot.pause();
(s.bots_paused_status, None)
}
TwapStatus::Paused => {
let completed = bot.resume();
let done_line = if completed {
Some(super::super::twap_scheduler::format_completion_status(bot))
} else {
None
};
(s.bots_resumed_status, done_line)
}
_ => ("", None),
}
});
if let Some((label, done)) = result {
if let Some(done_line) = done {
state.trading.set_status_title(done_line);
} else if !label.is_empty() {
state.trading.set_status_title(label);
}
}
KeyAction::Redraw
}
KeyCode::Char('s') => {
let can_stop = state
.twaps_view
.bots
.get(state.twaps_view.selected_index)
.map(|b| !b.status.is_terminal())
.unwrap_or(false);
if can_stop {
state.twaps_view.pending_confirm =
Some(TwapBotConfirm::Stop(state.twaps_view.selected_index));
}
KeyAction::Redraw
}
KeyCode::Char('r') => {
if state
.twaps_view
.bots
.get(state.twaps_view.selected_index)
.is_some()
{
state.twaps_view.pending_confirm =
Some(TwapBotConfirm::Restart(state.twaps_view.selected_index));
}
KeyAction::Redraw
}
KeyCode::Char('x') => {
if state
.twaps_view
.bots
.get(state.twaps_view.selected_index)
.is_some()
{
state.twaps_view.pending_confirm =
Some(TwapBotConfirm::Remove(state.twaps_view.selected_index));
}
KeyAction::Redraw
}
KeyCode::Esc | KeyCode::Char('b') | KeyCode::Char('q') => {
state.twaps_view.pending_confirm = None;
state.trading.input_mode = InputMode::Normal;
KeyAction::Redraw
}
_ => KeyAction::Nothing,
}
}
fn handle_bots_confirm_key(
code: KeyCode,
state: &mut TuiState,
pending: TwapBotConfirm,
) -> KeyAction {
match code {
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
let s = strings();
let live_authority: Option<solana_pubkey::Pubkey> =
state.trading.keypair.as_ref().map(|kp| {
use solana_signer::Signer;
kp.pubkey()
});
match pending {
TwapBotConfirm::Stop(i) => {
let was_active = state
.twaps_view
.bots
.get(i)
.map(|b| !b.status.is_terminal())
.unwrap_or(false);
if was_active {
if let Some(bot) = state.twaps_view.bots.get_mut(i) {
twap_scheduler::settle_in_flight_for_interrupt(
bot,
std::time::Instant::now(),
s.bots_stopped_status,
);
bot.stop();
state.trading.set_status_title(s.bots_stopped_status);
}
} else {
}
}
TwapBotConfirm::Restart(i) => {
let bot_authority = state.twaps_view.bots.get(i).map(|b| b.authority);
match (bot_authority, live_authority) {
(Some(bot_auth), Some(live)) if bot_auth == live => {
if let Some(bot) = state.twaps_view.bots.get_mut(i) {
if bot.in_flight.is_some() {
twap_scheduler::settle_in_flight_for_interrupt(
bot,
std::time::Instant::now(),
s.bots_stopped_status,
);
bot.stop();
state.trading.set_status_title(s.bots_stopped_status);
} else {
bot.restart();
state.trading.set_status_title(s.bots_restarted_status);
}
}
}
(Some(_), None) => {
state.trading.set_status_title(s.twap_err_no_wallet);
}
(Some(_), Some(_)) => {
state
.trading
.set_status_title(s.twap_restart_wallet_mismatch);
}
_ => {}
}
}
TwapBotConfirm::Remove(i) => {
if i == state.twaps_view.selected_index {
let has_in_flight = state
.twaps_view
.bots
.get(i)
.map(|bot| bot.in_flight.is_some())
.unwrap_or(false);
if has_in_flight {
if let Some(bot) = state.twaps_view.bots.get_mut(i) {
twap_scheduler::settle_in_flight_for_interrupt(
bot,
std::time::Instant::now(),
s.bots_stopped_status,
);
bot.stop();
}
state.trading.set_status_title(s.bots_stopped_status);
} else if state.twaps_view.remove_selected().is_some() {
state.trading.set_status_title(s.bots_removed_status);
}
}
}
}
state.twaps_view.pending_confirm = None;
KeyAction::Redraw
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
state.twaps_view.pending_confirm = None;
KeyAction::Redraw
}
_ => KeyAction::Nothing,
}
}