759 lines
34 KiB
Rust
759 lines
34 KiB
Rust
// src/ui/handlers/ui.rs
|
||
|
||
use crate::config::binds::config::Config;
|
||
use crate::config::colors::themes::Theme;
|
||
use crate::services::grpc_client::GrpcClient;
|
||
use crate::services::ui_service::UiService;
|
||
use crate::config::storage::storage::load_auth_data;
|
||
use crate::modes::common::commands::CommandHandler;
|
||
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
||
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
||
use crate::state::pages::auth::AuthState;
|
||
use crate::state::pages::auth::UserRole;
|
||
use crate::pages::login::LoginFormState;
|
||
use crate::pages::register::RegisterFormState;
|
||
use crate::pages::admin_panel::add_table;
|
||
use crate::pages::admin_panel::add_logic;
|
||
use crate::pages::admin::AdminState;
|
||
use crate::pages::admin::AdminFocus;
|
||
use crate::pages::admin::admin;
|
||
use crate::pages::intro::IntroState;
|
||
use crate::pages::forms::{FormState, FieldDefinition};
|
||
use crate::pages::forms;
|
||
use crate::pages::routing::{Router, Page};
|
||
use crate::buffer::state::BufferState;
|
||
use crate::buffer::state::AppView;
|
||
use crate::state::app::state::AppState;
|
||
use crate::tui::terminal::{EventReader, TerminalCore};
|
||
use crate::ui::handlers::render::render_ui;
|
||
use crate::input::leader::leader_has_any_start;
|
||
use crate::pages::login;
|
||
use crate::pages::register;
|
||
use crate::pages::login::LoginResult;
|
||
use crate::pages::login::LoginState;
|
||
use crate::pages::register::RegisterResult;
|
||
use crate::ui::handlers::context::DialogPurpose;
|
||
use crate::utils::columns::filter_user_columns;
|
||
use canvas::keymap::KeyEventOutcome;
|
||
use canvas::CursorManager;
|
||
use canvas::FormEditor;
|
||
use anyhow::{Context, Result};
|
||
use crossterm::cursor::{SetCursorStyle, MoveTo};
|
||
use crossterm::event as crossterm_event;
|
||
use crossterm::ExecutableCommand;
|
||
use tracing::{error, info, warn};
|
||
use tokio::sync::mpsc;
|
||
use std::time::Instant;
|
||
use std::time::Duration;
|
||
#[cfg(feature = "ui-debug")]
|
||
use crate::state::app::state::DebugState;
|
||
#[cfg(feature = "ui-debug")]
|
||
use crate::utils::debug_logger::pop_next_debug_message;
|
||
|
||
// Rest of the file remains the same...
|
||
pub async fn run_ui() -> Result<()> {
|
||
let config = Config::load().context("Failed to load configuration")?;
|
||
let theme = Theme::from_str(&config.colors.theme);
|
||
let mut terminal = TerminalCore::new().context("Failed to initialize terminal")?;
|
||
let mut grpc_client = GrpcClient::new().await.context("Failed to create GrpcClient")?;
|
||
let mut command_handler = CommandHandler::new();
|
||
|
||
let (login_result_sender, mut login_result_receiver) = mpsc::channel::<LoginResult>(1);
|
||
let (register_result_sender, mut register_result_receiver) = mpsc::channel::<RegisterResult>(1);
|
||
let (save_table_result_sender, mut save_table_result_receiver) = mpsc::channel::<Result<String>>(1);
|
||
let (save_logic_result_sender, _save_logic_result_receiver) = mpsc::channel::<Result<String>>(1);
|
||
|
||
let mut event_handler = EventHandler::new(
|
||
login_result_sender.clone(),
|
||
register_result_sender.clone(),
|
||
save_table_result_sender.clone(),
|
||
save_logic_result_sender.clone(),
|
||
grpc_client.clone(),
|
||
)
|
||
.await
|
||
.context("Failed to create event handler")?;
|
||
let event_reader = EventReader::new();
|
||
|
||
let mut auth_state = AuthState::default();
|
||
let mut login_state = LoginFormState::new();
|
||
login_state.editor.set_keymap(config.build_canvas_keymap());
|
||
let mut register_state = RegisterFormState::default();
|
||
register_state.editor.set_keymap(config.build_canvas_keymap());
|
||
let mut intro_state = IntroState::default();
|
||
let mut admin_state = AdminState::default();
|
||
let mut router = Router::new();
|
||
let mut buffer_state = BufferState::default();
|
||
let mut app_state = AppState::new().context("Failed to create initial app state")?;
|
||
|
||
let mut auto_logged_in = false;
|
||
match load_auth_data() {
|
||
Ok(Some(stored_data)) => {
|
||
auth_state.auth_token = Some(stored_data.access_token);
|
||
auth_state.user_id = Some(stored_data.user_id);
|
||
auth_state.role = Some(UserRole::from_str(&stored_data.role));
|
||
auth_state.decoded_username = Some(stored_data.username);
|
||
auto_logged_in = true;
|
||
info!("Auth data loaded from file. User is auto-logged in.");
|
||
}
|
||
Ok(None) => {
|
||
info!("No stored auth data found. User will see intro/login.");
|
||
}
|
||
Err(e) => {
|
||
error!("Failed to load auth data: {}", e);
|
||
}
|
||
}
|
||
|
||
let (initial_profile, initial_table, initial_columns_from_service) =
|
||
UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state)
|
||
.await
|
||
.context("Failed to initialize app state and form")?;
|
||
|
||
let initial_field_defs: Vec<FieldDefinition> = filter_user_columns(initial_columns_from_service)
|
||
.into_iter()
|
||
.map(|col_name| FieldDefinition {
|
||
display_name: col_name.clone(),
|
||
data_key: col_name,
|
||
is_link: false,
|
||
link_target_table: None,
|
||
})
|
||
.collect();
|
||
|
||
// Replace local form_state with app_state.form_editor
|
||
let path = format!("{}/{}", initial_profile, initial_table);
|
||
app_state.ensure_form_editor(&path, &config, || {
|
||
FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs)
|
||
});
|
||
#[cfg(feature = "validation")]
|
||
UiService::apply_validation1_for_form(&mut grpc_client, &mut app_state, &path)
|
||
.await
|
||
.ok();
|
||
buffer_state.update_history(AppView::Form(path.clone()));
|
||
router.navigate(Page::Form(path.clone()));
|
||
|
||
// Fetch initial count using app_state accessor
|
||
if let Some(form_state) = app_state.form_state_for_path(&path) {
|
||
UiService::fetch_and_set_table_count(&mut grpc_client, form_state)
|
||
.await
|
||
.context(format!(
|
||
"Failed to fetch initial count for table {}.{}",
|
||
initial_profile, initial_table
|
||
))?;
|
||
|
||
if form_state.total_count > 0 {
|
||
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, form_state).await {
|
||
event_handler.command_message = format!("Error loading initial data: {}", e);
|
||
}
|
||
} else {
|
||
form_state.reset_to_empty();
|
||
}
|
||
}
|
||
|
||
if auto_logged_in {
|
||
let path = format!("{}/{}", initial_profile, initial_table);
|
||
buffer_state.history = vec![AppView::Form(path.clone())];
|
||
router.navigate(Page::Form(path));
|
||
buffer_state.active_index = 0;
|
||
info!("Initial view set to Form due to auto-login.");
|
||
}
|
||
|
||
let mut last_frame_time = Instant::now();
|
||
let mut current_fps = 0.0;
|
||
let mut needs_redraw = true;
|
||
let mut prev_view_profile_name = app_state.current_view_profile_name.clone();
|
||
let mut prev_view_table_name = app_state.current_view_table_name.clone();
|
||
let mut table_just_switched = false;
|
||
|
||
loop {
|
||
let position_before_event = if let Page::Form(path) = &router.current {
|
||
app_state.form_state_for_path(path).map(|fs| fs.current_position).unwrap_or(1)
|
||
} else {
|
||
1
|
||
};
|
||
let mut event_processed = false;
|
||
|
||
// --- CHANNEL RECEIVERS ---
|
||
|
||
// For main search palette
|
||
match event_handler.search_result_receiver.try_recv() {
|
||
Ok(hits) => {
|
||
info!("--- 4. Main loop received message from channel. ---");
|
||
if let Some(search_state) = app_state.search_state.as_mut() {
|
||
search_state.results = hits;
|
||
search_state.is_loading = false;
|
||
}
|
||
needs_redraw = true;
|
||
}
|
||
Err(mpsc::error::TryRecvError::Empty) => {
|
||
}
|
||
Err(mpsc::error::TryRecvError::Disconnected) => {
|
||
error!("Search result channel disconnected!");
|
||
}
|
||
}
|
||
|
||
// --- ADDED: For live form autocomplete ---
|
||
match event_handler.autocomplete_result_receiver.try_recv() {
|
||
Ok(hits) => {
|
||
if let Page::Form(path) = &router.current {
|
||
if let Some(form_state) = app_state.form_state_for_path(path) {
|
||
if form_state.autocomplete_active {
|
||
form_state.autocomplete_suggestions = hits;
|
||
form_state.autocomplete_loading = false;
|
||
if !form_state.autocomplete_suggestions.is_empty() {
|
||
form_state.selected_suggestion_index = Some(0);
|
||
} else {
|
||
form_state.selected_suggestion_index = None;
|
||
}
|
||
event_handler.command_message = format!("Found {} suggestions.", form_state.autocomplete_suggestions.len());
|
||
}
|
||
}
|
||
}
|
||
needs_redraw = true;
|
||
}
|
||
Err(mpsc::error::TryRecvError::Empty) => {}
|
||
Err(mpsc::error::TryRecvError::Disconnected) => {
|
||
error!("Autocomplete result channel disconnected!");
|
||
}
|
||
}
|
||
|
||
if app_state.ui.show_search_palette {
|
||
needs_redraw = true;
|
||
}
|
||
if crossterm_event::poll(std::time::Duration::from_millis(1))? {
|
||
let event = event_reader.read_event().context("Failed to read terminal event")?;
|
||
event_processed = true;
|
||
|
||
// Decouple Command Line and palettes from canvas:
|
||
// Only forward keys to Form canvas when:
|
||
// - not in command mode
|
||
// - no search/palette active
|
||
// - focus is inside the canvas
|
||
if let crossterm_event::Event::Key(key_event) = &event {
|
||
let overlay_active = event_handler.command_mode
|
||
|| app_state.ui.show_search_palette
|
||
|| event_handler.navigation_state.active;
|
||
if !overlay_active {
|
||
let inside_canvas = match &router.current {
|
||
Page::Form(_) => true,
|
||
Page::Login(state) => !state.focus_outside_canvas,
|
||
Page::Register(state) => !state.focus_outside_canvas,
|
||
Page::AddTable(state) => !state.focus_outside_canvas,
|
||
Page::AddLogic(state) => !state.focus_outside_canvas,
|
||
_ => false,
|
||
};
|
||
|
||
if inside_canvas {
|
||
// Do NOT forward to canvas while a leader is active or about to start.
|
||
// This prevents the canvas from stealing the second/third key (b/d/r).
|
||
let leader_in_progress = event_handler.input_engine.has_active_sequence();
|
||
let is_space = matches!(key_event.code, crossterm_event::KeyCode::Char(' '));
|
||
let can_start_leader = leader_has_any_start(&config);
|
||
let form_in_edit_mode = match &router.current {
|
||
Page::Form(path) => app_state
|
||
.editor_for_path_ref(path)
|
||
.map(|e| e.mode() == canvas::AppMode::Edit)
|
||
.unwrap_or(false),
|
||
_ => false,
|
||
};
|
||
|
||
let defer_to_engine_for_leader = leader_in_progress
|
||
|| (is_space && can_start_leader && !form_in_edit_mode);
|
||
|
||
if defer_to_engine_for_leader {
|
||
info!(
|
||
"Skipping canvas pre-handle: leader sequence active or starting"
|
||
);
|
||
} else {
|
||
if let Page::Form(path) = &router.current {
|
||
if let Some(editor) = app_state.editor_for_path(path) {
|
||
match editor.handle_key_event(*key_event) {
|
||
KeyEventOutcome::Consumed(Some(msg)) => {
|
||
event_handler.command_message = msg;
|
||
needs_redraw = true;
|
||
continue;
|
||
}
|
||
KeyEventOutcome::Consumed(None) => {
|
||
needs_redraw = true;
|
||
continue;
|
||
}
|
||
KeyEventOutcome::Pending => {
|
||
needs_redraw = true;
|
||
continue;
|
||
}
|
||
KeyEventOutcome::NotMatched => {
|
||
// fall through to client-level handling
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Call handle_event directly
|
||
let event_outcome_result = event_handler.handle_event(
|
||
event,
|
||
&config,
|
||
&mut terminal,
|
||
&mut command_handler,
|
||
&mut auth_state,
|
||
&mut buffer_state,
|
||
&mut app_state,
|
||
&mut router,
|
||
).await;
|
||
let mut should_exit = false;
|
||
match event_outcome_result {
|
||
Ok(outcome) => match outcome {
|
||
EventOutcome::Ok(message) => {
|
||
if !message.is_empty() {
|
||
event_handler.command_message = message;
|
||
}
|
||
}
|
||
EventOutcome::Exit(message) => {
|
||
event_handler.command_message = message;
|
||
should_exit = true;
|
||
}
|
||
EventOutcome::DataSaved(save_outcome, message) => {
|
||
event_handler.command_message = message;
|
||
if let Page::Form(path) = &router.current {
|
||
if let Some(mut temp_form_state) = app_state.form_state_for_path(path).cloned() {
|
||
if let Err(e) = UiService::handle_save_outcome(
|
||
save_outcome,
|
||
&mut grpc_client,
|
||
&mut app_state,
|
||
&mut temp_form_state,
|
||
).await {
|
||
event_handler.command_message =
|
||
format!("Error handling save outcome: {}", e);
|
||
}
|
||
// Update app_state with changes
|
||
if let Some(form_state) = app_state.form_state_for_path(path) {
|
||
*form_state = temp_form_state;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
EventOutcome::ButtonSelected { .. } => {}
|
||
EventOutcome::TableSelected { path } => {
|
||
let parts: Vec<&str> = path.split('/').collect();
|
||
if parts.len() == 2 {
|
||
let profile_name = parts[0].to_string();
|
||
let table_name = parts[1].to_string();
|
||
|
||
app_state.set_current_view_table(profile_name, table_name);
|
||
buffer_state.update_history(AppView::Form(path.clone()));
|
||
event_handler.command_message = format!("Loading table: {}", path);
|
||
} else {
|
||
event_handler.command_message = format!("Invalid table path: {}", path);
|
||
}
|
||
}
|
||
},
|
||
Err(e) => {
|
||
event_handler.command_message = format!("Error: {}", e);
|
||
}
|
||
}
|
||
if should_exit {
|
||
return Ok(());
|
||
}
|
||
}
|
||
|
||
match login_result_receiver.try_recv() {
|
||
Ok(result) => {
|
||
// Apply result to the active router Login page if present,
|
||
// otherwise update the local copy.
|
||
let updated = if let Page::Login(page) = &mut router.current {
|
||
login::handle_login_result(
|
||
result,
|
||
&mut app_state,
|
||
&mut auth_state,
|
||
page,
|
||
)
|
||
} else {
|
||
login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state)
|
||
};
|
||
if updated { needs_redraw = true; }
|
||
}
|
||
Err(mpsc::error::TryRecvError::Empty) => {}
|
||
Err(mpsc::error::TryRecvError::Disconnected) => {
|
||
error!("Login result channel disconnected unexpectedly.");
|
||
}
|
||
}
|
||
|
||
match register_result_receiver.try_recv() {
|
||
Ok(result) => {
|
||
if register::handle_registration_result(result, &mut app_state, &mut register_state) {
|
||
needs_redraw = true;
|
||
}
|
||
}
|
||
Err(mpsc::error::TryRecvError::Empty) => {}
|
||
Err(mpsc::error::TryRecvError::Disconnected) => {
|
||
error!("Register result channel disconnected unexpectedly.");
|
||
}
|
||
}
|
||
|
||
match save_table_result_receiver.try_recv() {
|
||
Ok(result) => {
|
||
app_state.hide_dialog();
|
||
match result {
|
||
Ok(ref success_message) => {
|
||
app_state.show_dialog(
|
||
"Save Successful",
|
||
success_message,
|
||
vec!["OK".to_string()],
|
||
DialogPurpose::SaveTableSuccess,
|
||
);
|
||
if let Page::AddTable(page) = &mut router.current {
|
||
page.state.has_unsaved_changes = false;
|
||
}
|
||
}
|
||
Err(e) => {
|
||
event_handler.command_message = format!("Save failed: {}", e);
|
||
}
|
||
}
|
||
needs_redraw = true;
|
||
}
|
||
Err(mpsc::error::TryRecvError::Empty) => {}
|
||
Err(mpsc::error::TryRecvError::Disconnected) => {
|
||
error!("Save table result channel disconnected unexpectedly.");
|
||
}
|
||
}
|
||
|
||
if let Some(active_view) = buffer_state.get_active_view() {
|
||
match active_view {
|
||
AppView::Intro => {
|
||
// Keep external intro_state in sync with the live Router state
|
||
if let Page::Intro(current) = &router.current {
|
||
intro_state = current.clone();
|
||
}
|
||
// Navigate with the up-to-date state
|
||
router.navigate(Page::Intro(intro_state.clone()));
|
||
}
|
||
AppView::Login => {
|
||
// Do not re-create the page every frame. If we're already on Login,
|
||
// keep it. If we just switched into Login, create it once and
|
||
// inject the keymap.
|
||
if let Page::Login(_) = &router.current {
|
||
// Already on login page; keep existing state
|
||
} else {
|
||
let mut page = LoginFormState::new();
|
||
page.editor.set_keymap(config.build_canvas_keymap());
|
||
router.navigate(Page::Login(page));
|
||
}
|
||
}
|
||
AppView::Register => {
|
||
if let Page::Register(_) = &router.current {
|
||
// already on register page
|
||
} else {
|
||
let mut page = RegisterFormState::new();
|
||
page.editor.set_keymap(config.build_canvas_keymap());
|
||
router.navigate(Page::Register(page));
|
||
}
|
||
}
|
||
AppView::Admin => {
|
||
if let Page::Admin(current) = &router.current {
|
||
admin_state = current.clone();
|
||
}
|
||
info!("Auth role at render: {:?}", auth_state.role);
|
||
|
||
// Use the admin loader instead of inline logic
|
||
if let Err(e) = admin::loader::refresh_admin_state(&mut grpc_client, &mut app_state, &mut admin_state).await {
|
||
error!("Failed to refresh admin state: {}", e);
|
||
event_handler.command_message = format!("Error refreshing admin data: {}", e);
|
||
}
|
||
|
||
router.navigate(Page::Admin(admin_state.clone()));
|
||
}
|
||
AppView::AddTable => {
|
||
if let Page::AddTable(page) = &mut router.current {
|
||
// Ensure keymap is set once (same as AddLogic)
|
||
page.editor.set_keymap(config.build_canvas_keymap());
|
||
} else {
|
||
// Page is created by admin navigation (Button2). No-op here.
|
||
}
|
||
}
|
||
AppView::AddLogic => {
|
||
if let Page::AddLogic(page) = &mut router.current {
|
||
// Ensure keymap is set once
|
||
page.editor.set_keymap(config.build_canvas_keymap());
|
||
}
|
||
}
|
||
AppView::Form(path) => {
|
||
// Keep current_view_* consistent with the active buffer path
|
||
if let Some((profile, table)) = path.split_once('/') {
|
||
app_state.set_current_view_table(
|
||
profile.to_string(),
|
||
table.to_string(),
|
||
);
|
||
}
|
||
router.navigate(Page::Form(path.clone()));
|
||
}
|
||
AppView::Scratch => {}
|
||
}
|
||
}
|
||
|
||
if let Page::Form(_current_path) = &router.current {
|
||
let current_view_profile = app_state.current_view_profile_name.clone();
|
||
let current_view_table = app_state.current_view_table_name.clone();
|
||
|
||
if prev_view_profile_name != current_view_profile
|
||
|| prev_view_table_name != current_view_table
|
||
{
|
||
if let (Some(prof_name), Some(tbl_name)) =
|
||
(current_view_profile.as_ref(), current_view_table.as_ref())
|
||
{
|
||
app_state.show_loading_dialog(
|
||
"Loading Table",
|
||
&format!("Fetching data for {}.{}...", prof_name, tbl_name),
|
||
);
|
||
needs_redraw = true;
|
||
|
||
// DELEGATE to the forms loader
|
||
match forms::loader::ensure_form_loaded_and_count(
|
||
&mut grpc_client,
|
||
&mut app_state,
|
||
&config,
|
||
prof_name,
|
||
tbl_name,
|
||
).await {
|
||
Ok(()) => {
|
||
app_state.hide_dialog();
|
||
prev_view_profile_name = current_view_profile;
|
||
prev_view_table_name = current_view_table;
|
||
table_just_switched = true;
|
||
// Apply character-limit validation for the new form
|
||
#[cfg(feature = "validation")]
|
||
if let (Some(prof), Some(tbl)) = (
|
||
app_state.current_view_profile_name.as_ref(),
|
||
app_state.current_view_table_name.as_ref(),
|
||
) {
|
||
let p = format!("{}/{}", prof, tbl);
|
||
UiService::apply_validation1_for_form(
|
||
&mut grpc_client,
|
||
&mut app_state,
|
||
&p,
|
||
)
|
||
.await
|
||
.ok();
|
||
}
|
||
}
|
||
Err(e) => {
|
||
app_state.update_dialog_content(
|
||
&format!("Error loading table: {}", e),
|
||
vec!["OK".to_string()],
|
||
DialogPurpose::LoginFailed,
|
||
);
|
||
// Reset to previous state on error
|
||
app_state.current_view_profile_name = prev_view_profile_name.clone();
|
||
app_state.current_view_table_name = prev_view_table_name.clone();
|
||
}
|
||
}
|
||
}
|
||
needs_redraw = true;
|
||
}
|
||
}
|
||
|
||
let needs_redraw_from_fetch = add_logic::loader::process_pending_table_structure_fetch(
|
||
&mut app_state,
|
||
&mut router,
|
||
&mut grpc_client,
|
||
&mut event_handler.command_message,
|
||
).await.unwrap_or(false);
|
||
|
||
if needs_redraw_from_fetch {
|
||
needs_redraw = true;
|
||
}
|
||
|
||
if let Page::AddLogic(state) = &mut router.current {
|
||
let needs_redraw_from_columns = add_logic::loader::maybe_fetch_columns_for_awaiting_table(
|
||
&mut grpc_client,
|
||
state,
|
||
&mut event_handler.command_message,
|
||
).await.unwrap_or(false);
|
||
|
||
if needs_redraw_from_columns {
|
||
needs_redraw = true;
|
||
}
|
||
}
|
||
|
||
let current_position = if let Page::Form(path) = &router.current {
|
||
app_state.form_state_for_path(path).map(|fs| fs.current_position).unwrap_or(1)
|
||
} else {
|
||
1
|
||
};
|
||
let position_changed = current_position != position_before_event;
|
||
let mut position_logic_needs_redraw = false;
|
||
|
||
if let Page::Form(path) = &router.current {
|
||
if !table_just_switched {
|
||
if position_changed && !app_state.is_canvas_edit_mode_at(path) {
|
||
position_logic_needs_redraw = true;
|
||
|
||
if let Some(form_state) = app_state.form_state_for_path(path) {
|
||
if form_state.current_position > form_state.total_count {
|
||
form_state.reset_to_empty();
|
||
event_handler.command_message = format!(
|
||
"New entry for {}.{}",
|
||
form_state.profile_name,
|
||
form_state.table_name
|
||
);
|
||
} else {
|
||
match UiService::load_table_data_by_position(&mut grpc_client, form_state).await {
|
||
Ok(load_message) => {
|
||
if event_handler.command_message.is_empty()
|
||
|| !load_message.starts_with("Error")
|
||
{
|
||
event_handler.command_message = load_message;
|
||
}
|
||
}
|
||
Err(e) => {
|
||
event_handler.command_message =
|
||
format!("Error loading data: {}", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
let current_input_after_load_str = form_state.get_current_input();
|
||
let current_input_len_after_load =
|
||
current_input_after_load_str.chars().count();
|
||
let max_cursor_pos = if current_input_len_after_load > 0 {
|
||
current_input_len_after_load.saturating_sub(1)
|
||
} else {
|
||
0
|
||
};
|
||
form_state.current_cursor_pos =
|
||
event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||
}
|
||
} else if !position_changed && !app_state.is_canvas_edit_mode_at(path) {
|
||
if let Some(form_state) = app_state.form_state_for_path(path) {
|
||
let current_input_str = form_state.get_current_input();
|
||
let current_input_len = current_input_str.chars().count();
|
||
let max_cursor_pos = if current_input_len > 0 {
|
||
current_input_len.saturating_sub(1)
|
||
} else {
|
||
0
|
||
};
|
||
form_state.current_cursor_pos =
|
||
event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||
}
|
||
}
|
||
}
|
||
} else if let Page::Register(state) = &mut router.current {
|
||
if !app_state.is_canvas_edit_mode() {
|
||
let current_input = state.get_current_input();
|
||
let max_cursor_pos =
|
||
if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
||
state.set_current_cursor_pos(event_handler.ideal_cursor_column.min(max_cursor_pos));
|
||
}
|
||
} else if let Page::Login(state) = &mut router.current {
|
||
if !app_state.is_canvas_edit_mode() {
|
||
let current_input = state.get_current_input();
|
||
let max_cursor_pos =
|
||
if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
||
state.set_current_cursor_pos(event_handler.ideal_cursor_column.min(max_cursor_pos));
|
||
}
|
||
}
|
||
|
||
if position_logic_needs_redraw {
|
||
needs_redraw = true;
|
||
}
|
||
|
||
if app_state.ui.dialog.is_loading {
|
||
needs_redraw = true;
|
||
}
|
||
|
||
#[cfg(feature = "ui-debug")]
|
||
{
|
||
let can_display_next = match &app_state.debug_state {
|
||
Some(current) => current.display_start_time.elapsed() >= Duration::from_secs(2),
|
||
None => true,
|
||
};
|
||
|
||
if can_display_next {
|
||
if let Some((new_message, is_error)) = pop_next_debug_message() {
|
||
app_state.debug_state = Some(DebugState {
|
||
displayed_message: new_message,
|
||
is_error,
|
||
display_start_time: Instant::now(),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
if event_processed || needs_redraw || position_changed {
|
||
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router);
|
||
|
||
match current_mode {
|
||
AppMode::General => {
|
||
let outside_canvas = match &router.current {
|
||
Page::Login(state) => state.focus_outside_canvas,
|
||
Page::Register(state) => state.focus_outside_canvas,
|
||
Page::AddTable(state) => state.focus_outside_canvas,
|
||
Page::AddLogic(state) => state.focus_outside_canvas,
|
||
_ => false, // Form and Admin don’t use this flag
|
||
};
|
||
|
||
if outside_canvas {
|
||
// Outside canvas → app decides
|
||
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
|
||
terminal.show_cursor()?;
|
||
} else {
|
||
// Inside canvas → let canvas handle it
|
||
if let Page::Form(path) = &router.current {
|
||
if let Some(editor) = app_state.editor_for_path(path) {
|
||
let _ = CursorManager::update_for_mode(editor.mode());
|
||
}
|
||
}
|
||
if let Page::Login(page) = &router.current {
|
||
let _ = CursorManager::update_for_mode(page.editor.mode());
|
||
}
|
||
if let Page::Register(page) = &router.current {
|
||
let _ = CursorManager::update_for_mode(page.editor.mode());
|
||
}
|
||
if let Page::AddTable(page) = &router.current {
|
||
let _ = CursorManager::update_for_mode(page.editor.mode());
|
||
}
|
||
if let Page::AddLogic(page) = &router.current {
|
||
let _ = CursorManager::update_for_mode(page.editor.mode());
|
||
}
|
||
}
|
||
}
|
||
AppMode::Command => {
|
||
// Command line overlay → app decides
|
||
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
||
terminal.show_cursor()?;
|
||
}
|
||
}
|
||
|
||
let current_dir = app_state.current_dir.clone();
|
||
terminal
|
||
.draw(|f| {
|
||
render_ui(
|
||
f,
|
||
&mut router,
|
||
&buffer_state,
|
||
&theme,
|
||
&event_handler.command_input,
|
||
event_handler.command_mode,
|
||
&event_handler.command_message,
|
||
&event_handler.navigation_state,
|
||
¤t_dir,
|
||
current_fps,
|
||
&app_state,
|
||
&auth_state,
|
||
);
|
||
})
|
||
.context("Terminal draw call failed")?;
|
||
needs_redraw = false;
|
||
}
|
||
|
||
let now = Instant::now();
|
||
let frame_duration = now.duration_since(last_frame_time);
|
||
last_frame_time = now;
|
||
if frame_duration.as_secs_f64() > 1e-6 {
|
||
current_fps = 1.0 / frame_duration.as_secs_f64();
|
||
}
|
||
|
||
table_just_switched = false;
|
||
}
|
||
}
|