Compare commits

...

20 Commits

Author SHA1 Message Date
filipriec
944131d5a6 from 4e01740a61 till now, fully integrated working register 2025-04-11 08:52:01 +02:00
filipriec
eff46ac9bf working read only mode in login and register properly workig now 2025-04-11 08:50:54 +02:00
filipriec
14f9b254b2 read only mode needs adjustement to 4 fields from 2 2025-04-10 23:04:25 +02:00
filipriec
337d6050ff common mode register fix 2025-04-10 22:18:01 +02:00
filipriec
d36348b84f gRPC implementation of a registration working 2025-04-10 21:14:32 +02:00
filipriec
dfb6f5b375 registration auth services added 2025-04-10 21:00:41 +02:00
filipriec
a9089bc2ff displaying register 2025-04-10 20:01:03 +02:00
filipriec
5f5d690ff3 compiled time to render register properly well 2025-04-10 19:32:49 +02:00
filipriec
431882ece9 register 2 2025-04-10 19:27:04 +02:00
filipriec
b51e76e366 register on the way 2025-04-10 18:50:54 +02:00
filipriec
4e01740a61 fixed exit in the login dialog 2025-04-10 16:34:29 +02:00
filipriec
e729ed9df3 compiled 2025-04-10 16:05:06 +02:00
filipriec
6b241304fb dialog login functionality 2025-04-10 15:36:43 +02:00
filipriec
3ed8764087 cargo fix 2025-04-10 14:45:16 +02:00
filipriec
df61b245ab back to main 2025-04-10 14:42:11 +02:00
filipriec
7a364d654c repaired 2025-04-10 14:22:30 +02:00
filipriec
0d1a0be1a0 dialog movement fixed 2025-04-10 14:13:39 +02:00
filipriec
5da9f5aaf4 better, but more dialog logic is needed 2025-04-10 13:45:36 +02:00
filipriec
2bec0f5850 dialog 2 2025-04-10 13:40:24 +02:00
filipriec
5bc28ec38b dialog 2025-04-10 13:37:15 +02:00
25 changed files with 840 additions and 119 deletions

6
Cargo.lock generated
View File

@@ -421,7 +421,7 @@ dependencies = [
[[package]]
name = "client"
version = "0.3.3"
version = "0.3.5"
dependencies = [
"async-trait",
"common",
@@ -458,7 +458,7 @@ dependencies = [
[[package]]
name = "common"
version = "0.3.3"
version = "0.3.5"
dependencies = [
"prost",
"serde",
@@ -2589,7 +2589,7 @@ dependencies = [
[[package]]
name = "server"
version = "0.3.3"
version = "0.3.5"
dependencies = [
"bcrypt",
"chrono",

View File

@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
# TODO: idk how to do the name, fix later
# name = "Multieko2"
version = "0.3.3"
version = "0.3.5"
edition = "2021"
license = "GPL-3.0-or-later"
authors = ["Filip Priečinský <filippriec@gmail.com>"]

View File

@@ -3,3 +3,4 @@ pub mod login;
pub mod register;
pub use login::*;
pub use register::*;

View File

@@ -6,7 +6,6 @@ use crate::{
components::common::dialog,
state::state::AppState,
};
use crate::modes::handlers::mode_manager::AppMode;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
style::{Style, Modifier, Color},

View File

@@ -0,0 +1,153 @@
// src/components/auth/register.rs
use crate::{
config::colors::themes::Theme,
state::pages::auth::RegisterState, // Use RegisterState
components::common::dialog,
state::state::AppState,
};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
style::{Style, Modifier, Color},
widgets::{Block, BorderType, Borders, Paragraph},
Frame,
};
pub fn render_register(
f: &mut Frame,
area: Rect,
theme: &Theme,
state: &RegisterState, // Use RegisterState
app_state: &AppState,
is_edit_mode: bool,
) {
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain)
.border_style(Style::default().fg(theme.border))
.title(" Register ") // Update title
.style(Style::default().bg(theme.bg));
f.render_widget(block, area);
let inner_area = area.inner(Margin {
horizontal: 1,
vertical: 1,
});
// Adjust constraints for 4 fields + error + buttons
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(6), // Form (4 fields + padding)
Constraint::Length(1), // Error message
Constraint::Length(3), // Buttons
])
.split(inner_area);
// --- FORM RENDERING ---
crate::components::handlers::canvas::render_canvas(
f,
chunks[0],
state, // Pass RegisterState
&[ // Update field labels
"Username",
"Email (Optional)",
"Password (Optional)",
"Confirm Password",
],
&state.current_field,
&[ // Update values from RegisterState
&state.username,
&state.email,
&state.password,
&state.password_confirmation,
],
theme,
is_edit_mode,
);
// --- ERROR MESSAGE ---
if let Some(err) = &state.error_message {
f.render_widget(
Paragraph::new(err.as_str())
.style(Style::default().fg(Color::Red))
.alignment(Alignment::Center),
chunks[1],
);
}
// --- BUTTONS ---
let button_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[2]);
// Register Button
let register_button_index = 0;
let register_active = if app_state.ui.focus_outside_canvas {
app_state.general.selected_item == register_button_index
} else {
false
};
let mut register_style = Style::default().fg(theme.fg);
let mut register_border = Style::default().fg(theme.border);
if register_active {
register_style = register_style.fg(theme.highlight).add_modifier(Modifier::BOLD);
register_border = register_border.fg(theme.accent);
}
f.render_widget(
Paragraph::new("Register") // Update button text
.style(register_style)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain)
.border_style(register_border),
),
button_chunks[0],
);
// Return Button (logic remains similar)
let return_button_index = 1;
let return_active = if app_state.ui.focus_outside_canvas {
app_state.general.selected_item == return_button_index
} else {
false
};
let mut return_style = Style::default().fg(theme.fg);
let mut return_border = Style::default().fg(theme.border);
if return_active {
return_style = return_style.fg(theme.highlight).add_modifier(Modifier::BOLD);
return_border = return_border.fg(theme.accent);
}
f.render_widget(
Paragraph::new("Return")
.style(return_style)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain)
.border_style(return_border),
),
button_chunks[1],
);
// --- DIALOG --- (Keep dialog logic)
if app_state.ui.dialog.dialog_show {
dialog::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,
);
}
}

View File

@@ -33,7 +33,7 @@ impl IntroState {
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(40),
Constraint::Length(5), // Increased to accommodate 3 buttons
Constraint::Length(5),
Constraint::Percentage(40),
])
.split(inner_area);
@@ -48,51 +48,44 @@ impl IntroState {
.alignment(Alignment::Center);
f.render_widget(title_para, chunks[1]);
// Buttons - now with 3 options
// Buttons - now with 4 options
let button_area = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(chunks[1].inner(Margin {
horizontal: 1,
vertical: 1
}));
self.render_button(
f,
button_area[0],
"Continue",
self.selected_option == 0,
theme,
);
self.render_button(
f,
button_area[1],
"Admin",
self.selected_option == 1,
theme,
);
self.render_button(
f,
button_area[2],
"Login",
self.selected_option == 2,
theme,
);
let buttons = ["Continue", "Admin", "Login", "Register"];
for (i, &text) in buttons.iter().enumerate() {
self.render_button(
f,
button_area[i],
text,
self.selected_option == i,
theme,
);
}
}
fn render_button(&self, f: &mut Frame, area: Rect, text: &str, selected: bool, theme: &Theme) {
let button_style = if selected {
Style::default()
.fg(theme.highlight)
.bg(theme.bg)
.add_modifier(ratatui::style::Modifier::BOLD)
} else {
Style::default().fg(theme.fg).bg(theme.bg)
};
let button_style = Style::default()
.fg(if selected { theme.highlight } else { theme.fg })
.bg(theme.bg)
.add_modifier(if selected {
ratatui::style::Modifier::BOLD
} else {
ratatui::style::Modifier::empty()
});
let border_style = Style::default()
.fg(if selected { theme.accent } else { theme.border });
let button = Paragraph::new(text)
.style(button_style)
@@ -101,21 +94,17 @@ impl IntroState {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Double)
.border_style(if selected {
Style::default().fg(theme.accent)
} else {
Style::default().fg(theme.border)
}),
.border_style(border_style),
);
f.render_widget(button, area);
}
pub fn next_option(&mut self) {
self.selected_option = (self.selected_option + 1) % 3;
pub fn next_option(&mut self) {
self.selected_option = (self.selected_option + 1) % 4;
}
pub fn previous_option(&mut self) {
self.selected_option = if self.selected_option == 0 { 2 } else { self.selected_option - 1 };
self.selected_option = if self.selected_option == 0 { 3 } else { self.selected_option - 1 };
}
}

View File

@@ -2,7 +2,7 @@
use crate::services::grpc_client::GrpcClient;
use crate::state::canvas_state::CanvasState;
use crate::state::pages::{auth::AuthState, form::FormState};
use crate::state::pages::form::FormState;
use crate::tui::functions::common::form::{revert, save};
use crossterm::event::{KeyCode, KeyEvent};
use std::any::Any;

View File

@@ -1,7 +1,7 @@
// src/modes/canvas/common_mode.rs
use crate::tui::terminal::core::TerminalCore;
use crate::state::pages::{form::FormState, auth::AuthState};
use crate::state::pages::{form::FormState, auth::AuthState, auth::RegisterState};
use crate::state::state::AppState;
use crate::services::grpc_client::GrpcClient;
use crate::services::auth::AuthClient;
@@ -9,13 +9,15 @@ use crate::modes::handlers::event::EventOutcome;
use crate::tui::functions::common::form::SaveOutcome;
use crate::tui::functions::common::{
form::{save as form_save, revert as form_revert},
login::{save as login_save, revert as login_revert}
login::{save as login_save, revert as login_revert},
register::{save as register_save, revert as register_revert},
};
pub async fn handle_core_action(
action: &str,
form_state: &mut FormState,
auth_state: &mut AuthState,
register_state: &mut RegisterState,
grpc_client: &mut GrpcClient,
auth_client: &mut AuthClient,
terminal: &mut TerminalCore,
@@ -28,6 +30,9 @@ pub async fn handle_core_action(
if app_state.ui.show_login {
let message = login_save(auth_state, auth_client, app_state).await?;
Ok(EventOutcome::Ok(message))
} else if app_state.ui.show_register {
let message = register_save(register_state, auth_client, app_state).await?;
Ok(EventOutcome::Ok(message))
} else {
let save_outcome = form_save(
form_state,
@@ -50,6 +55,8 @@ pub async fn handle_core_action(
"save_and_quit" => {
let message = if app_state.ui.show_login {
login_save(auth_state, auth_client, app_state).await?
} else if app_state.ui.show_register {
register_save(register_state, auth_client, app_state).await?
} else {
let save_outcome = form_save(
form_state,
@@ -70,6 +77,9 @@ pub async fn handle_core_action(
if app_state.ui.show_login {
let message = login_revert(auth_state, app_state).await;
Ok(EventOutcome::Ok(message))
} else if app_state.ui.show_register {
let message = register_revert(register_state, app_state).await;
Ok(EventOutcome::Ok(message))
} else {
let message = form_revert(
form_state,

View File

@@ -2,22 +2,25 @@
use crate::config::binds::config::Config;
use crate::services::grpc_client::GrpcClient;
use crate::state::pages::{auth::AuthState, form::FormState};
use crate::state::pages::{auth::{AuthState, RegisterState}};
use crate::state::pages::form::FormState;
use crate::functions::modes::edit::{auth_e, form_e};
use crate::modes::handlers::event::EventOutcome;
use crate::state::state::AppState;
use crossterm::event::{KeyCode, KeyEvent};
pub async fn handle_edit_event(
is_auth_context: bool,
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
auth_state: &mut AuthState,
register_state: &mut RegisterState,
ideal_cursor_column: &mut usize,
command_message: &mut String,
current_position: &mut u64,
total_count: u64,
grpc_client: &mut GrpcClient,
app_state: &AppState,
) -> Result<String, Box<dyn std::error::Error>> {
// Global command mode check
@@ -37,7 +40,7 @@ pub async fn handle_edit_event(
key.modifiers
) {
if matches!(action, "save" | "revert") {
let message = if is_auth_context {
let message = if app_state.ui.show_login {
auth_e::execute_common_action(
action,
auth_state, // Concrete AuthState
@@ -45,6 +48,14 @@ pub async fn handle_edit_event(
current_position,
total_count
).await
} else if app_state.ui.show_register {
auth_e::execute_common_action(
action,
register_state,
grpc_client,
current_position,
total_count
).await
} else {
form_e::execute_common_action(
action,
@@ -55,7 +66,7 @@ pub async fn handle_edit_event(
).await
.map(|outcome| match outcome {
EventOutcome::Ok(msg) => msg,
EventOutcome::Exit(msg) => format!("Exit requested: {}", msg), // Or handle differently
EventOutcome::Exit(msg) => format!("Exit requested: {}", msg),
EventOutcome::DataSaved(save_outcome, msg) => format!("Data saved ({:?}): {}", save_outcome, msg),
EventOutcome::ButtonSelected { context, index } => {
"Unexpected action in edit mode".to_string()
@@ -68,11 +79,21 @@ pub async fn handle_edit_event(
// Edit-specific actions
if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) {
return if is_auth_context {
return if app_state.ui.show_login {
auth_e::execute_edit_action(
action,
key,
auth_state, // Full access to AuthState fields
auth_state,
ideal_cursor_column,
grpc_client,
current_position,
total_count
).await
} else if app_state.ui.show_register {
auth_e::execute_edit_action(
action,
key,
register_state,
ideal_cursor_column,
grpc_client,
current_position,
@@ -82,7 +103,7 @@ pub async fn handle_edit_event(
form_e::execute_edit_action(
action,
key,
form_state, // Full access to FormState fields
form_state,
ideal_cursor_column,
grpc_client,
current_position,
@@ -93,7 +114,7 @@ pub async fn handle_edit_event(
// Character insertion
if let KeyCode::Char(_) = key.code {
return if is_auth_context {
return if app_state.ui.show_login {
auth_e::execute_edit_action(
"insert_char",
key,
@@ -103,6 +124,16 @@ pub async fn handle_edit_event(
current_position,
total_count
).await
} else if app_state.ui.show_register {
auth_e::execute_edit_action(
"insert_char",
key,
register_state,
ideal_cursor_column,
grpc_client,
current_position,
total_count
).await
} else {
form_e::execute_edit_action(
"insert_char",

View File

@@ -3,7 +3,7 @@
use crate::config::binds::config::Config;
use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::services::grpc_client::GrpcClient;
use crate::state::canvas_state::CanvasState;
use crate::state::{canvas_state::CanvasState, pages::auth::RegisterState};
use crate::state::pages::auth::AuthState;
use crate::state::pages::form::FormState;
use crate::state::state::AppState;
@@ -16,6 +16,7 @@ pub async fn handle_read_only_event(
config: &Config,
form_state: &mut FormState,
auth_state: &mut AuthState,
register_state: &mut RegisterState,
key_sequence_tracker: &mut KeySequenceTracker,
current_position: &mut u64,
total_count: u64,
@@ -31,23 +32,31 @@ pub async fn handle_read_only_event(
}
if config.is_enter_edit_mode_after(key.code, key.modifiers) {
let (current_input, current_pos) = if app_state.ui.show_login {
let (current_input, current_pos) = if app_state.ui.show_login { // Check Login first
(
auth_state.get_current_input(),
auth_state.current_cursor_pos(),
)
} else if app_state.ui.show_register { // Then check Register
(
register_state.get_current_input(),
register_state.current_cursor_pos(),
)
} else {
(
form_state.get_current_input(),
form_state.current_cursor_pos(),
)
) // Default to Form
};
if !current_input.is_empty() && current_pos < current_input.len() {
if app_state.ui.show_login {
auth_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = auth_state.current_cursor_pos();
} else {
} else if app_state.ui.show_register {
register_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = register_state.current_cursor_pos();
} else { // Default to Form
form_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = form_state.current_cursor_pos();
}
@@ -65,6 +74,10 @@ pub async fn handle_read_only_event(
"previous_entry",
"next_entry",
];
// Add context actions specific to register if needed, otherwise reuse login/form ones
const CONTEXT_ACTIONS_REGISTER: &[&str] = &[
// Add actions like "next_field", "prev_field" if handled differently than general read-only
];
if key.modifiers.is_empty() {
key_sequence_tracker.add_key(key.code);
@@ -81,13 +94,22 @@ pub async fn handle_read_only_event(
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions
crate::tui::functions::login::handle_action(
action,
auth_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_register{
auth_ro::execute_action(
action,
app_state,
register_state,
ideal_cursor_column,
key_sequence_tracker,
command_message,
).await?
} else if app_state.ui.show_login {
auth_ro::execute_action(
action,
@@ -128,14 +150,23 @@ pub async fn handle_read_only_event(
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions
crate::tui::functions::login::handle_action(
action,
auth_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_login {
} else if app_state.ui.show_register /* && CONTEXT_ACTIONS_REGISTER.contains(&action) */ { // Handle register general actions
auth_ro::execute_action(
action,
app_state,
register_state,
ideal_cursor_column,
key_sequence_tracker,
command_message,
).await?
} else if app_state.ui.show_login { // Handle login general actions
auth_ro::execute_action(
action,
app_state,
@@ -174,14 +205,23 @@ pub async fn handle_read_only_event(
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions
crate::tui::functions::login::handle_action(
action,
auth_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_login {
} else if app_state.ui.show_register /* && CONTEXT_ACTIONS_REGISTER.contains(&action) */ { // Handle register general actions
auth_ro::execute_action(
action,
app_state,
register_state,
ideal_cursor_column,
key_sequence_tracker,
command_message,
).await?
} else if app_state.ui.show_login { // Handle login general actions
auth_ro::execute_action(
action,
app_state,

View File

@@ -1,2 +1,3 @@
// src/client/modes/general.rs
pub mod navigation;
pub mod dialog;

View File

@@ -0,0 +1,127 @@
// src/modes/general/dialog.rs
use crossterm::event::{Event, KeyCode};
use crate::config::binds::config::Config;
use crate::ui::handlers::context::DialogPurpose;
use crate::state::state::AppState;
use crate::state::pages::auth::AuthState;
use crate::state::pages::auth::RegisterState;
use crate::services::auth::AuthClient;
use crate::modes::handlers::event::EventOutcome;
use crate::tui::functions::common::{login, register};
/// Handles key events specifically when a dialog is active.
/// Returns Some(Result<EventOutcome, Error>) if the event was handled (consumed),
/// otherwise returns None.
pub async fn handle_dialog_event(
event: &Event,
config: &Config,
app_state: &mut AppState,
auth_state: &mut AuthState,
register_state: &mut RegisterState,
auth_client: &mut AuthClient,
) -> Option<Result<EventOutcome, Box<dyn std::error::Error>>> {
if let Event::Key(key) = event {
// Always allow Esc to dismiss
if key.code == KeyCode::Esc {
app_state.hide_dialog();
return Some(Ok(EventOutcome::Ok("Dialog dismissed".to_string())));
}
// Check general bindings for dialog actions
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
match action {
"move_down" | "next_option" => {
let current_index = app_state.ui.dialog.dialog_active_button_index;
let num_buttons = app_state.ui.dialog.dialog_buttons.len();
if num_buttons > 0 && current_index < num_buttons - 1 {
app_state.ui.dialog.dialog_active_button_index += 1;
}
return Some(Ok(EventOutcome::Ok(String::new())));
}
"move_up" | "previous_option" => {
let current_index = app_state.ui.dialog.dialog_active_button_index;
if current_index > 0 {
app_state.ui.dialog.dialog_active_button_index -= 1;
}
return Some(Ok(EventOutcome::Ok(String::new())));
}
"select" => {
let selected_index = app_state.ui.dialog.dialog_active_button_index;
let purpose = match app_state.ui.dialog.purpose {
Some(p) => p,
None => {
app_state.hide_dialog();
return Some(Ok(EventOutcome::Ok("Internal Error: Dialog context lost".to_string())));
}
};
// Handle Dialog Actions Directly Here
match purpose {
DialogPurpose::LoginSuccess => {
match selected_index {
0 => { // "Menu" button selected
app_state.hide_dialog();
let message = login::back_to_main(auth_state, app_state).await;
return Some(Ok(EventOutcome::Ok(message)));
}
1 => {
app_state.hide_dialog();
return Some(Ok(EventOutcome::Ok("Exiting dialog".to_string())));
}
_ => {
app_state.hide_dialog();
return Some(Ok(EventOutcome::Ok("Unknown dialog button selected".to_string())));
}
}
}
DialogPurpose::LoginFailed => {
match selected_index {
0 => { // "OK" button selected
app_state.hide_dialog();
return Some(Ok(EventOutcome::Ok("Login failed dialog dismissed".to_string())));
}
_ => {
app_state.hide_dialog();
return Some(Ok(EventOutcome::Ok("Unknown dialog button selected".to_string())));
}
}
}
DialogPurpose::RegisterSuccess => { // Add this arm
match selected_index {
0 => { // "OK" button for RegisterSuccess
app_state.hide_dialog();
// Go back to intro after successful registration dialog
let message = register::back_to_main(register_state, app_state).await;
return Some(Ok(EventOutcome::Ok(message)));
}
_ => { // Default for RegisterSuccess
app_state.hide_dialog();
return Some(Ok(EventOutcome::Ok("Unknown dialog button selected".to_string())));
}
}
}
DialogPurpose::RegisterFailed => { // Add this arm
match selected_index {
0 => { // "OK" button for RegisterFailed
app_state.hide_dialog(); // Just dismiss
return Some(Ok(EventOutcome::Ok("Register failed dialog dismissed".to_string())));
}
_ => { // Default for RegisterFailed
app_state.hide_dialog();
return Some(Ok(EventOutcome::Ok("Unknown dialog button selected".to_string())));
}
}
}
}
}
_ => {} // Ignore other general actions when dialog is shown
}
}
// If it was a key event but not handled above, consume it
Some(Ok(EventOutcome::Ok(String::new())))
} else {
// If it wasn't a key event, consume it too while dialog is active
Some(Ok(EventOutcome::Ok(String::new())))
}
}

View File

@@ -60,6 +60,8 @@ pub async fn handle_navigation_event(
(UiContext::Intro, app_state.ui.intro_state.selected_option)
} else if app_state.ui.show_login && app_state.ui.focus_outside_canvas {
(UiContext::Login, app_state.general.selected_item)
} else if app_state.ui.show_register && app_state.ui.focus_outside_canvas {
(UiContext::Register, app_state.general.selected_item)
} else if app_state.ui.show_admin {
(UiContext::Admin, app_state.general.selected_item)
} else if app_state.ui.dialog.dialog_show {

View File

@@ -8,15 +8,16 @@ use crate::modes::common::commands::CommandHandler;
use crate::config::binds::config::Config;
use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState;
use crate::state::pages::auth::RegisterState;
use crate::state::canvas_state::CanvasState;
use crate::ui::handlers::rat_state::UiStateHandler;
use crate::ui::handlers::context::UiContext;
use crate::tui::functions::{intro, admin};
use crate::tui::functions::common::login;
use crate::tui::functions::common::{login, register};
use crate::modes::{
common::command_mode,
canvas::{edit, read_only, common_mode},
general::navigation,
general::{navigation, dialog},
};
use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::modes::handlers::mode_manager::{ModeManager, AppMode};
@@ -64,6 +65,7 @@ impl EventHandler {
command_handler: &mut CommandHandler,
form_state: &mut FormState,
auth_state: &mut AuthState,
register_state: &mut RegisterState,
app_state: &mut crate::state::state::AppState,
total_count: u64,
current_position: &mut u64,
@@ -71,6 +73,17 @@ impl EventHandler {
let current_mode = ModeManager::derive_mode(app_state, self);
app_state.update_mode(current_mode);
// --- DIALOG MODALITY ---
if app_state.ui.dialog.dialog_show {
if let Some(dialog_result) = dialog::handle_dialog_event(
&event, config, app_state, auth_state, register_state, &mut self.auth_client
).await {
return dialog_result;
}
return Ok(EventOutcome::Ok(String::new()));
}
// --- END DIALOG MODALITY CHECK ---
if let Event::Key(key) = event {
let key_code = key.code;
let modifiers = key.modifiers;
@@ -105,13 +118,14 @@ impl EventHandler {
UiContext::Login => {
message = match index {
0 => login::save(auth_state, &mut self.auth_client, app_state).await?,
1 => {
let msg = login::revert(auth_state, app_state).await;
// Optional: Add navigation logic here if revert should change screen
// app_state.ui.show_login = false;
// app_state.ui.show_intro = true;
msg
}
1 => login::back_to_main(auth_state, app_state).await,
_ => "Invalid Login Option".to_string(),
};
}
UiContext::Register => {
message = match index {
0 => register::save(register_state, &mut self.auth_client, app_state).await?,
1 => register::back_to_main(register_state, app_state).await,
_ => "Invalid Login Option".to_string(),
};
}
@@ -121,9 +135,7 @@ impl EventHandler {
message = format!("Admin Option {} selected", index);
}
UiContext::Dialog => {
// Add specific dialog handling logic here
message = format!("Dialog Button {} selected", index);
app_state.hide_dialog(); // Example action
message = "Internal error: Unexpected dialog state".to_string();
}
}
return Ok(EventOutcome::Ok(message)); // Return Ok with message
@@ -144,19 +156,19 @@ impl EventHandler {
if config.is_enter_edit_mode_after(key_code, modifiers) &&
ModeManager::can_enter_edit_mode(current_mode) {
let current_input = if app_state.ui.show_login {
let current_input = if app_state.ui.show_login || app_state.ui.show_register{
auth_state.get_current_input()
} else {
form_state.get_current_input()
};
let current_cursor_pos = if app_state.ui.show_login {
let current_cursor_pos = if app_state.ui.show_login || app_state.ui.show_register{
auth_state.current_cursor_pos()
} else {
form_state.current_cursor_pos()
};
if !current_input.is_empty() && current_cursor_pos < current_input.len() {
if app_state.ui.show_login {
if app_state.ui.show_login || app_state.ui.show_register{
auth_state.set_current_cursor_pos(current_cursor_pos + 1);
self.ideal_cursor_column = auth_state.current_cursor_pos();
} else {
@@ -191,6 +203,7 @@ impl EventHandler {
action,
form_state,
auth_state,
register_state,
grpc_client,
&mut self.auth_client,
terminal,
@@ -209,6 +222,7 @@ impl EventHandler {
config,
form_state,
auth_state,
register_state,
&mut self.key_sequence_tracker,
current_position,
total_count,
@@ -225,7 +239,7 @@ impl EventHandler {
self.is_edit_mode = false;
self.edit_mode_cooldown = true;
let has_changes = if app_state.ui.show_login {
let has_changes = if app_state.ui.show_login || app_state.ui.show_register{
auth_state.has_unsaved_changes()
} else {
form_state.has_unsaved_changes()
@@ -239,12 +253,12 @@ impl EventHandler {
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
let current_input = if app_state.ui.show_login {
let current_input = if app_state.ui.show_login || app_state.ui.show_register{
auth_state.get_current_input()
} else {
form_state.get_current_input()
};
let current_cursor_pos = if app_state.ui.show_login {
let current_cursor_pos = if app_state.ui.show_login || app_state.ui.show_register{
auth_state.current_cursor_pos()
} else {
form_state.current_cursor_pos()
@@ -252,7 +266,7 @@ impl EventHandler {
if !current_input.is_empty() && current_cursor_pos >= current_input.len() {
let new_pos = current_input.len() - 1;
if app_state.ui.show_login {
if app_state.ui.show_login || app_state.ui.show_register{
auth_state.set_current_cursor_pos(new_pos);
self.ideal_cursor_column = auth_state.current_cursor_pos();
} else {
@@ -274,6 +288,7 @@ impl EventHandler {
action,
form_state,
auth_state,
register_state,
grpc_client,
&mut self.auth_client,
terminal,
@@ -287,16 +302,17 @@ impl EventHandler {
}
let message = edit::handle_edit_event(
app_state.ui.show_login,
key,
config,
form_state,
auth_state,
register_state,
&mut self.ideal_cursor_column,
&mut self.command_message,
current_position,
total_count,
grpc_client,
app_state,
).await?;
self.key_sequence_tracker.reset();
@@ -318,7 +334,7 @@ impl EventHandler {
current_position,
total_count,
).await?;
if let EventOutcome::Ok(msg) = &outcome {
if msg == "Exited command mode" {
self.command_mode = false;

View File

@@ -23,7 +23,7 @@ impl ModeManager {
return AppMode::General;
}
if app_state.ui.show_login {
if app_state.ui.show_login || app_state.ui.show_register {
if event_handler.is_edit_mode {
AppMode::Edit
} else {

View File

@@ -2,7 +2,8 @@
use tonic::transport::Channel;
use common::proto::multieko2::auth::{
auth_service_client::AuthServiceClient,
LoginRequest, LoginResponse
LoginRequest, LoginResponse,
RegisterRequest, AuthResponse,
};
pub struct AuthClient {
@@ -15,9 +16,28 @@ impl AuthClient {
Ok(Self { client })
}
/// Login user via gRPC.
pub async fn login(&mut self, identifier: String, password: String) -> Result<LoginResponse, Box<dyn std::error::Error>> {
let request = tonic::Request::new(LoginRequest { identifier, password });
let response = self.client.login(request).await?.into_inner();
Ok(response)
}
/// Registers a new user via gRPC.
pub async fn register(
&mut self,
username: String,
email: String,
password: Option<String>, // Use Option for optional fields
password_confirmation: Option<String>, // Use Option for optional fields
) -> Result<AuthResponse, Box<dyn std::error::Error>> {
let request = tonic::Request::new(RegisterRequest {
username,
email,
password: password.unwrap_or_default(), // Send empty string if None
password_confirmation: password_confirmation.unwrap_or_default(), // Send empty string if None
});
let response = self.client.register(request).await?.into_inner();
Ok(response)
}
}

View File

@@ -15,6 +15,18 @@ pub struct AuthState {
pub has_unsaved_changes: bool,
}
#[derive(Default, Clone)] // Add Clone derive
pub struct RegisterState {
pub username: String,
pub email: String,
pub password: String,
pub password_confirmation: String,
pub error_message: Option<String>,
pub current_field: usize,
pub current_cursor_pos: usize,
pub has_unsaved_changes: bool,
}
impl AuthState {
pub fn new() -> Self {
Self {
@@ -32,6 +44,21 @@ impl AuthState {
}
}
impl RegisterState {
pub fn new() -> Self {
Self {
username: String::new(),
email: String::new(),
password: String::new(),
password_confirmation: String::new(),
error_message: None,
current_field: 0,
current_cursor_pos: 0,
has_unsaved_changes: false,
}
}
}
impl CanvasState for AuthState {
fn current_field(&self) -> usize {
self.current_field
@@ -102,3 +129,91 @@ impl CanvasState for AuthState {
self.has_unsaved_changes = changed;
}
}
impl CanvasState for RegisterState {
fn current_field(&self) -> usize {
self.current_field
}
fn current_cursor_pos(&self) -> usize {
let len = match self.current_field {
0 => self.username.len(),
1 => self.email.len(),
2 => self.password.len(),
3 => self.password_confirmation.len(),
_ => 0,
};
self.current_cursor_pos.min(len)
}
fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
fn inputs(&self) -> Vec<&String> {
vec![
&self.username,
&self.email,
&self.password,
&self.password_confirmation,
]
}
fn get_current_input(&self) -> &str {
match self.current_field {
0 => &self.username,
1 => &self.email,
2 => &self.password,
3 => &self.password_confirmation,
_ => "",
}
}
fn get_current_input_mut(&mut self) -> &mut String {
match self.current_field {
0 => &mut self.username,
1 => &mut self.email,
2 => &mut self.password,
3 => &mut self.password_confirmation,
_ => panic!("Invalid current_field index in RegisterState"),
}
}
fn fields(&self) -> Vec<&str> {
vec![
"Username",
"Email (Optional)",
"Password (Optional)",
"Confirm Password",
]
}
fn set_current_field(&mut self, index: usize) {
if index < 4 { // RegisterState has 4 fields
self.current_field = index;
let len = match self.current_field {
0 => self.username.len(),
1 => self.email.len(),
2 => self.password.len(),
3 => self.password_confirmation.len(),
_ => 0,
};
self.current_cursor_pos = self.current_cursor_pos.min(len);
}
}
fn set_current_cursor_pos(&mut self, pos: usize) {
let len = match self.current_field {
0 => self.username.len(),
1 => self.email.len(),
2 => self.password.len(),
3 => self.password_confirmation.len(),
_ => 0,
};
self.current_cursor_pos = pos.min(len);
}
fn set_has_unsaved_changes(&mut self, changed: bool) {
self.has_unsaved_changes = changed;
}
}

View File

@@ -4,6 +4,7 @@ use std::env;
use common::proto::multieko2::table_definition::ProfileTreeResponse;
use crate::components::IntroState;
use crate::modes::handlers::mode_manager::AppMode;
use crate::ui::handlers::context::DialogPurpose;
pub struct DialogState {
pub dialog_show: bool,
@@ -11,6 +12,7 @@ pub struct DialogState {
pub dialog_message: String,
pub dialog_buttons: Vec<String>,
pub dialog_active_button_index: usize,
pub purpose: Option<DialogPurpose>,
}
pub struct UiState {
@@ -19,9 +21,10 @@ pub struct UiState {
pub show_admin: bool,
pub show_form: bool,
pub show_login: bool,
pub show_register: bool,
pub intro_state: IntroState,
pub focus_outside_canvas: bool,
pub dialog: DialogState, // Add dialog state here
pub dialog: DialogState,
}
pub struct GeneralState {
@@ -84,21 +87,26 @@ impl AppState {
title: &str,
message: &str,
buttons: Vec<String>,
purpose: DialogPurpose,
) {
self.ui.dialog.dialog_title = title.to_string();
self.ui.dialog.dialog_message = message.to_string();
self.ui.dialog.dialog_buttons = buttons;
self.ui.dialog.dialog_active_button_index = 0; // Default to first button
self.ui.dialog.dialog_show = true; // Use new name
self.ui.dialog.dialog_active_button_index = 0;
self.ui.dialog.purpose = Some(purpose);
self.ui.dialog.dialog_show = true;
self.ui.focus_outside_canvas = true;
}
/// Hides the dialog and clears its content.
pub fn hide_dialog(&mut self) {
self.ui.dialog.dialog_show = false; // Use new name
self.ui.dialog.dialog_show = false;
self.ui.dialog.dialog_title.clear();
self.ui.dialog.dialog_message.clear();
self.ui.dialog.dialog_buttons.clear();
self.ui.dialog.dialog_active_button_index = 0;
self.ui.dialog.purpose = None;
self.ui.focus_outside_canvas = false;
}
/// Sets the active button index, wrapping around if necessary.
@@ -137,6 +145,7 @@ impl Default for UiState {
show_admin: false,
show_form: false,
show_login: false,
show_register: false,
intro_state: IntroState::new(),
focus_outside_canvas: false,
dialog: DialogState::default(),
@@ -148,11 +157,12 @@ impl Default for UiState {
impl Default for DialogState {
fn default() -> Self {
Self {
dialog_show: false, // Use new name
dialog_title: String::new(), // Use new name
dialog_message: String::new(), // Use new name
dialog_buttons: Vec::new(), // Use new name
dialog_active_button_index: 0, // Use new name
dialog_show: false,
dialog_title: String::new(),
dialog_message: String::new(),
dialog_buttons: Vec::new(),
dialog_active_button_index: 0,
purpose: None,
}
}
}

View File

@@ -2,4 +2,4 @@
pub mod form;
pub mod login;
pub mod register;

View File

@@ -1,10 +1,10 @@
// src/tui/functions/common/login.rs
use crate::services::auth::AuthClient;
use crate::state::pages::auth::AuthState;
use crate::state::state::AppState;
use crate::state::canvas_state::CanvasState;
// Remove unused import if CanvasState is not directly used here
// use crate::state::canvas_state::CanvasState;
use crate::ui::handlers::context::DialogPurpose;
/// Attempts to log the user in using the provided credentials via gRPC.
/// Updates AuthState and AppState on success or failure.
@@ -51,6 +51,7 @@ pub async fn save(
"Login Success",
&success_message,
vec!["Menu".to_string(), "Exit".to_string()],
DialogPurpose::LoginSuccess,
);
Ok("Login successful, details shown in dialog.".to_string())
@@ -62,7 +63,8 @@ pub async fn save(
app_state.show_dialog(
"Login Failed",
&error_message,
vec!["OK".to_string()], // Pass buttons here
vec!["OK".to_string()],
DialogPurpose::LoginFailed,
);
// REMOVE these lines:
// app_state.ui.dialog.dialog_title = "Login Failed".to_string();
@@ -88,13 +90,29 @@ pub async fn revert(
auth_state.error_message = None;
auth_state.set_has_unsaved_changes(false);
// Ensure dialog is hidden if revert is called
// _app_state.hide_dialog(); // Uncomment if needed
// Navigation logic (currently disabled in original code)
// _app_state.ui.show_login = false;
// _app_state.ui.show_intro = true;
"Login reverted".to_string()
}
pub async fn back_to_main(
auth_state: &mut AuthState,
app_state: &mut AppState,
) -> String {
// Clear the input fields
auth_state.username.clear();
auth_state.password.clear();
auth_state.error_message = None;
auth_state.set_has_unsaved_changes(false);
// Ensure dialog is hidden if revert is called
app_state.hide_dialog(); // Uncomment if needed
// Navigation logic (currently disabled in original code)
app_state.ui.show_login = false;
app_state.ui.show_intro = true;
// Reset focus state
app_state.ui.focus_outside_canvas = false;
app_state.general.selected_item = 0;
"Returned to main menu".to_string()
}

View File

@@ -0,0 +1,148 @@
// src/tui/functions/common/register.rs
use crate::{
services::auth::AuthClient,
state::{
pages::auth::RegisterState,
state::AppState,
canvas_state::CanvasState,
},
ui::handlers::context::DialogPurpose,
};
/// Attempts to register the user using the provided details via gRPC.
/// Updates RegisterState and AppState on success or failure.
pub async fn save(
register_state: &mut RegisterState,
auth_client: &mut AuthClient,
app_state: &mut AppState,
) -> Result<String, Box<dyn std::error::Error>> {
let username = register_state.username.clone();
let email = register_state.email.clone();
// Handle optional passwords: send None if empty, Some(value) otherwise
let password = if register_state.password.is_empty() {
None
} else {
Some(register_state.password.clone())
};
let password_confirmation = if register_state.password_confirmation.is_empty() {
None
} else {
Some(register_state.password_confirmation.clone())
};
// Basic client-side validation (example)
if username.is_empty() {
app_state.show_dialog(
"Registration Failed",
"Username cannot be empty.",
vec!["OK".to_string()],
DialogPurpose::RegisterFailed,
);
register_state.error_message = Some("Username cannot be empty.".to_string());
return Ok("Registration failed: Username cannot be empty.".to_string());
}
if password.is_some() && password != password_confirmation {
app_state.show_dialog(
"Registration Failed",
"Passwords do not match.",
vec!["OK".to_string()],
DialogPurpose::RegisterFailed,
);
register_state.error_message = Some("Passwords do not match.".to_string());
return Ok("Registration failed: Passwords do not match.".to_string());
}
// Clear previous error/dialog state before attempting
register_state.error_message = None;
app_state.hide_dialog();
// Call the gRPC register method
match auth_client.register(username, email, password, password_confirmation).await {
Ok(response) => {
// Clear fields on success? Optional, maybe wait for dialog confirmation.
// register_state.username.clear();
// register_state.email.clear();
// register_state.password.clear();
// register_state.password_confirmation.clear();
register_state.set_has_unsaved_changes(false);
let success_message = format!(
"Registration Successful!\n\n\
User ID: {}\n\
Username: {}\n\
Email: {}\n\
Role: {}",
response.id,
response.username,
response.email,
response.role
);
// Show success dialog
app_state.show_dialog(
"Registration Success",
&success_message,
vec!["OK".to_string()], // Simple OK for now
DialogPurpose::RegisterSuccess,
);
Ok("Registration successful, details shown in dialog.".to_string())
}
Err(e) => {
let error_message = format!("{}", e);
register_state.error_message = Some(error_message.clone());
register_state.set_has_unsaved_changes(true); // Keep changes on error
// Show error dialog
app_state.show_dialog(
"Registration Failed",
&error_message,
vec!["OK".to_string()],
DialogPurpose::RegisterFailed,
);
Ok(format!("Registration failed: {}", error_message))
}
}
}
/// Clears the registration form fields.
pub async fn revert(
register_state: &mut RegisterState,
_app_state: &mut AppState, // Keep signature consistent if needed elsewhere
) -> String {
register_state.username.clear();
register_state.email.clear();
register_state.password.clear();
register_state.password_confirmation.clear();
register_state.error_message = None;
register_state.set_has_unsaved_changes(false);
register_state.current_field = 0; // Reset focus to first field
register_state.current_cursor_pos = 0;
"Registration form cleared".to_string()
}
/// Clears the form and returns to the intro screen.
pub async fn back_to_main(
register_state: &mut RegisterState,
app_state: &mut AppState,
) -> String {
// Clear fields first
let _ = revert(register_state, app_state).await;
// Ensure dialog is hidden
app_state.hide_dialog();
// Navigation logic
app_state.ui.show_register = false;
app_state.ui.show_intro = true;
// Reset focus state
app_state.ui.focus_outside_canvas = false;
app_state.general.selected_item = 0; // Reset intro selection
"Returned to main menu".to_string()
}

View File

@@ -17,6 +17,12 @@ pub fn handle_intro_selection(app_state: &mut AppState, index: usize) { // Add i
app_state.ui.show_admin = false;
app_state.ui.show_login = true;
}
3 => { // Register
app_state.ui.show_intro = false;
app_state.ui.show_register = true;
app_state.ui.focus_outside_canvas = false;
app_state.general.selected_item = 0;
}
_ => {}
}
app_state.ui.show_intro = false;

View File

@@ -1,8 +1,21 @@
// src/ui/handlers/context.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UiContext {
Intro,
Login,
Register,
Admin,
Dialog,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DialogPurpose {
LoginSuccess,
LoginFailed,
RegisterSuccess,
RegisterFailed,
// TODO in the future:
// ConfirmQuit,
}

View File

@@ -7,19 +7,21 @@ use crate::components::{
handlers::sidebar::{self, calculate_sidebar_layout},
form::form::render_form,
admin::{admin_panel::AdminPanelState},
auth::login::render_login,
auth::{login::render_login, register::render_register},
};
use crate::config::colors::themes::Theme;
use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::Frame;
use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState;
use crate::state::pages::auth::RegisterState;
use crate::state::state::AppState;
pub fn render_ui(
f: &mut Frame,
form_state: &mut FormState,
auth_state: &mut AuthState,
register_state: &RegisterState,
theme: &Theme,
is_edit_mode: bool,
total_count: u64,
@@ -29,7 +31,6 @@ pub fn render_ui(
command_mode: bool,
command_message: &str,
app_state: &AppState,
// intro_state parameter removed
) {
render_background(f, f.area(), theme);
@@ -44,15 +45,23 @@ pub fn render_ui(
let main_content_area = root[0];
if app_state.ui.show_intro {
// Use app_state's intro_state directly
app_state.ui.intro_state.render(f, main_content_area, theme);
} else if app_state.ui.show_register {
render_register(
f,
main_content_area,
theme,
register_state,
app_state,
register_state.current_field < 4
);
}else if app_state.ui.show_login {
render_login(
f,
main_content_area,
theme,
auth_state,
app_state, // Add AppState reference here
app_state,
auth_state.current_field < 2
);
} else if app_state.ui.show_admin {

View File

@@ -10,8 +10,9 @@ use crate::services::ui_service::UiService;
use crate::state::canvas_state::CanvasState;
use crate::state::pages::auth::AuthState;
use crate::state::pages::form::FormState;
use crate::state::pages::auth::RegisterState;
use crate::state::state::AppState;
use crate::tui::functions::common::form::SaveOutcome; // Import SaveOutcome
// Import SaveOutcome
use crate::tui::terminal::{EventReader, TerminalCore};
use crate::ui::handlers::render::render_ui;
use crossterm::cursor::SetCursorStyle;
@@ -22,7 +23,8 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let mut grpc_client = GrpcClient::new().await?;
let mut command_handler = CommandHandler::new();
let theme = Theme::from_str(&config.colors.theme);
let mut auth_state = AuthState::default(); // The single source of truth for AuthState
let mut auth_state = AuthState::default();
let mut register_state = RegisterState::default();
// Initialize app_state first
let mut app_state = AppState::new()?;
@@ -52,9 +54,10 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
render_ui(
f,
&mut form_state,
&mut auth_state, // Pass the single AuthState instance
&mut auth_state,
&register_state,
&theme,
is_edit_mode, // Use determined edit mode
is_edit_mode,
app_state.total_count,
app_state.current_position,
&app_state.current_dir,
@@ -110,6 +113,7 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
&mut command_handler,
&mut form_state,
&mut auth_state,
&mut register_state,
&mut app_state,
total_count, // Pass the count *before* potential save
&mut current_position,
@@ -239,8 +243,17 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
form_state.current_cursor_pos =
event_handler.ideal_cursor_column.min(max_cursor_pos);
}
} else if app_state.ui.show_register {
if !event_handler.is_edit_mode {
let current_input = register_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
0
};
register_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
}
} else if app_state.ui.show_login {
// Handle cursor updates for AuthState if needed, similar to FormState
if !event_handler.is_edit_mode {
let current_input = auth_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {