Compare commits

...

5 Commits

Author SHA1 Message Date
filipriec
82e96f7b86 vim or default mode workin properly now 2025-05-24 19:25:35 +02:00
filipriec
7229e2abbd weird highlight is gone 2025-05-24 18:53:06 +02:00
filipriec
4e35043da0 vim keybinings are now working properly well 2025-05-24 18:41:41 +02:00
filipriec
56fe1c2ccc text area working now perfectly well 2025-05-24 16:34:59 +02:00
filipriec
a874edf2a1 text area on focus is now big 2025-05-24 14:24:19 +02:00
10 changed files with 730 additions and 358 deletions

82
Cargo.lock generated
View File

@@ -402,7 +402,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"common", "common",
"crossterm 0.29.0", "crossterm",
"dirs 6.0.0", "dirs 6.0.0",
"dotenvy", "dotenvy",
"lazy_static", "lazy_static",
@@ -415,6 +415,7 @@ dependencies = [
"tonic", "tonic",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"tui-textarea",
"unicode-segmentation", "unicode-segmentation",
"unicode-width 0.2.0", "unicode-width 0.2.0",
] ]
@@ -484,15 +485,6 @@ version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e"
[[package]]
name = "convert_case"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -620,24 +612,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "crossterm"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags",
"crossterm_winapi",
"derive_more",
"document-features",
"mio",
"parking_lot",
"rustix 1.0.5",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]] [[package]]
name = "crossterm_winapi" name = "crossterm_winapi"
version = "0.9.1" version = "0.9.1"
@@ -726,27 +700,6 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -812,15 +765,6 @@ dependencies = [
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]]
name = "document-features"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
dependencies = [
"litrs",
]
[[package]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
@@ -1681,12 +1625,6 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
[[package]]
name = "litrs"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.12"
@@ -2354,7 +2292,7 @@ dependencies = [
"bitflags", "bitflags",
"cassowary", "cassowary",
"compact_str", "compact_str",
"crossterm 0.28.1", "crossterm",
"indoc", "indoc",
"instability", "instability",
"itertools 0.13.0", "itertools 0.13.0",
@@ -3606,6 +3544,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78122066b0cb818b8afd08f7ed22f7fdbc3e90815035726f0840d0d26c0747a" checksum = "e78122066b0cb818b8afd08f7ed22f7fdbc3e90815035726f0840d0d26c0747a"
[[package]]
name = "tui-textarea"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae"
dependencies = [
"crossterm",
"ratatui",
"regex",
"unicode-width 0.2.0",
]
[[package]] [[package]]
name = "typed-arena" name = "typed-arena"
version = "2.0.2" version = "2.0.2"
@@ -3899,7 +3849,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]

View File

@@ -9,12 +9,12 @@ anyhow = "1.0.98"
async-trait = "0.1.88" async-trait = "0.1.88"
common = { path = "../common" } common = { path = "../common" }
crossterm = "0.29.0" crossterm = "0.28.1"
dirs = "6.0.0" dirs = "6.0.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
lazy_static = "1.5.0" lazy_static = "1.5.0"
prost = "0.13.5" prost = "0.13.5"
ratatui = "0.29.0" ratatui = { version = "0.29.0", features = ["crossterm"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
time = "0.3.41" time = "0.3.41"
tokio = { version = "1.44.2", features = ["full", "macros"] } tokio = { version = "1.44.2", features = ["full", "macros"] }
@@ -22,5 +22,6 @@ toml = "0.8.20"
tonic = "0.13.0" tonic = "0.13.0"
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = "0.3.19" tracing-subscriber = "0.3.19"
tui-textarea = { version = "0.7.0", features = ["crossterm", "ratatui", "search"] }
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
unicode-width = "0.2.0" unicode-width = "0.2.0"

View File

@@ -83,6 +83,9 @@ force_quit = ["q!"]
save_and_quit = ["wq"] save_and_quit = ["wq"]
revert = ["r"] revert = ["r"]
[editor]
keybinding_mode = "vim" # Options: "default", "vim", "emacs"
[colors] [colors]
theme = "dark" theme = "dark"
# Options: "light", "dark", "high_contrast" # Options: "light", "dark", "high_contrast"

View File

@@ -7,12 +7,14 @@ use crate::state::pages::canvas_state::CanvasState;
use ratatui::{ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style}, style::{Modifier, Style},
text::{Line, Span, Text}, text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph}, widgets::{Block, BorderType, Borders, Paragraph},
Frame, Frame,
}; };
use crate::components::handlers::canvas::render_canvas; use crate::components::handlers::canvas::render_canvas;
use crate::components::common::dialog; use crate::components::common::dialog;
use crate::config::binds::config::EditorKeybindingMode;
use crate::components::common::text_editor::TextEditor;
pub fn render_add_logic( pub fn render_add_logic(
f: &mut Frame, f: &mut Frame,
@@ -33,14 +35,52 @@ pub fn render_add_logic(
let inner_area = main_block.inner(area); let inner_area = main_block.inner(area);
f.render_widget(main_block, area); f.render_widget(main_block, area);
// Calculate areas dynamically like add_table if add_logic_state.current_focus == AddLogicFocus::InputScriptContent {
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
let border_style_color = if is_edit_mode { theme.highlight } else { theme.secondary };
let border_style = Style::default().fg(border_style_color);
editor_ref.set_cursor_line_style(Style::default());
let script_title_hint = match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => {
let vim_mode_status = TextEditor::get_vim_mode_status(&add_logic_state.vim_state);
if is_edit_mode {
format!("Script (VIM {}) - Esc for Normal. Tab navigates from Normal.", vim_mode_status)
} else {
format!("Script (VIM {}) - 'i'/'a'/'o' for Insert. Tab to navigate.", vim_mode_status)
}
}
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
if is_edit_mode {
"Script (Editing - Esc to exit edit. Tab navigates after exit.)".to_string()
} else {
"Script (Press Enter or Ctrl+E to edit. Tab to navigate.)".to_string()
}
}
};
editor_ref.set_block(
Block::default()
.title(Span::styled(script_title_hint, Style::default().fg(theme.fg)))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(border_style),
);
// Remove .widget() call - just pass the reference directly
f.render_widget(&*editor_ref, inner_area);
return;
}
// ... rest of the layout code ...
let main_chunks = Layout::default() let main_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
Constraint::Length(3), // Top Info (Profile/Table) Constraint::Length(3),
Constraint::Length(9), // Canvas Area (3 input fields × 3 lines each) Constraint::Length(9),
Constraint::Min(5), // Script Content Area Constraint::Min(5),
Constraint::Length(3), // Bottom Buttons Constraint::Length(3),
]) ])
.split(inner_area); .split(inner_area);
@@ -49,19 +89,22 @@ pub fn render_add_logic(
let script_content_area = main_chunks[2]; let script_content_area = main_chunks[2];
let buttons_area = main_chunks[3]; let buttons_area = main_chunks[3];
// Top Info Rendering (like add_table)
let profile_text = Paragraph::new(vec![ let profile_text = Paragraph::new(vec![
Line::from(Span::styled( Line::from(Span::styled(
format!("Profile: {}", add_logic_state.profile_name), format!("Profile: {}", add_logic_state.profile_name),
theme.fg, Style::default().fg(theme.fg),
)), )),
Line::from(Span::styled( Line::from(Span::styled(
format!("Table: {}", format!(
add_logic_state.selected_table_id "Table: {}",
add_logic_state
.selected_table_name
.clone()
.unwrap_or_else(|| add_logic_state.selected_table_id
.map(|id| format!("ID {}", id)) .map(|id| format!("ID {}", id))
.unwrap_or_else(|| "Global".to_string()) .unwrap_or_else(|| "Global (Not Selected)".to_string()))
), ),
theme.fg, Style::default().fg(theme.fg),
)), )),
]) ])
.block( .block(
@@ -71,14 +114,12 @@ pub fn render_add_logic(
); );
f.render_widget(profile_text, top_info_area); f.render_widget(profile_text, top_info_area);
// Canvas rendering for input fields (like add_table)
let focus_on_canvas_inputs = matches!( let focus_on_canvas_inputs = matches!(
add_logic_state.current_focus, add_logic_state.current_focus,
AddLogicFocus::InputLogicName AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn | AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription | AddLogicFocus::InputDescription
); );
render_canvas( render_canvas(
f, f,
canvas_area, canvas_area,
@@ -91,27 +132,32 @@ pub fn render_add_logic(
highlight_state, highlight_state,
); );
// Script Content Area {
let script_block_border_style = if add_logic_state.current_focus == AddLogicFocus::InputScriptContent { let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
Style::default().fg(theme.highlight) editor_ref.set_cursor_line_style(Style::default());
let border_style_color = if add_logic_state.current_focus == AddLogicFocus::InputScriptContent {
theme.highlight
} else { } else {
Style::default().fg(theme.secondary) theme.secondary
}; };
let script_block = Block::default() let title_hint = match add_logic_state.editor_keybinding_mode {
.title(" Steel Script Content ") EditorKeybindingMode::Vim => "Script Preview (VIM - Focus with Tab, then 'i'/'a'/'o' to edit)",
_ => "Script Preview (Focus with Tab, then Enter/Ctrl+E to edit)",
};
editor_ref.set_block(
Block::default()
.title(title_hint)
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(script_block_border_style); .border_style(Style::default().fg(border_style_color)),
);
// Remove .widget() call here too
f.render_widget(&*editor_ref, script_content_area);
}
let script_text = Text::from(add_logic_state.script_content_input.as_str());
let script_paragraph = Paragraph::new(script_text)
.block(script_block)
.scroll(add_logic_state.script_content_scroll)
.style(Style::default().fg(theme.fg));
f.render_widget(script_paragraph, script_content_area);
// Button Style Helpers (same as add_table)
let get_button_style = |button_focus: AddLogicFocus, current_focus| { let get_button_style = |button_focus: AddLogicFocus, current_focus| {
let is_focused = current_focus == button_focus; let is_focused = current_focus == button_focus;
let base_style = Style::default().fg(if is_focused { let base_style = Style::default().fg(if is_focused {
@@ -134,12 +180,11 @@ pub fn render_add_logic(
} }
}; };
// Bottom Buttons (same style as add_table)
let button_chunks = Layout::default() let button_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([ .constraints([
Constraint::Percentage(50), // Save Button Constraint::Percentage(50),
Constraint::Percentage(50), // Cancel Button Constraint::Percentage(50),
]) ])
.split(buttons_area); .split(buttons_area);
@@ -177,7 +222,6 @@ pub fn render_add_logic(
); );
f.render_widget(cancel_button, button_chunks[1]); f.render_widget(cancel_button, button_chunks[1]);
// Dialog rendering (same as add_table)
if app_state.ui.dialog.dialog_show { if app_state.ui.dialog.dialog_show {
dialog::render_dialog( dialog::render_dialog(
f, f,
@@ -191,4 +235,3 @@ pub fn render_add_logic(
); );
} }
} }

View File

@@ -1,12 +1,14 @@
// src/components/common.rs // src/components/common.rs
pub mod command_line; pub mod command_line;
pub mod status_line; pub mod status_line;
pub mod text_editor;
pub mod background; pub mod background;
pub mod dialog; pub mod dialog;
pub mod autocomplete; pub mod autocomplete;
pub use command_line::*; pub use command_line::*;
pub use status_line::*; pub use status_line::*;
pub use text_editor::*;
pub use background::*; pub use background::*;
pub use dialog::*; pub use dialog::*;
pub use autocomplete::*; pub use autocomplete::*;

View File

@@ -0,0 +1,331 @@
// src/components/common/text_editor.rs
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
use ratatui::style::{Color, Style, Modifier};
use tui_textarea::{Input, Key, TextArea, CursorMove, Scrolling};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VimMode {
Normal,
Insert,
Visual,
Operator(char),
}
impl VimMode {
pub fn cursor_style(&self) -> Style {
let color = match self {
Self::Normal => Color::Reset,
Self::Insert => Color::LightBlue,
Self::Visual => Color::LightYellow,
Self::Operator(_) => Color::LightGreen,
};
Style::default().fg(color).add_modifier(Modifier::REVERSED)
}
}
impl fmt::Display for VimMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
Self::Normal => write!(f, "NORMAL"),
Self::Insert => write!(f, "INSERT"),
Self::Visual => write!(f, "VISUAL"),
Self::Operator(c) => write!(f, "OPERATOR({})", c),
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum Transition {
Nop,
Mode(VimMode),
Pending(Input),
}
#[derive(Debug, Clone)]
pub struct VimState {
pub mode: VimMode,
pub pending: Input,
}
impl Default for VimState {
fn default() -> Self {
Self {
mode: VimMode::Normal,
pending: Input::default(),
}
}
}
impl VimState {
pub fn new(mode: VimMode) -> Self {
Self {
mode,
pending: Input::default(),
}
}
fn with_pending(self, pending: Input) -> Self {
Self {
mode: self.mode,
pending,
}
}
fn transition(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
if input.key == Key::Null {
return Transition::Nop;
}
match self.mode {
VimMode::Normal | VimMode::Visual | VimMode::Operator(_) => {
match input {
Input { key: Key::Char('h'), .. } => textarea.move_cursor(CursorMove::Back),
Input { key: Key::Char('j'), .. } => textarea.move_cursor(CursorMove::Down),
Input { key: Key::Char('k'), .. } => textarea.move_cursor(CursorMove::Up),
Input { key: Key::Char('l'), .. } => textarea.move_cursor(CursorMove::Forward),
Input { key: Key::Char('w'), .. } => textarea.move_cursor(CursorMove::WordForward),
Input { key: Key::Char('e'), ctrl: false, .. } => {
textarea.move_cursor(CursorMove::WordEnd);
if matches!(self.mode, VimMode::Operator(_)) {
textarea.move_cursor(CursorMove::Forward);
}
}
Input { key: Key::Char('b'), ctrl: false, .. } => textarea.move_cursor(CursorMove::WordBack),
Input { key: Key::Char('^'), .. } => textarea.move_cursor(CursorMove::Head),
Input { key: Key::Char('$'), .. } => textarea.move_cursor(CursorMove::End),
Input { key: Key::Char('0'), .. } => textarea.move_cursor(CursorMove::Head),
Input { key: Key::Char('D'), .. } => {
textarea.delete_line_by_end();
return Transition::Mode(VimMode::Normal);
}
Input { key: Key::Char('C'), .. } => {
textarea.delete_line_by_end();
textarea.cancel_selection();
return Transition::Mode(VimMode::Insert);
}
Input { key: Key::Char('p'), .. } => {
textarea.paste();
return Transition::Mode(VimMode::Normal);
}
Input { key: Key::Char('u'), ctrl: false, .. } => {
textarea.undo();
return Transition::Mode(VimMode::Normal);
}
Input { key: Key::Char('r'), ctrl: true, .. } => {
textarea.redo();
return Transition::Mode(VimMode::Normal);
}
Input { key: Key::Char('x'), .. } => {
textarea.delete_next_char();
return Transition::Mode(VimMode::Normal);
}
Input { key: Key::Char('i'), .. } => {
textarea.cancel_selection();
return Transition::Mode(VimMode::Insert);
}
Input { key: Key::Char('a'), .. } => {
textarea.cancel_selection();
textarea.move_cursor(CursorMove::Forward);
return Transition::Mode(VimMode::Insert);
}
Input { key: Key::Char('A'), .. } => {
textarea.cancel_selection();
textarea.move_cursor(CursorMove::End);
return Transition::Mode(VimMode::Insert);
}
Input { key: Key::Char('o'), .. } => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
return Transition::Mode(VimMode::Insert);
}
Input { key: Key::Char('O'), .. } => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
return Transition::Mode(VimMode::Insert);
}
Input { key: Key::Char('I'), .. } => {
textarea.cancel_selection();
textarea.move_cursor(CursorMove::Head);
return Transition::Mode(VimMode::Insert);
}
Input { key: Key::Char('v'), ctrl: false, .. } if self.mode == VimMode::Normal => {
textarea.start_selection();
return Transition::Mode(VimMode::Visual);
}
Input { key: Key::Char('V'), ctrl: false, .. } if self.mode == VimMode::Normal => {
textarea.move_cursor(CursorMove::Head);
textarea.start_selection();
textarea.move_cursor(CursorMove::End);
return Transition::Mode(VimMode::Visual);
}
Input { key: Key::Esc, .. } | Input { key: Key::Char('v'), ctrl: false, .. } if self.mode == VimMode::Visual => {
textarea.cancel_selection();
return Transition::Mode(VimMode::Normal);
}
Input { key: Key::Char('g'), ctrl: false, .. } if matches!(
self.pending,
Input { key: Key::Char('g'), ctrl: false, .. }
) => {
textarea.move_cursor(CursorMove::Top)
}
Input { key: Key::Char('G'), ctrl: false, .. } => textarea.move_cursor(CursorMove::Bottom),
Input { key: Key::Char(c), ctrl: false, .. } if self.mode == VimMode::Operator(c) => {
textarea.move_cursor(CursorMove::Head);
textarea.start_selection();
let cursor = textarea.cursor();
textarea.move_cursor(CursorMove::Down);
if cursor == textarea.cursor() {
textarea.move_cursor(CursorMove::End);
}
}
Input { key: Key::Char(op @ ('y' | 'd' | 'c')), ctrl: false, .. } if self.mode == VimMode::Normal => {
textarea.start_selection();
return Transition::Mode(VimMode::Operator(op));
}
Input { key: Key::Char('y'), ctrl: false, .. } if self.mode == VimMode::Visual => {
textarea.move_cursor(CursorMove::Forward);
textarea.copy();
return Transition::Mode(VimMode::Normal);
}
Input { key: Key::Char('d'), ctrl: false, .. } if self.mode == VimMode::Visual => {
textarea.move_cursor(CursorMove::Forward);
textarea.cut();
return Transition::Mode(VimMode::Normal);
}
Input { key: Key::Char('c'), ctrl: false, .. } if self.mode == VimMode::Visual => {
textarea.move_cursor(CursorMove::Forward);
textarea.cut();
return Transition::Mode(VimMode::Insert);
}
// Arrow keys work in normal mode
Input { key: Key::Up, .. } => textarea.move_cursor(CursorMove::Up),
Input { key: Key::Down, .. } => textarea.move_cursor(CursorMove::Down),
Input { key: Key::Left, .. } => textarea.move_cursor(CursorMove::Back),
Input { key: Key::Right, .. } => textarea.move_cursor(CursorMove::Forward),
input => return Transition::Pending(input),
}
// Handle the pending operator
match self.mode {
VimMode::Operator('y') => {
textarea.copy();
Transition::Mode(VimMode::Normal)
}
VimMode::Operator('d') => {
textarea.cut();
Transition::Mode(VimMode::Normal)
}
VimMode::Operator('c') => {
textarea.cut();
Transition::Mode(VimMode::Insert)
}
_ => Transition::Nop,
}
}
VimMode::Insert => match input {
Input { key: Key::Esc, .. } | Input { key: Key::Char('c'), ctrl: true, .. } => {
Transition::Mode(VimMode::Normal)
}
input => {
textarea.input(input);
Transition::Mode(VimMode::Insert)
}
},
}
}
}
pub struct TextEditor;
impl TextEditor {
pub fn new_textarea(editor_config: &EditorConfig) -> TextArea<'static> {
let mut textarea = TextArea::default();
if editor_config.show_line_numbers {
textarea.set_line_number_style(Style::default().fg(Color::DarkGray));
}
textarea.set_tab_length(editor_config.tab_width);
textarea
}
pub fn handle_input(
textarea: &mut TextArea<'static>,
key_event: KeyEvent,
keybinding_mode: &EditorKeybindingMode,
vim_state: &mut VimState,
) -> bool {
match keybinding_mode {
EditorKeybindingMode::Vim => {
Self::handle_vim_input(textarea, key_event, vim_state)
}
_ => {
let tui_input: Input = key_event.into();
textarea.input(tui_input)
}
}
}
fn handle_vim_input(
textarea: &mut TextArea<'static>,
key_event: KeyEvent,
vim_state: &mut VimState,
) -> bool {
let input = Self::convert_key_event_to_input(key_event);
*vim_state = match vim_state.transition(input, textarea) {
Transition::Mode(mode) if vim_state.mode != mode => {
// Update cursor style based on mode
textarea.set_cursor_style(mode.cursor_style());
VimState::new(mode)
}
Transition::Nop | Transition::Mode(_) => vim_state.clone(),
Transition::Pending(input) => vim_state.clone().with_pending(input),
};
true // Always consider input as handled in vim mode
}
fn convert_key_event_to_input(key_event: KeyEvent) -> Input {
let key = match key_event.code {
KeyCode::Char(c) => Key::Char(c),
KeyCode::Enter => Key::Enter,
KeyCode::Left => Key::Left,
KeyCode::Right => Key::Right,
KeyCode::Up => Key::Up,
KeyCode::Down => Key::Down,
KeyCode::Backspace => Key::Backspace,
KeyCode::Delete => Key::Delete,
KeyCode::Home => Key::Home,
KeyCode::End => Key::End,
KeyCode::PageUp => Key::PageUp,
KeyCode::PageDown => Key::PageDown,
KeyCode::Tab => Key::Tab,
KeyCode::Esc => Key::Esc,
_ => Key::Null,
};
Input {
key,
ctrl: key_event.modifiers.contains(KeyModifiers::CONTROL),
alt: key_event.modifiers.contains(KeyModifiers::ALT),
shift: key_event.modifiers.contains(KeyModifiers::SHIFT),
}
}
pub fn get_vim_mode_status(vim_state: &VimState) -> String {
vim_state.mode.to_string()
}
pub fn is_vim_insert_mode(vim_state: &VimState) -> bool {
matches!(vim_state.mode, VimMode::Insert)
}
pub fn is_vim_normal_mode(vim_state: &VimState) -> bool {
matches!(vim_state.mode, VimMode::Normal)
}
}

View File

@@ -1,11 +1,57 @@
// src/config/binds/config.rs // src/config/binds/config.rs
use serde::Deserialize; use serde::{Deserialize, Serialize}; // Added Serialize for EditorKeybindingMode if needed elsewhere
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use crossterm::event::{KeyCode, KeyModifiers}; use crossterm::event::{KeyCode, KeyModifiers};
// NEW: Editor Keybinding Mode Enum
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum EditorKeybindingMode {
#[serde(rename = "default")]
Default,
#[serde(rename = "vim")]
Vim,
#[serde(rename = "emacs")]
Emacs,
}
impl Default for EditorKeybindingMode {
fn default() -> Self {
EditorKeybindingMode::Default
}
}
// NEW: Editor Configuration Struct
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditorConfig {
#[serde(default)]
pub keybinding_mode: EditorKeybindingMode,
#[serde(default = "default_show_line_numbers")]
pub show_line_numbers: bool,
#[serde(default = "default_tab_width")]
pub tab_width: u8,
}
fn default_show_line_numbers() -> bool {
true
}
fn default_tab_width() -> u8 {
4
}
impl Default for EditorConfig {
fn default() -> Self {
EditorConfig {
keybinding_mode: EditorKeybindingMode::default(),
show_line_numbers: default_show_line_numbers(),
tab_width: default_tab_width(),
}
}
}
#[derive(Debug, Deserialize, Default)] #[derive(Debug, Deserialize, Default)]
pub struct ColorsConfig { pub struct ColorsConfig {
#[serde(default = "default_theme")] #[serde(default = "default_theme")]
@@ -22,9 +68,14 @@ pub struct Config {
pub keybindings: ModeKeybindings, pub keybindings: ModeKeybindings,
#[serde(default)] #[serde(default)]
pub colors: ColorsConfig, pub colors: ColorsConfig,
// NEW: Add editor configuration
#[serde(default)]
pub editor: EditorConfig,
} }
#[derive(Debug, Deserialize)] // ... (rest of your Config struct and impl Config remains the same)
// Make sure ModeKeybindings is also deserializable if it's not already
#[derive(Debug, Deserialize, Default)] // Added Default here if not present
pub struct ModeKeybindings { pub struct ModeKeybindings {
#[serde(default)] #[serde(default)]
pub general: HashMap<String, Vec<String>>, pub general: HashMap<String, Vec<String>>,
@@ -49,11 +100,11 @@ impl Config {
let config_path = Path::new(manifest_dir).join("config.toml"); let config_path = Path::new(manifest_dir).join("config.toml");
let config_str = std::fs::read_to_string(&config_path) let config_str = std::fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config file at {:?}", config_path))?; .with_context(|| format!("Failed to read config file at {:?}", config_path))?;
let config: Config = toml::from_str(&config_str)?; let config: Config = toml::from_str(&config_str)
.with_context(|| format!("Failed to parse config file: {}. Check for syntax errors or missing fields like an empty [editor] section if you added it.", config_str))?; // Enhanced error message
Ok(config) Ok(config)
} }
pub fn get_general_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { pub fn get_general_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
self.get_action_for_key_in_mode(&self.keybindings.general, key, modifiers) self.get_action_for_key_in_mode(&self.keybindings.general, key, modifiers)
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers)) .or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))

View File

@@ -1,22 +1,23 @@
// client/src/functions/modes/navigation/add_logic_nav.rs // src/functions/modes/navigation/add_logic_nav.rs
use crate::config::binds::config::Config; use crate::config::binds::config::{Config, EditorKeybindingMode};
use crate::state::{ use crate::state::{
app::state::AppState, app::state::AppState,
pages::add_logic::{AddLogicFocus, AddLogicState}, pages::add_logic::{AddLogicFocus, AddLogicState},
app::buffer::AppView, app::buffer::AppView,
app::buffer::BufferState, app::buffer::BufferState,
}; };
use crate::state::pages::canvas_state::CanvasState; use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
use crossterm::event::{KeyEvent};
use crate::services::GrpcClient; use crate::services::GrpcClient;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use anyhow::Result; use anyhow::Result;
use common::proto::multieko2::table_script::{PostTableScriptRequest}; use common::proto::multieko2::table_script::PostTableScriptRequest;
use crate::components::common::text_editor::TextEditor;
use tui_textarea::Input as TextAreaInput;
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>; pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
pub fn handle_add_logic_navigation( pub fn handle_add_logic_navigation(
key: KeyEvent, key_event: KeyEvent,
config: &Config, config: &Config,
app_state: &mut AppState, app_state: &mut AppState,
add_logic_state: &mut AddLogicState, add_logic_state: &mut AddLogicState,
@@ -26,250 +27,236 @@ pub fn handle_add_logic_navigation(
save_logic_sender: SaveLogicResultSender, save_logic_sender: SaveLogicResultSender,
command_message: &mut String, command_message: &mut String,
) -> bool { ) -> bool {
let action = config.get_general_action(key.code, key.modifiers).map(String::from);
let mut handled = false; let mut handled = false;
let general_action = config.get_general_action(key_event.code, key_event.modifiers);
// Check if focus is on canvas input fields if add_logic_state.current_focus == AddLogicFocus::InputScriptContent {
let focus_on_canvas_inputs = matches!( let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
add_logic_state.current_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => {
if *is_edit_mode { // App considers textarea to be in "typing" (Insert) mode
let changed = TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
); );
if changed { add_logic_state.has_unsaved_changes = true; }
// Handle script content editing separately (multiline) // Check if we've transitioned to Normal mode
if *is_edit_mode && add_logic_state.current_focus == AddLogicFocus::InputScriptContent { if key_event.code == KeyCode::Esc && TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) {
match key.code { *is_edit_mode = false;
crossterm::event::KeyCode::Char(c) => { *command_message = "VIM: Normal Mode. Tab to navigate.".to_string();
add_logic_state.script_content_input.push(c); }
add_logic_state.has_unsaved_changes = true; handled = true;
} else { // App considers textarea to be in "navigation" (Normal) mode
match key_event.code {
// Keys to enter Vim Insert mode
KeyCode::Char('i') | KeyCode::Char('a') | KeyCode::Char('o') |
KeyCode::Char('I') | KeyCode::Char('A') | KeyCode::Char('O') => {
*is_edit_mode = true;
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state
);
*command_message = "VIM: Insert Mode.".to_string();
handled = true; handled = true;
} }
crossterm::event::KeyCode::Enter => { _ => {
add_logic_state.script_content_input.push('\n'); if general_action.is_none() {
add_logic_state.has_unsaved_changes = true; let changed = TextEditor::handle_input(
add_logic_state.script_content_scroll.0 = add_logic_state.script_content_scroll.0.saturating_add(1); &mut editor_borrow,
handled = true; key_event,
} &add_logic_state.editor_keybinding_mode,
crossterm::event::KeyCode::Backspace => { &mut add_logic_state.vim_state,
if !add_logic_state.script_content_input.is_empty() { );
add_logic_state.script_content_input.pop(); if changed { add_logic_state.has_unsaved_changes = true; }
add_logic_state.has_unsaved_changes = true;
handled = true; handled = true;
} }
} }
_ => {}
} }
} }
}
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
if *is_edit_mode {
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
*is_edit_mode = false;
*command_message = "Exited script edit. Tab to navigate.".to_string();
handled = true;
} else if general_action.is_some() && (general_action.unwrap() == "next_field" || general_action.unwrap() == "prev_field") {
let changed = TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state
);
if changed { add_logic_state.has_unsaved_changes = true; }
handled = true;
} else {
let changed = TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state
);
if changed { add_logic_state.has_unsaved_changes = true; }
handled = true;
}
}
}
}
if handled { return true; }
}
if !handled { // If not handled above (e.g., Tab/Shift+Tab, or Enter when script content not in edit mode),
match action.as_deref() { // process general application-level actions.
let action_str = general_action.map(String::from);
match action_str.as_deref() {
Some("exit_view") | Some("cancel_action") => { Some("exit_view") | Some("cancel_action") => {
buffer_state.update_history(AppView::Admin); // Fixed: was AdminPanel buffer_state.update_history(AppView::Admin);
app_state.ui.focus_outside_canvas = true; app_state.ui.show_add_logic = false;
*command_message = "Exited Add Logic".to_string(); *command_message = "Exited Add Logic".to_string();
*is_edit_mode = false;
handled = true; handled = true;
} }
Some("next_field") => { Some("next_field") | Some("prev_field") => {
let is_next = action_str.as_deref() == Some("next_field");
let previous_focus = add_logic_state.current_focus; let previous_focus = add_logic_state.current_focus;
add_logic_state.current_focus = match add_logic_state.current_focus {
add_logic_state.current_focus = if is_next {
match add_logic_state.current_focus {
AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn, AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn,
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription, AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription,
AddLogicFocus::InputDescription => AddLogicFocus::InputScriptContent, AddLogicFocus::InputDescription => AddLogicFocus::InputScriptContent,
AddLogicFocus::InputScriptContent => AddLogicFocus::SaveButton, AddLogicFocus::InputScriptContent => AddLogicFocus::SaveButton,
AddLogicFocus::SaveButton => AddLogicFocus::CancelButton, AddLogicFocus::SaveButton => AddLogicFocus::CancelButton,
AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName, AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName,
};
// Update canvas field index only when moving between canvas inputs
if matches!(previous_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn) {
if matches!(add_logic_state.current_focus, AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription) {
let new_field = match add_logic_state.current_focus {
AddLogicFocus::InputTargetColumn => 1,
AddLogicFocus::InputDescription => 2,
_ => 0,
};
add_logic_state.set_current_field(new_field);
} }
} } else {
match add_logic_state.current_focus {
// Update focus outside canvas flag
app_state.ui.focus_outside_canvas = !matches!(
add_logic_state.current_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription
);
*command_message = format!("Focus: {:?}", add_logic_state.current_focus);
*is_edit_mode = matches!(add_logic_state.current_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn |
AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent);
handled = true;
}
Some("prev_field") => {
let previous_focus = add_logic_state.current_focus;
add_logic_state.current_focus = match add_logic_state.current_focus {
AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton, AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton,
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName, AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName,
AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn, AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn,
AddLogicFocus::InputScriptContent => AddLogicFocus::InputDescription, AddLogicFocus::InputScriptContent => AddLogicFocus::InputDescription,
AddLogicFocus::SaveButton => AddLogicFocus::InputScriptContent, AddLogicFocus::SaveButton => AddLogicFocus::InputScriptContent,
AddLogicFocus::CancelButton => AddLogicFocus::SaveButton, AddLogicFocus::CancelButton => AddLogicFocus::SaveButton,
}
}; };
// Update canvas field index only when moving between canvas inputs if add_logic_state.current_focus == AddLogicFocus::InputScriptContent {
if matches!(previous_focus, AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription) { *is_edit_mode = false;
if matches!(add_logic_state.current_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn) { let mode_hint = match add_logic_state.editor_keybinding_mode {
let new_field = match add_logic_state.current_focus { EditorKeybindingMode::Vim => "'i'/'a'/'o' to insert",
AddLogicFocus::InputLogicName => 0, _ => "Enter/Ctrl+E to edit",
AddLogicFocus::InputTargetColumn => 1,
_ => 0,
}; };
add_logic_state.set_current_field(new_field); *command_message = format!("Focus: Script Content. Press {} or Tab.", mode_hint);
} } else if matches!(add_logic_state.current_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription) {
*is_edit_mode = true;
*command_message = format!("Focus: {:?}. Edit mode ON.", add_logic_state.current_focus);
} else {
*is_edit_mode = false;
*command_message = format!("Focus: {:?}", add_logic_state.current_focus);
} }
// Update focus outside canvas flag
app_state.ui.focus_outside_canvas = !matches!( app_state.ui.focus_outside_canvas = !matches!(
add_logic_state.current_focus, add_logic_state.current_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent
); );
*command_message = format!("Focus: {:?}", add_logic_state.current_focus);
*is_edit_mode = matches!(add_logic_state.current_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn |
AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent);
handled = true;
}
Some("next_option") => { // Horizontal next
let previous_focus = add_logic_state.current_focus;
add_logic_state.current_focus = match add_logic_state.current_focus {
AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn,
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription,
AddLogicFocus::InputDescription => AddLogicFocus::InputScriptContent,
AddLogicFocus::InputScriptContent => AddLogicFocus::SaveButton,
AddLogicFocus::SaveButton => AddLogicFocus::CancelButton,
AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName, // Cycle back
};
// Update canvas field index if moving within canvas inputs
if matches!(add_logic_state.current_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription) {
let new_field = match add_logic_state.current_focus {
AddLogicFocus::InputLogicName => 0,
AddLogicFocus::InputTargetColumn => 1,
AddLogicFocus::InputDescription => 2,
_ => add_logic_state.current_field(), // Should not happen
};
add_logic_state.set_current_field(new_field);
}
app_state.ui.focus_outside_canvas = !matches!(
add_logic_state.current_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription
);
*command_message = format!("Focus: {:?}", add_logic_state.current_focus);
*is_edit_mode = matches!(add_logic_state.current_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn |
AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent);
handled = true;
}
Some("previous_option") => { // Horizontal previous
let previous_focus = add_logic_state.current_focus;
add_logic_state.current_focus = match add_logic_state.current_focus {
AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton, // Cycle back
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName,
AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn,
AddLogicFocus::InputScriptContent => AddLogicFocus::InputDescription,
AddLogicFocus::SaveButton => AddLogicFocus::InputScriptContent,
AddLogicFocus::CancelButton => AddLogicFocus::SaveButton,
};
// Update canvas field index if moving within canvas inputs
if matches!(add_logic_state.current_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription) {
let new_field = match add_logic_state.current_focus {
AddLogicFocus::InputLogicName => 0,
AddLogicFocus::InputTargetColumn => 1,
AddLogicFocus::InputDescription => 2,
_ => add_logic_state.current_field(), // Should not happen
};
add_logic_state.set_current_field(new_field);
}
app_state.ui.focus_outside_canvas = !matches!(
add_logic_state.current_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription
);
*command_message = format!("Focus: {:?}", add_logic_state.current_focus);
*is_edit_mode = matches!(add_logic_state.current_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn |
AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent);
handled = true; handled = true;
} }
Some("select") => { Some("select") => {
match add_logic_state.current_focus { match add_logic_state.current_focus {
AddLogicFocus::SaveButton => { AddLogicFocus::InputScriptContent => {
if let Some(table_def_id) = add_logic_state.selected_table_id {
if add_logic_state.target_column_input.trim().is_empty() {
*command_message = "Cannot save: Target Column cannot be empty.".to_string();
} else if add_logic_state.script_content_input.trim().is_empty() {
*command_message = "Cannot save: Script Content cannot be empty.".to_string();
} else {
*command_message = "Saving logic script...".to_string();
app_state.show_loading_dialog("Saving Script", "Please wait...");
let request = PostTableScriptRequest {
table_definition_id: table_def_id,
target_column: add_logic_state.target_column_input.trim().to_string(),
script: add_logic_state.script_content_input.trim().to_string(),
description: add_logic_state.description_input.trim().to_string(),
};
let mut client_clone = grpc_client.clone();
let sender_clone = save_logic_sender.clone();
tokio::spawn(async move {
let result = client_clone.post_table_script(request).await
.map(|res| format!("Script saved with ID: {}", res.id))
.map_err(|e| anyhow::anyhow!("gRPC call failed: {}", e));
let _ = sender_clone.send(result).await;
});
}
} else {
*command_message = "Cannot save: Table Definition ID is missing.".to_string();
}
handled = true;
}
AddLogicFocus::CancelButton => {
buffer_state.update_history(AppView::Admin); // Fixed: was AdminPanel
app_state.ui.focus_outside_canvas = true;
*command_message = "Cancelled Add Logic".to_string();
handled = true;
}
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn |
AddLogicFocus::InputDescription | AddLogicFocus::InputScriptContent => {
if !*is_edit_mode {
*is_edit_mode = true; *is_edit_mode = true;
*command_message = "Edit mode: ON".to_string(); let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => {
TextEditor::handle_input(
&mut editor_borrow,
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
*command_message = "VIM: Insert Mode.".to_string();
} }
_ => {
*command_message = "Entered script edit mode.".to_string();
}
}
handled = true;
}
AddLogicFocus::SaveButton => { handled = true; }
AddLogicFocus::CancelButton => { *is_edit_mode = false; handled = true; }
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
*is_edit_mode = !*is_edit_mode;
*command_message = format!("Field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
handled = true; handled = true;
} }
} }
} }
Some("toggle_edit_mode") => { Some("toggle_edit_mode") => {
match add_logic_state.current_focus {
AddLogicFocus::InputScriptContent => {
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => {
if *is_edit_mode {
TextEditor::handle_input(
&mut editor_borrow,
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
if TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) {
*is_edit_mode = false;
*command_message = "VIM: Normal Mode. Tab to navigate.".to_string();
} else {
*command_message = "VIM: Still in Insert Mode (toggle error?).".to_string();
}
} else {
TextEditor::handle_input(
&mut editor_borrow,
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
*is_edit_mode = true;
*command_message = "VIM: Insert Mode.".to_string();
}
}
_ => {
*is_edit_mode = !*is_edit_mode; *is_edit_mode = !*is_edit_mode;
*command_message = format!("Edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" }); *command_message = format!("Script edit mode: {}", if *is_edit_mode { "ON" } else { "OFF. Tab to navigate." });
}
}
handled = true; handled = true;
} }
// Handle script content scrolling when not in edit mode AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
_ if !*is_edit_mode && add_logic_state.current_focus == AddLogicFocus::InputScriptContent => { *is_edit_mode = !*is_edit_mode;
match action.as_deref() { *command_message = format!("Canvas field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
Some("move_up") => {
add_logic_state.script_content_scroll.0 = add_logic_state.script_content_scroll.0.saturating_sub(1);
handled = true; handled = true;
} }
Some("move_down") => { _ => { *command_message = "Cannot toggle edit mode here.".to_string(); handled = true; }
add_logic_state.script_content_scroll.0 = add_logic_state.script_content_scroll.0.saturating_add(1); }
}
_ => {
if add_logic_state.current_focus == AddLogicFocus::InputScriptContent &&
!*is_edit_mode &&
add_logic_state.editor_keybinding_mode == EditorKeybindingMode::Vim {
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
let changed = TextEditor::handle_input(
&mut editor_borrow,
key_event,
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state
);
if changed { add_logic_state.has_unsaved_changes = true; }
handled = true; handled = true;
} }
_ => {}
}
}
_ => {}
} }
} }
handled handled
} }

View File

@@ -181,6 +181,7 @@ pub fn handle_admin_navigation(
} }
} }
// In src/functions/modes/navigation/admin_nav.rs
AdminFocus::Button1 => { // Add Logic AdminFocus::Button1 => { // Add Logic
let mut logic_state_profile_name = "None (Global)".to_string(); let mut logic_state_profile_name = "None (Global)".to_string();
let mut selected_table_id: Option<i64> = None; let mut selected_table_id: Option<i64> = None;
@@ -207,27 +208,16 @@ pub fn handle_admin_navigation(
// Keep logic_state_profile_name as "None (Global)" // Keep logic_state_profile_name as "None (Global)"
} }
// Create AddLogicState with the loaded config
admin_state.add_logic_state = AddLogicState { admin_state.add_logic_state = AddLogicState {
profile_name: logic_state_profile_name.clone(), profile_name: logic_state_profile_name.clone(),
editor_keybinding_mode: config.editor.keybinding_mode.clone(), // Add this line
..AddLogicState::default() ..AddLogicState::default()
}; };
buffer_state.update_history(AppView::AddLogic); buffer_state.update_history(AppView::AddLogic);
app_state.ui.focus_outside_canvas = false; app_state.ui.focus_outside_canvas = false;
// Command message might be overwritten if profile selection had an issue, // Rest of the code remains the same...
// so set the navigation message last if no error.
if !command_message.starts_with("Error:") && !command_message.contains("associating Logic with 'None (Global)'") {
*command_message = format!(
"Navigating to Add Logic for profile '{}'...",
logic_state_profile_name
);
} else if command_message.contains("associating Logic with 'None (Global)'") {
// Append to existing message
let existing_msg = command_message.clone();
*command_message = format!(
"{} Navigating to Add Logic...",
existing_msg
);
}
} }
AdminFocus::Button2 => { AdminFocus::Button2 => {
if let Some(p_idx) = admin_state.selected_profile_index { if let Some(p_idx) = admin_state.selected_profile_index {

View File

@@ -1,5 +1,10 @@
// src/state/pages/add_logic.rs // src/state/pages/add_logic.rs
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
use crate::state::pages::canvas_state::CanvasState; use crate::state::pages::canvas_state::CanvasState;
use crate::components::common::text_editor::{TextEditor, VimState}; // Add VimState import
use std::cell::RefCell;
use std::rc::Rc;
use tui_textarea::TextArea;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AddLogicFocus { pub enum AddLogicFocus {
@@ -12,55 +17,62 @@ pub enum AddLogicFocus {
CancelButton, CancelButton,
} }
#[derive(Debug, Clone)] #[derive(Clone, Debug)]
pub struct AddLogicState { pub struct AddLogicState {
pub profile_name: String, pub profile_name: String,
pub selected_table_id: Option<i64>, pub selected_table_id: Option<i64>,
pub selected_table_name: Option<String>, pub selected_table_name: Option<String>,
pub logic_name_input: String, pub logic_name_input: String,
pub target_column_input: String, pub target_column_input: String,
pub script_content_input: String, pub script_content_editor: Rc<RefCell<TextArea<'static>>>,
pub description_input: String, pub description_input: String,
pub current_focus: AddLogicFocus, pub current_focus: AddLogicFocus,
pub logic_name_cursor_pos: usize, pub logic_name_cursor_pos: usize,
pub target_column_cursor_pos: usize, pub target_column_cursor_pos: usize,
pub script_content_scroll: (u16, u16), // (vertical, horizontal)
pub description_cursor_pos: usize, pub description_cursor_pos: usize,
pub has_unsaved_changes: bool, pub has_unsaved_changes: bool,
pub editor_keybinding_mode: EditorKeybindingMode,
pub vim_state: VimState, // Add this field
} }
impl Default for AddLogicState { impl AddLogicState {
fn default() -> Self { pub fn new(editor_config: &EditorConfig) -> Self {
let editor = TextEditor::new_textarea(editor_config);
AddLogicState { AddLogicState {
profile_name: "default".to_string(), profile_name: "default".to_string(),
selected_table_id: None, selected_table_id: None,
selected_table_name: None, selected_table_name: None,
logic_name_input: String::new(), logic_name_input: String::new(),
target_column_input: String::new(), target_column_input: String::new(),
script_content_input: String::new(), script_content_editor: Rc::new(RefCell::new(editor)),
description_input: String::new(), description_input: String::new(),
current_focus: AddLogicFocus::InputLogicName, current_focus: AddLogicFocus::InputLogicName,
logic_name_cursor_pos: 0, logic_name_cursor_pos: 0,
target_column_cursor_pos: 0, target_column_cursor_pos: 0,
script_content_scroll: (0, 0),
description_cursor_pos: 0, description_cursor_pos: 0,
has_unsaved_changes: false, has_unsaved_changes: false,
} editor_keybinding_mode: editor_config.keybinding_mode.clone(),
vim_state: VimState::default(), // Add this field initialization
} }
} }
impl AddLogicState { pub const INPUT_FIELD_COUNT: usize = 3;
// Number of canvas-editable fields
pub const INPUT_FIELD_COUNT: usize = 3; // Logic Name, Target Column, Description
} }
impl Default for AddLogicState {
fn default() -> Self {
Self::new(&EditorConfig::default())
}
}
// ... rest of the CanvasState implementation remains the same
impl CanvasState for AddLogicState { impl CanvasState for AddLogicState {
fn current_field(&self) -> usize { fn current_field(&self) -> usize {
match self.current_focus { match self.current_focus {
AddLogicFocus::InputLogicName => 0, AddLogicFocus::InputLogicName => 0,
AddLogicFocus::InputTargetColumn => 1, AddLogicFocus::InputTargetColumn => 1,
AddLogicFocus::InputDescription => 2, AddLogicFocus::InputDescription => 2,
_ => 0, // Default or non-input focus _ => 0,
} }
} }
@@ -99,7 +111,7 @@ impl CanvasState for AddLogicState {
AddLogicFocus::InputLogicName => &mut self.logic_name_input, AddLogicFocus::InputLogicName => &mut self.logic_name_input,
AddLogicFocus::InputTargetColumn => &mut self.target_column_input, AddLogicFocus::InputTargetColumn => &mut self.target_column_input,
AddLogicFocus::InputDescription => &mut self.description_input, AddLogicFocus::InputDescription => &mut self.description_input,
_ => &mut self.logic_name_input, // Placeholder, should not be hit if focus is correct _ => &mut self.logic_name_input,
} }
} }
@@ -112,7 +124,7 @@ impl CanvasState for AddLogicState {
0 => AddLogicFocus::InputLogicName, 0 => AddLogicFocus::InputLogicName,
1 => AddLogicFocus::InputTargetColumn, 1 => AddLogicFocus::InputTargetColumn,
2 => AddLogicFocus::InputDescription, 2 => AddLogicFocus::InputDescription,
_ => self.current_focus, // Stay if out of bounds _ => self.current_focus,
}; };
} }
@@ -122,10 +134,12 @@ impl CanvasState for AddLogicState {
self.logic_name_cursor_pos = pos.min(self.logic_name_input.len()); self.logic_name_cursor_pos = pos.min(self.logic_name_input.len());
} }
AddLogicFocus::InputTargetColumn => { AddLogicFocus::InputTargetColumn => {
self.target_column_cursor_pos = pos.min(self.target_column_input.len()); self.target_column_cursor_pos =
pos.min(self.target_column_input.len());
} }
AddLogicFocus::InputDescription => { AddLogicFocus::InputDescription => {
self.description_cursor_pos = pos.min(self.description_input.len()); self.description_cursor_pos =
pos.min(self.description_input.len());
} }
_ => {} _ => {}
} }