720 lines
32 KiB
Rust
720 lines
32 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::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::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)
|
|
});
|
|
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 {
|
|
if let Page::Form(path) = &router.current {
|
|
if !app_state.ui.focus_outside_canvas {
|
|
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,
|
|
);
|
|
admin_state.add_table_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 => router.navigate(Page::AddTable(admin_state.add_table_state.clone())),
|
|
AppView::AddLogic => router.navigate(Page::AddLogic(admin_state.add_logic_state.clone())),
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Continue with the rest of the positioning logic...
|
|
// Now we can use CanvasState methods like get_current_input(), current_field(), etc.
|
|
|
|
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
|
if let Page::AddLogic(state) = &mut router.current {
|
|
if state.profile_name == profile_name
|
|
&& state.selected_table_name.as_deref() == Some(table_name.as_str())
|
|
{
|
|
info!("Fetching table structure for {}.{}", profile_name, table_name);
|
|
let fetch_message = UiService::initialize_add_logic_table_data(
|
|
&mut grpc_client,
|
|
state,
|
|
&app_state.profile_tree,
|
|
)
|
|
.await
|
|
.unwrap_or_else(|e| {
|
|
error!("Error initializing add_logic_table_data: {}", e);
|
|
format!("Error fetching table structure: {}", e)
|
|
});
|
|
|
|
if !fetch_message.contains("Error") && !fetch_message.contains("Warning") {
|
|
info!("{}", fetch_message);
|
|
} else {
|
|
event_handler.command_message = fetch_message;
|
|
}
|
|
needs_redraw = true;
|
|
} else {
|
|
error!(
|
|
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, but AddLogic state is for {}.{:?}",
|
|
profile_name,
|
|
table_name,
|
|
state.profile_name,
|
|
state.selected_table_name
|
|
);
|
|
}
|
|
} else {
|
|
warn!(
|
|
"Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.",
|
|
profile_name, table_name
|
|
);
|
|
}
|
|
}
|
|
|
|
if let Page::AddLogic(state) = &mut router.current {
|
|
if let Some(table_name) = state.script_editor_awaiting_column_autocomplete.clone() {
|
|
let profile_name = state.profile_name.clone();
|
|
|
|
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
|
|
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
|
|
Ok(columns) => {
|
|
state.set_columns_for_table_autocomplete(columns.clone());
|
|
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
|
|
event_handler.command_message = format!("Columns for '{}' loaded. Select a column.", table_name);
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
|
|
state.script_editor_awaiting_column_autocomplete = None;
|
|
state.deactivate_script_editor_autocomplete();
|
|
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
|
|
}
|
|
}
|
|
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 => {
|
|
if app_state.ui.focus_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());
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|