Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
355aff3032 | ||
|
|
3bb771187a | ||
|
|
aa3ff18f9c | ||
|
|
1fc9a0e1ff | ||
|
|
cdac78c1bc | ||
|
|
bdb6cd4069 | ||
|
|
ec5802b3a2 | ||
|
|
1eb2edc1df | ||
|
|
2da009eede | ||
|
|
b2fd44df49 | ||
|
|
78e8cce08b | ||
|
|
2cf4cd6748 | ||
|
|
7caa4d8c3c | ||
|
|
9917195fc4 | ||
|
|
fbcea1b270 | ||
|
|
87b07db26a | ||
|
|
4481560025 | ||
|
|
d1d33b5752 | ||
|
|
c6c6c5ed81 | ||
|
|
4ddcb34205 | ||
|
|
83393a20e2 | ||
|
|
13d501e6d7 | ||
|
|
993febd204 | ||
|
|
49fe2aa793 | ||
|
|
87a572783a | ||
|
|
ca8dea53fd | ||
|
|
fef2f12c9a | ||
|
|
1a529a70bf | ||
|
|
8da29376ab | ||
|
|
8ad5fedcea | ||
|
|
16a7fa0bcc | ||
|
|
5f6858251c | ||
|
|
73567ae5cf | ||
|
|
fe2d1e4684 | ||
|
|
7d4b043d63 | ||
|
|
04b4220c76 | ||
|
|
841418759b | ||
|
|
ccd76eabdd | ||
|
|
fabe1e0ca7 | ||
|
|
9bf1d065d5 | ||
|
|
62aed812b6 | ||
|
|
a58e976227 | ||
|
|
c198297a5c | ||
|
|
c592dfc7f5 | ||
|
|
1b0aaa55c9 | ||
|
|
44c5963c71 | ||
|
|
911dba9bce | ||
|
|
d55dff8a3e | ||
|
|
8b2120bdc8 | ||
|
|
8ce90f3c42 | ||
|
|
62d7fb6bda | ||
|
|
27cca8763b | ||
|
|
74054f2724 |
@@ -1,6 +1,18 @@
|
|||||||
# config.toml
|
# config.toml
|
||||||
[keybindings]
|
[keybindings]
|
||||||
|
|
||||||
|
enter_command_mode = [":", "ctrl+;"]
|
||||||
|
|
||||||
|
[keybindings.general]
|
||||||
|
move_up = ["k", "Up"]
|
||||||
|
move_down = ["j", "Down"]
|
||||||
|
next_option = ["l", "Right"]
|
||||||
|
previous_option = ["h", "Left"]
|
||||||
|
select = ["Enter"]
|
||||||
|
toggle_sidebar = ["ctrl+t"]
|
||||||
|
next_field = ["Tab"]
|
||||||
|
prev_field = ["Shift+Tab"]
|
||||||
|
|
||||||
[keybindings.common]
|
[keybindings.common]
|
||||||
save = ["ctrl+s"]
|
save = ["ctrl+s"]
|
||||||
quit = ["ctrl+q"]
|
quit = ["ctrl+q"]
|
||||||
@@ -33,7 +45,6 @@ move_line_start = ["0"]
|
|||||||
move_line_end = ["$"]
|
move_line_end = ["$"]
|
||||||
move_first_line = ["gg"]
|
move_first_line = ["gg"]
|
||||||
move_last_line = ["x"]
|
move_last_line = ["x"]
|
||||||
enter_command_mode = [":", "ctrl+;"]
|
|
||||||
|
|
||||||
[keybindings.edit]
|
[keybindings.edit]
|
||||||
exit_edit_mode = ["esc","ctrl+e"]
|
exit_edit_mode = ["esc","ctrl+e"]
|
||||||
|
|||||||
4
client/src/components/admin.rs
Normal file
4
client/src/components/admin.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/components/admin.rs
|
||||||
|
pub mod admin_panel;
|
||||||
|
|
||||||
|
pub use admin_panel::*;
|
||||||
118
client/src/components/admin/admin_panel.rs
Normal file
118
client/src/components/admin/admin_panel.rs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// src/components/admin/admin_panel.rs
|
||||||
|
|
||||||
|
use ratatui::{
|
||||||
|
widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph},
|
||||||
|
style::Style,
|
||||||
|
text::{Line, Span, Text},
|
||||||
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use common::proto::multieko2::table_definition::ProfileTreeResponse;
|
||||||
|
use crate::config::colors::themes::Theme;
|
||||||
|
|
||||||
|
pub struct AdminPanelState {
|
||||||
|
pub list_state: ListState,
|
||||||
|
pub profiles: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminPanelState {
|
||||||
|
pub fn new(profiles: Vec<String>) -> Self {
|
||||||
|
let mut list_state = ListState::default();
|
||||||
|
if !profiles.is_empty() {
|
||||||
|
list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
Self { list_state, profiles }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
let i = self.list_state.selected().map_or(0, |i|
|
||||||
|
if i >= self.profiles.len() - 1 { 0 } else { i + 1 });
|
||||||
|
self.list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&mut self) {
|
||||||
|
let i = self.list_state.selected().map_or(0, |i|
|
||||||
|
if i == 0 { self.profiles.len() - 1 } else { i - 1 });
|
||||||
|
self.list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&mut self,
|
||||||
|
f: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
theme: &Theme,
|
||||||
|
profile_tree: &ProfileTreeResponse,
|
||||||
|
selected_profile: &Option<String>,
|
||||||
|
) {
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(theme.accent))
|
||||||
|
.style(Style::default().bg(theme.bg));
|
||||||
|
|
||||||
|
let inner_area = block.inner(area);
|
||||||
|
f.render_widget(block, area);
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(1)])
|
||||||
|
.split(inner_area);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
let title = Line::from(Span::styled("Admin Panel", Style::default().fg(theme.highlight)));
|
||||||
|
let title_widget = Paragraph::new(title).alignment(Alignment::Center);
|
||||||
|
f.render_widget(title_widget, chunks[0]);
|
||||||
|
|
||||||
|
// Content
|
||||||
|
let content_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||||
|
.split(chunks[1]);
|
||||||
|
|
||||||
|
// Profile list
|
||||||
|
let items: Vec<ListItem> = self.profiles.iter()
|
||||||
|
.map(|p| ListItem::new(Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
if Some(p) == selected_profile.as_ref() { "✓ " } else { " " },
|
||||||
|
Style::default().fg(theme.accent)
|
||||||
|
),
|
||||||
|
Span::styled(p, Style::default().fg(theme.fg)),
|
||||||
|
])))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(items)
|
||||||
|
.block(Block::default().title("Profiles"))
|
||||||
|
.highlight_style(Style::default().bg(theme.highlight).fg(theme.bg));
|
||||||
|
|
||||||
|
f.render_stateful_widget(list, content_chunks[0], &mut self.list_state);
|
||||||
|
|
||||||
|
// Profile details
|
||||||
|
if let Some(profile) = self.list_state.selected()
|
||||||
|
.and_then(|i| profile_tree.profiles.get(i))
|
||||||
|
{
|
||||||
|
let mut text = Text::default();
|
||||||
|
text.lines.push(Line::from(vec![
|
||||||
|
Span::styled("Profile: ", Style::default().fg(theme.accent)),
|
||||||
|
Span::styled(&profile.name, Style::default().fg(theme.highlight)),
|
||||||
|
]));
|
||||||
|
|
||||||
|
text.lines.push(Line::from(""));
|
||||||
|
text.lines.push(Line::from(Span::styled("Tables:", Style::default().fg(theme.accent))));
|
||||||
|
|
||||||
|
for table in &profile.tables {
|
||||||
|
let mut line = vec![Span::styled(format!("├─ {}", table.name), theme.fg)];
|
||||||
|
if !table.depends_on.is_empty() {
|
||||||
|
line.push(Span::styled(
|
||||||
|
format!(" → {}", table.depends_on.join(", ")),
|
||||||
|
Style::default().fg(theme.secondary)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
text.lines.push(Line::from(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
let details_widget = Paragraph::new(text)
|
||||||
|
.block(Block::default().title("Details"));
|
||||||
|
f.render_widget(details_widget, content_chunks[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
client/src/components/common.rs
Normal file
8
client/src/components/common.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// src/components/common.rs
|
||||||
|
pub mod command_line;
|
||||||
|
pub mod status_line;
|
||||||
|
pub mod background;
|
||||||
|
|
||||||
|
pub use command_line::*;
|
||||||
|
pub use status_line::*;
|
||||||
|
pub use background::*;
|
||||||
15
client/src/components/common/background.rs
Normal file
15
client/src/components/common/background.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// src/components/handlers/background.rs
|
||||||
|
use ratatui::{
|
||||||
|
widgets::{Block},
|
||||||
|
layout::Rect,
|
||||||
|
style::Style,
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use crate::config::colors::themes::Theme;
|
||||||
|
|
||||||
|
pub fn render_background(f: &mut Frame, area: Rect, theme: &Theme) {
|
||||||
|
let background = Block::default()
|
||||||
|
.style(Style::default().bg(theme.bg));
|
||||||
|
|
||||||
|
f.render_widget(background, area);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ use ratatui::{
|
|||||||
layout::Rect,
|
layout::Rect,
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
|
|
||||||
pub fn render_command_line(f: &mut Frame, area: Rect, input: &str, active: bool, theme: &Theme, message: &str) {
|
pub fn render_command_line(f: &mut Frame, area: Rect, input: &str, active: bool, theme: &Theme, message: &str) {
|
||||||
let prompt = if active {
|
let prompt = if active {
|
||||||
@@ -6,7 +6,7 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub fn render_status_line(
|
pub fn render_status_line(
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
// src/components/handlers.rs
|
// src/components/handlers.rs
|
||||||
pub mod form;
|
pub mod form;
|
||||||
pub mod preview_card;
|
|
||||||
pub mod command_line;
|
|
||||||
pub mod status_line;
|
|
||||||
pub mod canvas;
|
pub mod canvas;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
|
|
||||||
pub use command_line::render_command_line;
|
|
||||||
pub use form::*;
|
pub use form::*;
|
||||||
pub use preview_card::render_preview_card;
|
|
||||||
pub use status_line::render_status_line;
|
|
||||||
pub use canvas::*;
|
pub use canvas::*;
|
||||||
pub use sidebar::*;
|
pub use sidebar::*;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
prelude::Alignment,
|
prelude::Alignment,
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::ui::form::FormState;
|
use crate::ui::form::FormState;
|
||||||
|
|
||||||
pub fn render_canvas(
|
pub fn render_canvas(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use ratatui::{
|
|||||||
style::Style,
|
style::Style,
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::ui::form::FormState;
|
use crate::ui::form::FormState;
|
||||||
use super::canvas::render_canvas; // Changed to canvas
|
use super::canvas::render_canvas; // Changed to canvas
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
// src/client/components/preview_card.rs
|
|
||||||
use ratatui::{
|
|
||||||
widgets::{Block, Borders, List, ListItem},
|
|
||||||
layout::Rect,
|
|
||||||
style::Style,
|
|
||||||
text::Text,
|
|
||||||
Frame,
|
|
||||||
};
|
|
||||||
use crate::config::colors::Theme;
|
|
||||||
|
|
||||||
pub fn render_preview_card(f: &mut Frame, area: Rect, fields: &[&String], theme: &Theme) {
|
|
||||||
let card = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(theme.border))
|
|
||||||
.title(" Preview Card ")
|
|
||||||
.style(Style::default().bg(theme.bg).fg(theme.fg));
|
|
||||||
|
|
||||||
let items = vec![
|
|
||||||
ListItem::new(Text::from(format!("Firma: {}", fields[0]))),
|
|
||||||
ListItem::new(Text::from(format!("Ulica: {}", fields[1]))),
|
|
||||||
ListItem::new(Text::from(format!("Mesto: {}", fields[2]))),
|
|
||||||
ListItem::new(Text::from(format!("PSC: {}", fields[3]))),
|
|
||||||
ListItem::new(Text::from(format!("ICO: {}", fields[4]))),
|
|
||||||
ListItem::new(Text::from(format!("Kontakt: {}", fields[5]))),
|
|
||||||
ListItem::new(Text::from(format!("Telefon: {}", fields[6]))),
|
|
||||||
];
|
|
||||||
|
|
||||||
let list = List::new(items)
|
|
||||||
.block(card)
|
|
||||||
.style(Style::default().bg(theme.bg).fg(theme.fg));
|
|
||||||
|
|
||||||
f.render_widget(list, area);
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,77 @@
|
|||||||
// src/components/handlers/sidebar.rs
|
// src/components/handlers/sidebar.rs
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
widgets::{Block, List, ListItem},
|
widgets::{Block, List, ListItem},
|
||||||
layout::Rect,
|
layout::{Rect, Direction, Layout, Constraint},
|
||||||
style::Style,
|
style::Style,
|
||||||
text::Text,
|
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
|
use common::proto::multieko2::table_definition::{ProfileTreeResponse};
|
||||||
|
use ratatui::text::{Span, Line};
|
||||||
|
|
||||||
pub fn render_sidebar(f: &mut Frame, area: Rect, theme: &Theme) {
|
const SIDEBAR_WIDTH: u16 = 16;
|
||||||
let sidebar_block = Block::default()
|
|
||||||
.style(Style::default().bg(theme.bg));
|
|
||||||
|
|
||||||
let items = vec![
|
pub fn calculate_sidebar_layout(show_sidebar: bool, main_content_area: Rect) -> (Option<Rect>, Rect) {
|
||||||
ListItem::new(Text::from(" Navigation ")),
|
if show_sidebar {
|
||||||
ListItem::new(Text::from(" Search ")),
|
let chunks = Layout::default()
|
||||||
ListItem::new(Text::from(" Settings ")),
|
.direction(Direction::Horizontal)
|
||||||
];
|
.constraints([
|
||||||
|
Constraint::Length(SIDEBAR_WIDTH),
|
||||||
|
Constraint::Min(0),
|
||||||
|
])
|
||||||
|
.split(main_content_area);
|
||||||
|
(Some(chunks[0]), chunks[1])
|
||||||
|
} else {
|
||||||
|
(None, main_content_area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_sidebar(
|
||||||
|
f: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
theme: &Theme,
|
||||||
|
profile_tree: &ProfileTreeResponse,
|
||||||
|
selected_profile: &Option<String>,
|
||||||
|
) {
|
||||||
|
let sidebar_block = Block::default().style(Style::default().bg(theme.bg));
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
if let Some(profile_name) = selected_profile {
|
||||||
|
if let Some(profile) = profile_tree.profiles.iter()
|
||||||
|
.find(|p| &p.name == profile_name)
|
||||||
|
{
|
||||||
|
// Profile header
|
||||||
|
items.push(ListItem::new(Line::from(vec![
|
||||||
|
Span::styled("📁 ", Style::default().fg(theme.accent)),
|
||||||
|
Span::styled(&profile.name, Style::default().fg(theme.highlight)),
|
||||||
|
])));
|
||||||
|
|
||||||
|
// Tables
|
||||||
|
for (table_idx, table) in profile.tables.iter().enumerate() {
|
||||||
|
let is_last = table_idx == profile.tables.len() - 1;
|
||||||
|
let prefix = if is_last { "└─ " } else { "├─ " };
|
||||||
|
|
||||||
|
let mut line = vec![
|
||||||
|
Span::styled(format!(" {}", prefix), Style::default().fg(theme.fg)),
|
||||||
|
Span::styled(&table.name, Style::default().fg(theme.fg)),
|
||||||
|
];
|
||||||
|
|
||||||
|
if !table.depends_on.is_empty() {
|
||||||
|
line.push(Span::styled(
|
||||||
|
format!(" → {}", table.depends_on.join(", ")),
|
||||||
|
Style::default().fg(theme.secondary)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(ListItem::new(Line::from(line)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.push(ListItem::new(Span::styled(
|
||||||
|
"No profile selected",
|
||||||
|
Style::default().fg(theme.secondary)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
let list = List::new(items)
|
let list = List::new(items)
|
||||||
.block(sidebar_block)
|
.block(sidebar_block)
|
||||||
|
|||||||
4
client/src/components/intro.rs
Normal file
4
client/src/components/intro.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/components/intro.rs
|
||||||
|
pub mod intro;
|
||||||
|
|
||||||
|
pub use intro::*;
|
||||||
110
client/src/components/intro/intro.rs
Normal file
110
client/src/components/intro/intro.rs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// src/components/handlers/intro.rs
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
style::Style,
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, BorderType, Borders, Paragraph},
|
||||||
|
prelude::Margin,
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use crate::config::colors::themes::Theme;
|
||||||
|
|
||||||
|
pub struct IntroState {
|
||||||
|
pub selected_option: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntroState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { selected_option: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, f: &mut Frame, area: Rect, theme: &Theme) {
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(theme.accent))
|
||||||
|
.style(Style::default().bg(theme.bg));
|
||||||
|
|
||||||
|
let inner_area = block.inner(area);
|
||||||
|
f.render_widget(block, area);
|
||||||
|
|
||||||
|
// Center layout
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Percentage(35),
|
||||||
|
Constraint::Length(5),
|
||||||
|
Constraint::Percentage(35),
|
||||||
|
])
|
||||||
|
.split(inner_area);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
let title = Line::from(vec![
|
||||||
|
Span::styled("multieko2", Style::default().fg(theme.highlight)),
|
||||||
|
Span::styled(" v", Style::default().fg(theme.fg)),
|
||||||
|
Span::styled(env!("CARGO_PKG_VERSION"), Style::default().fg(theme.secondary)),
|
||||||
|
]);
|
||||||
|
let title_para = Paragraph::new(title)
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
f.render_widget(title_para, chunks[1]);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
let button_area = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
|
.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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = Paragraph::new(text)
|
||||||
|
.style(button_style)
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Double)
|
||||||
|
.border_style(if selected {
|
||||||
|
Style::default().fg(theme.accent)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(theme.border)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
f.render_widget(button, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_option(&mut self) {
|
||||||
|
self.selected_option = (self.selected_option + 1) % 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous_option(&mut self) {
|
||||||
|
self.selected_option = if self.selected_option == 0 { 1 } else { 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
// src/components/mod.rs
|
// src/components/mod.rs
|
||||||
pub mod models;
|
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
pub mod intro;
|
||||||
|
pub mod admin;
|
||||||
|
pub mod common;
|
||||||
|
|
||||||
pub use handlers::*;
|
pub use handlers::*;
|
||||||
|
pub use intro::*;
|
||||||
|
pub use admin::*;
|
||||||
|
pub use common::*;
|
||||||
|
|||||||
7
client/src/config/binds.rs
Normal file
7
client/src/config/binds.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// src/config/binds.rs
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod key_sequences;
|
||||||
|
|
||||||
|
pub use config::*;
|
||||||
|
pub use key_sequences::*;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// client/src/config/config.rs
|
// src/config/binds/config.rs
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -25,6 +25,8 @@ pub struct Config {
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ModeKeybindings {
|
pub struct ModeKeybindings {
|
||||||
|
#[serde(default)]
|
||||||
|
pub general: HashMap<String, Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub read_only: HashMap<String, Vec<String>>,
|
pub read_only: HashMap<String, Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -33,7 +35,6 @@ pub struct ModeKeybindings {
|
|||||||
pub command: HashMap<String, Vec<String>>,
|
pub command: HashMap<String, Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub common: HashMap<String, Vec<String>>,
|
pub common: HashMap<String, Vec<String>>,
|
||||||
// Store top-level keybindings that aren't in a specific mode section
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub global: HashMap<String, Vec<String>>,
|
pub global: HashMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,17 @@ impl Config {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn get_general_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common actions for Edit/Read-only modes
|
||||||
|
pub fn get_common_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||||
|
self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets an action for a key in Read-Only mode, also checking common keybindings.
|
/// Gets an action for a key in Read-Only mode, also checking common keybindings.
|
||||||
pub fn get_read_only_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
pub fn get_read_only_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||||
self.get_action_for_key_in_mode(&self.keybindings.read_only, key, modifiers)
|
self.get_action_for_key_in_mode(&self.keybindings.read_only, key, modifiers)
|
||||||
@@ -70,6 +82,25 @@ impl Config {
|
|||||||
.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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Context-aware keybinding resolution
|
||||||
|
pub fn get_action_for_current_context(
|
||||||
|
&self,
|
||||||
|
is_edit_mode: bool,
|
||||||
|
command_mode: bool,
|
||||||
|
key: KeyCode,
|
||||||
|
modifiers: KeyModifiers
|
||||||
|
) -> Option<&str> {
|
||||||
|
match (command_mode, is_edit_mode) {
|
||||||
|
(true, _) => self.get_command_action_for_key(key, modifiers),
|
||||||
|
(_, true) => self.get_edit_action_for_key(key, modifiers)
|
||||||
|
.or_else(|| self.get_common_action(key, modifiers)),
|
||||||
|
_ => self.get_read_only_action_for_key(key, modifiers)
|
||||||
|
.or_else(|| self.get_common_action(key, modifiers))
|
||||||
|
// Add global bindings check for read-only mode
|
||||||
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper function to get an action for a key in a specific mode.
|
/// Helper function to get an action for a key in a specific mode.
|
||||||
pub fn get_action_for_key_in_mode<'a>(
|
pub fn get_action_for_key_in_mode<'a>(
|
||||||
&self,
|
&self,
|
||||||
@@ -355,13 +386,13 @@ impl Config {
|
|||||||
|
|
||||||
// Get string representations of the sequence
|
// Get string representations of the sequence
|
||||||
let sequence_str = sequence.iter()
|
let sequence_str = sequence.iter()
|
||||||
.map(|k| crate::config::key_sequences::key_to_string(k))
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
// Add the missing sequence_plus definition
|
// Add the missing sequence_plus definition
|
||||||
let sequence_plus = sequence.iter()
|
let sequence_plus = sequence.iter()
|
||||||
.map(|k| crate::config::key_sequences::key_to_string(k))
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("+");
|
.join("+");
|
||||||
|
|
||||||
@@ -414,7 +445,7 @@ impl Config {
|
|||||||
// Special case for + format in bindings
|
// Special case for + format in bindings
|
||||||
if binding.contains('+') {
|
if binding.contains('+') {
|
||||||
let normalized_sequence = sequence.iter()
|
let normalized_sequence = sequence.iter()
|
||||||
.map(|k| crate::config::key_sequences::key_to_string(k))
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
let binding_parts: Vec<&str> = binding.split('+').collect();
|
let binding_parts: Vec<&str> = binding.split('+').collect();
|
||||||
@@ -442,7 +473,7 @@ impl Config {
|
|||||||
|
|
||||||
// Get string representation of the sequence
|
// Get string representation of the sequence
|
||||||
let sequence_str = sequence.iter()
|
let sequence_str = sequence.iter()
|
||||||
.map(|k| crate::config::key_sequences::key_to_string(k))
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
@@ -491,7 +522,7 @@ impl Config {
|
|||||||
if binding.contains('+') {
|
if binding.contains('+') {
|
||||||
let binding_parts: Vec<&str> = binding.split('+').collect();
|
let binding_parts: Vec<&str> = binding.split('+').collect();
|
||||||
let sequence_parts = sequence.iter()
|
let sequence_parts = sequence.iter()
|
||||||
.map(|k| crate::config::key_sequences::key_to_string(k))
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
if binding_parts.len() > sequence_parts.len() {
|
if binding_parts.len() > sequence_parts.len() {
|
||||||
@@ -1,68 +1,4 @@
|
|||||||
// src/client/colors.rs
|
// src/config/colors.rs
|
||||||
use ratatui::style::Color;
|
pub mod themes;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
pub use themes::*;
|
||||||
pub struct Theme {
|
|
||||||
pub bg: Color,
|
|
||||||
pub fg: Color,
|
|
||||||
pub accent: Color,
|
|
||||||
pub secondary: Color,
|
|
||||||
pub highlight: Color,
|
|
||||||
pub warning: Color,
|
|
||||||
pub border: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Theme {
|
|
||||||
pub fn from_str(theme_name: &str) -> Self {
|
|
||||||
match theme_name.to_lowercase().as_str() {
|
|
||||||
"dark" => Self::dark(),
|
|
||||||
"high_contrast" => Self::high_contrast(),
|
|
||||||
_ => Self::light(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default light theme
|
|
||||||
pub fn light() -> Self {
|
|
||||||
Self {
|
|
||||||
bg: Color::Rgb(245, 245, 245), // Light gray
|
|
||||||
fg: Color::Rgb(64, 64, 64), // Dark gray
|
|
||||||
accent: Color::Rgb(173, 216, 230), // Pastel blue
|
|
||||||
secondary: Color::Rgb(255, 165, 0), // Orange for secondary
|
|
||||||
highlight: Color::Rgb(152, 251, 152), // Pastel green
|
|
||||||
warning: Color::Rgb(255, 182, 193), // Pastel pink
|
|
||||||
border: Color::Rgb(220, 220, 220), // Light gray border
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// High-contrast dark theme
|
|
||||||
pub fn dark() -> Self {
|
|
||||||
Self {
|
|
||||||
bg: Color::Rgb(30, 30, 30), // Dark background
|
|
||||||
fg: Color::Rgb(255, 255, 255), // White text
|
|
||||||
accent: Color::Rgb(0, 191, 255), // Bright blue
|
|
||||||
secondary: Color::Rgb(255, 215, 0), // Gold for secondary
|
|
||||||
highlight: Color::Rgb(50, 205, 50), // Bright green
|
|
||||||
warning: Color::Rgb(255, 99, 71), // Bright red
|
|
||||||
border: Color::Rgb(100, 100, 100), // Medium gray border
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// High-contrast light theme
|
|
||||||
pub fn high_contrast() -> Self {
|
|
||||||
Self {
|
|
||||||
bg: Color::Rgb(255, 255, 255), // White background
|
|
||||||
fg: Color::Rgb(0, 0, 0), // Black text
|
|
||||||
accent: Color::Rgb(0, 0, 255), // Blue
|
|
||||||
secondary: Color::Rgb(255, 140, 0), // Dark orange for secondary
|
|
||||||
highlight: Color::Rgb(0, 128, 0), // Green
|
|
||||||
warning: Color::Rgb(255, 0, 0), // Red
|
|
||||||
border: Color::Rgb(0, 0, 0), // Black border
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Theme {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::light() // Default to light theme
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
68
client/src/config/colors/themes.rs
Normal file
68
client/src/config/colors/themes.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// src/client/themes/colors.rs
|
||||||
|
use ratatui::style::Color;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Theme {
|
||||||
|
pub bg: Color,
|
||||||
|
pub fg: Color,
|
||||||
|
pub accent: Color,
|
||||||
|
pub secondary: Color,
|
||||||
|
pub highlight: Color,
|
||||||
|
pub warning: Color,
|
||||||
|
pub border: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
pub fn from_str(theme_name: &str) -> Self {
|
||||||
|
match theme_name.to_lowercase().as_str() {
|
||||||
|
"dark" => Self::dark(),
|
||||||
|
"high_contrast" => Self::high_contrast(),
|
||||||
|
_ => Self::light(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default light theme
|
||||||
|
pub fn light() -> Self {
|
||||||
|
Self {
|
||||||
|
bg: Color::Rgb(245, 245, 245), // Light gray
|
||||||
|
fg: Color::Rgb(64, 64, 64), // Dark gray
|
||||||
|
accent: Color::Rgb(173, 216, 230), // Pastel blue
|
||||||
|
secondary: Color::Rgb(255, 165, 0), // Orange for secondary
|
||||||
|
highlight: Color::Rgb(152, 251, 152), // Pastel green
|
||||||
|
warning: Color::Rgb(255, 182, 193), // Pastel pink
|
||||||
|
border: Color::Rgb(220, 220, 220), // Light gray border
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-contrast dark theme
|
||||||
|
pub fn dark() -> Self {
|
||||||
|
Self {
|
||||||
|
bg: Color::Rgb(30, 30, 30), // Dark background
|
||||||
|
fg: Color::Rgb(255, 255, 255), // White text
|
||||||
|
accent: Color::Rgb(0, 191, 255), // Bright blue
|
||||||
|
secondary: Color::Rgb(255, 215, 0), // Gold for secondary
|
||||||
|
highlight: Color::Rgb(50, 205, 50), // Bright green
|
||||||
|
warning: Color::Rgb(255, 99, 71), // Bright red
|
||||||
|
border: Color::Rgb(100, 100, 100), // Medium gray border
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-contrast light theme
|
||||||
|
pub fn high_contrast() -> Self {
|
||||||
|
Self {
|
||||||
|
bg: Color::Rgb(255, 255, 255), // White background
|
||||||
|
fg: Color::Rgb(0, 0, 0), // Black text
|
||||||
|
accent: Color::Rgb(0, 0, 255), // Blue
|
||||||
|
secondary: Color::Rgb(255, 140, 0), // Dark orange for secondary
|
||||||
|
highlight: Color::Rgb(0, 128, 0), // Green
|
||||||
|
warning: Color::Rgb(255, 0, 0), // Red
|
||||||
|
border: Color::Rgb(0, 0, 0), // Black border
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Theme {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::light() // Default to light theme
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/config/mod.rs
|
// src/config/mod.rs
|
||||||
|
|
||||||
|
pub mod binds;
|
||||||
pub mod colors;
|
pub mod colors;
|
||||||
pub mod config;
|
|
||||||
pub mod key_sequences;
|
|
||||||
|
|||||||
4
client/src/modes/canvas.rs
Normal file
4
client/src/modes/canvas.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/client/modes/canvas.rs
|
||||||
|
pub mod edit;
|
||||||
|
pub mod common;
|
||||||
|
pub mod read_only;
|
||||||
@@ -1,9 +1,76 @@
|
|||||||
// src/modes/handlers/common.rs
|
// src/modes/canvas/common.rs
|
||||||
|
|
||||||
|
use crossterm::event::{KeyEvent};
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
use crate::tui::terminal::grpc_client::GrpcClient;
|
use crate::tui::terminal::grpc_client::GrpcClient;
|
||||||
|
use crate::tui::terminal::core::TerminalCore;
|
||||||
|
use crate::tui::terminal::commands::CommandHandler;
|
||||||
use crate::ui::handlers::form::FormState;
|
use crate::ui::handlers::form::FormState;
|
||||||
|
use crate::state::state::AppState;
|
||||||
use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest};
|
use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest};
|
||||||
|
|
||||||
|
/// Main handler for common core actions
|
||||||
|
pub async fn handle_core_action(
|
||||||
|
action: &str,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
grpc_client: &mut GrpcClient,
|
||||||
|
command_handler: &mut CommandHandler,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
current_position: &mut u64,
|
||||||
|
total_count: u64,
|
||||||
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
|
match action {
|
||||||
|
"save" => {
|
||||||
|
let message = save(
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
&mut app_state.ui.is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await?;
|
||||||
|
Ok((false, message))
|
||||||
|
},
|
||||||
|
"force_quit" => {
|
||||||
|
let (should_exit, message) = command_handler.handle_command("force_quit", terminal).await?;
|
||||||
|
Ok((should_exit, message))
|
||||||
|
},
|
||||||
|
"save_and_quit" => {
|
||||||
|
let (should_exit, message) = command_handler.handle_command("save_and_quit", terminal).await?;
|
||||||
|
Ok((should_exit, message))
|
||||||
|
},
|
||||||
|
"revert" => {
|
||||||
|
let message = revert(
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await?;
|
||||||
|
Ok((false, message))
|
||||||
|
},
|
||||||
|
// We should never hit this case with proper filtering
|
||||||
|
_ => Ok((false, format!("Core action not handled: {}", action))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to check if a key event should trigger a core action
|
||||||
|
pub fn is_core_action(config: &Config, key_code: crossterm::event::KeyCode, modifiers: crossterm::event::KeyModifiers) -> Option<String> {
|
||||||
|
// Check for core application actions (save, quit, etc.)
|
||||||
|
if let Some(action) = config.get_action_for_key_in_mode(
|
||||||
|
&config.keybindings.common,
|
||||||
|
key_code,
|
||||||
|
modifiers
|
||||||
|
) {
|
||||||
|
match action {
|
||||||
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
|
return Some(action.to_string())
|
||||||
|
},
|
||||||
|
_ => {} // Other actions are handled by their respective mode handlers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Shared logic for saving the current form state
|
/// Shared logic for saving the current form state
|
||||||
pub async fn save(
|
pub async fn save(
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState,
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
// src/modes/handlers/edit.rs
|
// src/modes/canvas/edit.rs
|
||||||
|
|
||||||
|
// TODO THIS is freaking bloated with functions it never uses REFACTOR 200 LOC can be gone
|
||||||
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
||||||
use crate::tui::terminal::{
|
use crate::tui::terminal::{
|
||||||
grpc_client::GrpcClient,
|
grpc_client::GrpcClient,
|
||||||
};
|
};
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::ui::handlers::form::FormState;
|
use crate::ui::handlers::form::FormState;
|
||||||
use super::common;
|
use crate::modes::canvas::common;
|
||||||
|
|
||||||
pub async fn handle_edit_event_internal(
|
pub async fn handle_edit_event_internal(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
@@ -19,6 +20,24 @@ pub async fn handle_edit_event_internal(
|
|||||||
total_count: u64,
|
total_count: u64,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
if let Some("enter_command_mode") = config.get_action_for_key_in_mode(&config.keybindings.global, key.code, key.modifiers) {
|
||||||
|
// Ignore in edit mode and process as normal input
|
||||||
|
handle_edit_specific_input(key, form_state, ideal_cursor_column);
|
||||||
|
return Ok(command_message.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check common actions first
|
||||||
|
if let Some(action) = config.get_action_for_key_in_mode(&config.keybindings.common, key.code, key.modifiers) {
|
||||||
|
return execute_common_action(
|
||||||
|
action,
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) {
|
if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) {
|
||||||
return execute_edit_action(
|
return execute_edit_action(
|
||||||
action,
|
action,
|
||||||
@@ -40,6 +59,48 @@ pub async fn handle_edit_event_internal(
|
|||||||
Ok(command_message.clone())
|
Ok(command_message.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn execute_common_action(
|
||||||
|
action: &str,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
grpc_client: &mut GrpcClient,
|
||||||
|
is_saved: &mut bool,
|
||||||
|
current_position: &mut u64,
|
||||||
|
total_count: u64,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
match action {
|
||||||
|
"save" => {
|
||||||
|
common::save(
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await
|
||||||
|
},
|
||||||
|
"revert" => {
|
||||||
|
common::revert(
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await
|
||||||
|
},
|
||||||
|
"move_up" | "move_down" => {
|
||||||
|
// Reuse edit mode's existing logic
|
||||||
|
execute_edit_action(
|
||||||
|
action,
|
||||||
|
form_state,
|
||||||
|
&mut 0, // Dummy ideal_cursor_column (not used here)
|
||||||
|
grpc_client,
|
||||||
|
is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await
|
||||||
|
},
|
||||||
|
_ => Ok(format!("Common action not handled: {}", action)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_edit_specific_input(
|
fn handle_edit_specific_input(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState,
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// src/modes/handlers/read_only.rs
|
// src/modes/handlers/read_only.rs
|
||||||
|
|
||||||
use crossterm::event::{KeyEvent};
|
use crossterm::event::{KeyEvent};
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::ui::handlers::form::FormState;
|
use crate::ui::handlers::form::FormState;
|
||||||
use crate::config::key_sequences::KeySequenceTracker;
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
use crate::tui::terminal::grpc_client::GrpcClient;
|
use crate::tui::terminal::grpc_client::GrpcClient;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
3
client/src/modes/common.rs
Normal file
3
client/src/modes/common.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// src/client/modes/common.rs
|
||||||
|
pub mod command_mode;
|
||||||
|
pub mod highlight;
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
||||||
use crate::tui::terminal::grpc_client::GrpcClient;
|
use crate::tui::terminal::grpc_client::GrpcClient;
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::ui::handlers::form::FormState;
|
use crate::ui::handlers::form::FormState;
|
||||||
use super::common;
|
use crate::modes::{
|
||||||
|
canvas::{common},
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn handle_command_event(
|
pub async fn handle_command_event(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
2
client/src/modes/general.rs
Normal file
2
client/src/modes/general.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// src/client/modes/general.rs
|
||||||
|
pub mod navigation;
|
||||||
175
client/src/modes/general/navigation.rs
Normal file
175
client/src/modes/general/navigation.rs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// src/modes/general/navigation.rs
|
||||||
|
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::state::state::AppState;
|
||||||
|
use crate::ui::handlers::form::FormState;
|
||||||
|
|
||||||
|
pub async fn handle_navigation_event(
|
||||||
|
key: KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
command_mode: &mut bool,
|
||||||
|
command_input: &mut String,
|
||||||
|
command_message: &mut String,
|
||||||
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
|
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
||||||
|
match action {
|
||||||
|
"move_up" => {
|
||||||
|
move_up(app_state);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"move_down" => {
|
||||||
|
let item_count = if app_state.ui.show_intro {
|
||||||
|
2 // Intro options count
|
||||||
|
} else {
|
||||||
|
app_state.profile_tree.profiles.len() // Admin panel items
|
||||||
|
};
|
||||||
|
move_down(app_state, item_count);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"next_option" => {
|
||||||
|
next_option(app_state, 2); // Intro has 2 options
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"previous_option" => {
|
||||||
|
previous_option(app_state);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"select" => {
|
||||||
|
select(app_state);
|
||||||
|
return Ok((false, "Selected".to_string()));
|
||||||
|
}
|
||||||
|
"toggle_sidebar" => {
|
||||||
|
toggle_sidebar(app_state);
|
||||||
|
return Ok((false, format!("Sidebar {}",
|
||||||
|
if app_state.ui.show_sidebar { "shown" } else { "hidden" }
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
"next_field" => {
|
||||||
|
next_field(form_state);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"prev_field" => {
|
||||||
|
prev_field(form_state);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"enter_command_mode" => {
|
||||||
|
handle_enter_command_mode(command_mode, command_input, command_message);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((false, String::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_up(app_state: &mut AppState) {
|
||||||
|
if app_state.ui.show_intro {
|
||||||
|
app_state.ui.intro_state.previous_option();
|
||||||
|
} else if app_state.ui.show_admin {
|
||||||
|
// Assuming profile_tree.profiles is the list we're navigating
|
||||||
|
let profile_count = app_state.profile_tree.profiles.len();
|
||||||
|
if profile_count == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use general state for tracking selection in admin panel
|
||||||
|
if app_state.general.selected_item == 0 {
|
||||||
|
app_state.general.selected_item = profile_count - 1;
|
||||||
|
} else {
|
||||||
|
app_state.general.selected_item = app_state.general.selected_item.saturating_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_down(app_state: &mut AppState, item_count: usize) {
|
||||||
|
if app_state.ui.show_intro {
|
||||||
|
app_state.ui.intro_state.next_option();
|
||||||
|
} else if app_state.ui.show_admin {
|
||||||
|
// Assuming profile_tree.profiles is the list we're navigating
|
||||||
|
let profile_count = app_state.profile_tree.profiles.len();
|
||||||
|
if profile_count == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app_state.general.selected_item = (app_state.general.selected_item + 1) % profile_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_option(app_state: &mut AppState, option_count: usize) {
|
||||||
|
if app_state.ui.show_intro {
|
||||||
|
app_state.ui.intro_state.next_option();
|
||||||
|
} else {
|
||||||
|
// For other screens that might have options
|
||||||
|
app_state.general.current_option = (app_state.general.current_option + 1) % option_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous_option(app_state: &mut AppState) {
|
||||||
|
if app_state.ui.show_intro {
|
||||||
|
app_state.ui.intro_state.previous_option();
|
||||||
|
} else {
|
||||||
|
// For other screens that might have options
|
||||||
|
if app_state.general.current_option == 0 {
|
||||||
|
// We'd need the option count here, but since it's not passed we can't wrap around correctly
|
||||||
|
// For now, just stay at 0
|
||||||
|
} else {
|
||||||
|
app_state.general.current_option -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select(app_state: &mut AppState) {
|
||||||
|
if app_state.ui.show_intro {
|
||||||
|
// Handle selection in intro screen
|
||||||
|
if app_state.ui.intro_state.selected_option == 0 {
|
||||||
|
// First option selected - show form
|
||||||
|
app_state.ui.show_form = true;
|
||||||
|
app_state.ui.show_admin = false;
|
||||||
|
} else {
|
||||||
|
// Second option selected - show admin
|
||||||
|
app_state.ui.show_form = false;
|
||||||
|
app_state.ui.show_admin = true;
|
||||||
|
}
|
||||||
|
app_state.ui.show_intro = false;
|
||||||
|
} else if app_state.ui.show_admin {
|
||||||
|
// Handle selection in admin panel
|
||||||
|
let profiles = &app_state.profile_tree.profiles;
|
||||||
|
if !profiles.is_empty() && app_state.general.selected_item < profiles.len() {
|
||||||
|
// Set the selected profile
|
||||||
|
app_state.selected_profile = Some(profiles[app_state.general.selected_item].name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_sidebar(app_state: &mut AppState) {
|
||||||
|
app_state.ui.show_sidebar = !app_state.ui.show_sidebar;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_field(form_state: &mut FormState) {
|
||||||
|
if !form_state.fields.is_empty() {
|
||||||
|
form_state.current_field = (form_state.current_field + 1) % form_state.fields.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_field(form_state: &mut FormState) {
|
||||||
|
if !form_state.fields.is_empty() {
|
||||||
|
if form_state.current_field == 0 {
|
||||||
|
form_state.current_field = form_state.fields.len() - 1;
|
||||||
|
} else {
|
||||||
|
form_state.current_field -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_enter_command_mode(
|
||||||
|
command_mode: &mut bool,
|
||||||
|
command_input: &mut String,
|
||||||
|
command_message: &mut String
|
||||||
|
) {
|
||||||
|
*command_mode = true;
|
||||||
|
command_input.clear();
|
||||||
|
command_message.clear();
|
||||||
|
}
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
// src/client/modes/handlers.rs
|
// src/client/modes/handlers.rs
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod edit;
|
pub mod mode_manager;
|
||||||
pub mod common;
|
|
||||||
pub mod command_mode;
|
|
||||||
pub mod read_only;
|
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
// src/modes/handlers/event.rs
|
// src/modes/handlers/event.rs
|
||||||
|
use crossterm::event::{Event, KeyEvent};
|
||||||
use crossterm::event::Event;
|
|
||||||
use crossterm::cursor::SetCursorStyle;
|
use crossterm::cursor::SetCursorStyle;
|
||||||
use crate::tui::terminal::{
|
use crate::tui::terminal::{
|
||||||
core::TerminalCore,
|
core::TerminalCore,
|
||||||
grpc_client::GrpcClient,
|
grpc_client::GrpcClient,
|
||||||
commands::CommandHandler,
|
commands::CommandHandler,
|
||||||
};
|
};
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::ui::handlers::form::FormState;
|
use crate::ui::handlers::form::FormState;
|
||||||
use crate::ui::handlers::rat_state::UiStateHandler;
|
use crate::ui::handlers::rat_state::UiStateHandler;
|
||||||
use crate::modes::handlers::{edit, command_mode, read_only};
|
use crate::modes::{
|
||||||
use crate::config::key_sequences::KeySequenceTracker;
|
common::{command_mode},
|
||||||
use super::common;
|
canvas::{edit, read_only, common},
|
||||||
|
general::navigation,
|
||||||
|
};
|
||||||
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
|
use crate::modes::handlers::mode_manager::{ModeManager, AppMode};
|
||||||
|
|
||||||
pub struct EventHandler {
|
pub struct EventHandler {
|
||||||
pub command_mode: bool,
|
pub command_mode: bool,
|
||||||
@@ -49,158 +52,196 @@ impl EventHandler {
|
|||||||
total_count: u64,
|
total_count: u64,
|
||||||
current_position: &mut u64,
|
current_position: &mut u64,
|
||||||
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
|
// Determine current mode based on app state and event handler state
|
||||||
|
let current_mode = ModeManager::derive_mode(app_state, self);
|
||||||
|
app_state.update_mode(current_mode);
|
||||||
|
|
||||||
if let Event::Key(key) = event {
|
if let Event::Key(key) = event {
|
||||||
let key_code = key.code;
|
let key_code = key.code;
|
||||||
let modifiers = key.modifiers;
|
let modifiers = key.modifiers;
|
||||||
|
|
||||||
if UiStateHandler::toggle_sidebar(
|
// Handle common actions across all modes
|
||||||
&mut app_state.ui,
|
if UiStateHandler::toggle_sidebar(&mut app_state.ui, config, key_code, modifiers) {
|
||||||
config,
|
return Ok((false, format!("Sidebar {}",
|
||||||
key_code,
|
if app_state.ui.show_sidebar { "shown" } else { "hidden" }
|
||||||
modifiers,
|
)));
|
||||||
) {
|
|
||||||
return Ok((false, format!("Sidebar {}",
|
|
||||||
if app_state.ui.show_sidebar { "shown" } else { "hidden" }
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(action) = config.get_action_for_key_in_mode(
|
|
||||||
&config.keybindings.common,
|
|
||||||
key_code,
|
|
||||||
modifiers
|
|
||||||
) {
|
|
||||||
match action {
|
|
||||||
"save" => {
|
|
||||||
let message = common::save(
|
|
||||||
form_state,
|
|
||||||
grpc_client,
|
|
||||||
&mut app_state.is_saved,
|
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
|
||||||
return Ok((false, message));
|
|
||||||
},
|
|
||||||
"force_quit" => {
|
|
||||||
let (should_exit, message) = command_handler.handle_command("force_quit", terminal).await?;
|
|
||||||
return Ok((should_exit, message));
|
|
||||||
},
|
|
||||||
"save_and_quit" => {
|
|
||||||
let (should_exit, message) = command_handler.handle_command("save_and_quit", terminal).await?;
|
|
||||||
return Ok((should_exit, message));
|
|
||||||
},
|
|
||||||
"revert" => {
|
|
||||||
let message = common::revert(
|
|
||||||
form_state,
|
|
||||||
grpc_client,
|
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
|
||||||
return Ok((false, message));
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.command_mode {
|
// Mode-specific handling
|
||||||
let (should_exit, message, exit_command_mode) = command_mode::handle_command_event(
|
match current_mode {
|
||||||
key,
|
AppMode::General => {
|
||||||
config,
|
return navigation::handle_navigation_event(
|
||||||
form_state,
|
key,
|
||||||
&mut self.command_input,
|
config,
|
||||||
&mut self.command_message,
|
form_state,
|
||||||
grpc_client,
|
app_state,
|
||||||
&mut app_state.is_saved,
|
&mut self.command_mode,
|
||||||
current_position,
|
&mut self.command_input,
|
||||||
total_count,
|
&mut self.command_message,
|
||||||
).await?;
|
).await;
|
||||||
|
},
|
||||||
|
|
||||||
if exit_command_mode {
|
AppMode::ReadOnly => {
|
||||||
self.command_mode = false;
|
// Check for mode transitions first
|
||||||
}
|
if config.is_enter_edit_mode_before(key_code, modifiers) &&
|
||||||
|
ModeManager::can_enter_edit_mode(current_mode) {
|
||||||
return Ok((should_exit, message));
|
self.is_edit_mode = true;
|
||||||
}
|
self.edit_mode_cooldown = true;
|
||||||
|
self.command_message = "Edit mode".to_string();
|
||||||
if self.is_edit_mode {
|
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
||||||
if config.is_exit_edit_mode(key_code, modifiers) {
|
|
||||||
if form_state.has_unsaved_changes {
|
|
||||||
self.command_message = "Unsaved changes! Use :w to save or :q! to discard".to_string();
|
|
||||||
return Ok((false, self.command_message.clone()));
|
return Ok((false, self.command_message.clone()));
|
||||||
}
|
}
|
||||||
self.is_edit_mode = false;
|
|
||||||
self.edit_mode_cooldown = true;
|
|
||||||
self.command_message = "Read-only mode".to_string();
|
|
||||||
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
|
||||||
|
|
||||||
let current_input = form_state.get_current_input();
|
if config.is_enter_edit_mode_after(key_code, modifiers) &&
|
||||||
if !current_input.is_empty() && form_state.current_cursor_pos >= current_input.len() {
|
ModeManager::can_enter_edit_mode(current_mode) {
|
||||||
form_state.current_cursor_pos = current_input.len() - 1;
|
let current_input = form_state.get_current_input();
|
||||||
self.ideal_cursor_column = form_state.current_cursor_pos;
|
if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() {
|
||||||
|
form_state.current_cursor_pos += 1;
|
||||||
|
self.ideal_cursor_column = form_state.current_cursor_pos;
|
||||||
|
}
|
||||||
|
self.is_edit_mode = true;
|
||||||
|
self.edit_mode_cooldown = true;
|
||||||
|
self.command_message = "Edit mode (after cursor)".to_string();
|
||||||
|
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
||||||
|
return Ok((false, self.command_message.clone()));
|
||||||
}
|
}
|
||||||
return Ok((false, self.command_message.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = edit::handle_edit_event_internal(
|
// Check for entering command mode
|
||||||
key,
|
if let Some(action) = config.get_read_only_action_for_key(key_code, modifiers) {
|
||||||
config,
|
if action == "enter_command_mode" && ModeManager::can_enter_command_mode(current_mode) {
|
||||||
form_state,
|
self.command_mode = true;
|
||||||
&mut self.ideal_cursor_column,
|
self.command_input.clear();
|
||||||
&mut self.command_message,
|
self.command_message.clear();
|
||||||
&mut app_state.is_saved,
|
return Ok((false, String::new()));
|
||||||
current_position,
|
}
|
||||||
total_count,
|
|
||||||
grpc_client,
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
return Ok((false, result));
|
|
||||||
} else {
|
|
||||||
if let Some(action) = config.get_read_only_action_for_key(key_code, modifiers) {
|
|
||||||
if action == "enter_command_mode" {
|
|
||||||
self.command_mode = true;
|
|
||||||
self.command_input.clear();
|
|
||||||
self.command_message.clear();
|
|
||||||
return Ok((false, String::new()));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if config.is_enter_edit_mode_before(key_code, modifiers) {
|
// Check for core application actions (save, quit, etc.)
|
||||||
self.is_edit_mode = true;
|
// ONLY handle a limited subset of core actions here
|
||||||
self.edit_mode_cooldown = true;
|
if let Some(action) = config.get_action_for_key_in_mode(
|
||||||
self.command_message = "Edit mode".to_string();
|
&config.keybindings.common,
|
||||||
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
key_code,
|
||||||
return Ok((false, self.command_message.clone()));
|
modifiers
|
||||||
}
|
) {
|
||||||
|
match action {
|
||||||
if config.is_enter_edit_mode_after(key_code, modifiers) {
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
let current_input = form_state.get_current_input();
|
return common::handle_core_action(
|
||||||
if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() {
|
action,
|
||||||
form_state.current_cursor_pos += 1;
|
form_state,
|
||||||
self.ideal_cursor_column = form_state.current_cursor_pos;
|
grpc_client,
|
||||||
|
command_handler,
|
||||||
|
terminal,
|
||||||
|
app_state,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await;
|
||||||
|
},
|
||||||
|
_ => {} // For other actions, let the mode-specific handler take care of it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.is_edit_mode = true;
|
|
||||||
self.edit_mode_cooldown = true;
|
|
||||||
self.command_message = "Edit mode (after cursor)".to_string();
|
|
||||||
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
|
||||||
return Ok((false, self.command_message.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return read_only::handle_read_only_event(
|
// Let read_only mode handle its own actions (including navigation from common bindings)
|
||||||
key,
|
return read_only::handle_read_only_event(
|
||||||
config,
|
key,
|
||||||
form_state,
|
config,
|
||||||
&mut self.key_sequence_tracker,
|
form_state,
|
||||||
current_position,
|
&mut self.key_sequence_tracker,
|
||||||
total_count,
|
current_position,
|
||||||
grpc_client,
|
total_count,
|
||||||
&mut self.command_message,
|
grpc_client,
|
||||||
&mut self.edit_mode_cooldown,
|
&mut self.command_message,
|
||||||
&mut self.ideal_cursor_column,
|
&mut self.edit_mode_cooldown,
|
||||||
).await;
|
&mut self.ideal_cursor_column,
|
||||||
|
).await;
|
||||||
|
},
|
||||||
|
|
||||||
|
AppMode::Edit => {
|
||||||
|
// Check for exiting edit mode
|
||||||
|
if config.is_exit_edit_mode(key_code, modifiers) {
|
||||||
|
if form_state.has_unsaved_changes {
|
||||||
|
self.command_message = "Unsaved changes! Use :w to save or :q! to discard".to_string();
|
||||||
|
return Ok((false, self.command_message.clone()));
|
||||||
|
}
|
||||||
|
self.is_edit_mode = false;
|
||||||
|
self.edit_mode_cooldown = true;
|
||||||
|
self.command_message = "Read-only mode".to_string();
|
||||||
|
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
||||||
|
|
||||||
|
let current_input = form_state.get_current_input();
|
||||||
|
if !current_input.is_empty() && form_state.current_cursor_pos >= current_input.len() {
|
||||||
|
form_state.current_cursor_pos = current_input.len() - 1;
|
||||||
|
self.ideal_cursor_column = form_state.current_cursor_pos;
|
||||||
|
}
|
||||||
|
return Ok((false, self.command_message.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for core application actions (save, quit, etc.)
|
||||||
|
// ONLY handle a limited subset of core actions here
|
||||||
|
if let Some(action) = config.get_action_for_key_in_mode(
|
||||||
|
&config.keybindings.common,
|
||||||
|
key_code,
|
||||||
|
modifiers
|
||||||
|
) {
|
||||||
|
match action {
|
||||||
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
|
return common::handle_core_action(
|
||||||
|
action,
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
command_handler,
|
||||||
|
terminal,
|
||||||
|
app_state,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await;
|
||||||
|
},
|
||||||
|
_ => {} // For other actions, let the mode-specific handler take care of it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let edit mode handle its own actions (including navigation from common bindings)
|
||||||
|
let result = edit::handle_edit_event_internal(
|
||||||
|
key,
|
||||||
|
config,
|
||||||
|
form_state,
|
||||||
|
&mut self.ideal_cursor_column,
|
||||||
|
&mut self.command_message,
|
||||||
|
&mut app_state.ui.is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
grpc_client,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
self.key_sequence_tracker.reset();
|
||||||
|
return Ok((false, result));
|
||||||
|
},
|
||||||
|
|
||||||
|
AppMode::Command => {
|
||||||
|
let (should_exit, message, exit_command_mode) = command_mode::handle_command_event(
|
||||||
|
key,
|
||||||
|
config,
|
||||||
|
form_state,
|
||||||
|
&mut self.command_input,
|
||||||
|
&mut self.command_message,
|
||||||
|
grpc_client,
|
||||||
|
&mut app_state.ui.is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
if exit_command_mode {
|
||||||
|
self.command_mode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok((should_exit, message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-key events or if no specific handler was matched
|
||||||
self.edit_mode_cooldown = false;
|
self.edit_mode_cooldown = false;
|
||||||
Ok((false, self.command_message.clone()))
|
Ok((false, self.command_message.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
50
client/src/modes/handlers/mode_manager.rs
Normal file
50
client/src/modes/handlers/mode_manager.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// src/modes/handlers/mode_manager.rs
|
||||||
|
use crate::state::state::AppState;
|
||||||
|
use crate::modes::handlers::event::EventHandler;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AppMode {
|
||||||
|
General, // For intro and admin screens
|
||||||
|
ReadOnly, // Canvas read-only mode
|
||||||
|
Edit, // Canvas edit mode
|
||||||
|
Command, // Command mode overlay
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModeManager;
|
||||||
|
|
||||||
|
impl ModeManager {
|
||||||
|
// Determine current mode based on app state
|
||||||
|
pub fn derive_mode(app_state: &AppState, event_handler: &EventHandler) -> AppMode {
|
||||||
|
// Command mode takes precedence if active
|
||||||
|
if event_handler.command_mode {
|
||||||
|
return AppMode::Command;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check UI state flags
|
||||||
|
if app_state.ui.show_intro || app_state.ui.show_admin {
|
||||||
|
AppMode::General
|
||||||
|
} else if app_state.ui.show_form {
|
||||||
|
if event_handler.is_edit_mode {
|
||||||
|
AppMode::Edit
|
||||||
|
} else {
|
||||||
|
AppMode::ReadOnly
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
AppMode::General
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode transition rules
|
||||||
|
pub fn can_enter_command_mode(current_mode: AppMode) -> bool {
|
||||||
|
!matches!(current_mode, AppMode::Edit) // Can't enter from Edit mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_enter_edit_mode(current_mode: AppMode) -> bool {
|
||||||
|
matches!(current_mode, AppMode::ReadOnly) // Only from ReadOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool {
|
||||||
|
matches!(current_mode, AppMode::Edit | AppMode::Command)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
// src/client/modes/mod.rs
|
// src/client/modes/mod.rs
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
pub mod canvas;
|
||||||
|
pub mod general;
|
||||||
|
pub mod common;
|
||||||
|
|
||||||
pub use handlers::*;
|
pub use handlers::*;
|
||||||
|
pub use canvas::*;
|
||||||
|
pub use general::*;
|
||||||
|
pub use common::*;
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
// src/client/ui/handlers/state.rs
|
// src/state/state.rs
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use common::proto::multieko2::table_definition::ProfileTreeResponse;
|
||||||
|
use crate::components::IntroState;
|
||||||
|
use crate::modes::handlers::mode_manager::AppMode;
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct UiState {
|
pub struct UiState {
|
||||||
pub show_sidebar: bool,
|
pub show_sidebar: bool,
|
||||||
// Add other UI-related states here
|
pub is_saved: bool,
|
||||||
|
pub show_intro: bool,
|
||||||
|
pub show_admin: bool,
|
||||||
|
pub show_form: bool,
|
||||||
|
pub intro_state: IntroState,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GeneralState {
|
||||||
|
pub selected_item: usize,
|
||||||
|
pub current_option: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
// Core editor state
|
// Core editor state
|
||||||
pub is_saved: bool,
|
|
||||||
pub current_dir: String,
|
pub current_dir: String,
|
||||||
pub total_count: u64,
|
pub total_count: u64,
|
||||||
pub current_position: u64,
|
pub current_position: u64,
|
||||||
|
pub profile_tree: ProfileTreeResponse,
|
||||||
|
pub selected_profile: Option<String>,
|
||||||
|
pub current_mode: AppMode,
|
||||||
|
|
||||||
// UI preferences
|
// UI preferences
|
||||||
pub ui: UiState,
|
pub ui: UiState,
|
||||||
|
pub general: GeneralState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@@ -25,11 +39,17 @@ impl AppState {
|
|||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
Ok(AppState {
|
Ok(AppState {
|
||||||
is_saved: false,
|
|
||||||
current_dir,
|
current_dir,
|
||||||
total_count: 0,
|
total_count: 0,
|
||||||
current_position: 0,
|
current_position: 0,
|
||||||
|
profile_tree: ProfileTreeResponse::default(),
|
||||||
|
selected_profile: None,
|
||||||
|
current_mode: AppMode::General,
|
||||||
ui: UiState::default(),
|
ui: UiState::default(),
|
||||||
|
general: GeneralState {
|
||||||
|
selected_item: 0,
|
||||||
|
current_option: 0,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,4 +61,21 @@ impl AppState {
|
|||||||
pub fn update_current_position(&mut self, current_position: u64) {
|
pub fn update_current_position(&mut self, current_position: u64) {
|
||||||
self.current_position = current_position;
|
self.current_position = current_position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_mode(&mut self, mode: AppMode) {
|
||||||
|
self.current_mode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UiState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
show_sidebar: true,
|
||||||
|
is_saved: false,
|
||||||
|
show_intro: true,
|
||||||
|
show_admin: false,
|
||||||
|
show_form: false,
|
||||||
|
intro_state: IntroState::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
cat src/modes/handlers/event.rs src/state/state.rs src/ui/handlers.rs src/ui/handlers/render.rs src/ui/handlers/ui.rs src/components/handlers.rs
|
|
||||||
@@ -6,17 +6,28 @@ use common::proto::multieko2::adresar::{AdresarResponse, PostAdresarRequest, Put
|
|||||||
use common::proto::multieko2::common::{CountResponse, PositionRequest, Empty};
|
use common::proto::multieko2::common::{CountResponse, PositionRequest, Empty};
|
||||||
use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient;
|
use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient;
|
||||||
use common::proto::multieko2::table_structure::TableStructureResponse;
|
use common::proto::multieko2::table_structure::TableStructureResponse;
|
||||||
|
use common::proto::multieko2::table_definition::{
|
||||||
|
table_definition_client::TableDefinitionClient,
|
||||||
|
ProfileTreeResponse
|
||||||
|
};
|
||||||
|
|
||||||
pub struct GrpcClient {
|
pub struct GrpcClient {
|
||||||
adresar_client: AdresarClient<Channel>,
|
adresar_client: AdresarClient<Channel>,
|
||||||
table_structure_client: TableStructureServiceClient<Channel>,
|
table_structure_client: TableStructureServiceClient<Channel>,
|
||||||
|
table_definition_client: TableDefinitionClient<Channel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GrpcClient {
|
impl GrpcClient {
|
||||||
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let adresar_client = AdresarClient::connect("http://[::1]:50051").await?;
|
let adresar_client = AdresarClient::connect("http://[::1]:50051").await?;
|
||||||
let table_structure_client = TableStructureServiceClient::connect("http://[::1]:50051").await?;
|
let table_structure_client = TableStructureServiceClient::connect("http://[::1]:50051").await?;
|
||||||
Ok(Self { adresar_client, table_structure_client })
|
let table_definition_client = TableDefinitionClient::connect("http://[::1]:50051").await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
adresar_client,
|
||||||
|
table_structure_client,
|
||||||
|
table_definition_client,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_adresar_count(&mut self) -> Result<u64, Box<dyn std::error::Error>> {
|
pub async fn get_adresar_count(&mut self) -> Result<u64, Box<dyn std::error::Error>> {
|
||||||
@@ -48,4 +59,10 @@ impl GrpcClient {
|
|||||||
let response = self.table_structure_client.get_adresar_table_structure(request).await?;
|
let response = self.table_structure_client.get_adresar_table_structure(request).await?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_profile_tree(&mut self) -> Result<ProfileTreeResponse, Box<dyn std::error::Error>> {
|
||||||
|
let request = tonic::Request::new(Empty::default());
|
||||||
|
let response = self.table_definition_client.get_profile_tree(request).await?;
|
||||||
|
Ok(response.into_inner())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/client/ui/handlers/form.rs
|
// src/client/ui/handlers/form.rs
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/ui/handlers/rat_state.rs
|
// src/ui/handlers/rat_state.rs
|
||||||
use crossterm::event::{KeyCode, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::state::state::UiState;
|
use crate::state::state::UiState;
|
||||||
|
|
||||||
pub struct UiStateHandler;
|
pub struct UiStateHandler;
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
// src/ui/handlers/render.rs
|
// src/ui/handlers/render.rs
|
||||||
|
|
||||||
use crate::components::{render_command_line, render_preview_card, render_status_line};
|
use crate::components::{
|
||||||
use crate::config::colors::Theme;
|
render_background,
|
||||||
|
render_command_line,
|
||||||
|
render_status_line,
|
||||||
|
handlers::{sidebar::{self, calculate_sidebar_layout}, form::render_form},
|
||||||
|
intro::{intro},
|
||||||
|
admin::{admin_panel::AdminPanelState},
|
||||||
|
};
|
||||||
|
use crate::config::colors::themes::Theme;
|
||||||
use ratatui::layout::{Constraint, Direction, Layout};
|
use ratatui::layout::{Constraint, Direction, Layout};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use super::form::FormState;
|
use super::form::FormState;
|
||||||
use crate::state::state::UiState;
|
use crate::state::state::AppState;
|
||||||
|
|
||||||
pub fn render_ui(
|
pub fn render_ui(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -18,84 +25,109 @@ pub fn render_ui(
|
|||||||
command_input: &str,
|
command_input: &str,
|
||||||
command_mode: bool,
|
command_mode: bool,
|
||||||
command_message: &str,
|
command_message: &str,
|
||||||
ui_state: &UiState,
|
app_state: &AppState,
|
||||||
|
// intro_state parameter removed
|
||||||
) {
|
) {
|
||||||
// Root layout - vertical split for main content, status, and command line
|
render_background(f, f.area(), theme);
|
||||||
|
|
||||||
let root = Layout::default()
|
let root = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Min(10), // Main content area
|
Constraint::Min(1),
|
||||||
Constraint::Length(1), // Status line
|
Constraint::Length(1),
|
||||||
Constraint::Length(1), // Command line
|
Constraint::Length(1),
|
||||||
])
|
])
|
||||||
.split(f.area());
|
.split(f.area());
|
||||||
|
|
||||||
// Main content area layout
|
|
||||||
let main_content_area = root[0];
|
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_admin {
|
||||||
|
// Create temporary AdminPanelState for rendering
|
||||||
|
let mut admin_state = AdminPanelState::new(
|
||||||
|
app_state.profile_tree.profiles
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.name.clone())
|
||||||
|
.collect()
|
||||||
|
);
|
||||||
|
|
||||||
// Split into sidebar + content or just content
|
// Set the selected item - FIXED
|
||||||
let (sidebar_area, content_area) = if ui_state.show_sidebar {
|
if !admin_state.profiles.is_empty() {
|
||||||
let chunks = Layout::default()
|
let selected_index = std::cmp::min(
|
||||||
.direction(Direction::Horizontal)
|
app_state.general.selected_item,
|
||||||
.constraints([
|
admin_state.profiles.len() - 1
|
||||||
Constraint::Length(16), // Fixed sidebar width
|
);
|
||||||
Constraint::Fill(1), // Remaining space for form/preview
|
admin_state.list_state.select(Some(selected_index));
|
||||||
])
|
}
|
||||||
.split(main_content_area);
|
|
||||||
(Some(chunks[0]), chunks[1])
|
|
||||||
} else {
|
|
||||||
(None, main_content_area)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Split content area into form and preview
|
admin_state.render(
|
||||||
let content_chunks = Layout::default()
|
f,
|
||||||
.direction(Direction::Horizontal)
|
main_content_area,
|
||||||
.constraints([
|
theme,
|
||||||
Constraint::Percentage(60),
|
&app_state.profile_tree,
|
||||||
Constraint::Percentage(40),
|
&app_state.selected_profile,
|
||||||
])
|
);
|
||||||
.split(content_area);
|
} else if app_state.ui.show_form {
|
||||||
|
let (sidebar_area, form_area) = calculate_sidebar_layout(
|
||||||
|
app_state.ui.show_sidebar,
|
||||||
|
main_content_area
|
||||||
|
);
|
||||||
|
|
||||||
// Render form in the left content area
|
if let Some(sidebar_rect) = sidebar_area {
|
||||||
form_state.render(
|
sidebar::render_sidebar(
|
||||||
f,
|
f,
|
||||||
content_chunks[0],
|
sidebar_rect,
|
||||||
theme,
|
theme,
|
||||||
is_edit_mode,
|
&app_state.profile_tree,
|
||||||
total_count,
|
&app_state.selected_profile
|
||||||
current_position,
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
// Render preview card in the right content area
|
// This change makes the form stay stationary when toggling sidebar
|
||||||
let preview_values: Vec<&String> = form_state.values.iter().collect();
|
let available_width = form_area.width;
|
||||||
render_preview_card(
|
let form_constraint = if available_width >= 80 {
|
||||||
f,
|
// Use main_content_area for centering when enough space
|
||||||
content_chunks[1],
|
Layout::default()
|
||||||
&preview_values,
|
.direction(Direction::Horizontal)
|
||||||
theme,
|
.constraints([
|
||||||
);
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(80),
|
||||||
|
Constraint::Min(0),
|
||||||
|
])
|
||||||
|
.split(main_content_area)[1]
|
||||||
|
} else {
|
||||||
|
// Use form_area (post sidebar) when limited space
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(80.min(available_width)),
|
||||||
|
Constraint::Min(0),
|
||||||
|
])
|
||||||
|
.split(form_area)[1]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert fields to &[&str] and values to &[&String]
|
||||||
|
let fields: Vec<&str> = form_state.fields.iter().map(|s| s.as_str()).collect();
|
||||||
|
let values: Vec<&String> = form_state.values.iter().collect();
|
||||||
|
|
||||||
|
render_form(
|
||||||
|
f,
|
||||||
|
form_constraint,
|
||||||
|
form_state,
|
||||||
|
&fields,
|
||||||
|
&form_state.current_field,
|
||||||
|
&values,
|
||||||
|
theme,
|
||||||
|
is_edit_mode,
|
||||||
|
total_count,
|
||||||
|
current_position,
|
||||||
|
);
|
||||||
|
} else{
|
||||||
|
|
||||||
// Render sidebar if enabled
|
|
||||||
if let Some(sidebar_rect) = sidebar_area {
|
|
||||||
crate::components::handlers::sidebar::render_sidebar(f, sidebar_rect, theme);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status line
|
render_status_line(f, root[1], current_dir, theme, is_edit_mode);
|
||||||
render_status_line(
|
render_command_line(f, root[2], command_input, command_mode, theme, command_message);
|
||||||
f,
|
|
||||||
root[1],
|
|
||||||
current_dir,
|
|
||||||
theme,
|
|
||||||
is_edit_mode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Command line
|
|
||||||
render_command_line(
|
|
||||||
f,
|
|
||||||
root[2],
|
|
||||||
command_input,
|
|
||||||
command_mode,
|
|
||||||
theme,
|
|
||||||
command_message,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,41 @@
|
|||||||
// src/client/ui/handlers/ui.rs
|
// src/ui/handlers/ui.rs
|
||||||
|
|
||||||
use crate::tui::terminal::TerminalCore;
|
use crate::tui::terminal::TerminalCore;
|
||||||
use crate::tui::terminal::GrpcClient;
|
use crate::tui::terminal::GrpcClient;
|
||||||
use crate::tui::terminal::CommandHandler;
|
use crate::tui::terminal::CommandHandler;
|
||||||
use crate::tui::terminal::EventReader;
|
use crate::tui::terminal::EventReader;
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::ui::handlers::{form::FormState, render::render_ui};
|
use crate::ui::handlers::{form::FormState, render::render_ui};
|
||||||
use crate::modes::handlers::event::EventHandler;
|
use crate::modes::handlers::event::EventHandler;
|
||||||
use crate::state::state::AppState;
|
use crate::state::state::AppState;
|
||||||
|
use crate::components::admin::{admin_panel::AdminPanelState};
|
||||||
|
use crate::components::intro::{intro::IntroState};
|
||||||
|
|
||||||
pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load()?;
|
let config = Config::load()?;
|
||||||
let mut terminal = TerminalCore::new()?; // Remove .await
|
let mut terminal = TerminalCore::new()?;
|
||||||
let mut grpc_client = GrpcClient::new().await?;
|
let mut grpc_client = GrpcClient::new().await?;
|
||||||
let mut command_handler = CommandHandler::new();
|
let mut command_handler = CommandHandler::new();
|
||||||
let theme = Theme::from_str(&config.colors.theme);
|
let theme = Theme::from_str(&config.colors.theme);
|
||||||
|
let mut intro_state = IntroState::new();
|
||||||
|
|
||||||
|
// Initialize app_state first
|
||||||
|
let mut app_state = AppState::new()?;
|
||||||
|
|
||||||
|
// Fetch profile tree and table structure
|
||||||
|
let profile_tree = grpc_client.get_profile_tree().await?;
|
||||||
|
app_state.profile_tree = profile_tree;
|
||||||
|
|
||||||
|
// Now create admin panel with profiles from app_state
|
||||||
|
if intro_state.selected_option == 1 {
|
||||||
|
app_state.ui.show_admin = true;
|
||||||
|
app_state.general.selected_item = 0;
|
||||||
|
app_state.general.current_option = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch table structure at startup (one-time)
|
// Fetch table structure at startup (one-time)
|
||||||
// TODO: Later, consider implementing a live update for table structure changes.
|
let table_structure = grpc_client.get_table_structure().await?;
|
||||||
let table_structure = grpc_client.get_table_structure().await?; // Changed
|
|
||||||
|
|
||||||
// Extract the column names from the response
|
// Extract the column names from the response
|
||||||
let column_names: Vec<String> = table_structure
|
let column_names: Vec<String> = table_structure
|
||||||
@@ -35,7 +50,6 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// The rest of your UI initialization remains the same
|
// The rest of your UI initialization remains the same
|
||||||
let mut event_handler = EventHandler::new();
|
let mut event_handler = EventHandler::new();
|
||||||
let event_reader = EventReader::new();
|
let event_reader = EventReader::new();
|
||||||
let mut app_state = AppState::new()?;
|
|
||||||
|
|
||||||
// Fetch the total count of Adresar entries
|
// Fetch the total count of Adresar entries
|
||||||
let total_count = grpc_client.get_adresar_count().await?;
|
let total_count = grpc_client.get_adresar_count().await?;
|
||||||
@@ -59,7 +73,7 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
&event_handler.command_input,
|
&event_handler.command_input,
|
||||||
event_handler.command_mode,
|
event_handler.command_mode,
|
||||||
&event_handler.command_message,
|
&event_handler.command_message,
|
||||||
&app_state.ui,
|
&app_state,
|
||||||
);
|
);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -91,7 +105,6 @@ 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);
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||||||
|
|
||||||
|
|
||||||
// Ensure position never exceeds total_count + 1
|
// Ensure position never exceeds total_count + 1
|
||||||
if app_state.current_position > total_count + 1 {
|
if app_state.current_position > total_count + 1 {
|
||||||
app_state.current_position = total_count + 1;
|
app_state.current_position = total_count + 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user