Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ac96a8486 | ||
|
|
b8e6cc22af | ||
|
|
634a01f618 | ||
|
|
6abea062ba | ||
|
|
f50887a326 | ||
|
|
3c0af05a3c | ||
|
|
c9131d4457 | ||
|
|
2af79a3ef2 |
@@ -17,6 +17,7 @@ toggle_buffer_list = ["ctrl+b"]
|
|||||||
next_field = ["Tab"]
|
next_field = ["Tab"]
|
||||||
prev_field = ["Shift+Tab"]
|
prev_field = ["Shift+Tab"]
|
||||||
exit_table_scroll = ["esc"]
|
exit_table_scroll = ["esc"]
|
||||||
|
open_search = ["ctrl+f"]
|
||||||
|
|
||||||
[keybindings.common]
|
[keybindings.common]
|
||||||
save = ["ctrl+s"]
|
save = ["ctrl+s"]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod text_editor;
|
|||||||
pub mod background;
|
pub mod background;
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
pub mod autocomplete;
|
pub mod autocomplete;
|
||||||
|
pub mod search_palette;
|
||||||
pub mod find_file_palette;
|
pub mod find_file_palette;
|
||||||
|
|
||||||
pub use command_line::*;
|
pub use command_line::*;
|
||||||
@@ -13,4 +14,5 @@ pub use text_editor::*;
|
|||||||
pub use background::*;
|
pub use background::*;
|
||||||
pub use dialog::*;
|
pub use dialog::*;
|
||||||
pub use autocomplete::*;
|
pub use autocomplete::*;
|
||||||
|
pub use search_palette::*;
|
||||||
pub use find_file_palette::*;
|
pub use find_file_palette::*;
|
||||||
|
|||||||
121
client/src/components/common/search_palette.rs
Normal file
121
client/src/components/common/search_palette.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// src/components/common/search_palette.rs
|
||||||
|
|
||||||
|
use crate::config::colors::themes::Theme;
|
||||||
|
use crate::state::app::search::SearchState;
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Modifier, Style},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Borders, Clear, List, ListItem, Paragraph},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Renders the search palette dialog over the main UI.
|
||||||
|
pub fn render_search_palette(
|
||||||
|
f: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
theme: &Theme,
|
||||||
|
state: &SearchState,
|
||||||
|
) {
|
||||||
|
// --- Dialog Area Calculation ---
|
||||||
|
let height = (area.height as f32 * 0.7).min(30.0) as u16;
|
||||||
|
let width = (area.width as f32 * 0.6).min(100.0) as u16;
|
||||||
|
let dialog_area = Rect {
|
||||||
|
x: area.x + (area.width - width) / 2,
|
||||||
|
y: area.y + (area.height - height) / 4,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
f.render_widget(Clear, dialog_area); // Clear background
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title(format!(" Search in '{}' ", state.table_name))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(theme.accent));
|
||||||
|
f.render_widget(block.clone(), dialog_area);
|
||||||
|
|
||||||
|
// --- Inner Layout (Input + Results) ---
|
||||||
|
let inner_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(1)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3), // For input box
|
||||||
|
Constraint::Min(0), // For results list
|
||||||
|
])
|
||||||
|
.split(dialog_area);
|
||||||
|
|
||||||
|
// --- Render Input Box ---
|
||||||
|
let input_block = Block::default()
|
||||||
|
.title("Query")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(theme.border));
|
||||||
|
let input_text = Paragraph::new(state.input.as_str())
|
||||||
|
.block(input_block)
|
||||||
|
.style(Style::default().fg(theme.fg));
|
||||||
|
f.render_widget(input_text, inner_chunks[0]);
|
||||||
|
// Set cursor position
|
||||||
|
f.set_cursor(
|
||||||
|
inner_chunks[0].x + state.cursor_position as u16 + 1,
|
||||||
|
inner_chunks[0].y + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Render Results List ---
|
||||||
|
if state.is_loading {
|
||||||
|
let loading_p = Paragraph::new("Searching...")
|
||||||
|
.style(Style::default().fg(theme.fg).add_modifier(Modifier::ITALIC));
|
||||||
|
f.render_widget(loading_p, inner_chunks[1]);
|
||||||
|
} else {
|
||||||
|
let list_items: Vec<ListItem> = state
|
||||||
|
.results
|
||||||
|
.iter()
|
||||||
|
.map(|hit| {
|
||||||
|
// Parse the JSON string to make it readable
|
||||||
|
let content_summary = match serde_json::from_str::<
|
||||||
|
serde_json::Value,
|
||||||
|
>(&hit.content_json)
|
||||||
|
{
|
||||||
|
Ok(json) => {
|
||||||
|
if let Some(obj) = json.as_object() {
|
||||||
|
// Create a summary from the first few non-null string values
|
||||||
|
obj.values()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.take(3)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" | ")
|
||||||
|
} else {
|
||||||
|
"Non-object JSON".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => "Invalid JSON content".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let line = Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
format!("{:<4.2} ", hit.score),
|
||||||
|
Style::default().fg(theme.accent),
|
||||||
|
),
|
||||||
|
Span::raw(content_summary),
|
||||||
|
]);
|
||||||
|
ListItem::new(line)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let results_list = List::new(list_items)
|
||||||
|
.block(Block::default().title("Results"))
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.bg(theme.highlight)
|
||||||
|
.fg(theme.bg)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
.highlight_symbol(">> ");
|
||||||
|
|
||||||
|
// We need a mutable ListState to render the selection
|
||||||
|
let mut list_state =
|
||||||
|
ratatui::widgets::ListState::default().with_selected(Some(state.selected_index));
|
||||||
|
|
||||||
|
f.render_stateful_widget(results_list, inner_chunks[1], &mut list_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
// src/components/common/status_line.rs
|
// client/src/components/common/status_line.rs
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::Style,
|
style::Style,
|
||||||
text::{Line, Span},
|
text::{Line, Span, Text},
|
||||||
widgets::Paragraph,
|
widgets::{Paragraph, Wrap}, // Make sure Wrap is imported
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -20,22 +20,39 @@ pub fn render_status_line(
|
|||||||
current_fps: f64,
|
current_fps: f64,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
) {
|
) {
|
||||||
// --- START FIX ---
|
|
||||||
// Ensure debug_text is always a &str, which implements UnicodeWidthStr.
|
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
let debug_text = app_state.debug_info.as_str();
|
{
|
||||||
#[cfg(not(feature = "ui-debug"))]
|
if let Some(debug_state) = &app_state.debug_state {
|
||||||
let debug_text = "";
|
let paragraph = if debug_state.is_error {
|
||||||
// --- END FIX ---
|
// --- THIS IS THE CRITICAL LOGIC FOR ERRORS ---
|
||||||
|
// 1. Create a `Text` object, which can contain multiple lines.
|
||||||
|
let error_text = Text::from(debug_state.displayed_message.clone());
|
||||||
|
|
||||||
let debug_width = UnicodeWidthStr::width(debug_text);
|
// 2. Create a Paragraph from the Text and TELL IT TO WRAP.
|
||||||
let debug_separator_width = if !debug_text.is_empty() { UnicodeWidthStr::width(" | ") } else { 0 };
|
Paragraph::new(error_text)
|
||||||
|
.wrap(Wrap { trim: true }) // This line makes the text break into new rows.
|
||||||
|
.style(Style::default().bg(theme.highlight).fg(theme.bg))
|
||||||
|
} else {
|
||||||
|
// --- This is for normal, single-line info messages ---
|
||||||
|
Paragraph::new(debug_state.displayed_message.as_str())
|
||||||
|
.style(Style::default().fg(theme.accent).bg(theme.bg))
|
||||||
|
};
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
} else {
|
||||||
|
// Fallback for when debug state is None
|
||||||
|
let paragraph = Paragraph::new("").style(Style::default().bg(theme.bg));
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
return; // Stop here and don't render the normal status line.
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- The normal status line rendering logic (unchanged) ---
|
||||||
let program_info = format!("multieko2 v{}", env!("CARGO_PKG_VERSION"));
|
let program_info = format!("multieko2 v{}", env!("CARGO_PKG_VERSION"));
|
||||||
let mode_text = if is_edit_mode { "[EDIT]" } else { "[READ-ONLY]" };
|
let mode_text = if is_edit_mode { "[EDIT]" } else { "[READ-ONLY]" };
|
||||||
|
|
||||||
let home_dir =
|
let home_dir = dirs::home_dir()
|
||||||
dirs::home_dir().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
|
.map(|p| p.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
let display_dir = if current_dir.starts_with(&home_dir) {
|
let display_dir = if current_dir.starts_with(&home_dir) {
|
||||||
current_dir.replacen(&home_dir, "~", 1)
|
current_dir.replacen(&home_dir, "~", 1)
|
||||||
} else {
|
} else {
|
||||||
@@ -50,19 +67,30 @@ pub fn render_status_line(
|
|||||||
let separator = " | ";
|
let separator = " | ";
|
||||||
let separator_width = UnicodeWidthStr::width(separator);
|
let separator_width = UnicodeWidthStr::width(separator);
|
||||||
|
|
||||||
let fixed_width_with_fps = mode_width + separator_width + separator_width +
|
let fixed_width_with_fps = mode_width
|
||||||
program_info_width + separator_width + fps_width +
|
+ separator_width
|
||||||
debug_separator_width + debug_width;
|
+ separator_width
|
||||||
|
+ program_info_width
|
||||||
|
+ separator_width
|
||||||
|
+ fps_width;
|
||||||
|
|
||||||
let show_fps = fixed_width_with_fps <= available_width;
|
let show_fps = fixed_width_with_fps <= available_width;
|
||||||
|
|
||||||
let remaining_width_for_dir = available_width.saturating_sub(
|
let remaining_width_for_dir = available_width.saturating_sub(
|
||||||
mode_width + separator_width +
|
mode_width
|
||||||
separator_width + program_info_width +
|
+ separator_width
|
||||||
(if show_fps { separator_width + fps_width } else { 0 }) +
|
+ separator_width
|
||||||
debug_separator_width + debug_width,
|
+ program_info_width
|
||||||
|
+ (if show_fps {
|
||||||
|
separator_width + fps_width
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let dir_display_text_str = if UnicodeWidthStr::width(display_dir.as_str()) <= remaining_width_for_dir {
|
let dir_display_text_str = if UnicodeWidthStr::width(display_dir.as_str())
|
||||||
|
<= remaining_width_for_dir
|
||||||
|
{
|
||||||
display_dir
|
display_dir
|
||||||
} else {
|
} else {
|
||||||
let dir_name = Path::new(current_dir)
|
let dir_name = Path::new(current_dir)
|
||||||
@@ -72,14 +100,18 @@ pub fn render_status_line(
|
|||||||
if UnicodeWidthStr::width(dir_name) <= remaining_width_for_dir {
|
if UnicodeWidthStr::width(dir_name) <= remaining_width_for_dir {
|
||||||
dir_name.to_string()
|
dir_name.to_string()
|
||||||
} else {
|
} else {
|
||||||
dir_name.chars().take(remaining_width_for_dir).collect::<String>()
|
dir_name
|
||||||
|
.chars()
|
||||||
|
.take(remaining_width_for_dir)
|
||||||
|
.collect::<String>()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut current_content_width = mode_width + separator_width +
|
let mut current_content_width = mode_width
|
||||||
UnicodeWidthStr::width(dir_display_text_str.as_str()) +
|
+ separator_width
|
||||||
separator_width + program_info_width +
|
+ UnicodeWidthStr::width(dir_display_text_str.as_str())
|
||||||
debug_separator_width + debug_width;
|
+ separator_width
|
||||||
|
+ program_info_width;
|
||||||
if show_fps {
|
if show_fps {
|
||||||
current_content_width += separator_width + fps_width;
|
current_content_width += separator_width + fps_width;
|
||||||
}
|
}
|
||||||
@@ -87,20 +119,24 @@ pub fn render_status_line(
|
|||||||
let mut line_spans = vec![
|
let mut line_spans = vec![
|
||||||
Span::styled(mode_text, Style::default().fg(theme.accent)),
|
Span::styled(mode_text, Style::default().fg(theme.accent)),
|
||||||
Span::styled(separator, Style::default().fg(theme.border)),
|
Span::styled(separator, Style::default().fg(theme.border)),
|
||||||
Span::styled(dir_display_text_str.as_str(), Style::default().fg(theme.fg)),
|
Span::styled(
|
||||||
|
dir_display_text_str.as_str(),
|
||||||
|
Style::default().fg(theme.fg),
|
||||||
|
),
|
||||||
Span::styled(separator, Style::default().fg(theme.border)),
|
Span::styled(separator, Style::default().fg(theme.border)),
|
||||||
Span::styled(program_info.as_str(), Style::default().fg(theme.secondary)),
|
Span::styled(
|
||||||
|
program_info.as_str(),
|
||||||
|
Style::default().fg(theme.secondary),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if show_fps {
|
if show_fps {
|
||||||
line_spans.push(Span::styled(separator, Style::default().fg(theme.border)));
|
line_spans
|
||||||
line_spans.push(Span::styled(fps_text.as_str(), Style::default().fg(theme.secondary)));
|
.push(Span::styled(separator, Style::default().fg(theme.border)));
|
||||||
}
|
line_spans.push(Span::styled(
|
||||||
|
fps_text.as_str(),
|
||||||
#[cfg(feature = "ui-debug")]
|
Style::default().fg(theme.secondary),
|
||||||
{
|
));
|
||||||
line_spans.push(Span::styled(separator, Style::default().fg(theme.border)));
|
|
||||||
line_spans.push(Span::styled(debug_text, Style::default().fg(theme.accent)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let padding_needed = available_width.saturating_sub(current_content_width);
|
let padding_needed = available_width.saturating_sub(current_content_width);
|
||||||
@@ -111,8 +147,8 @@ pub fn render_status_line(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let paragraph = Paragraph::new(Line::from(line_spans))
|
let paragraph =
|
||||||
.style(Style::default().bg(theme.bg));
|
Paragraph::new(Line::from(line_spans)).style(Style::default().bg(theme.bg));
|
||||||
|
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// client/src/main.rs
|
// client/src/main.rs
|
||||||
use client::run_ui;
|
use client::run_ui;
|
||||||
|
#[cfg(feature = "ui-debug")]
|
||||||
|
use client::utils::debug_logger::UiDebugWriter;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tracing_subscriber;
|
use tracing_subscriber;
|
||||||
@@ -7,9 +9,23 @@ use std::env;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
#[cfg(feature = "ui-debug")]
|
||||||
|
{
|
||||||
|
// If ui-debug is on, set up our custom writer.
|
||||||
|
let writer = UiDebugWriter::new();
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_level(false) // Don't show INFO, ERROR, etc.
|
||||||
|
.with_target(false) // Don't show the module path.
|
||||||
|
.without_time() // This is the correct and simpler method.
|
||||||
|
.with_writer(move || writer.clone())
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "ui-debug"))]
|
||||||
|
{
|
||||||
if env::var("ENABLE_TRACING").is_ok() {
|
if env::var("ENABLE_TRACING").is_ok() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
run_ui().await
|
run_ui().await
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use crate::state::{
|
|||||||
app::{
|
app::{
|
||||||
buffer::{AppView, BufferState},
|
buffer::{AppView, BufferState},
|
||||||
highlight::HighlightState,
|
highlight::HighlightState,
|
||||||
|
search::SearchState, // Correctly imported
|
||||||
state::AppState,
|
state::AppState,
|
||||||
},
|
},
|
||||||
pages::{
|
pages::{
|
||||||
@@ -41,10 +42,12 @@ use crate::tui::{
|
|||||||
use crate::ui::handlers::context::UiContext;
|
use crate::ui::handlers::context::UiContext;
|
||||||
use crate::ui::handlers::rat_state::UiStateHandler;
|
use crate::ui::handlers::rat_state::UiStateHandler;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use common::proto::multieko2::search::search_response::Hit;
|
||||||
use crossterm::cursor::SetCursorStyle;
|
use crossterm::cursor::SetCursorStyle;
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::{Event, KeyCode, KeyEvent};
|
||||||
use crossterm::event::{Event, KeyEvent};
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
|
use tracing::{info, error};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum EventOutcome {
|
pub enum EventOutcome {
|
||||||
@@ -74,11 +77,14 @@ pub struct EventHandler {
|
|||||||
pub ideal_cursor_column: usize,
|
pub ideal_cursor_column: usize,
|
||||||
pub key_sequence_tracker: KeySequenceTracker,
|
pub key_sequence_tracker: KeySequenceTracker,
|
||||||
pub auth_client: AuthClient,
|
pub auth_client: AuthClient,
|
||||||
|
pub grpc_client: GrpcClient,
|
||||||
pub login_result_sender: mpsc::Sender<LoginResult>,
|
pub login_result_sender: mpsc::Sender<LoginResult>,
|
||||||
pub register_result_sender: mpsc::Sender<RegisterResult>,
|
pub register_result_sender: mpsc::Sender<RegisterResult>,
|
||||||
pub save_table_result_sender: SaveTableResultSender,
|
pub save_table_result_sender: SaveTableResultSender,
|
||||||
pub save_logic_result_sender: SaveLogicResultSender,
|
pub save_logic_result_sender: SaveLogicResultSender,
|
||||||
pub navigation_state: NavigationState,
|
pub navigation_state: NavigationState,
|
||||||
|
pub search_result_sender: mpsc::UnboundedSender<Vec<Hit>>,
|
||||||
|
pub search_result_receiver: mpsc::UnboundedReceiver<Vec<Hit>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventHandler {
|
impl EventHandler {
|
||||||
@@ -87,7 +93,9 @@ impl EventHandler {
|
|||||||
register_result_sender: mpsc::Sender<RegisterResult>,
|
register_result_sender: mpsc::Sender<RegisterResult>,
|
||||||
save_table_result_sender: SaveTableResultSender,
|
save_table_result_sender: SaveTableResultSender,
|
||||||
save_logic_result_sender: SaveLogicResultSender,
|
save_logic_result_sender: SaveLogicResultSender,
|
||||||
|
grpc_client: GrpcClient,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
let (search_tx, search_rx) = unbounded_channel();
|
||||||
Ok(EventHandler {
|
Ok(EventHandler {
|
||||||
command_mode: false,
|
command_mode: false,
|
||||||
command_input: String::new(),
|
command_input: String::new(),
|
||||||
@@ -98,11 +106,14 @@ impl EventHandler {
|
|||||||
ideal_cursor_column: 0,
|
ideal_cursor_column: 0,
|
||||||
key_sequence_tracker: KeySequenceTracker::new(400),
|
key_sequence_tracker: KeySequenceTracker::new(400),
|
||||||
auth_client: AuthClient::new().await?,
|
auth_client: AuthClient::new().await?,
|
||||||
|
grpc_client,
|
||||||
login_result_sender,
|
login_result_sender,
|
||||||
register_result_sender,
|
register_result_sender,
|
||||||
save_table_result_sender,
|
save_table_result_sender,
|
||||||
save_logic_result_sender,
|
save_logic_result_sender,
|
||||||
navigation_state: NavigationState::new(),
|
navigation_state: NavigationState::new(),
|
||||||
|
search_result_sender: search_tx,
|
||||||
|
search_result_receiver: search_rx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,13 +125,105 @@ impl EventHandler {
|
|||||||
self.navigation_state.activate_find_file(options);
|
self.navigation_state.activate_find_file(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function handles state changes.
|
||||||
|
async fn handle_search_palette_event(
|
||||||
|
&mut self,
|
||||||
|
key_event: KeyEvent,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
) -> Result<EventOutcome> {
|
||||||
|
let mut should_close = false;
|
||||||
|
let mut outcome_message = String::new();
|
||||||
|
let mut trigger_search = false;
|
||||||
|
|
||||||
|
if let Some(search_state) = app_state.search_state.as_mut() {
|
||||||
|
match key_event.code {
|
||||||
|
KeyCode::Esc => {
|
||||||
|
should_close = true;
|
||||||
|
outcome_message = "Search cancelled".to_string();
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if let Some(selected_hit) = search_state.results.get(search_state.selected_index) {
|
||||||
|
if let Ok(data) = serde_json::from_str::<std::collections::HashMap<String, String>>(&selected_hit.content_json) {
|
||||||
|
let detached_pos = form_state.total_count + 2;
|
||||||
|
form_state.update_from_response(&data, detached_pos);
|
||||||
|
}
|
||||||
|
should_close = true;
|
||||||
|
outcome_message = format!("Loaded record ID {}", selected_hit.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Up => search_state.previous_result(),
|
||||||
|
KeyCode::Down => search_state.next_result(),
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
search_state.input.insert(search_state.cursor_position, c);
|
||||||
|
search_state.cursor_position += 1;
|
||||||
|
trigger_search = true;
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
if search_state.cursor_position > 0 {
|
||||||
|
search_state.cursor_position -= 1;
|
||||||
|
search_state.input.remove(search_state.cursor_position);
|
||||||
|
trigger_search = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
search_state.cursor_position = search_state.cursor_position.saturating_sub(1);
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
if search_state.cursor_position < search_state.input.len() {
|
||||||
|
search_state.cursor_position += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- START CORRECTED LOGIC ---
|
||||||
|
if trigger_search {
|
||||||
|
search_state.is_loading = true;
|
||||||
|
search_state.results.clear();
|
||||||
|
search_state.selected_index = 0;
|
||||||
|
|
||||||
|
let query = search_state.input.clone();
|
||||||
|
let table_name = search_state.table_name.clone();
|
||||||
|
let sender = self.search_result_sender.clone();
|
||||||
|
let mut grpc_client = self.grpc_client.clone();
|
||||||
|
|
||||||
|
info!("--- 1. Spawning search task for query: '{}' ---", query);
|
||||||
|
// We now move the grpc_client into the task, just like with login.
|
||||||
|
tokio::spawn(async move {
|
||||||
|
info!("--- 2. Background task started. ---");
|
||||||
|
match grpc_client.search_table(table_name, query).await {
|
||||||
|
Ok(response) => {
|
||||||
|
info!("--- 3a. gRPC call successful. Found {} hits. ---", response.hits.len());
|
||||||
|
let _ = sender.send(response.hits);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// THE FIX: Use the debug formatter `{:?}` to print the full error chain.
|
||||||
|
error!("--- 3b. gRPC call failed: {:?} ---", e);
|
||||||
|
let _ = sender.send(vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The borrow on `app_state.search_state` ends here.
|
||||||
|
// Now we can safely modify the Option itself.
|
||||||
|
if should_close {
|
||||||
|
app_state.search_state = None;
|
||||||
|
app_state.ui.show_search_palette = false;
|
||||||
|
app_state.ui.focus_outside_canvas = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EventOutcome::Ok(outcome_message))
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn handle_event(
|
pub async fn handle_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: Event,
|
event: Event,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
terminal: &mut TerminalCore,
|
terminal: &mut TerminalCore,
|
||||||
grpc_client: &mut GrpcClient,
|
|
||||||
command_handler: &mut CommandHandler,
|
command_handler: &mut CommandHandler,
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState,
|
||||||
auth_state: &mut AuthState,
|
auth_state: &mut AuthState,
|
||||||
@@ -131,6 +234,14 @@ impl EventHandler {
|
|||||||
buffer_state: &mut BufferState,
|
buffer_state: &mut BufferState,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
|
if app_state.ui.show_search_palette {
|
||||||
|
if let Event::Key(key_event) = event {
|
||||||
|
// The call no longer passes grpc_client
|
||||||
|
return self.handle_search_palette_event(key_event, form_state, app_state).await;
|
||||||
|
}
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
let mut current_mode = ModeManager::derive_mode(app_state, self, admin_state);
|
let mut current_mode = ModeManager::derive_mode(app_state, self, admin_state);
|
||||||
|
|
||||||
if current_mode == AppMode::General && self.navigation_state.active {
|
if current_mode == AppMode::General && self.navigation_state.active {
|
||||||
@@ -212,6 +323,19 @@ impl EventHandler {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(action) = config.get_general_action(key_code, modifiers) {
|
||||||
|
if action == "open_search" {
|
||||||
|
if app_state.ui.show_form {
|
||||||
|
if let Some(table_name) = app_state.current_view_table_name.clone() {
|
||||||
|
app_state.ui.show_search_palette = true;
|
||||||
|
app_state.search_state = Some(SearchState::new(table_name));
|
||||||
|
app_state.ui.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok("Search palette opened".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match current_mode {
|
match current_mode {
|
||||||
@@ -223,7 +347,7 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if app_state.ui.show_add_logic {
|
if app_state.ui.show_add_logic {
|
||||||
let client_clone = grpc_client.clone();
|
let client_clone = self.grpc_client.clone();
|
||||||
let sender_clone = self.save_logic_result_sender.clone();
|
let sender_clone = self.save_logic_result_sender.clone();
|
||||||
if add_logic_nav::handle_add_logic_navigation(
|
if add_logic_nav::handle_add_logic_navigation(
|
||||||
key_event, config, app_state, &mut admin_state.add_logic_state,
|
key_event, config, app_state, &mut admin_state.add_logic_state,
|
||||||
@@ -234,7 +358,7 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if app_state.ui.show_add_table {
|
if app_state.ui.show_add_table {
|
||||||
let client_clone = grpc_client.clone();
|
let client_clone = self.grpc_client.clone();
|
||||||
let sender_clone = self.save_table_result_sender.clone();
|
let sender_clone = self.save_table_result_sender.clone();
|
||||||
if add_table_nav::handle_add_table_navigation(
|
if add_table_nav::handle_add_table_navigation(
|
||||||
key_event, config, app_state, &mut admin_state.add_table_state,
|
key_event, config, app_state, &mut admin_state.add_table_state,
|
||||||
@@ -331,7 +455,7 @@ impl EventHandler {
|
|||||||
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
return common_mode::handle_core_action(
|
return common_mode::handle_core_action(
|
||||||
action, form_state, auth_state, login_state, register_state,
|
action, form_state, auth_state, login_state, register_state,
|
||||||
grpc_client, &mut self.auth_client, terminal, app_state,
|
&mut self.grpc_client, &mut self.auth_client, terminal, app_state,
|
||||||
).await;
|
).await;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -348,8 +472,7 @@ impl EventHandler {
|
|||||||
&mut admin_state.add_table_state,
|
&mut admin_state.add_table_state,
|
||||||
&mut admin_state.add_logic_state,
|
&mut admin_state.add_logic_state,
|
||||||
&mut self.key_sequence_tracker,
|
&mut self.key_sequence_tracker,
|
||||||
// No more current_position or total_count arguments
|
&mut self.grpc_client, // <-- FIX 1
|
||||||
grpc_client,
|
|
||||||
&mut self.command_message,
|
&mut self.command_message,
|
||||||
&mut self.edit_mode_cooldown,
|
&mut self.edit_mode_cooldown,
|
||||||
&mut self.ideal_cursor_column,
|
&mut self.ideal_cursor_column,
|
||||||
@@ -383,7 +506,7 @@ impl EventHandler {
|
|||||||
&mut admin_state.add_table_state,
|
&mut admin_state.add_table_state,
|
||||||
&mut admin_state.add_logic_state,
|
&mut admin_state.add_logic_state,
|
||||||
&mut self.key_sequence_tracker,
|
&mut self.key_sequence_tracker,
|
||||||
grpc_client,
|
&mut self.grpc_client, // <-- FIX 2
|
||||||
&mut self.command_message,
|
&mut self.command_message,
|
||||||
&mut self.edit_mode_cooldown,
|
&mut self.edit_mode_cooldown,
|
||||||
&mut self.ideal_cursor_column,
|
&mut self.ideal_cursor_column,
|
||||||
@@ -398,7 +521,7 @@ impl EventHandler {
|
|||||||
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
return common_mode::handle_core_action(
|
return common_mode::handle_core_action(
|
||||||
action, form_state, auth_state, login_state, register_state,
|
action, form_state, auth_state, login_state, register_state,
|
||||||
grpc_client, &mut self.auth_client, terminal, app_state,
|
&mut self.grpc_client, &mut self.auth_client, terminal, app_state, // <-- FIX 3
|
||||||
).await;
|
).await;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -410,7 +533,7 @@ impl EventHandler {
|
|||||||
let edit_result = edit::handle_edit_event(
|
let edit_result = edit::handle_edit_event(
|
||||||
key_event, config, form_state, login_state, register_state, admin_state,
|
key_event, config, form_state, login_state, register_state, admin_state,
|
||||||
&mut self.ideal_cursor_column, &mut current_position, total_count,
|
&mut self.ideal_cursor_column, &mut current_position, total_count,
|
||||||
grpc_client, app_state,
|
&mut self.grpc_client, app_state, // <-- FIX 4
|
||||||
).await;
|
).await;
|
||||||
|
|
||||||
match edit_result {
|
match edit_result {
|
||||||
@@ -453,7 +576,7 @@ impl EventHandler {
|
|||||||
let total_count = form_state.total_count;
|
let total_count = form_state.total_count;
|
||||||
let outcome = command_mode::handle_command_event(
|
let outcome = command_mode::handle_command_event(
|
||||||
key_event, config, app_state, login_state, register_state, form_state,
|
key_event, config, app_state, login_state, register_state, form_state,
|
||||||
&mut self.command_input, &mut self.command_message, grpc_client,
|
&mut self.command_input, &mut self.command_message, &mut self.grpc_client, // <-- FIX 5
|
||||||
command_handler, terminal, &mut current_position, total_count,
|
command_handler, terminal, &mut current_position, total_count,
|
||||||
).await?;
|
).await?;
|
||||||
form_state.current_position = current_position;
|
form_state.current_position = current_position;
|
||||||
@@ -477,7 +600,6 @@ impl EventHandler {
|
|||||||
|
|
||||||
if config.matches_key_sequence_generalized(&sequence) == Some("find_file_palette_toggle") {
|
if config.matches_key_sequence_generalized(&sequence) == Some("find_file_palette_toggle") {
|
||||||
if app_state.ui.show_form || app_state.ui.show_intro {
|
if app_state.ui.show_form || app_state.ui.show_intro {
|
||||||
// --- START FIX ---
|
|
||||||
let mut all_table_paths: Vec<String> = app_state
|
let mut all_table_paths: Vec<String> = app_state
|
||||||
.profile_tree
|
.profile_tree
|
||||||
.profiles
|
.profiles
|
||||||
@@ -491,7 +613,6 @@ impl EventHandler {
|
|||||||
all_table_paths.sort();
|
all_table_paths.sort();
|
||||||
|
|
||||||
self.navigation_state.activate_find_file(all_table_paths);
|
self.navigation_state.activate_find_file(all_table_paths);
|
||||||
// --- END FIX ---
|
|
||||||
|
|
||||||
self.command_mode = false;
|
self.command_mode = false;
|
||||||
self.command_input.clear();
|
self.command_input.clear();
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ use common::proto::multieko2::tables_data::{
|
|||||||
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
|
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
|
||||||
PutTableDataResponse,
|
PutTableDataResponse,
|
||||||
};
|
};
|
||||||
|
use common::proto::multieko2::search::{
|
||||||
|
searcher_client::SearcherClient, SearchRequest, SearchResponse,
|
||||||
|
};
|
||||||
use anyhow::{Context, Result}; // Added Context
|
use anyhow::{Context, Result}; // Added Context
|
||||||
use std::collections::HashMap; // NEW
|
use std::collections::HashMap; // NEW
|
||||||
|
|
||||||
@@ -28,36 +31,32 @@ pub struct GrpcClient {
|
|||||||
table_structure_client: TableStructureServiceClient<Channel>,
|
table_structure_client: TableStructureServiceClient<Channel>,
|
||||||
table_definition_client: TableDefinitionClient<Channel>,
|
table_definition_client: TableDefinitionClient<Channel>,
|
||||||
table_script_client: TableScriptClient<Channel>,
|
table_script_client: TableScriptClient<Channel>,
|
||||||
tables_data_client: TablesDataClient<Channel>, // NEW
|
tables_data_client: TablesDataClient<Channel>,
|
||||||
|
search_client: SearcherClient<Channel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GrpcClient {
|
impl GrpcClient {
|
||||||
pub async fn new() -> Result<Self> {
|
pub async fn new() -> Result<Self> {
|
||||||
let table_structure_client = TableStructureServiceClient::connect(
|
let channel = Channel::from_static("http://[::1]:50051")
|
||||||
"http://[::1]:50051",
|
.connect()
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.context("Failed to connect to TableStructureService")?;
|
.context("Failed to create gRPC channel")?;
|
||||||
let table_definition_client = TableDefinitionClient::connect(
|
|
||||||
"http://[::1]:50051",
|
let table_structure_client =
|
||||||
)
|
TableStructureServiceClient::new(channel.clone());
|
||||||
.await
|
let table_definition_client =
|
||||||
.context("Failed to connect to TableDefinitionService")?;
|
TableDefinitionClient::new(channel.clone());
|
||||||
let table_script_client =
|
let table_script_client = TableScriptClient::new(channel.clone());
|
||||||
TableScriptClient::connect("http://[::1]:50051")
|
let tables_data_client = TablesDataClient::new(channel.clone());
|
||||||
.await
|
// NEW: Instantiate the search client
|
||||||
.context("Failed to connect to TableScriptService")?;
|
let search_client = SearcherClient::new(channel.clone());
|
||||||
let tables_data_client =
|
|
||||||
TablesDataClient::connect("http://[::1]:50051")
|
|
||||||
.await
|
|
||||||
.context("Failed to connect to TablesDataService")?; // NEW
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
// adresar_client, // REMOVE
|
|
||||||
table_structure_client,
|
table_structure_client,
|
||||||
table_definition_client,
|
table_definition_client,
|
||||||
table_script_client,
|
table_script_client,
|
||||||
tables_data_client, // NEW
|
tables_data_client,
|
||||||
|
search_client, // NEW
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,4 +196,17 @@ impl GrpcClient {
|
|||||||
.context("gRPC PutTableData call failed")?;
|
.context("gRPC PutTableData call failed")?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn search_table(
|
||||||
|
&mut self,
|
||||||
|
table_name: String,
|
||||||
|
query: String,
|
||||||
|
) -> Result<SearchResponse> {
|
||||||
|
let request = tonic::Request::new(SearchRequest { table_name, query });
|
||||||
|
let response = self
|
||||||
|
.search_client
|
||||||
|
.search_table(request)
|
||||||
|
.await?;
|
||||||
|
Ok(response.into_inner())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
|
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod buffer;
|
pub mod buffer;
|
||||||
|
pub mod search;
|
||||||
pub mod highlight;
|
pub mod highlight;
|
||||||
|
|||||||
56
client/src/state/app/search.rs
Normal file
56
client/src/state/app/search.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// src/state/app/search.rs
|
||||||
|
|
||||||
|
use common::proto::multieko2::search::search_response::Hit;
|
||||||
|
|
||||||
|
/// Holds the complete state for the search palette.
|
||||||
|
pub struct SearchState {
|
||||||
|
/// The name of the table being searched.
|
||||||
|
pub table_name: String,
|
||||||
|
/// The current text entered by the user.
|
||||||
|
pub input: String,
|
||||||
|
/// The position of the cursor within the input text.
|
||||||
|
pub cursor_position: usize,
|
||||||
|
/// The search results returned from the server.
|
||||||
|
pub results: Vec<Hit>,
|
||||||
|
/// The index of the currently selected search result.
|
||||||
|
pub selected_index: usize,
|
||||||
|
/// A flag to indicate if a search is currently in progress.
|
||||||
|
pub is_loading: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchState {
|
||||||
|
/// Creates a new SearchState for a given table.
|
||||||
|
pub fn new(table_name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
table_name,
|
||||||
|
input: String::new(),
|
||||||
|
cursor_position: 0,
|
||||||
|
results: Vec::new(),
|
||||||
|
selected_index: 0,
|
||||||
|
is_loading: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the selection to the next item, wrapping around if at the end.
|
||||||
|
pub fn next_result(&mut self) {
|
||||||
|
if !self.results.is_empty() {
|
||||||
|
let next = self.selected_index + 1;
|
||||||
|
self.selected_index = if next >= self.results.len() {
|
||||||
|
0 // Wrap to the start
|
||||||
|
} else {
|
||||||
|
next
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the selection to the previous item, wrapping around if at the beginning.
|
||||||
|
pub fn previous_result(&mut self) {
|
||||||
|
if !self.results.is_empty() {
|
||||||
|
self.selected_index = if self.selected_index == 0 {
|
||||||
|
self.results.len() - 1 // Wrap to the end
|
||||||
|
} else {
|
||||||
|
self.selected_index - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
// src/state/state.rs
|
// src/state/app/state.rs
|
||||||
|
|
||||||
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;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
|
use crate::state::app::search::SearchState; // ADDED
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
#[cfg(feature = "ui-debug")]
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
// --- YOUR EXISTING DIALOGSTATE IS UNTOUCHED ---
|
||||||
pub struct DialogState {
|
pub struct DialogState {
|
||||||
pub dialog_show: bool,
|
pub dialog_show: bool,
|
||||||
pub dialog_title: String,
|
pub dialog_title: String,
|
||||||
@@ -26,10 +30,19 @@ pub struct UiState {
|
|||||||
pub show_form: bool,
|
pub show_form: bool,
|
||||||
pub show_login: bool,
|
pub show_login: bool,
|
||||||
pub show_register: bool,
|
pub show_register: bool,
|
||||||
|
pub show_search_palette: bool, // ADDED
|
||||||
pub focus_outside_canvas: bool,
|
pub focus_outside_canvas: bool,
|
||||||
pub dialog: DialogState,
|
pub dialog: DialogState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui-debug")]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DebugState {
|
||||||
|
pub displayed_message: String,
|
||||||
|
pub is_error: bool,
|
||||||
|
pub display_start_time: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
// Core editor state
|
// Core editor state
|
||||||
pub current_dir: String,
|
pub current_dir: String,
|
||||||
@@ -42,11 +55,14 @@ pub struct AppState {
|
|||||||
pub focused_button_index: usize,
|
pub focused_button_index: usize,
|
||||||
pub pending_table_structure_fetch: Option<(String, String)>,
|
pub pending_table_structure_fetch: Option<(String, String)>,
|
||||||
|
|
||||||
|
// ADDED: State for the search palette
|
||||||
|
pub search_state: Option<SearchState>,
|
||||||
|
|
||||||
// UI preferences
|
// UI preferences
|
||||||
pub ui: UiState,
|
pub ui: UiState,
|
||||||
|
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
pub debug_info: String,
|
pub debug_state: Option<DebugState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@@ -63,13 +79,16 @@ impl AppState {
|
|||||||
current_mode: AppMode::General,
|
current_mode: AppMode::General,
|
||||||
focused_button_index: 0,
|
focused_button_index: 0,
|
||||||
pending_table_structure_fetch: None,
|
pending_table_structure_fetch: None,
|
||||||
|
search_state: None, // ADDED
|
||||||
ui: UiState::default(),
|
ui: UiState::default(),
|
||||||
|
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
debug_info: String::new(),
|
debug_state: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- ALL YOUR EXISTING METHODS ARE UNTOUCHED ---
|
||||||
|
|
||||||
pub fn update_mode(&mut self, mode: AppMode) {
|
pub fn update_mode(&mut self, mode: AppMode) {
|
||||||
self.current_mode = mode;
|
self.current_mode = mode;
|
||||||
}
|
}
|
||||||
@@ -79,9 +98,6 @@ impl AppState {
|
|||||||
self.current_view_table_name = Some(table_name);
|
self.current_view_table_name = Some(table_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add dialog helper methods
|
|
||||||
/// Shows a dialog with the given title, message, and buttons.
|
|
||||||
/// The first button (index 0) is active by default.
|
|
||||||
pub fn show_dialog(
|
pub fn show_dialog(
|
||||||
&mut self,
|
&mut self,
|
||||||
title: &str,
|
title: &str,
|
||||||
@@ -99,19 +115,17 @@ impl AppState {
|
|||||||
self.ui.focus_outside_canvas = true;
|
self.ui.focus_outside_canvas = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a dialog specifically for loading states.
|
|
||||||
pub fn show_loading_dialog(&mut self, title: &str, message: &str) {
|
pub fn show_loading_dialog(&mut self, title: &str, message: &str) {
|
||||||
self.ui.dialog.dialog_title = title.to_string();
|
self.ui.dialog.dialog_title = title.to_string();
|
||||||
self.ui.dialog.dialog_message = message.to_string();
|
self.ui.dialog.dialog_message = message.to_string();
|
||||||
self.ui.dialog.dialog_buttons.clear(); // No buttons during loading
|
self.ui.dialog.dialog_buttons.clear();
|
||||||
self.ui.dialog.dialog_active_button_index = 0;
|
self.ui.dialog.dialog_active_button_index = 0;
|
||||||
self.ui.dialog.purpose = None; // Purpose is set when loading finishes
|
self.ui.dialog.purpose = None;
|
||||||
self.ui.dialog.is_loading = true;
|
self.ui.dialog.is_loading = true;
|
||||||
self.ui.dialog.dialog_show = true;
|
self.ui.dialog.dialog_show = true;
|
||||||
self.ui.focus_outside_canvas = true; // Keep focus management consistent
|
self.ui.focus_outside_canvas = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the content of an existing dialog, typically after loading.
|
|
||||||
pub fn update_dialog_content(
|
pub fn update_dialog_content(
|
||||||
&mut self,
|
&mut self,
|
||||||
message: &str,
|
message: &str,
|
||||||
@@ -121,16 +135,12 @@ impl AppState {
|
|||||||
if self.ui.dialog.dialog_show {
|
if self.ui.dialog.dialog_show {
|
||||||
self.ui.dialog.dialog_message = message.to_string();
|
self.ui.dialog.dialog_message = message.to_string();
|
||||||
self.ui.dialog.dialog_buttons = buttons;
|
self.ui.dialog.dialog_buttons = buttons;
|
||||||
self.ui.dialog.dialog_active_button_index = 0; // Reset focus
|
self.ui.dialog.dialog_active_button_index = 0;
|
||||||
self.ui.dialog.purpose = Some(purpose);
|
self.ui.dialog.purpose = Some(purpose);
|
||||||
self.ui.dialog.is_loading = false; // Loading finished
|
self.ui.dialog.is_loading = false;
|
||||||
// Keep dialog_show = true
|
|
||||||
// Keep focus_outside_canvas = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Hides the dialog and clears its content.
|
|
||||||
pub fn hide_dialog(&mut self) {
|
pub fn hide_dialog(&mut self) {
|
||||||
self.ui.dialog.dialog_show = false;
|
self.ui.dialog.dialog_show = false;
|
||||||
self.ui.dialog.dialog_title.clear();
|
self.ui.dialog.dialog_title.clear();
|
||||||
@@ -142,30 +152,27 @@ impl AppState {
|
|||||||
self.ui.dialog.is_loading = false;
|
self.ui.dialog.is_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the active button index, wrapping around if necessary.
|
|
||||||
pub fn next_dialog_button(&mut self) {
|
pub fn next_dialog_button(&mut self) {
|
||||||
if !self.ui.dialog.dialog_buttons.is_empty() {
|
if !self.ui.dialog.dialog_buttons.is_empty() {
|
||||||
let next_index = (self.ui.dialog.dialog_active_button_index + 1)
|
let next_index = (self.ui.dialog.dialog_active_button_index + 1)
|
||||||
% self.ui.dialog.dialog_buttons.len();
|
% self.ui.dialog.dialog_buttons.len();
|
||||||
self.ui.dialog.dialog_active_button_index = next_index; // Use new name
|
self.ui.dialog.dialog_active_button_index = next_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the active button index, wrapping around if necessary.
|
|
||||||
pub fn previous_dialog_button(&mut self) {
|
pub fn previous_dialog_button(&mut self) {
|
||||||
if !self.ui.dialog.dialog_buttons.is_empty() {
|
if !self.ui.dialog.dialog_buttons.is_empty() {
|
||||||
let len = self.ui.dialog.dialog_buttons.len();
|
let len = self.ui.dialog.dialog_buttons.len();
|
||||||
let prev_index =
|
let prev_index =
|
||||||
(self.ui.dialog.dialog_active_button_index + len - 1) % len;
|
(self.ui.dialog.dialog_active_button_index + len - 1) % len;
|
||||||
self.ui.dialog.dialog_active_button_index = prev_index; // Use new name
|
self.ui.dialog.dialog_active_button_index = prev_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the label of the currently active button, if any.
|
|
||||||
pub fn get_active_dialog_button_label(&self) -> Option<&str> {
|
pub fn get_active_dialog_button_label(&self) -> Option<&str> {
|
||||||
self.ui.dialog
|
self.ui.dialog
|
||||||
.dialog_buttons // Use new name
|
.dialog_buttons
|
||||||
.get(self.ui.dialog.dialog_active_button_index) // Use new name
|
.get(self.ui.dialog.dialog_active_button_index)
|
||||||
.map(|s| s.as_str())
|
.map(|s| s.as_str())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,13 +189,13 @@ impl Default for UiState {
|
|||||||
show_login: false,
|
show_login: false,
|
||||||
show_register: false,
|
show_register: false,
|
||||||
show_buffer_list: true,
|
show_buffer_list: true,
|
||||||
|
show_search_palette: false, // ADDED
|
||||||
focus_outside_canvas: false,
|
focus_outside_canvas: false,
|
||||||
dialog: DialogState::default(),
|
dialog: DialogState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the Default implementation for DialogState itself
|
|
||||||
impl Default for DialogState {
|
impl Default for DialogState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// src/state/canvas_state.rs
|
// src/state/canvas_state.rs
|
||||||
|
|
||||||
|
|
||||||
pub trait CanvasState {
|
pub trait CanvasState {
|
||||||
fn current_field(&self) -> usize;
|
fn current_field(&self) -> usize;
|
||||||
fn current_cursor_pos(&self) -> usize;
|
fn current_cursor_pos(&self) -> usize;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/state/pages/form.rs
|
// src/state/pages/form.rs
|
||||||
|
|
||||||
use std::collections::HashMap; // NEW
|
use std::collections::HashMap;
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -9,13 +9,11 @@ use crate::state::pages::canvas_state::CanvasState;
|
|||||||
|
|
||||||
pub struct FormState {
|
pub struct FormState {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
// NEW fields for dynamic table context
|
|
||||||
pub profile_name: String,
|
pub profile_name: String,
|
||||||
pub table_name: String,
|
pub table_name: String,
|
||||||
pub total_count: u64,
|
pub total_count: u64,
|
||||||
pub current_position: u64, // 1-based index, 0 or total_count + 1 for new entry
|
pub current_position: u64,
|
||||||
|
pub fields: Vec<String>,
|
||||||
pub fields: Vec<String>, // Already dynamic, which is good
|
|
||||||
pub values: Vec<String>,
|
pub values: Vec<String>,
|
||||||
pub current_field: usize,
|
pub current_field: usize,
|
||||||
pub has_unsaved_changes: bool,
|
pub has_unsaved_changes: bool,
|
||||||
@@ -23,9 +21,6 @@ pub struct FormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FormState {
|
impl FormState {
|
||||||
/// Creates a new, empty FormState for a given table.
|
|
||||||
/// The position defaults to 1, representing either the first record
|
|
||||||
/// or the position for a new entry if the table is empty.
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
profile_name: String,
|
profile_name: String,
|
||||||
table_name: String,
|
table_name: String,
|
||||||
@@ -33,11 +28,10 @@ impl FormState {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let values = vec![String::new(); fields.len()];
|
let values = vec![String::new(); fields.len()];
|
||||||
FormState {
|
FormState {
|
||||||
id: 0, // Default to 0, indicating a new or unloaded record
|
id: 0,
|
||||||
profile_name,
|
profile_name,
|
||||||
table_name,
|
table_name,
|
||||||
total_count: 0, // Will be fetched after initialization
|
total_count: 0,
|
||||||
// FIX: Default to 1. A position of 0 is an invalid state.
|
|
||||||
current_position: 1,
|
current_position: 1,
|
||||||
fields,
|
fields,
|
||||||
values,
|
values,
|
||||||
@@ -47,6 +41,7 @@ impl FormState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This signature is now correct and only deals with form-related state.
|
||||||
pub fn render(
|
pub fn render(
|
||||||
&self,
|
&self,
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -62,7 +57,7 @@ impl FormState {
|
|||||||
crate::components::form::form::render_form(
|
crate::components::form::form::render_form(
|
||||||
f,
|
f,
|
||||||
area,
|
area,
|
||||||
self, // Pass self as CanvasState
|
self,
|
||||||
&fields_str_slice,
|
&fields_str_slice,
|
||||||
&self.current_field,
|
&self.current_field,
|
||||||
&values_str_slice,
|
&values_str_slice,
|
||||||
@@ -75,19 +70,17 @@ impl FormState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the form to a state for creating a new entry.
|
// ... other methods are unchanged ...
|
||||||
/// It clears all values and sets the position to be one after the last record.
|
|
||||||
pub fn reset_to_empty(&mut self) {
|
pub fn reset_to_empty(&mut self) {
|
||||||
self.id = 0;
|
self.id = 0;
|
||||||
self.values.iter_mut().for_each(|v| v.clear());
|
self.values.iter_mut().for_each(|v| v.clear());
|
||||||
self.current_field = 0;
|
self.current_field = 0;
|
||||||
self.current_cursor_pos = 0;
|
self.current_cursor_pos = 0;
|
||||||
self.has_unsaved_changes = false;
|
self.has_unsaved_changes = false;
|
||||||
// Set the position for a new entry.
|
|
||||||
if self.total_count > 0 {
|
if self.total_count > 0 {
|
||||||
self.current_position = self.total_count + 1;
|
self.current_position = self.total_count + 1;
|
||||||
} else {
|
} else {
|
||||||
self.current_position = 1; // If table is empty, new record is at position 1
|
self.current_position = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,26 +97,19 @@ impl FormState {
|
|||||||
.expect("Invalid current_field index")
|
.expect("Invalid current_field index")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the form's values from a data response and sets its position.
|
|
||||||
/// This is the single source of truth for populating the form after a data fetch.
|
|
||||||
pub fn update_from_response(
|
pub fn update_from_response(
|
||||||
&mut self,
|
&mut self,
|
||||||
response_data: &HashMap<String, String>,
|
response_data: &HashMap<String, String>,
|
||||||
// FIX: Add new_position to make this method authoritative.
|
|
||||||
new_position: u64,
|
new_position: u64,
|
||||||
) {
|
) {
|
||||||
// Create a new vector for the values, ensuring they are in the correct order.
|
|
||||||
self.values = self.fields.iter().map(|field_from_schema| {
|
self.values = self.fields.iter().map(|field_from_schema| {
|
||||||
// For each field from our schema, find the corresponding key in the
|
|
||||||
// response data by doing a case-insensitive comparison.
|
|
||||||
response_data
|
response_data
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(key_from_data, _)| key_from_data.eq_ignore_ascii_case(field_from_schema))
|
.find(|(key_from_data, _)| key_from_data.eq_ignore_ascii_case(field_from_schema))
|
||||||
.map(|(_, value)| value.clone()) // If found, clone its value.
|
.map(|(_, value)| value.clone())
|
||||||
.unwrap_or_default() // If not found, use an empty string.
|
.unwrap_or_default()
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
// Now, do the same case-insensitive lookup for the 'id' field.
|
|
||||||
let id_str_opt = response_data
|
let id_str_opt = response_data
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(k, _)| k.eq_ignore_ascii_case("id"))
|
.find(|(k, _)| k.eq_ignore_ascii_case("id"))
|
||||||
@@ -140,7 +126,6 @@ impl FormState {
|
|||||||
self.id = 0;
|
self.id = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX: Set the position from the provided parameter.
|
|
||||||
self.current_position = new_position;
|
self.current_position = new_position;
|
||||||
self.has_unsaved_changes = false;
|
self.has_unsaved_changes = false;
|
||||||
self.current_field = 0;
|
self.current_field = 0;
|
||||||
@@ -166,12 +151,10 @@ impl CanvasState for FormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_input(&self) -> &str {
|
fn get_current_input(&self) -> &str {
|
||||||
// Re-use the struct's own method
|
|
||||||
FormState::get_current_input(self)
|
FormState::get_current_input(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_input_mut(&mut self) -> &mut String {
|
fn get_current_input_mut(&mut self) -> &mut String {
|
||||||
// Re-use the struct's own method
|
|
||||||
FormState::get_current_input_mut(self)
|
FormState::get_current_input_mut(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
// client/src/ui/handlers/render.rs
|
// src/ui/handlers/render.rs
|
||||||
|
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
|
admin::add_logic::render_add_logic,
|
||||||
|
admin::render_add_table,
|
||||||
|
auth::{login::render_login, register::render_register},
|
||||||
|
common::dialog::render_dialog,
|
||||||
|
common::find_file_palette,
|
||||||
|
common::search_palette::render_search_palette,
|
||||||
|
form::form::render_form,
|
||||||
|
handlers::sidebar::{self, calculate_sidebar_layout},
|
||||||
|
intro::intro::render_intro,
|
||||||
render_background,
|
render_background,
|
||||||
render_buffer_list,
|
render_buffer_list,
|
||||||
render_command_line,
|
render_command_line,
|
||||||
render_status_line,
|
render_status_line,
|
||||||
intro::intro::render_intro,
|
|
||||||
handlers::sidebar::{self, calculate_sidebar_layout},
|
|
||||||
form::form::render_form,
|
|
||||||
admin::render_add_table,
|
|
||||||
admin::add_logic::render_add_logic,
|
|
||||||
auth::{login::render_login, register::render_register},
|
|
||||||
common::find_file_palette,
|
|
||||||
};
|
};
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
|
use crate::modes::general::command_navigation::NavigationState;
|
||||||
|
use crate::state::pages::canvas_state::CanvasState;
|
||||||
|
use crate::state::app::buffer::BufferState;
|
||||||
|
use crate::state::app::highlight::HighlightState;
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
use crate::state::pages::admin::AdminState;
|
||||||
|
use crate::state::pages::auth::AuthState;
|
||||||
|
use crate::state::pages::auth::LoginState;
|
||||||
|
use crate::state::pages::auth::RegisterState;
|
||||||
|
use crate::state::pages::form::FormState;
|
||||||
|
use crate::state::pages::intro::IntroState;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
|
||||||
use crate::state::pages::form::FormState;
|
|
||||||
use crate::state::pages::auth::AuthState;
|
|
||||||
use crate::state::pages::auth::LoginState;
|
|
||||||
use crate::state::pages::auth::RegisterState;
|
|
||||||
use crate::state::pages::intro::IntroState;
|
|
||||||
use crate::state::app::buffer::BufferState;
|
|
||||||
use crate::state::app::state::AppState;
|
|
||||||
use crate::state::pages::admin::AdminState;
|
|
||||||
use crate::state::app::highlight::HighlightState;
|
|
||||||
use crate::modes::general::command_navigation::NavigationState;
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn render_ui(
|
pub fn render_ui(
|
||||||
@@ -53,16 +55,28 @@ pub fn render_ui(
|
|||||||
) {
|
) {
|
||||||
render_background(f, f.area(), theme);
|
render_background(f, f.area(), theme);
|
||||||
|
|
||||||
|
// --- START DYNAMIC LAYOUT LOGIC ---
|
||||||
|
let mut status_line_height = 1;
|
||||||
|
#[cfg(feature = "ui-debug")]
|
||||||
|
{
|
||||||
|
if let Some(debug_state) = &app_state.debug_state {
|
||||||
|
if debug_state.is_error {
|
||||||
|
status_line_height = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END DYNAMIC LAYOUT LOGIC ---
|
||||||
|
|
||||||
const PALETTE_OPTIONS_HEIGHT_FOR_LAYOUT: u16 = 15;
|
const PALETTE_OPTIONS_HEIGHT_FOR_LAYOUT: u16 = 15;
|
||||||
|
|
||||||
let mut bottom_area_constraints: Vec<Constraint> = vec![Constraint::Length(1)];
|
|
||||||
|
|
||||||
|
let mut bottom_area_constraints: Vec<Constraint> = vec![Constraint::Length(status_line_height)];
|
||||||
let command_palette_area_height = if navigation_state.active {
|
let command_palette_area_height = if navigation_state.active {
|
||||||
1 + PALETTE_OPTIONS_HEIGHT_FOR_LAYOUT
|
1 + PALETTE_OPTIONS_HEIGHT_FOR_LAYOUT
|
||||||
} else if event_handler_command_mode_active {
|
} else if event_handler_command_mode_active {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
0 // Neither is active
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
if command_palette_area_height > 0 {
|
if command_palette_area_height > 0 {
|
||||||
@@ -75,7 +89,6 @@ pub fn render_ui(
|
|||||||
}
|
}
|
||||||
main_layout_constraints.extend(bottom_area_constraints);
|
main_layout_constraints.extend(bottom_area_constraints);
|
||||||
|
|
||||||
|
|
||||||
let root_chunks = Layout::default()
|
let root_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(main_layout_constraints)
|
.constraints(main_layout_constraints)
|
||||||
@@ -106,77 +119,95 @@ pub fn render_ui(
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if app_state.ui.show_intro {
|
if app_state.ui.show_intro {
|
||||||
render_intro(f, intro_state, main_content_area, theme);
|
render_intro(f, intro_state, main_content_area, theme);
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
render_register(
|
render_register(
|
||||||
f, main_content_area, theme, register_state, app_state,
|
f,
|
||||||
|
main_content_area,
|
||||||
|
theme,
|
||||||
|
register_state,
|
||||||
|
app_state,
|
||||||
register_state.current_field() < 4,
|
register_state.current_field() < 4,
|
||||||
highlight_state,
|
highlight_state,
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_add_table {
|
} else if app_state.ui.show_add_table {
|
||||||
render_add_table(
|
render_add_table(
|
||||||
f, main_content_area, theme, app_state, &mut admin_state.add_table_state,
|
f,
|
||||||
|
main_content_area,
|
||||||
|
theme,
|
||||||
|
app_state,
|
||||||
|
&mut admin_state.add_table_state,
|
||||||
is_event_handler_edit_mode,
|
is_event_handler_edit_mode,
|
||||||
highlight_state,
|
highlight_state,
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_add_logic {
|
} else if app_state.ui.show_add_logic {
|
||||||
render_add_logic(
|
render_add_logic(
|
||||||
f, main_content_area, theme, app_state, &mut admin_state.add_logic_state,
|
f,
|
||||||
is_event_handler_edit_mode, highlight_state,
|
main_content_area,
|
||||||
|
theme,
|
||||||
|
app_state,
|
||||||
|
&mut admin_state.add_logic_state,
|
||||||
|
is_event_handler_edit_mode,
|
||||||
|
highlight_state,
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_login {
|
} else if app_state.ui.show_login {
|
||||||
render_login(
|
render_login(
|
||||||
f, main_content_area, theme, login_state, app_state,
|
f,
|
||||||
|
main_content_area,
|
||||||
|
theme,
|
||||||
|
login_state,
|
||||||
|
app_state,
|
||||||
login_state.current_field() < 2,
|
login_state.current_field() < 2,
|
||||||
highlight_state,
|
highlight_state,
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_admin {
|
} else if app_state.ui.show_admin {
|
||||||
crate::components::admin::admin_panel::render_admin_panel(
|
crate::components::admin::admin_panel::render_admin_panel(
|
||||||
f, app_state, auth_state, admin_state, main_content_area, theme,
|
f,
|
||||||
&app_state.profile_tree, &app_state.selected_profile,
|
app_state,
|
||||||
|
auth_state,
|
||||||
|
admin_state,
|
||||||
|
main_content_area,
|
||||||
|
theme,
|
||||||
|
&app_state.profile_tree,
|
||||||
|
&app_state.selected_profile,
|
||||||
);
|
);
|
||||||
|
|
||||||
} else if app_state.ui.show_form {
|
} else if app_state.ui.show_form {
|
||||||
let (sidebar_area, form_actual_area) = calculate_sidebar_layout(
|
let (sidebar_area, form_actual_area) =
|
||||||
app_state.ui.show_sidebar, main_content_area
|
calculate_sidebar_layout(app_state.ui.show_sidebar, main_content_area);
|
||||||
);
|
|
||||||
if let Some(sidebar_rect) = sidebar_area {
|
if let Some(sidebar_rect) = sidebar_area {
|
||||||
sidebar::render_sidebar(
|
sidebar::render_sidebar(
|
||||||
f, sidebar_rect, theme, &app_state.profile_tree, &app_state.selected_profile
|
f,
|
||||||
|
sidebar_rect,
|
||||||
|
theme,
|
||||||
|
&app_state.profile_tree,
|
||||||
|
&app_state.selected_profile,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let available_width = form_actual_area.width;
|
let available_width = form_actual_area.width;
|
||||||
let form_render_area = if available_width >= 80 {
|
let form_render_area = if available_width >= 80 {
|
||||||
Layout::default().direction(Direction::Horizontal)
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Min(0), Constraint::Length(80), Constraint::Min(0)])
|
.constraints([Constraint::Min(0), Constraint::Length(80), Constraint::Min(0)])
|
||||||
.split(form_actual_area)[1]
|
.split(form_actual_area)[1]
|
||||||
} else {
|
} else {
|
||||||
Layout::default().direction(Direction::Horizontal)
|
Layout::default()
|
||||||
.constraints([Constraint::Min(0), Constraint::Length(available_width), Constraint::Min(0)])
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(available_width),
|
||||||
|
Constraint::Min(0),
|
||||||
|
])
|
||||||
.split(form_actual_area)[1]
|
.split(form_actual_area)[1]
|
||||||
};
|
};
|
||||||
let fields_vec: Vec<&str> = form_state.fields.iter().map(AsRef::as_ref).collect();
|
|
||||||
let values_vec: Vec<&String> = form_state.values.iter().collect();
|
|
||||||
|
|
||||||
// --- START FIX ---
|
form_state.render(
|
||||||
// Add the missing `&form_state.table_name` argument to this function call.
|
|
||||||
render_form(
|
|
||||||
f,
|
f,
|
||||||
form_render_area,
|
form_render_area,
|
||||||
form_state,
|
|
||||||
&fields_vec,
|
|
||||||
&form_state.current_field,
|
|
||||||
&values_vec,
|
|
||||||
&form_state.table_name, // <-- THIS ARGUMENT WAS MISSING
|
|
||||||
theme,
|
theme,
|
||||||
is_event_handler_edit_mode,
|
is_event_handler_edit_mode,
|
||||||
highlight_state,
|
highlight_state,
|
||||||
form_state.total_count,
|
|
||||||
form_state.current_position,
|
|
||||||
);
|
);
|
||||||
// --- END FIX ---
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(area) = buffer_list_area {
|
if let Some(area) = buffer_list_area {
|
||||||
@@ -193,23 +224,41 @@ pub fn render_ui(
|
|||||||
app_state,
|
app_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(palette_or_command_area) = command_render_area { // Use the calculated area
|
if let Some(palette_or_command_area) = command_render_area {
|
||||||
if navigation_state.active {
|
if navigation_state.active {
|
||||||
find_file_palette::render_find_file_palette(
|
find_file_palette::render_find_file_palette(
|
||||||
f,
|
f,
|
||||||
palette_or_command_area, // Use the correct area
|
palette_or_command_area,
|
||||||
theme,
|
theme,
|
||||||
navigation_state, // Pass the navigation_state directly
|
navigation_state,
|
||||||
);
|
);
|
||||||
} else if event_handler_command_mode_active {
|
} else if event_handler_command_mode_active {
|
||||||
render_command_line(
|
render_command_line(
|
||||||
f,
|
f,
|
||||||
palette_or_command_area, // Use the correct area
|
palette_or_command_area,
|
||||||
event_handler_command_input,
|
event_handler_command_input,
|
||||||
true, // Assuming it's always active when this branch is hit
|
true,
|
||||||
theme,
|
theme,
|
||||||
event_handler_command_message,
|
event_handler_command_message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This block now correctly handles drawing popups over any view.
|
||||||
|
if app_state.ui.show_search_palette {
|
||||||
|
if let Some(search_state) = &app_state.search_state {
|
||||||
|
render_search_palette(f, f.area(), theme, search_state);
|
||||||
|
}
|
||||||
|
} else if app_state.ui.dialog.dialog_show {
|
||||||
|
render_dialog(
|
||||||
|
f,
|
||||||
|
f.area(),
|
||||||
|
theme,
|
||||||
|
&app_state.ui.dialog.dialog_title,
|
||||||
|
&app_state.ui.dialog.dialog_message,
|
||||||
|
&app_state.ui.dialog.dialog_buttons,
|
||||||
|
app_state.ui.dialog.dialog_active_button_index,
|
||||||
|
app_state.ui.dialog.is_loading,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,16 @@ use crate::ui::handlers::context::DialogPurpose;
|
|||||||
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 crate::utils::columns::filter_user_columns;
|
use crate::utils::columns::filter_user_columns;
|
||||||
use std::time::Instant;
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{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, warn};
|
use tracing::{error, info, warn};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
#[cfg(feature = "ui-debug")]
|
||||||
|
use crate::state::app::state::DebugState;
|
||||||
|
#[cfg(feature = "ui-debug")]
|
||||||
|
use crate::utils::debug_logger::pop_next_debug_message;
|
||||||
|
|
||||||
pub async fn run_ui() -> Result<()> {
|
pub async fn run_ui() -> Result<()> {
|
||||||
let config = Config::load().context("Failed to load configuration")?;
|
let config = Config::load().context("Failed to load configuration")?;
|
||||||
@@ -51,6 +55,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
register_result_sender.clone(),
|
register_result_sender.clone(),
|
||||||
save_table_result_sender.clone(),
|
save_table_result_sender.clone(),
|
||||||
save_logic_result_sender.clone(),
|
save_logic_result_sender.clone(),
|
||||||
|
grpc_client.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context("Failed to create event handler")?;
|
.context("Failed to create event handler")?;
|
||||||
@@ -126,6 +131,25 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
loop {
|
loop {
|
||||||
let position_before_event = form_state.current_position;
|
let position_before_event = form_state.current_position;
|
||||||
let mut event_processed = false;
|
let mut event_processed = false;
|
||||||
|
|
||||||
|
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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if app_state.ui.show_search_palette {
|
||||||
|
needs_redraw = true;
|
||||||
|
}
|
||||||
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;
|
event_processed = true;
|
||||||
@@ -133,7 +157,6 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
event,
|
event,
|
||||||
&config,
|
&config,
|
||||||
&mut terminal,
|
&mut terminal,
|
||||||
&mut grpc_client,
|
|
||||||
&mut command_handler,
|
&mut command_handler,
|
||||||
&mut form_state,
|
&mut form_state,
|
||||||
&mut auth_state,
|
&mut auth_state,
|
||||||
@@ -499,10 +522,20 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
|
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
{
|
{
|
||||||
app_state.debug_info = format!(
|
let can_display_next = match &app_state.debug_state {
|
||||||
"Redraw -> event: {}, needs_redraw: {}, pos_changed: {}",
|
Some(current) => current.display_start_time.elapsed() >= Duration::from_secs(2),
|
||||||
event_processed, needs_redraw, position_changed
|
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 {
|
if event_processed || needs_redraw || position_changed {
|
||||||
|
|||||||
46
client/src/utils/debug_logger.rs
Normal file
46
client/src/utils/debug_logger.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// client/src/utils/debug_logger.rs
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::collections::VecDeque; // <-- FIX: Import VecDeque
|
||||||
|
use std::io;
|
||||||
|
use std::sync::{Arc, Mutex}; // <-- FIX: Import Mutex
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref UI_DEBUG_BUFFER: Arc<Mutex<VecDeque<(String, bool)>>> =
|
||||||
|
Arc::new(Mutex::new(VecDeque::from([(String::from("Logger initialized..."), false)])));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UiDebugWriter;
|
||||||
|
|
||||||
|
impl Default for UiDebugWriter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiDebugWriter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for UiDebugWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
let mut buffer = UI_DEBUG_BUFFER.lock().unwrap();
|
||||||
|
let message = String::from_utf8_lossy(buf);
|
||||||
|
let trimmed_message = message.trim().to_string();
|
||||||
|
let is_error = trimmed_message.starts_with("ERROR");
|
||||||
|
// Add the new message to the back of the queue
|
||||||
|
buffer.push_back((trimmed_message, is_error));
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A public function to pop the next message from the front of the queue.
|
||||||
|
pub fn pop_next_debug_message() -> Option<(String, bool)> {
|
||||||
|
UI_DEBUG_BUFFER.lock().unwrap().pop_front()
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
// src/utils/mod.rs
|
// src/utils/mod.rs
|
||||||
|
|
||||||
pub mod columns;
|
pub mod columns;
|
||||||
|
pub mod debug_logger;
|
||||||
pub use columns::*;
|
pub use columns::*;
|
||||||
|
pub use debug_logger::*;
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ use common::proto::multieko2::search::{
|
|||||||
pub use common::proto::multieko2::search::searcher_server::SearcherServer;
|
pub use common::proto::multieko2::search::searcher_server::SearcherServer;
|
||||||
use common::proto::multieko2::search::searcher_server::Searcher;
|
use common::proto::multieko2::search::searcher_server::Searcher;
|
||||||
use common::search::register_slovak_tokenizers;
|
use common::search::register_slovak_tokenizers;
|
||||||
use sqlx::{PgPool, Row}; // <-- Import PgPool and Row
|
use sqlx::{PgPool, Row};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
// We need to hold the database pool in our service struct.
|
// We need to hold the database pool in our service struct.
|
||||||
pub struct SearcherService {
|
pub struct SearcherService {
|
||||||
@@ -263,6 +264,8 @@ impl Searcher for SearcherService {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
info!("--- SERVER: Successfully processed search. Returning {} hits. ---", hits.len());
|
||||||
|
|
||||||
let response = SearchResponse { hits };
|
let response = SearchResponse { hits };
|
||||||
Ok(Response::new(response))
|
Ok(Response::new(response))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user