Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9393294af8 | ||
|
|
24c426229c | ||
|
|
3ed6fd4ee8 | ||
|
|
70d83c284a | ||
|
|
8a248cab58 | ||
|
|
e6851e1fe4 | ||
|
|
65ff1256aa | ||
|
|
36dc4302a0 | ||
|
|
938a1f16f1 | ||
|
|
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 |
126
Cargo.lock
generated
126
Cargo.lock
generated
@@ -274,6 +274,19 @@ version = "1.6.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bcrypt"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"blowfish",
|
||||||
|
"getrandom 0.3.1",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bigdecimal"
|
name = "bigdecimal"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
@@ -323,6 +336,16 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blowfish"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.17.0"
|
version = "3.17.0"
|
||||||
@@ -386,9 +409,19 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "client"
|
name = "client"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"common",
|
"common",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
@@ -424,7 +457,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "common"
|
name = "common"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"prost",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -782,7 +815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1452,6 +1485,15 @@ version = "2.0.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instability"
|
name = "instability"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@@ -2030,6 +2072,28 @@ dependencies = [
|
|||||||
"toml_edit",
|
"toml_edit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr2"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.99",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.94"
|
version = "1.0.94"
|
||||||
@@ -2356,7 +2420,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.15",
|
"linux-raw-sys 0.4.15",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2369,7 +2433,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.9.2",
|
"linux-raw-sys 0.9.2",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2483,8 +2547,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "server"
|
name = "server"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bcrypt",
|
||||||
"chrono",
|
"chrono",
|
||||||
"common",
|
"common",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
@@ -2504,6 +2569,8 @@ dependencies = [
|
|||||||
"tonic",
|
"tonic",
|
||||||
"tonic-reflection",
|
"tonic-reflection",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2678,6 +2745,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2760,6 +2828,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2799,6 +2868,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2825,6 +2895,7 @@ dependencies = [
|
|||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3029,7 +3100,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.1",
|
"getrandom 0.3.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.0.1",
|
"rustix 1.0.1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3473,6 +3544,45 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "validator"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
|
||||||
|
dependencies = [
|
||||||
|
"idna",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
"validator_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "validator_derive"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro-error2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.99",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -3623,7 +3733,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.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ resolver = "2"
|
|||||||
[workspace.package]
|
[workspace.package]
|
||||||
# TODO: idk how to do the name, fix later
|
# TODO: idk how to do the name, fix later
|
||||||
# name = "Multieko2"
|
# name = "Multieko2"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
authors = ["Filip Priečinský <filippriec@gmail.com>"]
|
authors = ["Filip Priečinský <filippriec@gmail.com>"]
|
||||||
|
|||||||
@@ -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(
|
||||||
4
client/src/components/form.rs
Normal file
4
client/src/components/form.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/components/form.rs
|
||||||
|
pub mod form;
|
||||||
|
|
||||||
|
pub use form::*;
|
||||||
@@ -5,9 +5,9 @@ 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 crate::components::handlers::canvas::render_canvas;
|
||||||
|
|
||||||
pub fn render_form(
|
pub fn render_form(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
// src/components/handlers.rs
|
// src/components/handlers.rs
|
||||||
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 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(
|
||||||
|
|||||||
@@ -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,27 +1,109 @@
|
|||||||
// 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) {
|
// Reduced sidebar width
|
||||||
let sidebar_block = Block::default()
|
const SIDEBAR_WIDTH: u16 = 12;
|
||||||
.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 {
|
||||||
|
// Existing code for when a profile is selected...
|
||||||
|
} else {
|
||||||
|
// Show full profile tree when no profile is selected (compact version)
|
||||||
|
for (profile_idx, profile) in profile_tree.profiles.iter().enumerate() {
|
||||||
|
// Profile header - more compact
|
||||||
|
items.push(ListItem::new(Line::from(vec![
|
||||||
|
Span::styled("◆", Style::default().fg(theme.accent)),
|
||||||
|
Span::styled(&profile.name, Style::default().fg(theme.highlight)),
|
||||||
|
])));
|
||||||
|
|
||||||
|
// Tables with compact prefixes
|
||||||
|
for (table_idx, table) in profile.tables.iter().enumerate() {
|
||||||
|
let is_last_table = table_idx == profile.tables.len() - 1;
|
||||||
|
let is_last_profile = profile_idx == profile_tree.profiles.len() - 1;
|
||||||
|
|
||||||
|
// Shorter prefix characters
|
||||||
|
let prefix = match (is_last_profile, is_last_table) {
|
||||||
|
(true, true) => " └",
|
||||||
|
(true, false) => " ├",
|
||||||
|
(false, true) => "│└",
|
||||||
|
(false, false) => "│├",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get table name without year prefix to save space
|
||||||
|
let display_name = if table.name.starts_with("2025_") {
|
||||||
|
&table.name[5..] // Skip "2025_" prefix
|
||||||
|
} else {
|
||||||
|
&table.name
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut line = vec![
|
||||||
|
Span::styled(prefix, Style::default().fg(theme.fg)),
|
||||||
|
Span::styled(display_name, Style::default().fg(theme.fg)),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Show a simple indicator for dependencies instead of listing them
|
||||||
|
if !table.depends_on.is_empty() {
|
||||||
|
line.push(Span::styled(
|
||||||
|
"→",
|
||||||
|
Style::default().fg(theme.secondary)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(ListItem::new(Line::from(line)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact separator between profiles
|
||||||
|
if profile_idx < profile_tree.profiles.len() - 1 {
|
||||||
|
items.push(ListItem::new(Line::from(
|
||||||
|
Span::styled("│", Style::default().fg(theme.secondary))
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile_tree.profiles.is_empty() {
|
||||||
|
items.push(ListItem::new(Span::styled(
|
||||||
|
"No profiles",
|
||||||
|
Style::default().fg(theme.secondary)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let list = List::new(items)
|
let list = List::new(items)
|
||||||
.block(sidebar_block)
|
.block(sidebar_block)
|
||||||
.highlight_style(Style::default().fg(theme.highlight))
|
.highlight_style(Style::default().fg(theme.highlight))
|
||||||
.highlight_symbol(">>");
|
.highlight_symbol(">");
|
||||||
|
|
||||||
f.render_widget(list, area);
|
f.render_widget(list, area);
|
||||||
}
|
}
|
||||||
|
|||||||
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,12 @@
|
|||||||
// 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 mod form;
|
||||||
|
|
||||||
pub use handlers::*;
|
pub use handlers::*;
|
||||||
|
pub use intro::*;
|
||||||
|
pub use admin::*;
|
||||||
|
pub use common::*;
|
||||||
|
pub use form::*;
|
||||||
|
|||||||
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,83 @@
|
|||||||
// 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::controls::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" => {
|
||||||
|
terminal.cleanup()?;
|
||||||
|
Ok((true, "Force exiting without saving.".to_string()))
|
||||||
|
},
|
||||||
|
"save_and_quit" => {
|
||||||
|
let message = save(
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
&mut app_state.ui.is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await?;
|
||||||
|
terminal.cleanup()?;
|
||||||
|
Ok((true, format!("{}. Exiting application.", 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,
|
||||||
@@ -65,64 +139,6 @@ pub async fn save(
|
|||||||
Ok(message)
|
Ok(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared logic for force quitting the application
|
|
||||||
pub fn force_quit() -> (bool, String) {
|
|
||||||
(true, "Force quitting application".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shared logic for saving and quitting
|
|
||||||
pub async fn save_and_quit(
|
|
||||||
form_state: &mut FormState,
|
|
||||||
grpc_client: &mut GrpcClient,
|
|
||||||
current_position: &mut u64,
|
|
||||||
total_count: u64,
|
|
||||||
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
|
||||||
let is_new = *current_position == total_count + 1;
|
|
||||||
|
|
||||||
if is_new {
|
|
||||||
let post_request = PostAdresarRequest {
|
|
||||||
firma: form_state.values[0].clone(),
|
|
||||||
kz: form_state.values[1].clone(),
|
|
||||||
drc: form_state.values[2].clone(),
|
|
||||||
ulica: form_state.values[3].clone(),
|
|
||||||
psc: form_state.values[4].clone(),
|
|
||||||
mesto: form_state.values[5].clone(),
|
|
||||||
stat: form_state.values[6].clone(),
|
|
||||||
banka: form_state.values[7].clone(),
|
|
||||||
ucet: form_state.values[8].clone(),
|
|
||||||
skladm: form_state.values[9].clone(),
|
|
||||||
ico: form_state.values[10].clone(),
|
|
||||||
kontakt: form_state.values[11].clone(),
|
|
||||||
telefon: form_state.values[12].clone(),
|
|
||||||
skladu: form_state.values[13].clone(),
|
|
||||||
fax: form_state.values[14].clone(),
|
|
||||||
};
|
|
||||||
let _ = grpc_client.post_adresar(post_request).await?;
|
|
||||||
} else {
|
|
||||||
let put_request = PutAdresarRequest {
|
|
||||||
id: form_state.id,
|
|
||||||
firma: form_state.values[0].clone(),
|
|
||||||
kz: form_state.values[1].clone(),
|
|
||||||
drc: form_state.values[2].clone(),
|
|
||||||
ulica: form_state.values[3].clone(),
|
|
||||||
psc: form_state.values[4].clone(),
|
|
||||||
mesto: form_state.values[5].clone(),
|
|
||||||
stat: form_state.values[6].clone(),
|
|
||||||
banka: form_state.values[7].clone(),
|
|
||||||
ucet: form_state.values[8].clone(),
|
|
||||||
skladm: form_state.values[9].clone(),
|
|
||||||
ico: form_state.values[10].clone(),
|
|
||||||
kontakt: form_state.values[11].clone(),
|
|
||||||
telefon: form_state.values[12].clone(),
|
|
||||||
skladu: form_state.values[13].clone(),
|
|
||||||
fax: form_state.values[14].clone(),
|
|
||||||
};
|
|
||||||
let _ = grpc_client.put_adresar(put_request).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((true, "Saved and exiting application".to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Discard changes since last save
|
/// Discard changes since last save
|
||||||
pub async fn revert(
|
pub async fn revert(
|
||||||
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,13 @@
|
|||||||
|
|
||||||
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::tui::controls::commands::CommandHandler;
|
||||||
|
use crate::tui::terminal::core::TerminalCore;
|
||||||
|
use crate::modes::{
|
||||||
|
canvas::{common},
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn handle_command_event(
|
pub async fn handle_command_event(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
@@ -13,7 +17,8 @@ pub async fn handle_command_event(
|
|||||||
command_input: &mut String,
|
command_input: &mut String,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
is_saved: &mut bool,
|
command_handler: &mut CommandHandler,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
current_position: &mut u64,
|
current_position: &mut u64,
|
||||||
total_count: u64,
|
total_count: u64,
|
||||||
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
|
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
|
||||||
@@ -35,7 +40,8 @@ pub async fn handle_command_event(
|
|||||||
command_input,
|
command_input,
|
||||||
command_message,
|
command_message,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
is_saved,
|
command_handler,
|
||||||
|
terminal,
|
||||||
current_position,
|
current_position,
|
||||||
total_count,
|
total_count,
|
||||||
).await;
|
).await;
|
||||||
@@ -66,7 +72,8 @@ async fn process_command(
|
|||||||
command_input: &mut String,
|
command_input: &mut String,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
is_saved: &mut bool,
|
command_handler: &mut CommandHandler,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
current_position: &mut u64,
|
current_position: &mut u64,
|
||||||
total_count: u64,
|
total_count: u64,
|
||||||
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
|
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
|
||||||
@@ -82,32 +89,24 @@ async fn process_command(
|
|||||||
.unwrap_or("unknown");
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
|
"force_quit" | "save_and_quit" | "quit" => {
|
||||||
|
let (should_exit, message) = command_handler
|
||||||
|
.handle_command(action, terminal)
|
||||||
|
.await?;
|
||||||
|
command_input.clear();
|
||||||
|
Ok((should_exit, message, true))
|
||||||
|
},
|
||||||
"save" => {
|
"save" => {
|
||||||
let message = common::save(
|
let message = common::save(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
is_saved,
|
&mut command_handler.is_saved,
|
||||||
current_position,
|
current_position,
|
||||||
total_count,
|
total_count,
|
||||||
).await?;
|
).await?;
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
return Ok((false, message, true));
|
return Ok((false, message, true));
|
||||||
},
|
},
|
||||||
"force_quit" => {
|
|
||||||
let (should_exit, message) = common::force_quit();
|
|
||||||
command_input.clear();
|
|
||||||
return Ok((should_exit, message, true));
|
|
||||||
},
|
|
||||||
"save_and_quit" => {
|
|
||||||
let (should_exit, message) = common::save_and_quit(
|
|
||||||
form_state,
|
|
||||||
grpc_client,
|
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
|
||||||
command_input.clear();
|
|
||||||
return Ok((should_exit, message, true));
|
|
||||||
},
|
|
||||||
"revert" => {
|
"revert" => {
|
||||||
let message = common::revert(
|
let message = common::revert(
|
||||||
form_state,
|
form_state,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
use crate::config::config::Config;
|
use crate::tui::controls::commands::CommandHandler;
|
||||||
|
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,197 @@ 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,
|
||||||
|
command_handler,
|
||||||
|
terminal,
|
||||||
|
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
|
|
||||||
5
client/src/tui/controls.rs
Normal file
5
client/src/tui/controls.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// src/tui/controls.rs
|
||||||
|
|
||||||
|
pub mod commands;
|
||||||
|
|
||||||
|
pub use commands::*;
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
// src/tui/terminal/commands.rs
|
// src/tui/controls/commands.rs
|
||||||
|
|
||||||
use crate::tui::terminal::core::TerminalCore;
|
use crate::tui::terminal::core::TerminalCore;
|
||||||
|
|
||||||
pub struct CommandHandler {
|
pub struct CommandHandler {
|
||||||
is_saved: bool,
|
pub is_saved: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandHandler {
|
impl CommandHandler {
|
||||||
@@ -24,7 +23,10 @@ impl CommandHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_quit(&self, terminal: &mut TerminalCore) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
async fn handle_quit(
|
||||||
|
&self,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
if self.is_saved {
|
if self.is_saved {
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
Ok((true, "Exiting.".into()))
|
Ok((true, "Exiting.".into()))
|
||||||
@@ -33,12 +35,18 @@ impl CommandHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_force_quit(&self, terminal: &mut TerminalCore) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
async fn handle_force_quit(
|
||||||
|
&self,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
Ok((true, "Force exiting without saving.".into()))
|
Ok((true, "Force exiting without saving.".into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_save_quit(&mut self, terminal: &mut TerminalCore) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
async fn handle_save_quit(
|
||||||
|
&mut self,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
self.is_saved = true;
|
self.is_saved = true;
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
Ok((true, "State saved. Exiting.".into()))
|
Ok((true, "State saved. Exiting.".into()))
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
// src/tui/mod.rs
|
// src/tui/mod.rs
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
|
pub mod controls;
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod grpc_client;
|
pub mod grpc_client;
|
||||||
pub mod commands;
|
|
||||||
pub mod event_reader;
|
pub mod event_reader;
|
||||||
|
|
||||||
pub use core::TerminalCore;
|
pub use core::TerminalCore;
|
||||||
pub use grpc_client::GrpcClient;
|
pub use grpc_client::GrpcClient;
|
||||||
pub use commands::CommandHandler;
|
|
||||||
pub use event_reader::EventReader;
|
pub use event_reader::EventReader;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ impl FormState {
|
|||||||
let fields: Vec<&str> = self.fields.iter().map(|s| s.as_str()).collect();
|
let fields: Vec<&str> = self.fields.iter().map(|s| s.as_str()).collect();
|
||||||
let values: Vec<&String> = self.values.iter().collect();
|
let values: Vec<&String> = self.values.iter().collect();
|
||||||
|
|
||||||
crate::components::handlers::form::render_form(
|
crate::components::form::form::render_form(
|
||||||
f,
|
f,
|
||||||
area,
|
area,
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -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,19 @@
|
|||||||
// 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::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 +26,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::controls::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;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
&[
|
&[
|
||||||
"proto/common.proto",
|
"proto/common.proto",
|
||||||
"proto/adresar.proto",
|
"proto/adresar.proto",
|
||||||
|
"proto/auth.proto",
|
||||||
"proto/uctovnictvo.proto",
|
"proto/uctovnictvo.proto",
|
||||||
"proto/table_structure.proto",
|
"proto/table_structure.proto",
|
||||||
"proto/table_definition.proto",
|
"proto/table_definition.proto",
|
||||||
|
|||||||
23
common/proto/auth.proto
Normal file
23
common/proto/auth.proto
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// proto/auth.proto
|
||||||
|
syntax = "proto3";
|
||||||
|
package multieko2.auth;
|
||||||
|
|
||||||
|
import "common.proto";
|
||||||
|
|
||||||
|
service AuthService {
|
||||||
|
rpc Register(RegisterRequest) returns (AuthResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterRequest {
|
||||||
|
string username = 1;
|
||||||
|
string email = 2;
|
||||||
|
string password = 3;
|
||||||
|
string password_confirmation = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthResponse {
|
||||||
|
string id = 1; // UUID in string format
|
||||||
|
string username = 2; // Registered username
|
||||||
|
string email = 3; // Registered email (if provided)
|
||||||
|
string role = 4; // Default role: 'accountant'
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ pub mod proto {
|
|||||||
pub mod adresar {
|
pub mod adresar {
|
||||||
include!("proto/multieko2.adresar.rs");
|
include!("proto/multieko2.adresar.rs");
|
||||||
}
|
}
|
||||||
|
pub mod auth {
|
||||||
|
include!("proto/multieko2.auth.rs");
|
||||||
|
}
|
||||||
pub mod common {
|
pub mod common {
|
||||||
include!("proto/multieko2.common.rs");
|
include!("proto/multieko2.common.rs");
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
318
common/src/proto/multieko2.auth.rs
Normal file
318
common/src/proto/multieko2.auth.rs
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
// This file is @generated by prost-build.
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub username: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub email: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub password: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub password_confirmation: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct AuthResponse {
|
||||||
|
/// UUID in string format
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub id: ::prost::alloc::string::String,
|
||||||
|
/// Registered username
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub username: ::prost::alloc::string::String,
|
||||||
|
/// Registered email (if provided)
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub email: ::prost::alloc::string::String,
|
||||||
|
/// Default role: 'accountant'
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub role: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// Generated client implementations.
|
||||||
|
pub mod auth_service_client {
|
||||||
|
#![allow(
|
||||||
|
unused_variables,
|
||||||
|
dead_code,
|
||||||
|
missing_docs,
|
||||||
|
clippy::wildcard_imports,
|
||||||
|
clippy::let_unit_value,
|
||||||
|
)]
|
||||||
|
use tonic::codegen::*;
|
||||||
|
use tonic::codegen::http::Uri;
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AuthServiceClient<T> {
|
||||||
|
inner: tonic::client::Grpc<T>,
|
||||||
|
}
|
||||||
|
impl AuthServiceClient<tonic::transport::Channel> {
|
||||||
|
/// Attempt to create a new client by connecting to a given endpoint.
|
||||||
|
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||||
|
where
|
||||||
|
D: TryInto<tonic::transport::Endpoint>,
|
||||||
|
D::Error: Into<StdError>,
|
||||||
|
{
|
||||||
|
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||||
|
Ok(Self::new(conn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> AuthServiceClient<T>
|
||||||
|
where
|
||||||
|
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
||||||
|
T::Error: Into<StdError>,
|
||||||
|
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||||
|
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||||
|
{
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
let inner = tonic::client::Grpc::new(inner);
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||||
|
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
pub fn with_interceptor<F>(
|
||||||
|
inner: T,
|
||||||
|
interceptor: F,
|
||||||
|
) -> AuthServiceClient<InterceptedService<T, F>>
|
||||||
|
where
|
||||||
|
F: tonic::service::Interceptor,
|
||||||
|
T::ResponseBody: Default,
|
||||||
|
T: tonic::codegen::Service<
|
||||||
|
http::Request<tonic::body::BoxBody>,
|
||||||
|
Response = http::Response<
|
||||||
|
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
<T as tonic::codegen::Service<
|
||||||
|
http::Request<tonic::body::BoxBody>,
|
||||||
|
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||||
|
{
|
||||||
|
AuthServiceClient::new(InterceptedService::new(inner, interceptor))
|
||||||
|
}
|
||||||
|
/// Compress requests with the given encoding.
|
||||||
|
///
|
||||||
|
/// This requires the server to support it otherwise it might respond with an
|
||||||
|
/// error.
|
||||||
|
#[must_use]
|
||||||
|
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.inner = self.inner.send_compressed(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Enable decompressing responses.
|
||||||
|
#[must_use]
|
||||||
|
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.inner = self.inner.accept_compressed(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of a decoded message.
|
||||||
|
///
|
||||||
|
/// Default: `4MB`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.inner = self.inner.max_decoding_message_size(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of an encoded message.
|
||||||
|
///
|
||||||
|
/// Default: `usize::MAX`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.inner = self.inner.max_encoding_message_size(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub async fn register(
|
||||||
|
&mut self,
|
||||||
|
request: impl tonic::IntoRequest<super::RegisterRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<super::AuthResponse>, tonic::Status> {
|
||||||
|
self.inner
|
||||||
|
.ready()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tonic::Status::unknown(
|
||||||
|
format!("Service was not ready: {}", e.into()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let path = http::uri::PathAndQuery::from_static(
|
||||||
|
"/multieko2.auth.AuthService/Register",
|
||||||
|
);
|
||||||
|
let mut req = request.into_request();
|
||||||
|
req.extensions_mut()
|
||||||
|
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Register"));
|
||||||
|
self.inner.unary(req, path, codec).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Generated server implementations.
|
||||||
|
pub mod auth_service_server {
|
||||||
|
#![allow(
|
||||||
|
unused_variables,
|
||||||
|
dead_code,
|
||||||
|
missing_docs,
|
||||||
|
clippy::wildcard_imports,
|
||||||
|
clippy::let_unit_value,
|
||||||
|
)]
|
||||||
|
use tonic::codegen::*;
|
||||||
|
/// Generated trait containing gRPC methods that should be implemented for use with AuthServiceServer.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AuthService: std::marker::Send + std::marker::Sync + 'static {
|
||||||
|
async fn register(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<super::RegisterRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<super::AuthResponse>, tonic::Status>;
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AuthServiceServer<T> {
|
||||||
|
inner: Arc<T>,
|
||||||
|
accept_compression_encodings: EnabledCompressionEncodings,
|
||||||
|
send_compression_encodings: EnabledCompressionEncodings,
|
||||||
|
max_decoding_message_size: Option<usize>,
|
||||||
|
max_encoding_message_size: Option<usize>,
|
||||||
|
}
|
||||||
|
impl<T> AuthServiceServer<T> {
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self::from_arc(Arc::new(inner))
|
||||||
|
}
|
||||||
|
pub fn from_arc(inner: Arc<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
accept_compression_encodings: Default::default(),
|
||||||
|
send_compression_encodings: Default::default(),
|
||||||
|
max_decoding_message_size: None,
|
||||||
|
max_encoding_message_size: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn with_interceptor<F>(
|
||||||
|
inner: T,
|
||||||
|
interceptor: F,
|
||||||
|
) -> InterceptedService<Self, F>
|
||||||
|
where
|
||||||
|
F: tonic::service::Interceptor,
|
||||||
|
{
|
||||||
|
InterceptedService::new(Self::new(inner), interceptor)
|
||||||
|
}
|
||||||
|
/// Enable decompressing requests with the given encoding.
|
||||||
|
#[must_use]
|
||||||
|
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.accept_compression_encodings.enable(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Compress responses with the given encoding, if the client supports it.
|
||||||
|
#[must_use]
|
||||||
|
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.send_compression_encodings.enable(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of a decoded message.
|
||||||
|
///
|
||||||
|
/// Default: `4MB`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.max_decoding_message_size = Some(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of an encoded message.
|
||||||
|
///
|
||||||
|
/// Default: `usize::MAX`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.max_encoding_message_size = Some(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T, B> tonic::codegen::Service<http::Request<B>> for AuthServiceServer<T>
|
||||||
|
where
|
||||||
|
T: AuthService,
|
||||||
|
B: Body + std::marker::Send + 'static,
|
||||||
|
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||||
|
{
|
||||||
|
type Response = http::Response<tonic::body::BoxBody>;
|
||||||
|
type Error = std::convert::Infallible;
|
||||||
|
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||||
|
fn poll_ready(
|
||||||
|
&mut self,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<std::result::Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||||
|
match req.uri().path() {
|
||||||
|
"/multieko2.auth.AuthService/Register" => {
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
struct RegisterSvc<T: AuthService>(pub Arc<T>);
|
||||||
|
impl<
|
||||||
|
T: AuthService,
|
||||||
|
> tonic::server::UnaryService<super::RegisterRequest>
|
||||||
|
for RegisterSvc<T> {
|
||||||
|
type Response = super::AuthResponse;
|
||||||
|
type Future = BoxFuture<
|
||||||
|
tonic::Response<Self::Response>,
|
||||||
|
tonic::Status,
|
||||||
|
>;
|
||||||
|
fn call(
|
||||||
|
&mut self,
|
||||||
|
request: tonic::Request<super::RegisterRequest>,
|
||||||
|
) -> Self::Future {
|
||||||
|
let inner = Arc::clone(&self.0);
|
||||||
|
let fut = async move {
|
||||||
|
<T as AuthService>::register(&inner, request).await
|
||||||
|
};
|
||||||
|
Box::pin(fut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let accept_compression_encodings = self.accept_compression_encodings;
|
||||||
|
let send_compression_encodings = self.send_compression_encodings;
|
||||||
|
let max_decoding_message_size = self.max_decoding_message_size;
|
||||||
|
let max_encoding_message_size = self.max_encoding_message_size;
|
||||||
|
let inner = self.inner.clone();
|
||||||
|
let fut = async move {
|
||||||
|
let method = RegisterSvc(inner);
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let mut grpc = tonic::server::Grpc::new(codec)
|
||||||
|
.apply_compression_config(
|
||||||
|
accept_compression_encodings,
|
||||||
|
send_compression_encodings,
|
||||||
|
)
|
||||||
|
.apply_max_message_size_config(
|
||||||
|
max_decoding_message_size,
|
||||||
|
max_encoding_message_size,
|
||||||
|
);
|
||||||
|
let res = grpc.unary(method, req).await;
|
||||||
|
Ok(res)
|
||||||
|
};
|
||||||
|
Box::pin(fut)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut response = http::Response::new(empty_body());
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
headers
|
||||||
|
.insert(
|
||||||
|
tonic::Status::GRPC_STATUS,
|
||||||
|
(tonic::Code::Unimplemented as i32).into(),
|
||||||
|
);
|
||||||
|
headers
|
||||||
|
.insert(
|
||||||
|
http::header::CONTENT_TYPE,
|
||||||
|
tonic::metadata::GRPC_CONTENT_TYPE,
|
||||||
|
);
|
||||||
|
Ok(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Clone for AuthServiceServer<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
let inner = self.inner.clone();
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
accept_compression_encodings: self.accept_compression_encodings,
|
||||||
|
send_compression_encodings: self.send_compression_encodings,
|
||||||
|
max_decoding_message_size: self.max_decoding_message_size,
|
||||||
|
max_encoding_message_size: self.max_encoding_message_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Generated gRPC service name
|
||||||
|
pub const SERVICE_NAME: &str = "multieko2.auth.AuthService";
|
||||||
|
impl<T> tonic::server::NamedService for AuthServiceServer<T> {
|
||||||
|
const NAME: &'static str = SERVICE_NAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ dotenvy = "0.15.7"
|
|||||||
prost = "0.13.5"
|
prost = "0.13.5"
|
||||||
serde = { version = "1.0.218", features = ["derive"] }
|
serde = { version = "1.0.218", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
sqlx = { version = "0.8.3", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "time"] }
|
sqlx = { version = "0.8.3", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "time", "uuid"] }
|
||||||
tokio = { version = "1.43.0", features = ["full", "macros"] }
|
tokio = { version = "1.43.0", features = ["full", "macros"] }
|
||||||
tonic = "0.12.3"
|
tonic = "0.12.3"
|
||||||
tonic-reflection = "0.12.3"
|
tonic-reflection = "0.12.3"
|
||||||
@@ -24,6 +24,9 @@ thiserror = "2.0.12"
|
|||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
bcrypt = "0.17.0"
|
||||||
|
validator = { version = "0.20.0", features = ["derive"] }
|
||||||
|
uuid = { version = "1.16.0", features = ["v4"] }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "server"
|
name = "server"
|
||||||
|
|||||||
38
server/migrations/20250324192805_auth.sql
Normal file
38
server/migrations/20250324192805_auth.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
email VARCHAR(255) UNIQUE,
|
||||||
|
password_hash VARCHAR(255),
|
||||||
|
role VARCHAR(20) NOT NULL DEFAULT 'accountant',
|
||||||
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add an index for faster lookups
|
||||||
|
CREATE INDEX idx_users_email_username ON users(email, username);
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD CONSTRAINT valid_roles CHECK (role IN (
|
||||||
|
'admin',
|
||||||
|
'moderator',
|
||||||
|
'accountant',
|
||||||
|
'viewer'
|
||||||
|
));
|
||||||
|
|
||||||
|
-- Create JWT sessions table
|
||||||
|
CREATE TABLE user_sessions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
jwt_token TEXT NOT NULL UNIQUE,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
revoked BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add indexes
|
||||||
|
CREATE INDEX idx_sessions_user ON user_sessions(user_id);
|
||||||
|
CREATE INDEX idx_sessions_expires ON user_sessions(expires_at);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
10
server/src/auth/docs/first_reg.txt
Normal file
10
server/src/auth/docs/first_reg.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
❯ grpcurl -plaintext -d '{
|
||||||
|
"username": "testuser2",
|
||||||
|
"email": "test2@example.com"
|
||||||
|
}' localhost:50051 multieko2.auth.AuthService/Register
|
||||||
|
{
|
||||||
|
"id": "5fa9bbce-85e0-4b06-8364-b561770c2fdd",
|
||||||
|
"username": "testuser2",
|
||||||
|
"email": "test2@example.com",
|
||||||
|
"role": "accountant"
|
||||||
|
}
|
||||||
5
server/src/auth/handlers.rs
Normal file
5
server/src/auth/handlers.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// src/auth/handlers.rs
|
||||||
|
|
||||||
|
pub mod register;
|
||||||
|
|
||||||
|
pub use register::*;
|
||||||
64
server/src/auth/handlers/register.rs
Normal file
64
server/src/auth/handlers/register.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// src/auth/handlers/register.rs
|
||||||
|
|
||||||
|
use bcrypt::{hash, DEFAULT_COST};
|
||||||
|
use tonic::{Request, Response, Status};
|
||||||
|
use common::proto::multieko2::auth::{auth_service_server, RegisterRequest, AuthResponse};
|
||||||
|
use crate::db::PgPool;
|
||||||
|
use crate::auth::models::AuthError;
|
||||||
|
|
||||||
|
pub struct AuthService {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthService {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl auth_service_server::AuthService for AuthService {
|
||||||
|
async fn register(
|
||||||
|
&self,
|
||||||
|
request: Request<RegisterRequest>,
|
||||||
|
) -> Result<Response<AuthResponse>, Status> {
|
||||||
|
let payload = request.into_inner();
|
||||||
|
|
||||||
|
// Validate passwords match
|
||||||
|
if payload.password != payload.password_confirmation {
|
||||||
|
return Err(Status::invalid_argument(AuthError::PasswordMismatch.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
let password_hash = hash(payload.password, DEFAULT_COST)
|
||||||
|
.map_err(|e| Status::internal(AuthError::HashingError(e.to_string()).to_string()))?;
|
||||||
|
|
||||||
|
// Insert user
|
||||||
|
let user = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO users (username, email, password_hash, role)
|
||||||
|
VALUES ($1, $2, $3, 'accountant')
|
||||||
|
RETURNING id, username, email, role
|
||||||
|
"#,
|
||||||
|
payload.username,
|
||||||
|
payload.email,
|
||||||
|
password_hash
|
||||||
|
)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if e.to_string().contains("duplicate key") {
|
||||||
|
Status::already_exists(AuthError::UserExists.to_string())
|
||||||
|
} else {
|
||||||
|
Status::internal(AuthError::DatabaseError(e.to_string()).to_string())
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Response::new(AuthResponse {
|
||||||
|
id: user.id.to_string(),
|
||||||
|
username: user.username,
|
||||||
|
email: user.email.unwrap_or_default(),
|
||||||
|
role: user.role,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
5
server/src/auth/mod.rs
Normal file
5
server/src/auth/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// src/auth/mod.rs
|
||||||
|
|
||||||
|
pub mod models;
|
||||||
|
pub mod handlers;
|
||||||
|
|
||||||
27
server/src/auth/models.rs
Normal file
27
server/src/auth/models.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// src/auth/models.rs
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Debug, Validate, Deserialize)]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
#[validate(length(min = 1, max = 30))]
|
||||||
|
pub username: String,
|
||||||
|
#[validate(email)]
|
||||||
|
pub email: String,
|
||||||
|
#[validate(length(min = 1))]
|
||||||
|
pub password: String,
|
||||||
|
pub password_confirmation: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[error("Passwords do not match")]
|
||||||
|
PasswordMismatch,
|
||||||
|
#[error("User already exists")]
|
||||||
|
UserExists,
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DatabaseError(String),
|
||||||
|
#[error("Hashing error: {0}")]
|
||||||
|
HashingError(String),
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
// src/db.rs
|
// src/db.rs
|
||||||
use sqlx::postgres::{PgPool, PgPoolOptions};
|
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
pub use sqlx::postgres::PgPool;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// src/lib.rs
|
// src/lib.rs
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod auth;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod adresar;
|
pub mod adresar;
|
||||||
pub mod uctovnictvo;
|
pub mod uctovnictvo;
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ use crate::server::services::{
|
|||||||
TablesDataService,
|
TablesDataService,
|
||||||
TableScriptService,
|
TableScriptService,
|
||||||
};
|
};
|
||||||
use common::proto::multieko2::adresar::adresar_server::AdresarServer;
|
use common::proto::multieko2::{
|
||||||
use common::proto::multieko2::uctovnictvo::uctovnictvo_server::UctovnictvoServer;
|
adresar::adresar_server::AdresarServer,
|
||||||
use common::proto::multieko2::table_structure::table_structure_service_server::TableStructureServiceServer;
|
uctovnictvo::uctovnictvo_server::UctovnictvoServer,
|
||||||
use common::proto::multieko2::table_definition::table_definition_server::TableDefinitionServer;
|
table_structure::table_structure_service_server::TableStructureServiceServer,
|
||||||
use common::proto::multieko2::tables_data::tables_data_server::TablesDataServer;
|
table_definition::table_definition_server::TableDefinitionServer,
|
||||||
use common::proto::multieko2::table_script::table_script_server::TableScriptServer;
|
tables_data::tables_data_server::TablesDataServer,
|
||||||
|
table_script::table_script_server::TableScriptServer,
|
||||||
|
auth::auth_service_server::AuthServiceServer // Add this import
|
||||||
|
};
|
||||||
|
use crate::auth::handlers::AuthService; // Add this import
|
||||||
|
|
||||||
pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let addr = "[::1]:50051".parse()?;
|
let addr = "[::1]:50051".parse()?;
|
||||||
@@ -27,8 +31,9 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error:
|
|||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
let table_definition_service = TableDefinitionService { db_pool: db_pool.clone() };
|
let table_definition_service = TableDefinitionService { db_pool: db_pool.clone() };
|
||||||
let tables_data_service = TablesDataService { db_pool: db_pool.clone() }; // Add this
|
let tables_data_service = TablesDataService { db_pool: db_pool.clone() };
|
||||||
let table_script_service = TableScriptService { db_pool: db_pool.clone() };
|
let table_script_service = TableScriptService { db_pool: db_pool.clone() };
|
||||||
|
let auth_service = AuthService::new(db_pool.clone()); // Add this line
|
||||||
|
|
||||||
Server::builder()
|
Server::builder()
|
||||||
.add_service(AdresarServer::new(AdresarService { db_pool: db_pool.clone() }))
|
.add_service(AdresarServer::new(AdresarService { db_pool: db_pool.clone() }))
|
||||||
@@ -37,6 +42,7 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error:
|
|||||||
.add_service(TableDefinitionServer::new(table_definition_service))
|
.add_service(TableDefinitionServer::new(table_definition_service))
|
||||||
.add_service(TablesDataServer::new(tables_data_service))
|
.add_service(TablesDataServer::new(tables_data_service))
|
||||||
.add_service(TableScriptServer::new(table_script_service))
|
.add_service(TableScriptServer::new(table_script_service))
|
||||||
|
.add_service(AuthServiceServer::new(auth_service))
|
||||||
.add_service(reflection_service)
|
.add_service(reflection_service)
|
||||||
.serve(addr)
|
.serve(addr)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user