Compare commits

...

10 Commits

Author SHA1 Message Date
filipriec
7a8f18b116 cargo fix 2025-05-26 22:28:58 +02:00
filipriec
d255e4abb6 proper postiion of the cursor when using sql 2025-05-26 20:53:05 +02:00
filipriec
b770240f0d better autocomplete 2025-05-26 20:43:58 +02:00
filipriec
43b064673b autocomplete is now powerful 2025-05-26 20:22:47 +02:00
filipriec
bf2726c151 tablenames added properly well 2025-05-26 19:51:48 +02:00
filipriec
f3cd921c76 we are suggesting properly table column names now 2025-05-26 19:42:23 +02:00
filipriec
913f6b6b64 broken autocomplete in the add_logic, but its usable, we are keeping it as is, there is nothing more we can do 2025-05-26 16:37:01 +02:00
filipriec
3463a52960 working autocomplete, need more fixes soon 2025-05-26 11:54:28 +02:00
filipriec
116db3566f intro buffer can be killed now also 2025-05-25 22:37:27 +02:00
filipriec
32210a5f7c killing of the buffer now works amazingly well 2025-05-25 22:24:26 +02:00
15 changed files with 666 additions and 211 deletions

View File

@@ -14,15 +14,14 @@ use ratatui::{
use crate::components::handlers::canvas::render_canvas; use crate::components::handlers::canvas::render_canvas;
use crate::components::common::{dialog, autocomplete}; // Added autocomplete use crate::components::common::{dialog, autocomplete}; // Added autocomplete
use crate::config::binds::config::EditorKeybindingMode; use crate::config::binds::config::EditorKeybindingMode;
use crate::modes::handlers::mode_manager::AppMode; // For checking AppMode::Edit
pub fn render_add_logic( pub fn render_add_logic(
f: &mut Frame, f: &mut Frame,
area: Rect, area: Rect,
theme: &Theme, theme: &Theme,
app_state: &AppState, app_state: &AppState,
add_logic_state: &mut AddLogicState, // Changed to &mut add_logic_state: &mut AddLogicState,
is_edit_mode: bool, // This is the general edit mode from EventHandler is_edit_mode: bool,
highlight_state: &HighlightState, highlight_state: &HighlightState,
) { ) {
let main_block = Block::default() let main_block = Block::default()
@@ -47,13 +46,10 @@ pub fn render_add_logic(
let script_title_hint = match add_logic_state.editor_keybinding_mode { let script_title_hint = match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => { EditorKeybindingMode::Vim => {
let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(&add_logic_state.vim_state); let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(&add_logic_state.vim_state);
// Vim mode status is relevant regardless of the general `is_edit_mode`
format!("Script {}", vim_mode_status) format!("Script {}", vim_mode_status)
} }
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => { EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
// For default/emacs, the general `is_edit_mode` (passed to this function) if is_edit_mode {
// indicates if the text area itself is in an "editing" state.
if is_edit_mode { // This `is_edit_mode` refers to the text area's active editing.
"Script (Editing)".to_string() "Script (Editing)".to_string()
} else { } else {
"Script".to_string() "Script".to_string()
@@ -70,7 +66,48 @@ pub fn render_add_logic(
.border_style(border_style), .border_style(border_style),
); );
f.render_widget(&*editor_ref, inner_area); f.render_widget(&*editor_ref, inner_area);
return;
// Drop the editor borrow before accessing autocomplete state
drop(editor_ref);
// === SCRIPT EDITOR AUTOCOMPLETE RENDERING ===
if add_logic_state.script_editor_autocomplete_active && !add_logic_state.script_editor_suggestions.is_empty() {
// Get the current cursor position from textarea
let current_cursor = {
let editor_borrow = add_logic_state.script_content_editor.borrow();
editor_borrow.cursor() // Returns (row, col) as (usize, usize)
};
let (cursor_line, cursor_col) = current_cursor;
// Account for TextArea's block borders (1 for each side)
let block_offset_x = 1;
let block_offset_y = 1;
// Position autocomplete at current cursor position
// Add 1 to column to position dropdown right after the cursor
let autocomplete_x = cursor_col + 1;
let autocomplete_y = cursor_line;
let input_rect = Rect {
x: (inner_area.x + block_offset_x + autocomplete_x as u16).min(inner_area.right().saturating_sub(20)),
y: (inner_area.y + block_offset_y + autocomplete_y as u16).min(inner_area.bottom().saturating_sub(5)),
width: 1, // Minimum width for positioning
height: 1,
};
// Render autocomplete dropdown
autocomplete::render_autocomplete_dropdown(
f,
input_rect,
f.area(), // Full frame area for clamping
theme,
&add_logic_state.script_editor_suggestions,
add_logic_state.script_editor_selected_suggestion_index,
);
}
return; // Exit early for fullscreen mode
} }
// Regular layout with preview // Regular layout with preview

View File

@@ -2,7 +2,7 @@
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode}; use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers}; use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
use ratatui::style::{Color, Style, Modifier}; use ratatui::style::{Color, Style, Modifier};
use tui_textarea::{Input, Key, TextArea, CursorMove, Scrolling}; use tui_textarea::{Input, Key, TextArea, CursorMove};
use std::fmt; use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@@ -2,6 +2,7 @@
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use crate::state::app::buffer::BufferState; use crate::state::app::buffer::BufferState;
use crate::state::app::state::AppState; // Add this import
use ratatui::{ use ratatui::{
layout::{Alignment, Rect}, layout::{Alignment, Rect},
style::Style, style::Style,
@@ -17,6 +18,7 @@ pub fn render_buffer_list(
area: Rect, area: Rect,
theme: &Theme, theme: &Theme,
buffer_state: &BufferState, buffer_state: &BufferState,
app_state: &AppState, // Add this parameter
) { ) {
// --- Style Definitions --- // --- Style Definitions ---
let active_style = Style::default() let active_style = Style::default()
@@ -37,6 +39,9 @@ pub fn render_buffer_list(
let mut spans = Vec::new(); let mut spans = Vec::new();
let mut current_width = 0; let mut current_width = 0;
// TODO: Replace with actual table name from server response
let current_table_name = Some("2025_customer");
for (original_index, view) in buffer_state.history.iter().enumerate() { for (original_index, view) in buffer_state.history.iter().enumerate() {
// Filter: Only process views matching the active layer // Filter: Only process views matching the active layer
if get_view_layer(view) != active_layer { if get_view_layer(view) != active_layer {
@@ -44,7 +49,7 @@ pub fn render_buffer_list(
} }
let is_active = original_index == buffer_state.active_index; let is_active = original_index == buffer_state.active_index;
let buffer_name = view.display_name(); let buffer_name = view.display_name_with_context(current_table_name);
let buffer_text = format!(" {} ", buffer_name); let buffer_text = format!(" {} ", buffer_name);
let text_width = UnicodeWidthStr::width(buffer_text.as_str()); let text_width = UnicodeWidthStr::width(buffer_text.as_str());

View File

@@ -7,7 +7,7 @@ pub fn get_view_layer(view: &AppView) -> u8 {
match view { match view {
AppView::Intro => 1, AppView::Intro => 1,
AppView::Login | AppView::Register | AppView::Admin | AppView::AddTable | AppView::AddLogic => 2, AppView::Login | AppView::Register | AppView::Admin | AppView::AddTable | AppView::AddLogic => 2,
AppView::Form(_) | AppView::Scratch => 3, AppView::Form | AppView::Scratch => 3,
} }
} }

View File

@@ -11,6 +11,8 @@ use crate::services::GrpcClient;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use anyhow::Result; use anyhow::Result;
use crate::components::common::text_editor::TextEditor; use crate::components::common::text_editor::TextEditor;
use crate::services::ui_service::UiService;
use tui_textarea::CursorMove; // Ensure this import is present
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>; pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
@@ -22,31 +24,231 @@ pub fn handle_add_logic_navigation(
is_edit_mode: &mut bool, is_edit_mode: &mut bool,
buffer_state: &mut BufferState, buffer_state: &mut BufferState,
grpc_client: GrpcClient, grpc_client: GrpcClient,
save_logic_sender: SaveLogicResultSender, _save_logic_sender: SaveLogicResultSender, // Marked as unused
command_message: &mut String, command_message: &mut String,
) -> bool { ) -> bool {
// === FULLSCREEN SCRIPT EDITING - COMPLETE ISOLATION === // === FULLSCREEN SCRIPT EDITING - COMPLETE ISOLATION ===
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent { if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut(); // === AUTOCOMPLETE HANDLING ===
if add_logic_state.script_editor_autocomplete_active {
// Handle ONLY Escape to exit fullscreen mode match key_event.code {
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE { // ... (Char, Backspace, Tab, Down, Up cases remain the same) ...
match add_logic_state.editor_keybinding_mode { KeyCode::Char(c) if c.is_alphanumeric() || c == '_' => {
EditorKeybindingMode::Vim => { add_logic_state.script_editor_filter_text.push(c);
if *is_edit_mode { add_logic_state.update_script_editor_suggestions();
// First escape: try to go to Vim Normal mode {
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input( TextEditor::handle_input(
&mut editor_borrow, &mut editor_borrow,
key_event, key_event,
&add_logic_state.editor_keybinding_mode, &add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state, &mut add_logic_state.vim_state,
); );
}
*command_message = format!("Filtering: @{}", add_logic_state.script_editor_filter_text);
return true;
}
KeyCode::Backspace => {
if !add_logic_state.script_editor_filter_text.is_empty() {
add_logic_state.script_editor_filter_text.pop();
add_logic_state.update_script_editor_suggestions();
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
*command_message = if add_logic_state.script_editor_filter_text.is_empty() {
"Autocomplete: @".to_string()
} else {
format!("Filtering: @{}", add_logic_state.script_editor_filter_text)
};
} else {
let should_deactivate = if let Some((trigger_line, trigger_col)) = add_logic_state.script_editor_trigger_position {
let current_cursor = {
let editor_borrow = add_logic_state.script_content_editor.borrow();
editor_borrow.cursor()
};
current_cursor.0 == trigger_line && current_cursor.1 == trigger_col + 1
} else {
false
};
if should_deactivate {
add_logic_state.deactivate_script_editor_autocomplete();
*command_message = "Autocomplete cancelled".to_string();
}
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
}
return true;
}
KeyCode::Tab | KeyCode::Down => {
if !add_logic_state.script_editor_suggestions.is_empty() {
let current = add_logic_state.script_editor_selected_suggestion_index.unwrap_or(0);
let next = (current + 1) % add_logic_state.script_editor_suggestions.len();
add_logic_state.script_editor_selected_suggestion_index = Some(next);
*command_message = format!("Selected: {}", add_logic_state.script_editor_suggestions[next]);
}
return true;
}
KeyCode::Up => {
if !add_logic_state.script_editor_suggestions.is_empty() {
let current = add_logic_state.script_editor_selected_suggestion_index.unwrap_or(0);
let prev = if current == 0 {
add_logic_state.script_editor_suggestions.len() - 1
} else {
current - 1
};
add_logic_state.script_editor_selected_suggestion_index = Some(prev);
*command_message = format!("Selected: {}", add_logic_state.script_editor_suggestions[prev]);
}
return true;
}
KeyCode::Enter => {
if let Some(selected_idx) = add_logic_state.script_editor_selected_suggestion_index {
if let Some(suggestion) = add_logic_state.script_editor_suggestions.get(selected_idx).cloned() {
let trigger_pos = add_logic_state.script_editor_trigger_position;
let filter_len = add_logic_state.script_editor_filter_text.len();
add_logic_state.deactivate_script_editor_autocomplete();
add_logic_state.has_unsaved_changes = true;
if let Some(pos) = trigger_pos {
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
if suggestion == "sql" {
replace_autocomplete_text(&mut editor_borrow, pos, filter_len, "sql");
editor_borrow.insert_str("('')");
// Move cursor back twice to be between the single quotes
editor_borrow.move_cursor(CursorMove::Back); // Before ')'
editor_borrow.move_cursor(CursorMove::Back); // Before ''' (inside '')
*command_message = "Inserted: @sql('')".to_string();
} else {
let is_table_selection = add_logic_state.is_table_name_suggestion(&suggestion);
replace_autocomplete_text(&mut editor_borrow, pos, filter_len, &suggestion);
if is_table_selection {
editor_borrow.insert_str(".");
let new_cursor = editor_borrow.cursor();
drop(editor_borrow); // Release borrow before calling add_logic_state methods
add_logic_state.script_editor_trigger_position = Some(new_cursor);
add_logic_state.script_editor_autocomplete_active = true;
add_logic_state.script_editor_filter_text.clear();
add_logic_state.trigger_column_autocomplete_for_table(suggestion.clone());
let profile_name = add_logic_state.profile_name.clone();
let table_name_for_fetch = suggestion.clone();
let mut client_clone = grpc_client.clone();
tokio::spawn(async move {
match UiService::fetch_columns_for_table(&mut client_clone, &profile_name, &table_name_for_fetch).await {
Ok(_columns) => {
// Result handled by main UI loop
}
Err(e) => {
tracing::error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name_for_fetch, e);
}
}
});
*command_message = format!("Selected table '{}', fetching columns...", suggestion);
} else {
*command_message = format!("Inserted: {}", suggestion);
}
}
}
return true;
}
}
add_logic_state.deactivate_script_editor_autocomplete();
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
return true;
}
KeyCode::Esc => {
add_logic_state.deactivate_script_editor_autocomplete();
*command_message = "Autocomplete cancelled".to_string();
}
_ => {
add_logic_state.deactivate_script_editor_autocomplete();
*command_message = "Autocomplete cancelled".to_string();
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
return true;
}
}
}
if key_event.code == KeyCode::Char('@') && key_event.modifiers == KeyModifiers::NONE {
let should_trigger = match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => *is_edit_mode,
_ => true,
};
if should_trigger {
let cursor_before = {
let editor_borrow = add_logic_state.script_content_editor.borrow();
editor_borrow.cursor()
};
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
add_logic_state.script_editor_trigger_position = Some(cursor_before);
add_logic_state.script_editor_autocomplete_active = true;
add_logic_state.script_editor_filter_text.clear();
add_logic_state.update_script_editor_suggestions();
add_logic_state.has_unsaved_changes = true;
*command_message = "Autocomplete: @ (Tab/↑↓ to navigate, Enter to select, Esc to cancel)".to_string();
return true;
}
}
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => {
if *is_edit_mode {
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
}
if TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) { if TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) {
*is_edit_mode = false; *is_edit_mode = false;
*command_message = "VIM: Normal Mode. Esc again to exit script.".to_string(); *command_message = "VIM: Normal Mode. Esc again to exit script.".to_string();
} }
} else { } else {
// Second escape: exit fullscreen
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview; add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
app_state.ui.focus_outside_canvas = true; app_state.ui.focus_outside_canvas = true;
*is_edit_mode = false; *is_edit_mode = false;
@@ -58,7 +260,6 @@ pub fn handle_add_logic_navigation(
*is_edit_mode = false; *is_edit_mode = false;
*command_message = "Exited script edit. Esc again to exit script.".to_string(); *command_message = "Exited script edit. Esc again to exit script.".to_string();
} else { } else {
// Exit fullscreen
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview; add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
app_state.ui.focus_outside_canvas = true; app_state.ui.focus_outside_canvas = true;
*is_edit_mode = false; *is_edit_mode = false;
@@ -69,27 +270,24 @@ pub fn handle_add_logic_navigation(
return true; return true;
} }
// ALL OTHER KEYS: Pass directly to textarea without any interference let changed = {
let changed = TextEditor::handle_input( let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
&mut editor_borrow, TextEditor::handle_input(
key_event, &mut editor_borrow,
&add_logic_state.editor_keybinding_mode, key_event,
&mut add_logic_state.vim_state, &add_logic_state.editor_keybinding_mode,
); &mut add_logic_state.vim_state,
)
};
if changed { if changed {
add_logic_state.has_unsaved_changes = true; add_logic_state.has_unsaved_changes = true;
} }
// Update edit mode status for Vim
if add_logic_state.editor_keybinding_mode == EditorKeybindingMode::Vim { if add_logic_state.editor_keybinding_mode == EditorKeybindingMode::Vim {
*is_edit_mode = !TextEditor::is_vim_normal_mode(&add_logic_state.vim_state); *is_edit_mode = !TextEditor::is_vim_normal_mode(&add_logic_state.vim_state);
} }
return true;
return true; // Always consume the event in fullscreen mode
} }
// === END FULLSCREEN ISOLATION ===
// Regular navigation logic for non-fullscreen elements
let action = config.get_general_action(key_event.code, key_event.modifiers); let action = config.get_general_action(key_event.code, key_event.modifiers);
let current_focus = add_logic_state.current_focus; let current_focus = add_logic_state.current_focus;
let mut handled = true; let mut handled = true;
@@ -97,14 +295,11 @@ pub fn handle_add_logic_navigation(
match action.as_deref() { match action.as_deref() {
Some("exit_table_scroll") => { Some("exit_table_scroll") => {
// This shouldn't happen since we handle InsideScriptContent above
handled = false; handled = false;
} }
Some("move_up") => { Some("move_up") => {
match current_focus { match current_focus {
AddLogicFocus::InputLogicName => { AddLogicFocus::InputLogicName => {}
// Stay at top
}
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName, AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName,
AddLogicFocus::InputDescription => new_focus = AddLogicFocus::InputTargetColumn, AddLogicFocus::InputDescription => new_focus = AddLogicFocus::InputTargetColumn,
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription, AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
@@ -123,9 +318,7 @@ pub fn handle_add_logic_navigation(
}, },
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton, AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton, AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
AddLogicFocus::CancelButton => { AddLogicFocus::CancelButton => {}
// Stay at bottom
}
_ => handled = false, _ => handled = false,
} }
} }
@@ -135,14 +328,14 @@ pub fn handle_add_logic_navigation(
{ new_focus = AddLogicFocus::ScriptContentPreview; } { new_focus = AddLogicFocus::ScriptContentPreview; }
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton, AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton, AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
AddLogicFocus::CancelButton => { /* Stay at last */ } AddLogicFocus::CancelButton => { }
_ => handled = false, _ => handled = false,
} }
} }
Some("previous_option") => { Some("previous_option") => {
match current_focus { match current_focus {
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription =>
{ /* Stay at first */ } { }
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription, AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview, AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton, AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
@@ -175,8 +368,8 @@ pub fn handle_add_logic_navigation(
match current_focus { match current_focus {
AddLogicFocus::ScriptContentPreview => { AddLogicFocus::ScriptContentPreview => {
new_focus = AddLogicFocus::InsideScriptContent; new_focus = AddLogicFocus::InsideScriptContent;
*is_edit_mode = false; // Start in preview mode *is_edit_mode = false;
app_state.ui.focus_outside_canvas = false; // Script is like canvas app_state.ui.focus_outside_canvas = false;
let mode_hint = match add_logic_state.editor_keybinding_mode { let mode_hint = match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => "VIM mode - 'i'/'a'/'o' to edit", EditorKeybindingMode::Vim => "VIM mode - 'i'/'a'/'o' to edit",
_ => "Enter/Ctrl+E to edit", _ => "Enter/Ctrl+E to edit",
@@ -215,24 +408,33 @@ pub fn handle_add_logic_navigation(
if handled && current_focus != new_focus { if handled && current_focus != new_focus {
add_logic_state.current_focus = new_focus; add_logic_state.current_focus = new_focus;
// Set edit mode and canvas focus based on new focus
let new_is_canvas_input_focus = matches!(new_focus, let new_is_canvas_input_focus = matches!(new_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription
); );
if new_is_canvas_input_focus { if new_is_canvas_input_focus {
// Entering canvas - start in readonly mode
*is_edit_mode = false; *is_edit_mode = false;
app_state.ui.focus_outside_canvas = false; app_state.ui.focus_outside_canvas = false;
} else { } else {
// Outside canvas
app_state.ui.focus_outside_canvas = true; app_state.ui.focus_outside_canvas = true;
if matches!(new_focus, AddLogicFocus::ScriptContentPreview) { if matches!(new_focus, AddLogicFocus::ScriptContentPreview) {
*is_edit_mode = false; *is_edit_mode = false;
} }
} }
} }
handled handled
} }
fn replace_autocomplete_text(
editor: &mut tui_textarea::TextArea,
trigger_pos: (usize, usize),
filter_len: usize,
replacement: &str,
) {
// use tui_textarea::CursorMove; // Already imported at the top of the module
let filter_start_pos = (trigger_pos.0, trigger_pos.1 + 1);
editor.move_cursor(CursorMove::Jump(filter_start_pos.0 as u16, filter_start_pos.1 as u16));
for _ in 0..filter_len {
editor.delete_next_char();
}
editor.insert_str(replacement);
}

View File

@@ -234,31 +234,34 @@ pub fn handle_admin_navigation(
admin_state.add_logic_state = AddLogicState { admin_state.add_logic_state = AddLogicState {
profile_name: profile.name.clone(), profile_name: profile.name.clone(),
selected_table_name: Some(table.name.clone()), selected_table_name: Some(table.name.clone()),
// selected_table_id: table.id, // If you have table IDs selected_table_id: Some(table.id), // If you have table IDs
editor_keybinding_mode: config.editor.keybinding_mode.clone(), editor_keybinding_mode: config.editor.keybinding_mode.clone(),
current_focus: AddLogicFocus::default(), // Reset focus for the new screen current_focus: AddLogicFocus::default(),
..AddLogicState::default() ..AddLogicState::default()
}; };
buffer_state.update_history(AppView::AddLogic); // Switch view
app_state.ui.focus_outside_canvas = false; // Ensure canvas focus // Store table info for later fetching
app_state.pending_table_structure_fetch = Some((
profile.name.clone(),
table.name.clone()
));
buffer_state.update_history(AppView::AddLogic);
app_state.ui.focus_outside_canvas = false;
*command_message = format!( *command_message = format!(
"Opening Add Logic for table '{}' in profile '{}'...", "Opening Add Logic for table '{}' in profile '{}'...",
table.name, profile.name table.name, profile.name
); );
} else { } else {
// This case should ideally not be reached if indices are managed correctly
*command_message = "Error: Selected table data not found.".to_string(); *command_message = "Error: Selected table data not found.".to_string();
} }
} else { } else {
// Profile is selected, but table is not
*command_message = "Select a table first!".to_string(); *command_message = "Select a table first!".to_string();
} }
} else { } else {
// This case should ideally not be reached if p_idx is valid
*command_message = "Error: Selected profile data not found.".to_string(); *command_message = "Error: Selected profile data not found.".to_string();
} }
} else { } else {
// Profile is not selected
*command_message = "Select a profile first!".to_string(); *command_message = "Select a profile first!".to_string();
} }
handled = true; handled = true;

View File

@@ -121,10 +121,7 @@ impl EventHandler {
else if ui.show_admin { AppView::Admin } else if ui.show_admin { AppView::Admin }
else if ui.show_add_logic { AppView::AddLogic } else if ui.show_add_logic { AppView::AddLogic }
else if ui.show_add_table { AppView::AddTable } else if ui.show_add_table { AppView::AddTable }
else if ui.show_form { else if ui.show_form { AppView::Form } // Remove the dynamic name part
let form_name = app_state.selected_profile.clone().unwrap_or_else(|| "Data Form".to_string());
AppView::Form(form_name)
}
else { AppView::Scratch } else { AppView::Scratch }
}; };
buffer_state.update_history(current_view); buffer_state.update_history(current_view);
@@ -177,8 +174,14 @@ impl EventHandler {
return Ok(EventOutcome::Ok("Switched to previous buffer".to_string())); return Ok(EventOutcome::Ok("Switched to previous buffer".to_string()));
} }
} }
//"close_buffer" => {
// let message = buffer_state.close_buffer_with_intro_fallback();
// return Ok(EventOutcome::Ok(message));
//}
"close_buffer" => { "close_buffer" => {
let message = buffer_state.close_buffer_with_intro_fallback(); // TODO: Replace with actual table name from server response
let current_table_name = Some("2025_customer"); // Your hardcoded table name
let message = buffer_state.close_buffer_with_intro_fallback(current_table_name);
return Ok(EventOutcome::Ok(message)); return Ok(EventOutcome::Ok(message));
} }
_ => {} _ => {}

View File

@@ -3,12 +3,94 @@
use crate::services::grpc_client::GrpcClient; use crate::services::grpc_client::GrpcClient;
use crate::state::pages::form::FormState; use crate::state::pages::form::FormState;
use crate::tui::functions::common::form::SaveOutcome; use crate::tui::functions::common::form::SaveOutcome;
use crate::state::pages::add_logic::AddLogicState;
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
pub struct UiService; pub struct UiService;
impl UiService { impl UiService {
pub async fn initialize_add_logic_table_data(
grpc_client: &mut GrpcClient,
add_logic_state: &mut AddLogicState,
profile_tree: &common::proto::multieko2::table_definition::ProfileTreeResponse,
) -> Result<String> {
let profile_name_clone_opt = Some(add_logic_state.profile_name.clone());
let table_name_opt_clone = add_logic_state.selected_table_name.clone();
// Collect table names from SAME profile only
let same_profile_table_names: Vec<String> = profile_tree.profiles
.iter()
.find(|profile| profile.name == add_logic_state.profile_name)
.map(|profile| profile.tables.iter().map(|table| table.name.clone()).collect())
.unwrap_or_default();
// Set same profile table names for autocomplete
add_logic_state.set_same_profile_table_names(same_profile_table_names.clone());
if let (Some(profile_name_clone), Some(table_name_clone)) = (profile_name_clone_opt, table_name_opt_clone) {
match grpc_client.get_table_structure(profile_name_clone.clone(), table_name_clone.clone()).await {
Ok(response) => {
let column_names: Vec<String> = response.columns
.into_iter()
.map(|col| col.name)
.collect();
add_logic_state.set_table_columns(column_names.clone());
Ok(format!(
"Loaded {} columns for table '{}' and {} tables from profile '{}'",
column_names.len(),
table_name_clone,
same_profile_table_names.len(),
add_logic_state.profile_name
))
}
Err(e) => {
tracing::warn!(
"Failed to fetch table structure for {}.{}: {}",
profile_name_clone,
table_name_clone,
e
);
Ok(format!(
"Warning: Could not load table structure for '{}'. Autocomplete will use {} tables from profile '{}'.",
table_name_clone,
same_profile_table_names.len(),
add_logic_state.profile_name
))
}
}
} else {
Ok(format!(
"No table selected for Add Logic. Loaded {} tables from profile '{}' for autocomplete.",
same_profile_table_names.len(),
add_logic_state.profile_name
))
}
}
/// Fetches columns for a specific table (used for table.column autocomplete)
pub async fn fetch_columns_for_table(
grpc_client: &mut GrpcClient,
profile_name: &str,
table_name: &str,
) -> Result<Vec<String>> {
match grpc_client.get_table_structure(profile_name.to_string(), table_name.to_string()).await {
Ok(response) => {
let column_names: Vec<String> = response.columns
.into_iter()
.map(|col| col.name)
.collect();
Ok(column_names)
}
Err(e) => {
tracing::warn!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
Err(e.into())
}
}
}
pub async fn initialize_app_state( pub async fn initialize_app_state(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
app_state: &mut AppState, app_state: &mut AppState,

View File

@@ -8,11 +8,13 @@ pub enum AppView {
Admin, Admin,
AddTable, AddTable,
AddLogic, AddLogic,
Form(String), Form,
Scratch, Scratch,
} }
impl AppView { impl AppView {
/// Returns the display name for the view.
/// For Form, pass the current table name to get dynamic naming.
pub fn display_name(&self) -> &str { pub fn display_name(&self) -> &str {
match self { match self {
AppView::Intro => "Intro", AppView::Intro => "Intro",
@@ -21,10 +23,22 @@ impl AppView {
AppView::Admin => "Admin_Panel", AppView::Admin => "Admin_Panel",
AppView::AddTable => "Add_Table", AppView::AddTable => "Add_Table",
AppView::AddLogic => "Add_Logic", AppView::AddLogic => "Add_Logic",
AppView::Form(name) => name.as_str(), AppView::Form => "Form",
AppView::Scratch => "*scratch*", AppView::Scratch => "*scratch*",
} }
} }
/// Returns the display name with dynamic context (for Form buffers)
pub fn display_name_with_context(&self, current_table_name: Option<&str>) -> String {
match self {
AppView::Form => {
current_table_name
.unwrap_or("Data Form")
.to_string()
}
_ => self.display_name().to_string(),
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -66,11 +80,8 @@ impl BufferState {
} }
let current_index = self.active_index; let current_index = self.active_index;
if matches!(self.history.get(current_index), Some(AppView::Intro)) {
return false;
}
self.history.remove(current_index); self.history.remove(current_index);
if self.history.is_empty() { if self.history.is_empty() {
self.history.push(AppView::Intro); self.history.push(AppView::Intro);
self.active_index = 0; self.active_index = 0;
@@ -80,25 +91,29 @@ impl BufferState {
true true
} }
pub fn close_buffer_with_intro_fallback(&mut self) -> String { pub fn close_buffer_with_intro_fallback(&mut self, current_table_name: Option<&str>) -> String {
let current_view_cloned = self.get_active_view().cloned(); let current_view_cloned = self.get_active_view().cloned();
if let Some(AppView::Intro) = current_view_cloned { if let Some(AppView::Intro) = current_view_cloned {
return "Cannot close intro buffer".to_string(); if self.history.len() == 1 {
self.close_active_buffer();
return "Intro buffer reset".to_string();
}
} }
let closed_name = current_view_cloned let closed_name = current_view_cloned
.as_ref() .as_ref()
.map(|v| v.display_name().to_string()) .map(|v| v.display_name_with_context(current_table_name))
.unwrap_or_else(|| "Unknown".to_string()); .unwrap_or_else(|| "Unknown".to_string());
if self.close_active_buffer() { if self.close_active_buffer() {
if self.history.len() == 1 && matches!(self.history.get(0), Some(AppView::Intro)) { if self.history.len() == 1 && matches!(self.history.get(0), Some(AppView::Intro)) {
format!("Closed '{}' - returned to intro", closed_name) format!("Closed '{}' - returned to Intro", closed_name)
} else { } else {
format!("Closed '{}'", closed_name) format!("Closed '{}'", closed_name)
} }
} else { } else {
format!("Cannot close buffer: {}", closed_name) format!("Buffer '{}' could not be closed", closed_name)
} }
} }
} }

View File

@@ -3,7 +3,7 @@
use std::env; use std::env;
use common::proto::multieko2::table_definition::ProfileTreeResponse; use common::proto::multieko2::table_definition::ProfileTreeResponse;
use crate::modes::handlers::mode_manager::AppMode; use crate::modes::handlers::mode_manager::AppMode;
use crate::ui::handlers::context::{DialogPurpose, UiContext}; use crate::ui::handlers::context::DialogPurpose;
use anyhow::Result; use anyhow::Result;
pub struct DialogState { pub struct DialogState {
@@ -39,6 +39,7 @@ pub struct AppState {
pub selected_profile: Option<String>, pub selected_profile: Option<String>,
pub current_mode: AppMode, pub current_mode: AppMode,
pub focused_button_index: usize, pub focused_button_index: usize,
pub pending_table_structure_fetch: Option<(String, String)>,
// UI preferences // UI preferences
pub ui: UiState, pub ui: UiState,
@@ -57,6 +58,7 @@ impl AppState {
selected_profile: None, selected_profile: None,
current_mode: AppMode::General, current_mode: AppMode::General,
focused_button_index: 0, focused_button_index: 0,
pending_table_structure_fetch: None,
ui: UiState::default(), ui: UiState::default(),
}) })
} }

View File

@@ -42,6 +42,18 @@ pub struct AddLogicState {
pub show_target_column_suggestions: bool, pub show_target_column_suggestions: bool,
pub selected_target_column_suggestion_index: Option<usize>, pub selected_target_column_suggestion_index: Option<usize>,
pub in_target_column_suggestion_mode: bool, pub in_target_column_suggestion_mode: bool,
// Script Editor Autocomplete
pub script_editor_autocomplete_active: bool,
pub script_editor_suggestions: Vec<String>,
pub script_editor_selected_suggestion_index: Option<usize>,
pub script_editor_trigger_position: Option<(usize, usize)>, // (line, column)
pub all_table_names: Vec<String>,
pub script_editor_filter_text: String,
// New fields for same-profile table names and column autocomplete
pub same_profile_table_names: Vec<String>, // Tables from same profile only
pub script_editor_awaiting_column_autocomplete: Option<String>, // Table name waiting for column fetch
} }
impl AddLogicState { impl AddLogicState {
@@ -69,6 +81,16 @@ impl AddLogicState {
show_target_column_suggestions: false, show_target_column_suggestions: false,
selected_target_column_suggestion_index: None, selected_target_column_suggestion_index: None,
in_target_column_suggestion_mode: false, in_target_column_suggestion_mode: false,
script_editor_autocomplete_active: false,
script_editor_suggestions: Vec::new(),
script_editor_selected_suggestion_index: None,
script_editor_trigger_position: None,
all_table_names: Vec::new(),
script_editor_filter_text: String::new(),
same_profile_table_names: Vec::new(),
script_editor_awaiting_column_autocomplete: None,
} }
} }
@@ -97,16 +119,10 @@ impl AddLogicState {
self.show_target_column_suggestions = !self.target_column_suggestions.is_empty(); self.show_target_column_suggestions = !self.target_column_suggestions.is_empty();
if self.show_target_column_suggestions { if self.show_target_column_suggestions {
// If suggestions are shown, ensure a selection (usually the first)
// or maintain current if it's still valid.
if let Some(selected_idx) = self.selected_target_column_suggestion_index { if let Some(selected_idx) = self.selected_target_column_suggestion_index {
if selected_idx >= self.target_column_suggestions.len() { if selected_idx >= self.target_column_suggestions.len() {
self.selected_target_column_suggestion_index = Some(0); self.selected_target_column_suggestion_index = Some(0);
} }
// If the previously selected item is no longer in the filtered list, reset.
// This is a bit more complex to check perfectly without iterating again.
// For now, just ensuring it's within bounds is a good start.
// A more robust way would be to check if the string at selected_idx still matches.
} else { } else {
self.selected_target_column_suggestion_index = Some(0); self.selected_target_column_suggestion_index = Some(0);
} }
@@ -114,6 +130,101 @@ impl AddLogicState {
self.selected_target_column_suggestion_index = None; self.selected_target_column_suggestion_index = None;
} }
} }
/// Updates script editor suggestions based on current filter text
pub fn update_script_editor_suggestions(&mut self) {
let mut suggestions = vec!["sql".to_string()];
if self.selected_table_name.is_some() {
suggestions.extend(self.table_columns_for_suggestions.clone());
}
let current_selected_table_name = self.selected_table_name.as_deref();
suggestions.extend(
self.same_profile_table_names
.iter()
.filter(|tn| Some(tn.as_str()) != current_selected_table_name)
.cloned()
);
if self.script_editor_filter_text.is_empty() {
self.script_editor_suggestions = suggestions;
} else {
let filter_lower = self.script_editor_filter_text.to_lowercase();
self.script_editor_suggestions = suggestions
.into_iter()
.filter(|suggestion| suggestion.to_lowercase().contains(&filter_lower))
.collect();
}
// Update selection index
if self.script_editor_suggestions.is_empty() {
self.script_editor_selected_suggestion_index = None;
self.script_editor_autocomplete_active = false;
} else if let Some(selected_idx) = self.script_editor_selected_suggestion_index {
if selected_idx >= self.script_editor_suggestions.len() {
self.script_editor_selected_suggestion_index = Some(0);
}
} else {
self.script_editor_selected_suggestion_index = Some(0);
}
}
/// Checks if a suggestion is a table name (for triggering column autocomplete)
pub fn is_table_name_suggestion(&self, suggestion: &str) -> bool {
// Not "sql"
if suggestion == "sql" {
return false;
}
if self.table_columns_for_suggestions.contains(&suggestion.to_string()) {
return false;
}
self.same_profile_table_names.contains(&suggestion.to_string())
}
/// Sets table columns for autocomplete suggestions
pub fn set_table_columns(&mut self, columns: Vec<String>) {
self.table_columns_for_suggestions = columns.clone();
if !columns.is_empty() {
self.update_target_column_suggestions();
}
}
/// Sets all available table names for autocomplete suggestions
pub fn set_all_table_names(&mut self, table_names: Vec<String>) {
self.all_table_names = table_names;
}
/// Sets table names from the same profile for autocomplete suggestions
pub fn set_same_profile_table_names(&mut self, table_names: Vec<String>) {
self.same_profile_table_names = table_names;
}
/// Triggers waiting for column autocomplete for a specific table
pub fn trigger_column_autocomplete_for_table(&mut self, table_name: String) {
self.script_editor_awaiting_column_autocomplete = Some(table_name);
}
/// Updates autocomplete with columns for a specific table
pub fn set_columns_for_table_autocomplete(&mut self, columns: Vec<String>) {
self.script_editor_suggestions = columns;
self.script_editor_selected_suggestion_index = if self.script_editor_suggestions.is_empty() {
None
} else {
Some(0)
};
self.script_editor_autocomplete_active = !self.script_editor_suggestions.is_empty();
self.script_editor_awaiting_column_autocomplete = None;
}
/// Deactivates script editor autocomplete and clears related state
pub fn deactivate_script_editor_autocomplete(&mut self) {
self.script_editor_autocomplete_active = false;
self.script_editor_suggestions.clear();
self.script_editor_selected_suggestion_index = None;
self.script_editor_trigger_position = None;
self.script_editor_filter_text.clear();
}
} }
impl Default for AddLogicState { impl Default for AddLogicState {
@@ -180,10 +291,9 @@ impl CanvasState for AddLogicState {
0 => AddLogicFocus::InputLogicName, 0 => AddLogicFocus::InputLogicName,
1 => AddLogicFocus::InputTargetColumn, 1 => AddLogicFocus::InputTargetColumn,
2 => AddLogicFocus::InputDescription, 2 => AddLogicFocus::InputDescription,
_ => return, // Or handle error/default _ => return,
}; };
if self.current_focus != new_focus { if self.current_focus != new_focus {
// If changing field, exit suggestion mode for target column
if self.current_focus == AddLogicFocus::InputTargetColumn { if self.current_focus == AddLogicFocus::InputTargetColumn {
self.in_target_column_suggestion_mode = false; self.in_target_column_suggestion_mode = false;
self.show_target_column_suggestions = false; self.show_target_column_suggestions = false;
@@ -199,12 +309,10 @@ impl CanvasState for AddLogicState {
self.logic_name_cursor_pos = pos.min(self.logic_name_input.len()); self.logic_name_cursor_pos = pos.min(self.logic_name_input.len());
} }
AddLogicFocus::InputTargetColumn => { AddLogicFocus::InputTargetColumn => {
self.target_column_cursor_pos = self.target_column_cursor_pos = pos.min(self.target_column_input.len());
pos.min(self.target_column_input.len());
} }
AddLogicFocus::InputDescription => { AddLogicFocus::InputDescription => {
self.description_cursor_pos = self.description_cursor_pos = pos.min(self.description_input.len());
pos.min(self.description_input.len());
} }
_ => {} _ => {}
} }
@@ -215,7 +323,7 @@ impl CanvasState for AddLogicState {
} }
fn get_suggestions(&self) -> Option<&[String]> { fn get_suggestions(&self) -> Option<&[String]> {
if self.current_field() == 1 // Target Column field index if self.current_field() == 1
&& self.in_target_column_suggestion_mode && self.in_target_column_suggestion_mode
&& self.show_target_column_suggestions && self.show_target_column_suggestions
{ {
@@ -226,7 +334,7 @@ impl CanvasState for AddLogicState {
} }
fn get_selected_suggestion_index(&self) -> Option<usize> { fn get_selected_suggestion_index(&self) -> Option<usize> {
if self.current_field() == 1 // Target Column field index if self.current_field() == 1
&& self.in_target_column_suggestion_mode && self.in_target_column_suggestion_mode
&& self.show_target_column_suggestions && self.show_target_column_suggestions
{ {

View File

@@ -1,6 +1,6 @@
// src/tui/functions/common/add_table.rs // src/tui/functions/common/add_table.rs
use crate::state::pages::add_table::{ use crate::state::pages::add_table::{
AddTableFocus, AddTableState, ColumnDefinition, IndexDefinition, LinkDefinition, AddTableFocus, AddTableState, ColumnDefinition, IndexDefinition,
}; };
use crate::services::GrpcClient; use crate::services::GrpcClient;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};

View File

@@ -13,10 +13,7 @@ pub fn handle_intro_selection(
index: usize, index: usize,
) { ) {
let target_view = match index { let target_view = match index {
0 => { 0 => AppView::Form,
let form_name = app_state.selected_profile.clone().unwrap_or_else(|| "Data Form".to_string());
AppView::Form(form_name)
}
1 => AppView::Admin, 1 => AppView::Admin,
2 => AppView::Login, 2 => AppView::Login,
3 => AppView::Register, 3 => AppView::Register,

View File

@@ -201,7 +201,7 @@ pub fn render_ui(
// Render buffer list if enabled and area is available // Render buffer list if enabled and area is available
if let Some(area) = buffer_list_area { if let Some(area) = buffer_list_area {
if app_state.ui.show_buffer_list { if app_state.ui.show_buffer_list {
render_buffer_list(f, area, theme, buffer_state); render_buffer_list(f, area, theme, buffer_state, app_state);
} }
} }
render_status_line(f, status_line_area, current_dir, theme, is_edit_mode, current_fps); render_status_line(f, status_line_area, current_dir, theme, is_edit_mode, current_fps);

View File

@@ -23,16 +23,16 @@ use crate::tui::terminal::{EventReader, TerminalCore};
use crate::ui::handlers::render::render_ui; use crate::ui::handlers::render::render_ui;
use crate::tui::functions::common::login::LoginResult; use crate::tui::functions::common::login::LoginResult;
use crate::tui::functions::common::register::RegisterResult; use crate::tui::functions::common::register::RegisterResult;
use crate::tui::functions::common::add_table::handle_save_table_action; // Removed: use crate::tui::functions::common::add_table::handle_save_table_action;
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender; // Removed: use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
use crate::ui::handlers::context::{DialogPurpose, UiContext}; use crate::ui::handlers::context::DialogPurpose; // UiContext removed if not used directly
use crate::tui::functions::common::login; use crate::tui::functions::common::login;
use crate::tui::functions::common::register; use crate::tui::functions::common::register;
use std::time::Instant; use std::time::Instant;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use crossterm::cursor::SetCursorStyle; use crossterm::cursor::SetCursorStyle;
use crossterm::event as crossterm_event; use crossterm::event as crossterm_event;
use tracing::{error, info}; use tracing::{error, info, warn}; // Added warn
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -50,7 +50,7 @@ pub async fn run_ui() -> Result<()> {
mpsc::channel::<RegisterResult>(1); mpsc::channel::<RegisterResult>(1);
let (save_table_result_sender, mut save_table_result_receiver) = let (save_table_result_sender, mut save_table_result_receiver) =
mpsc::channel::<Result<String>>(1); mpsc::channel::<Result<String>>(1);
let (save_logic_result_sender, mut save_logic_result_receiver) = let (save_logic_result_sender, _save_logic_result_receiver) = // Prefixed and removed mut
mpsc::channel::<Result<String>>(1); mpsc::channel::<Result<String>>(1);
let mut event_handler = EventHandler::new( let mut event_handler = EventHandler::new(
@@ -73,8 +73,6 @@ pub async fn run_ui() -> Result<()> {
let mut auto_logged_in = false; let mut auto_logged_in = false;
match load_auth_data() { match load_auth_data() {
Ok(Some(stored_data)) => { Ok(Some(stored_data)) => {
// TODO: Optionally validate token with server here
// For now, assume valid if successfully loaded
auth_state.auth_token = Some(stored_data.access_token); auth_state.auth_token = Some(stored_data.access_token);
auth_state.user_id = Some(stored_data.user_id); auth_state.user_id = Some(stored_data.user_id);
auth_state.role = Some(stored_data.role); auth_state.role = Some(stored_data.role);
@@ -91,27 +89,20 @@ pub async fn run_ui() -> Result<()> {
} }
// --- END DATA --- // --- END DATA ---
// Initialize app state with profile tree and table structure
let column_names = let column_names =
UiService::initialize_app_state(&mut grpc_client, &mut app_state) UiService::initialize_app_state(&mut grpc_client, &mut app_state)
.await.context("Failed to initialize app state from UI service")?; .await.context("Failed to initialize app state from UI service")?;
let mut form_state = FormState::new(column_names); let mut form_state = FormState::new(column_names);
// Fetch the total count of Adresar entries
UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await?; UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await?;
form_state.reset_to_empty(); form_state.reset_to_empty();
// --- DATA2: Adjust initial view based on auth status ---
if auto_logged_in { if auto_logged_in {
// User is auto-logged in, go to main app view buffer_state.history = vec![AppView::Form];
buffer_state.history = vec![AppView::Form("Adresar".to_string())];
buffer_state.active_index = 0; buffer_state.active_index = 0;
info!("Initial view set to Form due to auto-login."); info!("Initial view set to Form due to auto-login.");
} }
// If not auto-logged in, BufferState default (Intro) will be used
// --- END DATA2 ---
// --- FPS Calculation State ---
let mut last_frame_time = Instant::now(); let mut last_frame_time = Instant::now();
let mut current_fps = 0.0; let mut current_fps = 0.0;
let mut needs_redraw = true; let mut needs_redraw = true;
@@ -119,7 +110,6 @@ pub async fn run_ui() -> Result<()> {
loop { loop {
// --- Synchronize UI View from Active Buffer --- // --- Synchronize UI View from Active Buffer ---
if let Some(active_view) = buffer_state.get_active_view() { if let Some(active_view) = buffer_state.get_active_view() {
// Reset all flags first
app_state.ui.show_intro = false; app_state.ui.show_intro = false;
app_state.ui.show_login = false; app_state.ui.show_login = false;
app_state.ui.show_register = false; app_state.ui.show_register = false;
@@ -142,13 +132,12 @@ pub async fn run_ui() -> Result<()> {
event_handler.command_message = format!("Error refreshing admin data: {}", e); event_handler.command_message = format!("Error refreshing admin data: {}", e);
} }
} }
app_state.ui.show_admin = true; // <<< RESTORE THIS app_state.ui.show_admin = true;
let profile_names = app_state.profile_tree.profiles.iter() // <<< RESTORE THIS let profile_names = app_state.profile_tree.profiles.iter()
.map(|p| p.name.clone()) // <<< RESTORE THIS .map(|p| p.name.clone())
.collect(); // <<< RESTORE THIS .collect();
admin_state.set_profiles(profile_names); admin_state.set_profiles(profile_names);
// Only reset to ProfilesPane if not already in a specific admin sub-focus
if admin_state.current_focus == AdminFocus::default() || if admin_state.current_focus == AdminFocus::default() ||
!matches!(admin_state.current_focus, !matches!(admin_state.current_focus,
AdminFocus::InsideProfilesList | AdminFocus::InsideProfilesList |
@@ -156,23 +145,58 @@ pub async fn run_ui() -> Result<()> {
AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3) { AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3) {
admin_state.current_focus = AdminFocus::ProfilesPane; admin_state.current_focus = AdminFocus::ProfilesPane;
} }
// Pre-select first profile item for visual consistency, but '>' won't show until 'select'
if admin_state.profile_list_state.selected().is_none() && !app_state.profile_tree.profiles.is_empty() { if admin_state.profile_list_state.selected().is_none() && !app_state.profile_tree.profiles.is_empty() {
admin_state.profile_list_state.select(Some(0)); admin_state.profile_list_state.select(Some(0));
} }
} }
AppView::AddTable => app_state.ui.show_add_table = true, AppView::AddTable => app_state.ui.show_add_table = true,
AppView::AddLogic => app_state.ui.show_add_logic = true, AppView::AddLogic => app_state.ui.show_add_logic = true,
AppView::Form(_) => app_state.ui.show_form = true, AppView::Form => app_state.ui.show_form = true,
AppView::Scratch => {} // Or show a scratchpad component AppView::Scratch => {}
} }
} }
// --- End Synchronization --- // --- End Synchronization ---
// --- Handle Pending Table Structure Fetches ---
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
if app_state.ui.show_add_logic {
// Ensure admin_state.add_logic_state matches the pending fetch
if admin_state.add_logic_state.profile_name == profile_name &&
admin_state.add_logic_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,
&mut admin_state.add_logic_state,
&app_state.profile_tree, // Pass the 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 add_logic_state is for {}.{:?}",
profile_name, table_name,
admin_state.add_logic_state.profile_name,
admin_state.add_logic_state.selected_table_name
);
}
} else {
warn!(
"Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.",
profile_name, table_name
);
}
}
// --- 3. Draw UI --- // --- 3. Draw UI ---
// Draw the current state *first*. This ensures the loading dialog
// set in the *previous* iteration gets rendered before the pending
// action check below.
if needs_redraw { if needs_redraw {
terminal.draw(|f| { terminal.draw(|f| {
render_ui( render_ui(
@@ -185,7 +209,7 @@ pub async fn run_ui() -> Result<()> {
&mut admin_state, &mut admin_state,
&buffer_state, &buffer_state,
&theme, &theme,
event_handler.is_edit_mode, // Use event_handler's state event_handler.is_edit_mode,
&event_handler.highlight_state, &event_handler.highlight_state,
app_state.total_count, app_state.total_count,
app_state.current_position, app_state.current_position,
@@ -200,8 +224,30 @@ pub async fn run_ui() -> Result<()> {
needs_redraw = false; needs_redraw = false;
} }
// --- Handle Pending Column Autocomplete for Table Selection ---
if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() {
if app_state.ui.show_add_logic {
let profile_name = admin_state.add_logic_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) => {
admin_state.add_logic_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);
admin_state.add_logic_state.script_editor_awaiting_column_autocomplete = None;
admin_state.add_logic_state.deactivate_script_editor_autocomplete();
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
}
}
needs_redraw = true;
}
}
// --- Cursor Visibility Logic --- // --- Cursor Visibility Logic ---
// (Keep existing cursor logic here - depends on state drawn above)
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state); let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
match current_mode { match current_mode {
AppMode::Edit => { terminal.show_cursor()?; } AppMode::Edit => { terminal.show_cursor()?; }
@@ -222,8 +268,6 @@ pub async fn run_ui() -> Result<()> {
let total_count = app_state.total_count; let total_count = app_state.total_count;
let mut current_position = app_state.current_position; let mut current_position = app_state.current_position;
let position_before_event = current_position; let position_before_event = current_position;
// --- Determine if redraw is needed based on active login ---
// Always redraw if the loading dialog is currently showing.
if app_state.ui.dialog.is_loading { if app_state.ui.dialog.is_loading {
needs_redraw = true; needs_redraw = true;
} }
@@ -231,10 +275,9 @@ pub async fn run_ui() -> Result<()> {
// --- 1. Handle Terminal Events --- // --- 1. Handle Terminal Events ---
let mut event_outcome_result = Ok(EventOutcome::Ok(String::new())); let mut event_outcome_result = Ok(EventOutcome::Ok(String::new()));
let mut event_processed = false; let mut event_processed = false;
// Poll for events *after* drawing and checking pending actions
if crossterm_event::poll(std::time::Duration::from_millis(1))? { if crossterm_event::poll(std::time::Duration::from_millis(1))? {
let event = event_reader.read_event().context("Failed to read terminal event")?; let event = event_reader.read_event().context("Failed to read terminal event")?;
event_processed = true; // Mark that we received and will process an event event_processed = true;
event_outcome_result = event_handler.handle_event( event_outcome_result = event_handler.handle_event(
event, event,
&config, &config,
@@ -257,9 +300,6 @@ pub async fn run_ui() -> Result<()> {
if event_processed { if event_processed {
needs_redraw = true; needs_redraw = true;
} }
// Update position based on handler's modification
// This happens *after* the event is handled
app_state.current_position = current_position; app_state.current_position = current_position;
// --- Check for Login Results from Channel --- // --- Check for Login Results from Channel ---
@@ -272,7 +312,6 @@ pub async fn run_ui() -> Result<()> {
Err(mpsc::error::TryRecvError::Empty) => { /* No message waiting */ } Err(mpsc::error::TryRecvError::Empty) => { /* No message waiting */ }
Err(mpsc::error::TryRecvError::Disconnected) => { Err(mpsc::error::TryRecvError::Disconnected) => {
error!("Login result channel disconnected unexpectedly."); error!("Login result channel disconnected unexpectedly.");
// Optionally show an error dialog here
} }
} }
@@ -291,7 +330,7 @@ pub async fn run_ui() -> Result<()> {
// --- Check for Save Table Results --- // --- Check for Save Table Results ---
match save_table_result_receiver.try_recv() { match save_table_result_receiver.try_recv() {
Ok(result) => { Ok(result) => {
app_state.hide_dialog(); // Hide loading indicator app_state.hide_dialog();
match result { match result {
Ok(ref success_message) => { Ok(ref success_message) => {
app_state.show_dialog( app_state.show_dialog(
@@ -304,12 +343,11 @@ pub async fn run_ui() -> Result<()> {
} }
Err(e) => { Err(e) => {
event_handler.command_message = format!("Save failed: {}", e); event_handler.command_message = format!("Save failed: {}", e);
// Optionally show an error dialog instead of just command message
} }
} }
needs_redraw = true; needs_redraw = true;
} }
Err(mpsc::error::TryRecvError::Empty) => {} // No message Err(mpsc::error::TryRecvError::Empty) => {}
Err(mpsc::error::TryRecvError::Disconnected) => { Err(mpsc::error::TryRecvError::Disconnected) => {
error!("Save table result channel disconnected unexpectedly."); error!("Save table result channel disconnected unexpectedly.");
} }
@@ -319,19 +357,15 @@ pub async fn run_ui() -> Result<()> {
let mut should_exit = false; let mut should_exit = false;
match event_outcome_result { match event_outcome_result {
Ok(outcome) => match outcome { Ok(outcome) => match outcome {
EventOutcome::Ok(message) => { EventOutcome::Ok(_message) => {
if !message.is_empty() { // Message is often set directly in event_handler.command_message
// Update command message only if event handling produced one
// Avoid overwriting messages potentially set by pending actions
// event_handler.command_message = message;
}
} }
EventOutcome::Exit(message) => { EventOutcome::Exit(message) => {
event_handler.command_message = message; event_handler.command_message = message;
should_exit = true; should_exit = true;
} }
EventOutcome::DataSaved(save_outcome, message) => { EventOutcome::DataSaved(save_outcome, message) => {
event_handler.command_message = message; // Show save status event_handler.command_message = message;
if let Err(e) = UiService::handle_save_outcome( if let Err(e) = UiService::handle_save_outcome(
save_outcome, save_outcome,
&mut grpc_client, &mut grpc_client,
@@ -345,119 +379,87 @@ pub async fn run_ui() -> Result<()> {
} }
} }
EventOutcome::ButtonSelected { context: _, index: _ } => { EventOutcome::ButtonSelected { context: _, index: _ } => {
// This case should ideally be fully handled within handle_event // Handled within event_handler or specific navigation modules
// If initiate_login was called, it returned early.
// If not, the message was set and returned via Ok(message).
// Log if necessary, but likely no action needed here.
// log::warn!("ButtonSelected outcome reached main loop unexpectedly.");
} }
}, },
Err(e) => { Err(e) => {
event_handler.command_message = format!("Error: {}", e); event_handler.command_message = format!("Error: {}", e);
} }
} // --- End Consequence Handling --- }
// --- End Consequence Handling ---
// --- Position Change Handling (after outcome processing and pending actions) --- // --- Position Change Handling ---
let position_changed = app_state.current_position != position_before_event; let position_changed = app_state.current_position != position_before_event;
let current_total_count = app_state.total_count; let current_total_count = app_state.total_count; // Use current total_count
let mut position_logic_needs_redraw = false; let mut position_logic_needs_redraw = false;
if app_state.ui.show_form { if app_state.ui.show_form {
if position_changed && !event_handler.is_edit_mode { if position_changed && !event_handler.is_edit_mode {
let current_input = form_state.get_current_input(); let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() { let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
current_input.len() - 1 // Limit to last character in readonly mode form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
} else {
0
};
form_state.current_cursor_pos =
event_handler.ideal_cursor_column.min(max_cursor_pos);
position_logic_needs_redraw = true; position_logic_needs_redraw = true;
// Ensure position never exceeds total_count + 1
if app_state.current_position > current_total_count + 1 { if app_state.current_position > current_total_count + 1 {
app_state.current_position = current_total_count + 1; app_state.current_position = current_total_count + 1;
} }
if app_state.current_position > current_total_count { if app_state.current_position > current_total_count {
// New entry - reset form
form_state.reset_to_empty(); form_state.reset_to_empty();
form_state.current_field = 0; form_state.current_field = 0;
} else if app_state.current_position >= 1 } else if app_state.current_position >= 1 && app_state.current_position <= current_total_count {
&& app_state.current_position <= current_total_count let current_position_to_load = app_state.current_position;
{
// Existing entry - load data
let current_position_to_load = app_state.current_position; // Use a copy
let load_message = UiService::load_adresar_by_position( let load_message = UiService::load_adresar_by_position(
&mut grpc_client, &mut grpc_client,
&mut app_state, // Pass app_state mutably if needed by the service &mut app_state,
&mut form_state, &mut form_state,
current_position_to_load, current_position_to_load,
) )
.await.with_context(|| format!("Failed to load adresar by position: {}", current_position_to_load))?; .await.with_context(|| format!("Failed to load adresar by position: {}", current_position_to_load))?;
let current_input = form_state.get_current_input(); let current_input_after_load = form_state.get_current_input();
let max_cursor_pos = if !event_handler.is_edit_mode let max_cursor_pos_after_load = if !event_handler.is_edit_mode && !current_input_after_load.is_empty() {
&& !current_input.is_empty() current_input_after_load.len() - 1
{
current_input.len() - 1 // In readonly mode, limit to last character
} else { } else {
current_input.len() current_input_after_load.len()
}; };
form_state.current_cursor_pos = event_handler form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_after_load);
.ideal_cursor_column
.min(max_cursor_pos); if !load_message.starts_with("Loaded entry") || event_handler.command_message.is_empty() {
// Don't overwrite message from handle_event if load_message is simple success
if !load_message.starts_with("Loaded entry")
|| event_handler.command_message.is_empty()
{
event_handler.command_message = load_message; event_handler.command_message = load_message;
} }
} else { } else { // current_position is 0 or invalid
// Invalid position (e.g., 0) - reset to first entry or new entry mode app_state.current_position = 1.min(current_total_count + 1);
app_state.current_position = if app_state.current_position > current_total_count { // Handles empty db case
1.min(current_total_count + 1); // Go to 1 or new entry if empty
if app_state.current_position > total_count {
form_state.reset_to_empty(); form_state.reset_to_empty();
form_state.current_field = 0; form_state.current_field = 0;
} }
// If db is not empty, this will trigger load in next iteration if position changed to 1
} }
} else if !position_changed && !event_handler.is_edit_mode { } else if !position_changed && !event_handler.is_edit_mode {
// If position didn't change but we are in read-only, just adjust cursor
let current_input = form_state.get_current_input(); let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() { let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
current_input.len() - 1 form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
} else {
0
};
form_state.current_cursor_pos =
event_handler.ideal_cursor_column.min(max_cursor_pos);
} }
} else if app_state.ui.show_register { } else if app_state.ui.show_register {
if !event_handler.is_edit_mode { if !event_handler.is_edit_mode {
let current_input = register_state.get_current_input(); let current_input = register_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() { let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
current_input.len() - 1
} else {
0
};
register_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); register_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
} }
} else if app_state.ui.show_login { } else if app_state.ui.show_login {
if !event_handler.is_edit_mode { if !event_handler.is_edit_mode {
let current_input = login_state.get_current_input(); let current_input = login_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() { let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
current_input.len() - 1
} else {
0
};
login_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); login_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
} }
} }
if position_logic_needs_redraw { if position_logic_needs_redraw {
needs_redraw = true; needs_redraw = true;
} }
// --- End Position Change Handling --- // --- End Position Change Handling ---
// Check exit condition *after* all processing for the iteration
if should_exit { if should_exit {
return Ok(()); return Ok(());
} }
@@ -466,9 +468,8 @@ pub async fn run_ui() -> Result<()> {
let now = Instant::now(); let now = Instant::now();
let frame_duration = now.duration_since(last_frame_time); let frame_duration = now.duration_since(last_frame_time);
last_frame_time = now; last_frame_time = now;
if frame_duration.as_secs_f64() > 1e-6 { if frame_duration.as_secs_f64() > 1e-6 { // Avoid division by zero
current_fps = 1.0 / frame_duration.as_secs_f64(); current_fps = 1.0 / frame_duration.as_secs_f64();
} }
} // End main loop } // End main loop
} }