Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88a4b2d69c | ||
|
|
e6072d25c5 | ||
|
|
fc2b65601e | ||
|
|
597bdde7e1 | ||
|
|
f56092e86c | ||
|
|
d5cfe59f47 | ||
|
|
f281eaa662 | ||
|
|
cbb3ed7c48 | ||
|
|
41a0b85376 | ||
|
|
b5a31ee81c | ||
|
|
dceb031822 | ||
|
|
78bc9fc432 | ||
|
|
b9072e4d7c | ||
|
|
5d97e63f93 | ||
|
|
957f5bf9f0 | ||
|
|
6833ac5fad | ||
|
|
3dff2ced6c | ||
|
|
ea7ff3796f | ||
|
|
310617d62b | ||
|
|
1d94e82f4b | ||
|
|
00dad5d673 | ||
|
|
414c6957e7 | ||
|
|
f127298e5a |
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -493,7 +493,7 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "canvas"
|
name = "canvas"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -584,7 +584,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "client"
|
name = "client"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -635,7 +635,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "common"
|
name = "common"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"prost",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
@@ -3022,7 +3022,7 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "search"
|
name = "search"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
@@ -3121,7 +3121,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "server"
|
name = "server"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
|
|||||||
@@ -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 = "komp_ac"
|
# name = "komp_ac"
|
||||||
version = "0.4.2"
|
version = "0.5.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,4 +1,4 @@
|
|||||||
// src/components/common/find_file_palette.rs
|
// src/bottom_panel/find_file_palette.rs
|
||||||
|
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::modes::general::command_navigation::NavigationState; // Corrected path
|
use crate::modes::general::command_navigation::NavigationState; // Corrected path
|
||||||
97
client/src/bottom_panel/layout.rs
Normal file
97
client/src/bottom_panel/layout.rs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// src/bottom_panel/layout.rs
|
||||||
|
|
||||||
|
use ratatui::{layout::Constraint, layout::Rect, Frame};
|
||||||
|
use crate::bottom_panel::{status_line::render_status_line, command_line::render_command_line};
|
||||||
|
use crate::bottom_panel::find_file_palette;
|
||||||
|
use crate::config::colors::themes::Theme;
|
||||||
|
use crate::modes::general::command_navigation::NavigationState;
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
|
||||||
|
/// Calculate the layout constraints for the bottom panel (status line + command line/palette).
|
||||||
|
pub fn bottom_panel_constraints(
|
||||||
|
app_state: &AppState,
|
||||||
|
navigation_state: &NavigationState,
|
||||||
|
event_handler_command_mode_active: bool,
|
||||||
|
) -> Vec<Constraint> {
|
||||||
|
let mut status_line_height = 1;
|
||||||
|
#[cfg(feature = "ui-debug")]
|
||||||
|
{
|
||||||
|
if let Some(debug_state) = &app_state.debug_state {
|
||||||
|
if debug_state.is_error {
|
||||||
|
status_line_height = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PALETTE_OPTIONS_HEIGHT_FOR_LAYOUT: u16 = 15;
|
||||||
|
let command_palette_area_height = if navigation_state.active {
|
||||||
|
1 + PALETTE_OPTIONS_HEIGHT_FOR_LAYOUT
|
||||||
|
} else if event_handler_command_mode_active {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut constraints = vec![Constraint::Length(status_line_height)];
|
||||||
|
if command_palette_area_height > 0 {
|
||||||
|
constraints.push(Constraint::Length(command_palette_area_height));
|
||||||
|
}
|
||||||
|
constraints
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the bottom panel (status line + command line/palette).
|
||||||
|
pub fn render_bottom_panel(
|
||||||
|
f: &mut Frame,
|
||||||
|
root_chunks: &[Rect],
|
||||||
|
chunk_idx: &mut usize,
|
||||||
|
current_dir: &str,
|
||||||
|
theme: &Theme,
|
||||||
|
is_event_handler_edit_mode: bool,
|
||||||
|
current_fps: f64,
|
||||||
|
app_state: &AppState,
|
||||||
|
navigation_state: &NavigationState,
|
||||||
|
event_handler_command_input: &str,
|
||||||
|
event_handler_command_mode_active: bool,
|
||||||
|
event_handler_command_message: &str,
|
||||||
|
) {
|
||||||
|
// --- Status line area ---
|
||||||
|
let status_line_area = root_chunks[*chunk_idx];
|
||||||
|
*chunk_idx += 1;
|
||||||
|
|
||||||
|
// --- Command line / palette area ---
|
||||||
|
let command_render_area = if root_chunks.len() > *chunk_idx {
|
||||||
|
Some(root_chunks[*chunk_idx])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if command_render_area.is_some() {
|
||||||
|
*chunk_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Render status line ---
|
||||||
|
render_status_line(
|
||||||
|
f,
|
||||||
|
status_line_area,
|
||||||
|
current_dir,
|
||||||
|
theme,
|
||||||
|
is_event_handler_edit_mode,
|
||||||
|
current_fps,
|
||||||
|
app_state,
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Render command line or palette ---
|
||||||
|
if let Some(area) = command_render_area {
|
||||||
|
if navigation_state.active {
|
||||||
|
find_file_palette::render_find_file_palette(f, area, theme, navigation_state);
|
||||||
|
} else if event_handler_command_mode_active {
|
||||||
|
render_command_line(
|
||||||
|
f,
|
||||||
|
area,
|
||||||
|
event_handler_command_input,
|
||||||
|
true,
|
||||||
|
theme,
|
||||||
|
event_handler_command_message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
client/src/bottom_panel/mod.rs
Normal file
6
client/src/bottom_panel/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// src/bottom_panel/mod.rs
|
||||||
|
|
||||||
|
pub mod status_line;
|
||||||
|
pub mod command_line;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod find_file_palette;
|
||||||
@@ -5,10 +5,9 @@ use ratatui::{
|
|||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::Style,
|
style::Style,
|
||||||
text::{Line, Span, Text},
|
text::{Line, Span, Text},
|
||||||
widgets::Paragraph,
|
widgets::{Paragraph, Wrap},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use ratatui::widgets::Wrap;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/functions/common/buffer.rs
|
// src/buffer/functions/buffer.rs
|
||||||
|
|
||||||
use crate::state::app::buffer::BufferState;
|
use crate::buffer::state::BufferState;
|
||||||
use crate::state::app::buffer::AppView;
|
use crate::buffer::state::AppView;
|
||||||
|
|
||||||
pub fn get_view_layer(view: &AppView) -> u8 {
|
pub fn get_view_layer(view: &AppView) -> u8 {
|
||||||
match view {
|
match view {
|
||||||
20
client/src/buffer/logic.rs
Normal file
20
client/src/buffer/logic.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// src/buffer/logic.rs
|
||||||
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::state::app::state::UiState;
|
||||||
|
|
||||||
|
/// Toggle the buffer list visibility based on keybindings.
|
||||||
|
pub fn toggle_buffer_list(
|
||||||
|
ui_state: &mut UiState,
|
||||||
|
config: &Config,
|
||||||
|
key: KeyCode,
|
||||||
|
modifiers: KeyModifiers,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(action) = config.get_common_action(key, modifiers) {
|
||||||
|
if action == "toggle_buffer_list" {
|
||||||
|
ui_state.show_buffer_list = !ui_state.show_buffer_list;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
11
client/src/buffer/mod.rs
Normal file
11
client/src/buffer/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// src/buffer/mod.rs
|
||||||
|
|
||||||
|
pub mod state;
|
||||||
|
pub mod functions;
|
||||||
|
pub mod ui;
|
||||||
|
pub mod logic;
|
||||||
|
|
||||||
|
pub use state::{AppView, BufferState};
|
||||||
|
pub use functions::*;
|
||||||
|
pub use ui::render_buffer_list;
|
||||||
|
pub use logic::toggle_buffer_list;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/state/app/buffer.rs
|
// src/buffer/state/buffer.rs
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum AppView {
|
pub enum AppView {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/components/handlers/buffer_list.rs
|
// src/buffer/ui.rs
|
||||||
|
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::buffer::BufferState;
|
use crate::buffer::state::BufferState;
|
||||||
use crate::state::app::state::AppState; // Add this import
|
use crate::state::app::state::AppState; // Add this import
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
@@ -11,7 +11,7 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
use crate::functions::common::buffer::get_view_layer;
|
use crate::buffer::functions::get_view_layer;
|
||||||
|
|
||||||
pub fn render_buffer_list(
|
pub fn render_buffer_list(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -10,7 +10,8 @@ use ratatui::{
|
|||||||
widgets::{Block, BorderType, Borders, Paragraph},
|
widgets::{Block, BorderType, Borders, Paragraph},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::components::common::{dialog, autocomplete}; // Added autocomplete
|
use crate::components::common::autocomplete;
|
||||||
|
use crate::dialog;
|
||||||
use crate::config::binds::config::EditorKeybindingMode;
|
use crate::config::binds::config::EditorKeybindingMode;
|
||||||
|
|
||||||
pub fn render_add_logic(
|
pub fn render_add_logic(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::add_table::{AddTableFocus, AddTableState};
|
use crate::state::pages::add_table::{AddTableFocus, AddTableState};
|
||||||
use canvas::{render_canvas_default, render_canvas, FormEditor};
|
use canvas::{render_canvas, FormEditor};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
@@ -10,7 +10,7 @@ use ratatui::{
|
|||||||
widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Table},
|
widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Table},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::components::common::dialog;
|
use crate::dialog;
|
||||||
|
|
||||||
/// Renders the Add New Table page layout, structuring the display of table information,
|
/// Renders the Add New Table page layout, structuring the display of table information,
|
||||||
/// input fields, and action buttons. Adapts layout based on terminal width.
|
/// input fields, and action buttons. Adapts layout based on terminal width.
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
// src/components/form.rs
|
|
||||||
pub mod login;
|
|
||||||
pub mod register;
|
|
||||||
|
|
||||||
pub use login::*;
|
|
||||||
pub use register::*;
|
|
||||||
@@ -1,18 +1,9 @@
|
|||||||
// src/components/common.rs
|
// src/components/common.rs
|
||||||
pub mod command_line;
|
|
||||||
pub mod status_line;
|
|
||||||
pub mod text_editor;
|
pub mod text_editor;
|
||||||
pub mod background;
|
pub mod background;
|
||||||
pub mod dialog;
|
|
||||||
pub mod autocomplete;
|
pub mod autocomplete;
|
||||||
pub mod search_palette;
|
|
||||||
pub mod find_file_palette;
|
|
||||||
|
|
||||||
pub use command_line::*;
|
|
||||||
pub use status_line::*;
|
|
||||||
pub use text_editor::*;
|
pub use text_editor::*;
|
||||||
pub use background::*;
|
pub use background::*;
|
||||||
pub use dialog::*;
|
|
||||||
pub use autocomplete::*;
|
pub use autocomplete::*;
|
||||||
pub use search_palette::*;
|
|
||||||
pub use find_file_palette::*;
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// src/components/common/autocomplete.rs
|
// src/components/common/autocomplete.rs
|
||||||
|
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::pages::form::FormState;
|
|
||||||
use common::proto::komp_ac::search::search_response::Hit;
|
use common::proto::komp_ac::search::search_response::Hit;
|
||||||
|
use crate::pages::forms::FormState;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
// src/components/form.rs
|
|
||||||
pub mod form;
|
|
||||||
|
|
||||||
pub use form::*;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// src/components/handlers.rs
|
|
||||||
pub mod sidebar;
|
|
||||||
pub mod buffer_list;
|
|
||||||
|
|
||||||
pub use sidebar::*;
|
|
||||||
pub use buffer_list::*;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// src/components/intro.rs
|
|
||||||
pub mod intro;
|
|
||||||
|
|
||||||
pub use intro::*;
|
|
||||||
@@ -1,16 +1,9 @@
|
|||||||
// src/components/mod.rs
|
// src/components/mod.rs
|
||||||
pub mod handlers;
|
|
||||||
pub mod intro;
|
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod form;
|
|
||||||
pub mod auth;
|
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
pub use handlers::*;
|
|
||||||
pub use intro::*;
|
|
||||||
pub use admin::*;
|
pub use admin::*;
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
pub use form::*;
|
|
||||||
pub use auth::*;
|
|
||||||
pub use utils::*;
|
pub use utils::*;
|
||||||
|
|||||||
85
client/src/dialog/functions.rs
Normal file
85
client/src/dialog/functions.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// src/dialog/functions.rs
|
||||||
|
|
||||||
|
use crate::dialog::DialogState;
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn show_dialog(
|
||||||
|
&mut self,
|
||||||
|
title: &str,
|
||||||
|
message: &str,
|
||||||
|
buttons: Vec<String>,
|
||||||
|
purpose: DialogPurpose,
|
||||||
|
) {
|
||||||
|
self.ui.dialog.dialog_title = title.to_string();
|
||||||
|
self.ui.dialog.dialog_message = message.to_string();
|
||||||
|
self.ui.dialog.dialog_buttons = buttons;
|
||||||
|
self.ui.dialog.dialog_active_button_index = 0;
|
||||||
|
self.ui.dialog.purpose = Some(purpose);
|
||||||
|
self.ui.dialog.is_loading = false;
|
||||||
|
self.ui.dialog.dialog_show = true;
|
||||||
|
self.ui.focus_outside_canvas = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_loading_dialog(&mut self, title: &str, message: &str) {
|
||||||
|
self.ui.dialog.dialog_title = title.to_string();
|
||||||
|
self.ui.dialog.dialog_message = message.to_string();
|
||||||
|
self.ui.dialog.dialog_buttons.clear();
|
||||||
|
self.ui.dialog.dialog_active_button_index = 0;
|
||||||
|
self.ui.dialog.purpose = None;
|
||||||
|
self.ui.dialog.is_loading = true;
|
||||||
|
self.ui.dialog.dialog_show = true;
|
||||||
|
self.ui.focus_outside_canvas = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_dialog_content(
|
||||||
|
&mut self,
|
||||||
|
message: &str,
|
||||||
|
buttons: Vec<String>,
|
||||||
|
purpose: DialogPurpose,
|
||||||
|
) {
|
||||||
|
if self.ui.dialog.dialog_show {
|
||||||
|
self.ui.dialog.dialog_message = message.to_string();
|
||||||
|
self.ui.dialog.dialog_buttons = buttons;
|
||||||
|
self.ui.dialog.dialog_active_button_index = 0;
|
||||||
|
self.ui.dialog.purpose = Some(purpose);
|
||||||
|
self.ui.dialog.is_loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide_dialog(&mut self) {
|
||||||
|
self.ui.dialog.dialog_show = false;
|
||||||
|
self.ui.dialog.dialog_title.clear();
|
||||||
|
self.ui.dialog.dialog_message.clear();
|
||||||
|
self.ui.dialog.dialog_buttons.clear();
|
||||||
|
self.ui.dialog.dialog_active_button_index = 0;
|
||||||
|
self.ui.dialog.purpose = None;
|
||||||
|
self.ui.focus_outside_canvas = false;
|
||||||
|
self.ui.dialog.is_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_dialog_button(&mut self) {
|
||||||
|
if !self.ui.dialog.dialog_buttons.is_empty() {
|
||||||
|
let next_index = (self.ui.dialog.dialog_active_button_index + 1)
|
||||||
|
% self.ui.dialog.dialog_buttons.len();
|
||||||
|
self.ui.dialog.dialog_active_button_index = next_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous_dialog_button(&mut self) {
|
||||||
|
if !self.ui.dialog.dialog_buttons.is_empty() {
|
||||||
|
let len = self.ui.dialog.dialog_buttons.len();
|
||||||
|
let prev_index =
|
||||||
|
(self.ui.dialog.dialog_active_button_index + len - 1) % len;
|
||||||
|
self.ui.dialog.dialog_active_button_index = prev_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_active_dialog_button_label(&self) -> Option<&str> {
|
||||||
|
self.ui.dialog
|
||||||
|
.dialog_buttons
|
||||||
|
.get(self.ui.dialog.dialog_active_button_index)
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
207
client/src/dialog/logic.rs
Normal file
207
client/src/dialog/logic.rs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
// src/dialog/logic.rs
|
||||||
|
|
||||||
|
// TODO(dialog-refactor):
|
||||||
|
// Currently this module (`handle_dialog_event`) contains page-specific logic
|
||||||
|
// (e.g. Login, Register, Admin, SaveTable). This couples the dialog crate
|
||||||
|
// to application pages and business logic.
|
||||||
|
//
|
||||||
|
// Refactor plan:
|
||||||
|
// 1. Keep dialog generic: only handle navigation (next/prev/select) and return
|
||||||
|
// a `DialogResult` (Dismissed | Selected { purpose, index }).
|
||||||
|
// 2. Move all page-specific actions (e.g. login::back_to_main, register::back_to_login,
|
||||||
|
// handle_delete_selected_columns, buffer_state.update_history) into the
|
||||||
|
// respective page or event handler (e.g. modes/handlers/event.rs).
|
||||||
|
// 3. Dialog crate should only provide state, rendering, and generic navigation.
|
||||||
|
// Pages decide what to do when a dialog button is pressed.
|
||||||
|
|
||||||
|
use crossterm::event::{Event, KeyCode};
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
use crate::buffer::AppView;
|
||||||
|
use crate::buffer::state::BufferState;
|
||||||
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
|
use crate::pages::register;
|
||||||
|
use crate::pages::login;
|
||||||
|
use crate::tui::functions::common::add_table::handle_delete_selected_columns;
|
||||||
|
use crate::pages::routing::{Router, Page};
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
/// Handles key events specifically when a dialog is active.
|
||||||
|
/// Returns Some(Result<EventOutcome, Error>) if the event was handled (consumed),
|
||||||
|
/// otherwise returns None.
|
||||||
|
pub async fn handle_dialog_event(
|
||||||
|
event: &Event,
|
||||||
|
config: &Config,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
buffer_state: &mut BufferState,
|
||||||
|
router: &mut Router,
|
||||||
|
) -> Option<Result<EventOutcome>> {
|
||||||
|
if let Event::Key(key) = event {
|
||||||
|
// Always allow Esc to dismiss
|
||||||
|
if key.code == KeyCode::Esc {
|
||||||
|
app_state.hide_dialog();
|
||||||
|
return Some(Ok(EventOutcome::Ok("Dialog dismissed".to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check general bindings for dialog actions
|
||||||
|
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
||||||
|
match action {
|
||||||
|
"move_down" | "next_option" => {
|
||||||
|
let current_index = app_state.ui.dialog.dialog_active_button_index;
|
||||||
|
let num_buttons = app_state.ui.dialog.dialog_buttons.len();
|
||||||
|
if num_buttons > 0 && current_index < num_buttons - 1 {
|
||||||
|
app_state.ui.dialog.dialog_active_button_index += 1;
|
||||||
|
}
|
||||||
|
return Some(Ok(EventOutcome::Ok(String::new())));
|
||||||
|
}
|
||||||
|
"move_up" | "previous_option" => {
|
||||||
|
let current_index = app_state.ui.dialog.dialog_active_button_index;
|
||||||
|
if current_index > 0 {
|
||||||
|
app_state.ui.dialog.dialog_active_button_index -= 1;
|
||||||
|
}
|
||||||
|
return Some(Ok(EventOutcome::Ok(String::new())));
|
||||||
|
}
|
||||||
|
"select" => {
|
||||||
|
let selected_index = app_state.ui.dialog.dialog_active_button_index;
|
||||||
|
let purpose = match app_state.ui.dialog.purpose {
|
||||||
|
Some(p) => p,
|
||||||
|
None => {
|
||||||
|
app_state.hide_dialog();
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Internal Error: Dialog context lost".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle Dialog Actions Directly Here
|
||||||
|
match purpose {
|
||||||
|
DialogPurpose::LoginSuccess => match selected_index {
|
||||||
|
0 => {
|
||||||
|
// "Menu" button selected
|
||||||
|
app_state.hide_dialog();
|
||||||
|
if let Page::Login(state) = &mut router.current {
|
||||||
|
let message =
|
||||||
|
login::back_to_main(state, app_state, buffer_state).await;
|
||||||
|
return Some(Ok(EventOutcome::Ok(message)));
|
||||||
|
}
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Login state not active".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
app_state.hide_dialog();
|
||||||
|
return Some(Ok(EventOutcome::Ok("Exiting dialog".to_string())));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
app_state.hide_dialog();
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Unknown dialog button selected".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DialogPurpose::LoginFailed => match selected_index {
|
||||||
|
0 => {
|
||||||
|
// "OK" button selected
|
||||||
|
app_state.hide_dialog();
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Login failed dialog dismissed".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
app_state.hide_dialog();
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Unknown dialog button selected".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DialogPurpose::RegisterSuccess => match selected_index {
|
||||||
|
0 => {
|
||||||
|
// "OK" button for RegisterSuccess
|
||||||
|
app_state.hide_dialog();
|
||||||
|
if let Page::Register(state) = &mut router.current {
|
||||||
|
let message =
|
||||||
|
register::back_to_login(state, app_state, buffer_state)
|
||||||
|
.await;
|
||||||
|
return Some(Ok(EventOutcome::Ok(message)));
|
||||||
|
}
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Register state not active".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
app_state.hide_dialog();
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Unknown dialog button selected".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DialogPurpose::RegisterFailed => match selected_index {
|
||||||
|
0 => {
|
||||||
|
// "OK" button for RegisterFailed
|
||||||
|
app_state.hide_dialog(); // Just dismiss
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Register failed dialog dismissed".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
app_state.hide_dialog();
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Unknown dialog button selected".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DialogPurpose::ConfirmDeleteColumns => match selected_index {
|
||||||
|
0 => {
|
||||||
|
// "Confirm" button selected
|
||||||
|
if let Page::Admin(state) = &mut router.current {
|
||||||
|
let outcome_message =
|
||||||
|
handle_delete_selected_columns(&mut state.add_table_state);
|
||||||
|
app_state.hide_dialog();
|
||||||
|
return Some(Ok(EventOutcome::Ok(outcome_message)));
|
||||||
|
}
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Admin state not active".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
// "Cancel" button selected
|
||||||
|
app_state.hide_dialog();
|
||||||
|
return Some(Ok(EventOutcome::Ok("Deletion cancelled.".to_string())));
|
||||||
|
}
|
||||||
|
_ => { /* Handle unexpected index */ }
|
||||||
|
},
|
||||||
|
DialogPurpose::SaveTableSuccess => match selected_index {
|
||||||
|
0 => {
|
||||||
|
// "OK" button selected
|
||||||
|
app_state.hide_dialog();
|
||||||
|
buffer_state.update_history(AppView::Admin); // Navigate back
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Save success dialog dismissed.".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
_ => { /* Handle unexpected index */ }
|
||||||
|
},
|
||||||
|
DialogPurpose::SaveLogicSuccess => match selected_index {
|
||||||
|
0 => {
|
||||||
|
// "OK" button selected
|
||||||
|
app_state.hide_dialog();
|
||||||
|
buffer_state.update_history(AppView::Admin);
|
||||||
|
return Some(Ok(EventOutcome::Ok(
|
||||||
|
"Save success dialog dismissed.".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
_ => { /* Handle unexpected index */ }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {} // Ignore other general actions when dialog is shown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If it was a key event but not handled above, consume it
|
||||||
|
Some(Ok(EventOutcome::Ok(String::new())))
|
||||||
|
} else {
|
||||||
|
// If it wasn't a key event, consume it too while dialog is active
|
||||||
|
Some(Ok(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
}
|
||||||
10
client/src/dialog/mod.rs
Normal file
10
client/src/dialog/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// src/dialog/mod.rs
|
||||||
|
|
||||||
|
pub mod ui;
|
||||||
|
pub mod logic;
|
||||||
|
pub mod state;
|
||||||
|
pub mod functions;
|
||||||
|
|
||||||
|
pub use ui::render_dialog;
|
||||||
|
pub use logic::handle_dialog_event;
|
||||||
|
pub use state::DialogState;
|
||||||
26
client/src/dialog/state.rs
Normal file
26
client/src/dialog/state.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// src/dialog/state.rs
|
||||||
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
|
|
||||||
|
pub struct DialogState {
|
||||||
|
pub dialog_show: bool,
|
||||||
|
pub dialog_title: String,
|
||||||
|
pub dialog_message: String,
|
||||||
|
pub dialog_buttons: Vec<String>,
|
||||||
|
pub dialog_active_button_index: usize,
|
||||||
|
pub purpose: Option<DialogPurpose>,
|
||||||
|
pub is_loading: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DialogState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
dialog_show: false,
|
||||||
|
dialog_title: String::new(),
|
||||||
|
dialog_message: String::new(),
|
||||||
|
dialog_buttons: Vec::new(),
|
||||||
|
dialog_active_button_index: 0,
|
||||||
|
purpose: None,
|
||||||
|
is_loading: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// src/dialog/ui.rs
|
||||||
|
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout, Margin, Rect},
|
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// src/functions/common.rs
|
|
||||||
|
|
||||||
pub mod buffer;
|
|
||||||
|
|
||||||
pub use buffer::*;
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
// src/functions/mod.rs
|
// src/functions/mod.rs
|
||||||
|
|
||||||
pub mod common;
|
|
||||||
pub mod modes;
|
pub mod modes;
|
||||||
|
|
||||||
pub use modes::*;
|
pub use modes::*;
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ use crate::config::binds::config::{Config, EditorKeybindingMode};
|
|||||||
use crate::state::{
|
use crate::state::{
|
||||||
app::state::AppState,
|
app::state::AppState,
|
||||||
pages::add_logic::{AddLogicFocus, AddLogicState},
|
pages::add_logic::{AddLogicFocus, AddLogicState},
|
||||||
app::buffer::AppView,
|
|
||||||
app::buffer::BufferState,
|
|
||||||
};
|
};
|
||||||
|
use crate::buffer::{AppView, BufferState};
|
||||||
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
||||||
use crate::services::GrpcClient;
|
use crate::services::GrpcClient;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crate::components::common::text_editor::TextEditor;
|
use crate::components::common::text_editor::TextEditor;
|
||||||
use crate::services::ui_service::UiService;
|
use crate::services::ui_service::UiService;
|
||||||
use tui_textarea::CursorMove; // Ensure this import is present
|
use tui_textarea::CursorMove;
|
||||||
|
use crate::state::pages::admin::AdminState;
|
||||||
|
use crate::pages::routing::{Router, Page};
|
||||||
|
|
||||||
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
|
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
|
||||||
|
|
||||||
@@ -20,37 +21,23 @@ pub fn handle_add_logic_navigation(
|
|||||||
key_event: KeyEvent,
|
key_event: KeyEvent,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
add_logic_state: &mut AddLogicState,
|
|
||||||
is_edit_mode: &mut bool,
|
is_edit_mode: &mut bool,
|
||||||
buffer_state: &mut BufferState,
|
buffer_state: &mut BufferState,
|
||||||
grpc_client: GrpcClient,
|
grpc_client: GrpcClient,
|
||||||
_save_logic_sender: SaveLogicResultSender, // Marked as unused
|
save_logic_sender: SaveLogicResultSender,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
|
router: &mut Router,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// === FULLSCREEN SCRIPT EDITING - COMPLETE ISOLATION ===
|
if let Page::AddLogic(add_logic_state) = &mut router.current {
|
||||||
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
|
||||||
// === AUTOCOMPLETE HANDLING ===
|
// === FULLSCREEN SCRIPT EDITING - COMPLETE ISOLATION ===
|
||||||
if add_logic_state.script_editor_autocomplete_active {
|
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
||||||
match key_event.code {
|
// === AUTOCOMPLETE HANDLING ===
|
||||||
// ... (Char, Backspace, Tab, Down, Up cases remain the same) ...
|
if add_logic_state.script_editor_autocomplete_active {
|
||||||
KeyCode::Char(c) if c.is_alphanumeric() || c == '_' => {
|
match key_event.code {
|
||||||
add_logic_state.script_editor_filter_text.push(c);
|
// ... (Char, Backspace, Tab, Down, Up cases remain the same) ...
|
||||||
add_logic_state.update_script_editor_suggestions();
|
KeyCode::Char(c) if c.is_alphanumeric() || c == '_' => {
|
||||||
{
|
add_logic_state.script_editor_filter_text.push(c);
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*command_message = format!("Filtering: @{}", add_logic_state.script_editor_filter_text);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
if !add_logic_state.script_editor_filter_text.is_empty() {
|
|
||||||
add_logic_state.script_editor_filter_text.pop();
|
|
||||||
add_logic_state.update_script_editor_suggestions();
|
add_logic_state.update_script_editor_suggestions();
|
||||||
{
|
{
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
@@ -61,180 +48,131 @@ pub fn handle_add_logic_navigation(
|
|||||||
&mut add_logic_state.vim_state,
|
&mut add_logic_state.vim_state,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
*command_message = if add_logic_state.script_editor_filter_text.is_empty() {
|
*command_message = format!("Filtering: @{}", add_logic_state.script_editor_filter_text);
|
||||||
"Autocomplete: @".to_string()
|
return true;
|
||||||
} else {
|
|
||||||
format!("Filtering: @{}", add_logic_state.script_editor_filter_text)
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
let should_deactivate = if let Some((trigger_line, trigger_col)) = add_logic_state.script_editor_trigger_position {
|
|
||||||
let current_cursor = {
|
|
||||||
let editor_borrow = add_logic_state.script_content_editor.borrow();
|
|
||||||
editor_borrow.cursor()
|
|
||||||
};
|
|
||||||
current_cursor.0 == trigger_line && current_cursor.1 == trigger_col + 1
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
if should_deactivate {
|
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
|
||||||
*command_message = "Autocomplete cancelled".to_string();
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
KeyCode::Backspace => {
|
||||||
}
|
if !add_logic_state.script_editor_filter_text.is_empty() {
|
||||||
KeyCode::Tab | KeyCode::Down => {
|
add_logic_state.script_editor_filter_text.pop();
|
||||||
if !add_logic_state.script_editor_suggestions.is_empty() {
|
add_logic_state.update_script_editor_suggestions();
|
||||||
let current = add_logic_state.script_editor_selected_suggestion_index.unwrap_or(0);
|
{
|
||||||
let next = (current + 1) % add_logic_state.script_editor_suggestions.len();
|
|
||||||
add_logic_state.script_editor_selected_suggestion_index = Some(next);
|
|
||||||
*command_message = format!("Selected: {}", add_logic_state.script_editor_suggestions[next]);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
KeyCode::Up => {
|
|
||||||
if !add_logic_state.script_editor_suggestions.is_empty() {
|
|
||||||
let current = add_logic_state.script_editor_selected_suggestion_index.unwrap_or(0);
|
|
||||||
let prev = if current == 0 {
|
|
||||||
add_logic_state.script_editor_suggestions.len() - 1
|
|
||||||
} else {
|
|
||||||
current - 1
|
|
||||||
};
|
|
||||||
add_logic_state.script_editor_selected_suggestion_index = Some(prev);
|
|
||||||
*command_message = format!("Selected: {}", add_logic_state.script_editor_suggestions[prev]);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
if let Some(selected_idx) = add_logic_state.script_editor_selected_suggestion_index {
|
|
||||||
if let Some(suggestion) = add_logic_state.script_editor_suggestions.get(selected_idx).cloned() {
|
|
||||||
let trigger_pos = add_logic_state.script_editor_trigger_position;
|
|
||||||
let filter_len = add_logic_state.script_editor_filter_text.len();
|
|
||||||
|
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
|
||||||
add_logic_state.has_unsaved_changes = true;
|
|
||||||
|
|
||||||
if let Some(pos) = trigger_pos {
|
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
|
TextEditor::handle_input(
|
||||||
|
&mut editor_borrow,
|
||||||
|
key_event,
|
||||||
|
&add_logic_state.editor_keybinding_mode,
|
||||||
|
&mut add_logic_state.vim_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*command_message = if add_logic_state.script_editor_filter_text.is_empty() {
|
||||||
|
"Autocomplete: @".to_string()
|
||||||
|
} else {
|
||||||
|
format!("Filtering: @{}", add_logic_state.script_editor_filter_text)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let should_deactivate = if let Some((trigger_line, trigger_col)) = add_logic_state.script_editor_trigger_position {
|
||||||
|
let current_cursor = {
|
||||||
|
let editor_borrow = add_logic_state.script_content_editor.borrow();
|
||||||
|
editor_borrow.cursor()
|
||||||
|
};
|
||||||
|
current_cursor.0 == trigger_line && current_cursor.1 == trigger_col + 1
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if should_deactivate {
|
||||||
|
add_logic_state.deactivate_script_editor_autocomplete();
|
||||||
|
*command_message = "Autocomplete cancelled".to_string();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
|
TextEditor::handle_input(
|
||||||
|
&mut editor_borrow,
|
||||||
|
key_event,
|
||||||
|
&add_logic_state.editor_keybinding_mode,
|
||||||
|
&mut add_logic_state.vim_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
KeyCode::Tab | KeyCode::Down => {
|
||||||
|
if !add_logic_state.script_editor_suggestions.is_empty() {
|
||||||
|
let current = add_logic_state.script_editor_selected_suggestion_index.unwrap_or(0);
|
||||||
|
let next = (current + 1) % add_logic_state.script_editor_suggestions.len();
|
||||||
|
add_logic_state.script_editor_selected_suggestion_index = Some(next);
|
||||||
|
*command_message = format!("Selected: {}", add_logic_state.script_editor_suggestions[next]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
if !add_logic_state.script_editor_suggestions.is_empty() {
|
||||||
|
let current = add_logic_state.script_editor_selected_suggestion_index.unwrap_or(0);
|
||||||
|
let prev = if current == 0 {
|
||||||
|
add_logic_state.script_editor_suggestions.len() - 1
|
||||||
|
} else {
|
||||||
|
current - 1
|
||||||
|
};
|
||||||
|
add_logic_state.script_editor_selected_suggestion_index = Some(prev);
|
||||||
|
*command_message = format!("Selected: {}", add_logic_state.script_editor_suggestions[prev]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if let Some(selected_idx) = add_logic_state.script_editor_selected_suggestion_index {
|
||||||
|
if let Some(suggestion) = add_logic_state.script_editor_suggestions.get(selected_idx).cloned() {
|
||||||
|
let trigger_pos = add_logic_state.script_editor_trigger_position;
|
||||||
|
let filter_len = add_logic_state.script_editor_filter_text.len();
|
||||||
|
|
||||||
if suggestion == "sql" {
|
add_logic_state.deactivate_script_editor_autocomplete();
|
||||||
replace_autocomplete_text(&mut editor_borrow, pos, filter_len, "sql");
|
add_logic_state.has_unsaved_changes = true;
|
||||||
editor_borrow.insert_str("('')");
|
|
||||||
// Move cursor back twice to be between the single quotes
|
|
||||||
editor_borrow.move_cursor(CursorMove::Back); // Before ')'
|
|
||||||
editor_borrow.move_cursor(CursorMove::Back); // Before ''' (inside '')
|
|
||||||
*command_message = "Inserted: @sql('')".to_string();
|
|
||||||
} else {
|
|
||||||
let is_table_selection = add_logic_state.is_table_name_suggestion(&suggestion);
|
|
||||||
replace_autocomplete_text(&mut editor_borrow, pos, filter_len, &suggestion);
|
|
||||||
|
|
||||||
if is_table_selection {
|
if let Some(pos) = trigger_pos {
|
||||||
editor_borrow.insert_str(".");
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
let new_cursor = editor_borrow.cursor();
|
|
||||||
drop(editor_borrow); // Release borrow before calling add_logic_state methods
|
|
||||||
|
|
||||||
add_logic_state.script_editor_trigger_position = Some(new_cursor);
|
if suggestion == "sql" {
|
||||||
add_logic_state.script_editor_autocomplete_active = true;
|
replace_autocomplete_text(&mut editor_borrow, pos, filter_len, "sql");
|
||||||
add_logic_state.script_editor_filter_text.clear();
|
editor_borrow.insert_str("('')");
|
||||||
add_logic_state.trigger_column_autocomplete_for_table(suggestion.clone());
|
// Move cursor back twice to be between the single quotes
|
||||||
|
editor_borrow.move_cursor(CursorMove::Back); // Before ')'
|
||||||
let profile_name = add_logic_state.profile_name.clone();
|
editor_borrow.move_cursor(CursorMove::Back); // Before ''' (inside '')
|
||||||
let table_name_for_fetch = suggestion.clone();
|
*command_message = "Inserted: @sql('')".to_string();
|
||||||
let mut client_clone = grpc_client.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
match UiService::fetch_columns_for_table(&mut client_clone, &profile_name, &table_name_for_fetch).await {
|
|
||||||
Ok(_columns) => {
|
|
||||||
// Result handled by main UI loop
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name_for_fetch, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*command_message = format!("Selected table '{}', fetching columns...", suggestion);
|
|
||||||
} else {
|
} else {
|
||||||
*command_message = format!("Inserted: {}", suggestion);
|
let is_table_selection = add_logic_state.is_table_name_suggestion(&suggestion);
|
||||||
|
replace_autocomplete_text(&mut editor_borrow, pos, filter_len, &suggestion);
|
||||||
|
|
||||||
|
if is_table_selection {
|
||||||
|
editor_borrow.insert_str(".");
|
||||||
|
let new_cursor = editor_borrow.cursor();
|
||||||
|
drop(editor_borrow); // Release borrow before calling add_logic_state methods
|
||||||
|
|
||||||
|
add_logic_state.script_editor_trigger_position = Some(new_cursor);
|
||||||
|
add_logic_state.script_editor_autocomplete_active = true;
|
||||||
|
add_logic_state.script_editor_filter_text.clear();
|
||||||
|
add_logic_state.trigger_column_autocomplete_for_table(suggestion.clone());
|
||||||
|
|
||||||
|
let profile_name = add_logic_state.profile_name.clone();
|
||||||
|
let table_name_for_fetch = suggestion.clone();
|
||||||
|
let mut client_clone = grpc_client.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match UiService::fetch_columns_for_table(&mut client_clone, &profile_name, &table_name_for_fetch).await {
|
||||||
|
Ok(_columns) => {
|
||||||
|
// Result handled by main UI loop
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name_for_fetch, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*command_message = format!("Selected table '{}', fetching columns...", suggestion);
|
||||||
|
} else {
|
||||||
|
*command_message = format!("Inserted: {}", suggestion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
add_logic_state.deactivate_script_editor_autocomplete();
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
|
||||||
{
|
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
KeyCode::Esc => {
|
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
|
||||||
*command_message = "Autocomplete cancelled".to_string();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
|
||||||
*command_message = "Autocomplete cancelled".to_string();
|
|
||||||
{
|
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if key_event.code == KeyCode::Char('@') && key_event.modifiers == KeyModifiers::NONE {
|
|
||||||
let should_trigger = match add_logic_state.editor_keybinding_mode {
|
|
||||||
EditorKeybindingMode::Vim => *is_edit_mode,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
if should_trigger {
|
|
||||||
let cursor_before = {
|
|
||||||
let editor_borrow = add_logic_state.script_content_editor.borrow();
|
|
||||||
editor_borrow.cursor()
|
|
||||||
};
|
|
||||||
{
|
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
add_logic_state.script_editor_trigger_position = Some(cursor_before);
|
|
||||||
add_logic_state.script_editor_autocomplete_active = true;
|
|
||||||
add_logic_state.script_editor_filter_text.clear();
|
|
||||||
add_logic_state.update_script_editor_suggestions();
|
|
||||||
add_logic_state.has_unsaved_changes = true;
|
|
||||||
*command_message = "Autocomplete: @ (Tab/↑↓ to navigate, Enter to select, Esc to cancel)".to_string();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
|
|
||||||
match add_logic_state.editor_keybinding_mode {
|
|
||||||
EditorKeybindingMode::Vim => {
|
|
||||||
if *is_edit_mode {
|
|
||||||
{
|
{
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
TextEditor::handle_input(
|
TextEditor::handle_input(
|
||||||
@@ -244,184 +182,252 @@ pub fn handle_add_logic_navigation(
|
|||||||
&mut add_logic_state.vim_state,
|
&mut add_logic_state.vim_state,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) {
|
return true;
|
||||||
*is_edit_mode = false;
|
}
|
||||||
*command_message = "VIM: Normal Mode. Esc again to exit script.".to_string();
|
KeyCode::Esc => {
|
||||||
|
add_logic_state.deactivate_script_editor_autocomplete();
|
||||||
|
*command_message = "Autocomplete cancelled".to_string();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
add_logic_state.deactivate_script_editor_autocomplete();
|
||||||
|
*command_message = "Autocomplete cancelled".to_string();
|
||||||
|
{
|
||||||
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
|
TextEditor::handle_input(
|
||||||
|
&mut editor_borrow,
|
||||||
|
key_event,
|
||||||
|
&add_logic_state.editor_keybinding_mode,
|
||||||
|
&mut add_logic_state.vim_state,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
return true;
|
||||||
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
|
|
||||||
app_state.ui.focus_outside_canvas = true;
|
|
||||||
*is_edit_mode = false;
|
|
||||||
*command_message = "Exited script editing.".to_string();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
}
|
||||||
if *is_edit_mode {
|
|
||||||
*is_edit_mode = false;
|
if key_event.code == KeyCode::Char('@') && key_event.modifiers == KeyModifiers::NONE {
|
||||||
*command_message = "Exited script edit. Esc again to exit script.".to_string();
|
let should_trigger = match add_logic_state.editor_keybinding_mode {
|
||||||
} else {
|
EditorKeybindingMode::Vim => *is_edit_mode,
|
||||||
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
|
_ => true,
|
||||||
app_state.ui.focus_outside_canvas = true;
|
};
|
||||||
*is_edit_mode = false;
|
if should_trigger {
|
||||||
*command_message = "Exited script editing.".to_string();
|
let cursor_before = {
|
||||||
|
let editor_borrow = add_logic_state.script_content_editor.borrow();
|
||||||
|
editor_borrow.cursor()
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
|
TextEditor::handle_input(
|
||||||
|
&mut editor_borrow,
|
||||||
|
key_event,
|
||||||
|
&add_logic_state.editor_keybinding_mode,
|
||||||
|
&mut add_logic_state.vim_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_logic_state.script_editor_trigger_position = Some(cursor_before);
|
||||||
|
add_logic_state.script_editor_autocomplete_active = true;
|
||||||
|
add_logic_state.script_editor_filter_text.clear();
|
||||||
|
add_logic_state.update_script_editor_suggestions();
|
||||||
|
add_logic_state.has_unsaved_changes = true;
|
||||||
|
*command_message = "Autocomplete: @ (Tab/↑↓ to navigate, Enter to select, Esc to cancel)".to_string();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
|
||||||
|
match add_logic_state.editor_keybinding_mode {
|
||||||
|
EditorKeybindingMode::Vim => {
|
||||||
|
if *is_edit_mode {
|
||||||
|
{
|
||||||
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
|
TextEditor::handle_input(
|
||||||
|
&mut editor_borrow,
|
||||||
|
key_event,
|
||||||
|
&add_logic_state.editor_keybinding_mode,
|
||||||
|
&mut add_logic_state.vim_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) {
|
||||||
|
*is_edit_mode = false;
|
||||||
|
*command_message = "VIM: Normal Mode. Esc again to exit script.".to_string();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
|
||||||
|
app_state.ui.focus_outside_canvas = true;
|
||||||
|
*is_edit_mode = false;
|
||||||
|
*command_message = "Exited script editing.".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if *is_edit_mode {
|
||||||
|
*is_edit_mode = false;
|
||||||
|
*command_message = "Exited script edit. Esc again to exit script.".to_string();
|
||||||
|
} else {
|
||||||
|
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
|
||||||
|
app_state.ui.focus_outside_canvas = true;
|
||||||
|
*is_edit_mode = false;
|
||||||
|
*command_message = "Exited script editing.".to_string();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = {
|
||||||
|
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
||||||
|
TextEditor::handle_input(
|
||||||
|
&mut editor_borrow,
|
||||||
|
key_event,
|
||||||
|
&add_logic_state.editor_keybinding_mode,
|
||||||
|
&mut add_logic_state.vim_state,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if changed {
|
||||||
|
add_logic_state.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
if add_logic_state.editor_keybinding_mode == EditorKeybindingMode::Vim {
|
||||||
|
*is_edit_mode = !TextEditor::is_vim_normal_mode(&add_logic_state.vim_state);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let changed = {
|
let action = config.get_general_action(key_event.code, key_event.modifiers);
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
let current_focus = add_logic_state.current_focus;
|
||||||
TextEditor::handle_input(
|
let mut handled = true;
|
||||||
&mut editor_borrow,
|
let mut new_focus = current_focus;
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if changed {
|
|
||||||
add_logic_state.has_unsaved_changes = true;
|
|
||||||
}
|
|
||||||
if add_logic_state.editor_keybinding_mode == EditorKeybindingMode::Vim {
|
|
||||||
*is_edit_mode = !TextEditor::is_vim_normal_mode(&add_logic_state.vim_state);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let action = config.get_general_action(key_event.code, key_event.modifiers);
|
match action.as_deref() {
|
||||||
let current_focus = add_logic_state.current_focus;
|
Some("exit_table_scroll") => {
|
||||||
let mut handled = true;
|
handled = false;
|
||||||
let mut new_focus = current_focus;
|
|
||||||
|
|
||||||
match action.as_deref() {
|
|
||||||
Some("exit_table_scroll") => {
|
|
||||||
handled = false;
|
|
||||||
}
|
|
||||||
Some("move_up") => {
|
|
||||||
match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName => {}
|
|
||||||
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName,
|
|
||||||
AddLogicFocus::InputDescription => new_focus = AddLogicFocus::InputTargetColumn,
|
|
||||||
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
|
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
|
|
||||||
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
}
|
||||||
}
|
Some("move_up") => {
|
||||||
Some("move_down") => {
|
match current_focus {
|
||||||
match current_focus {
|
AddLogicFocus::InputLogicName => {}
|
||||||
AddLogicFocus::InputLogicName => new_focus = AddLogicFocus::InputTargetColumn,
|
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName,
|
||||||
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputDescription,
|
AddLogicFocus::InputDescription => new_focus = AddLogicFocus::InputTargetColumn,
|
||||||
AddLogicFocus::InputDescription => {
|
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
|
||||||
add_logic_state.last_canvas_field = 2;
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
|
||||||
new_focus = AddLogicFocus::ScriptContentPreview;
|
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
||||||
},
|
_ => handled = false,
|
||||||
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
|
}
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
|
||||||
AddLogicFocus::CancelButton => {}
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
}
|
||||||
}
|
Some("move_down") => {
|
||||||
Some("next_option") => {
|
match current_focus {
|
||||||
match current_focus {
|
AddLogicFocus::InputLogicName => new_focus = AddLogicFocus::InputTargetColumn,
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription =>
|
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputDescription,
|
||||||
|
AddLogicFocus::InputDescription => {
|
||||||
|
add_logic_state.last_canvas_field = 2;
|
||||||
|
new_focus = AddLogicFocus::ScriptContentPreview;
|
||||||
|
},
|
||||||
|
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
|
||||||
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
||||||
|
AddLogicFocus::CancelButton => {}
|
||||||
|
_ => handled = false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("next_option") => {
|
||||||
|
match current_focus {
|
||||||
|
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription =>
|
||||||
{ new_focus = AddLogicFocus::ScriptContentPreview; }
|
{ new_focus = AddLogicFocus::ScriptContentPreview; }
|
||||||
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
|
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
||||||
AddLogicFocus::CancelButton => { }
|
AddLogicFocus::CancelButton => { }
|
||||||
_ => handled = false,
|
_ => handled = false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Some("previous_option") => {
|
||||||
Some("previous_option") => {
|
match current_focus {
|
||||||
match current_focus {
|
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription =>
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription =>
|
|
||||||
{ }
|
{ }
|
||||||
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
|
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
|
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
|
||||||
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
||||||
_ => handled = false,
|
_ => handled = false,
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("next_field") => {
|
|
||||||
new_focus = match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn,
|
|
||||||
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription,
|
|
||||||
AddLogicFocus::InputDescription => AddLogicFocus::ScriptContentPreview,
|
|
||||||
AddLogicFocus::ScriptContentPreview => AddLogicFocus::SaveButton,
|
|
||||||
AddLogicFocus::SaveButton => AddLogicFocus::CancelButton,
|
|
||||||
AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName,
|
|
||||||
_ => current_focus,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some("prev_field") => {
|
|
||||||
new_focus = match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton,
|
|
||||||
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName,
|
|
||||||
AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn,
|
|
||||||
AddLogicFocus::ScriptContentPreview => AddLogicFocus::InputDescription,
|
|
||||||
AddLogicFocus::SaveButton => AddLogicFocus::ScriptContentPreview,
|
|
||||||
AddLogicFocus::CancelButton => AddLogicFocus::SaveButton,
|
|
||||||
_ => current_focus,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some("select") => {
|
|
||||||
match current_focus {
|
|
||||||
AddLogicFocus::ScriptContentPreview => {
|
|
||||||
new_focus = AddLogicFocus::InsideScriptContent;
|
|
||||||
*is_edit_mode = false;
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
let mode_hint = match add_logic_state.editor_keybinding_mode {
|
|
||||||
EditorKeybindingMode::Vim => "VIM mode - 'i'/'a'/'o' to edit",
|
|
||||||
_ => "Enter/Ctrl+E to edit",
|
|
||||||
};
|
|
||||||
*command_message = format!("Fullscreen script editing. {} or Esc to exit.", mode_hint);
|
|
||||||
}
|
|
||||||
AddLogicFocus::SaveButton => {
|
|
||||||
*command_message = "Save logic action".to_string();
|
|
||||||
}
|
|
||||||
AddLogicFocus::CancelButton => {
|
|
||||||
buffer_state.update_history(AppView::Admin);
|
|
||||||
app_state.ui.show_add_logic = false;
|
|
||||||
*command_message = "Cancelled Add Logic".to_string();
|
|
||||||
*is_edit_mode = false;
|
|
||||||
}
|
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
|
|
||||||
*is_edit_mode = !*is_edit_mode;
|
|
||||||
*command_message = format!("Field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
|
|
||||||
}
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("toggle_edit_mode") => {
|
|
||||||
match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
|
|
||||||
*is_edit_mode = !*is_edit_mode;
|
|
||||||
*command_message = format!("Canvas field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
*command_message = "Cannot toggle edit mode here.".to_string();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Some("next_field") => {
|
||||||
_ => handled = false,
|
new_focus = match current_focus {
|
||||||
}
|
AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn,
|
||||||
|
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription,
|
||||||
|
AddLogicFocus::InputDescription => AddLogicFocus::ScriptContentPreview,
|
||||||
|
AddLogicFocus::ScriptContentPreview => AddLogicFocus::SaveButton,
|
||||||
|
AddLogicFocus::SaveButton => AddLogicFocus::CancelButton,
|
||||||
|
AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName,
|
||||||
|
_ => current_focus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some("prev_field") => {
|
||||||
|
new_focus = match current_focus {
|
||||||
|
AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton,
|
||||||
|
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName,
|
||||||
|
AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn,
|
||||||
|
AddLogicFocus::ScriptContentPreview => AddLogicFocus::InputDescription,
|
||||||
|
AddLogicFocus::SaveButton => AddLogicFocus::ScriptContentPreview,
|
||||||
|
AddLogicFocus::CancelButton => AddLogicFocus::SaveButton,
|
||||||
|
_ => current_focus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some("select") => {
|
||||||
|
match current_focus {
|
||||||
|
AddLogicFocus::ScriptContentPreview => {
|
||||||
|
new_focus = AddLogicFocus::InsideScriptContent;
|
||||||
|
*is_edit_mode = false;
|
||||||
|
app_state.ui.focus_outside_canvas = false;
|
||||||
|
let mode_hint = match add_logic_state.editor_keybinding_mode {
|
||||||
|
EditorKeybindingMode::Vim => "VIM mode - 'i'/'a'/'o' to edit",
|
||||||
|
_ => "Enter/Ctrl+E to edit",
|
||||||
|
};
|
||||||
|
*command_message = format!("Fullscreen script editing. {} or Esc to exit.", mode_hint);
|
||||||
|
}
|
||||||
|
AddLogicFocus::SaveButton => {
|
||||||
|
*command_message = "Save logic action".to_string();
|
||||||
|
}
|
||||||
|
AddLogicFocus::CancelButton => {
|
||||||
|
buffer_state.update_history(AppView::Admin);
|
||||||
|
*command_message = "Cancelled Add Logic".to_string();
|
||||||
|
*is_edit_mode = false;
|
||||||
|
|
||||||
if handled && current_focus != new_focus {
|
}
|
||||||
add_logic_state.current_focus = new_focus;
|
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
|
||||||
let new_is_canvas_input_focus = matches!(new_focus,
|
*is_edit_mode = !*is_edit_mode;
|
||||||
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription
|
*command_message = format!("Field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
|
||||||
);
|
}
|
||||||
if new_is_canvas_input_focus {
|
_ => handled = false,
|
||||||
*is_edit_mode = false;
|
}
|
||||||
app_state.ui.focus_outside_canvas = false;
|
}
|
||||||
} else {
|
Some("toggle_edit_mode") => {
|
||||||
app_state.ui.focus_outside_canvas = true;
|
match current_focus {
|
||||||
if matches!(new_focus, AddLogicFocus::ScriptContentPreview) {
|
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {
|
||||||
|
*is_edit_mode = !*is_edit_mode;
|
||||||
|
*command_message = format!("Canvas field edit mode: {}", if *is_edit_mode { "ON" } else { "OFF" });
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
*command_message = "Cannot toggle edit mode here.".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => handled = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if handled && current_focus != new_focus {
|
||||||
|
add_logic_state.current_focus = new_focus;
|
||||||
|
let new_is_canvas_input_focus = matches!(new_focus,
|
||||||
|
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription
|
||||||
|
);
|
||||||
|
if new_is_canvas_input_focus {
|
||||||
*is_edit_mode = false;
|
*is_edit_mode = false;
|
||||||
|
app_state.ui.focus_outside_canvas = false;
|
||||||
|
} else {
|
||||||
|
app_state.ui.focus_outside_canvas = true;
|
||||||
|
if matches!(new_focus, AddLogicFocus::ScriptContentPreview) {
|
||||||
|
*is_edit_mode = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
handled
|
||||||
|
} else {
|
||||||
|
return false; // not on AddLogic page
|
||||||
}
|
}
|
||||||
handled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_autocomplete_text(
|
fn replace_autocomplete_text(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
use crate::state::pages::admin::{AdminFocus, AdminState};
|
use crate::state::pages::admin::{AdminFocus, AdminState};
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::state::app::buffer::{BufferState, AppView};
|
use crate::buffer::state::{BufferState, AppView};
|
||||||
use crate::state::pages::add_table::{AddTableState, LinkDefinition};
|
use crate::state::pages::add_table::{AddTableState, LinkDefinition};
|
||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
use crate::state::pages::add_logic::{AddLogicState, AddLogicFocus}; // Added AddLogicFocus import
|
use crate::state::pages::add_logic::{AddLogicState, AddLogicFocus}; // Added AddLogicFocus import
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ pub mod modes;
|
|||||||
pub mod functions;
|
pub mod functions;
|
||||||
pub mod services;
|
pub mod services;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
pub mod buffer;
|
||||||
|
pub mod sidebar;
|
||||||
|
pub mod dialog;
|
||||||
|
pub mod search;
|
||||||
|
pub mod bottom_panel;
|
||||||
|
pub mod pages;
|
||||||
|
|
||||||
pub use ui::run_ui;
|
pub use ui::run_ui;
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +1,95 @@
|
|||||||
// src/modes/canvas/common_mode.rs
|
// src/modes/canvas/common_mode.rs
|
||||||
|
|
||||||
use crate::tui::terminal::core::TerminalCore;
|
use crate::tui::terminal::core::TerminalCore;
|
||||||
use crate::state::pages::{form::FormState, auth::LoginState, auth::RegisterState, auth::AuthState};
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use crate::services::auth::AuthClient;
|
use crate::services::auth::AuthClient;
|
||||||
use crate::modes::handlers::event::EventOutcome;
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
use crate::tui::functions::common::form::SaveOutcome;
|
crate::pages::forms::logic::SaveOutcome;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use crate::tui::functions::common::{
|
use crate::tui::functions::common::{
|
||||||
form::{save as form_save, revert as form_revert},
|
form::{save as form_save, revert as form_revert},
|
||||||
login::{save as login_save, revert as login_revert},
|
login::{save as login_save, revert as login_revert},
|
||||||
register::{revert as register_revert},
|
register::{revert as register_revert},
|
||||||
};
|
};
|
||||||
|
use crate::pages::routing::{Router, Page};
|
||||||
|
|
||||||
pub async fn handle_core_action(
|
pub async fn handle_core_action(
|
||||||
action: &str,
|
action: &str,
|
||||||
form_state: &mut FormState,
|
|
||||||
auth_state: &mut AuthState,
|
auth_state: &mut AuthState,
|
||||||
login_state: &mut LoginState,
|
|
||||||
register_state: &mut RegisterState,
|
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
auth_client: &mut AuthClient,
|
auth_client: &mut AuthClient,
|
||||||
terminal: &mut TerminalCore,
|
terminal: &mut TerminalCore,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
|
router: &mut Router,
|
||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
match action {
|
match action {
|
||||||
"save" => {
|
"save" => {
|
||||||
if app_state.ui.show_login {
|
match &mut router.current {
|
||||||
let message = login_save(auth_state, login_state, auth_client, app_state).await.context("Login save action failed")?;
|
Page::Login(state) => {
|
||||||
Ok(EventOutcome::Ok(message))
|
let message = login_save(auth_state, state, auth_client, app_state)
|
||||||
} else {
|
.await
|
||||||
let save_outcome = form_save(
|
.context("Login save action failed")?;
|
||||||
app_state,
|
Ok(EventOutcome::Ok(message))
|
||||||
form_state,
|
}
|
||||||
grpc_client,
|
Page::Form(form_state) => {
|
||||||
).await.context("Register save action failed")?;
|
let save_outcome = form_save(app_state, form_state, grpc_client)
|
||||||
let message = match save_outcome {
|
.await
|
||||||
SaveOutcome::NoChange => "No changes to save.".to_string(),
|
.context("Form save action failed")?;
|
||||||
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
|
let message = match save_outcome {
|
||||||
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
|
SaveOutcome::NoChange => "No changes to save.".to_string(),
|
||||||
};
|
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
|
||||||
Ok(EventOutcome::DataSaved(save_outcome, message))
|
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
|
||||||
|
};
|
||||||
|
Ok(EventOutcome::DataSaved(save_outcome, message))
|
||||||
|
}
|
||||||
|
_ => Ok(EventOutcome::Ok("Save not applicable".into())),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"force_quit" => {
|
"force_quit" => {
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
Ok(EventOutcome::Exit("Force exiting without saving.".to_string()))
|
Ok(EventOutcome::Exit("Force exiting without saving.".to_string()))
|
||||||
},
|
}
|
||||||
"save_and_quit" => {
|
"save_and_quit" => {
|
||||||
let message = if app_state.ui.show_login {
|
let message = match &mut router.current {
|
||||||
login_save(auth_state, login_state, auth_client, app_state).await.context("Login save n quit action failed")?
|
Page::Login(state) => {
|
||||||
} else {
|
login_save(auth_state, state, auth_client, app_state)
|
||||||
let save_outcome = form_save(
|
.await
|
||||||
app_state,
|
.context("Login save and quit action failed")?
|
||||||
form_state,
|
|
||||||
grpc_client,
|
|
||||||
).await?;
|
|
||||||
match save_outcome {
|
|
||||||
SaveOutcome::NoChange => "No changes to save.".to_string(),
|
|
||||||
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
|
|
||||||
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
|
|
||||||
}
|
}
|
||||||
|
Page::Form(form_state) => {
|
||||||
|
let save_outcome = form_save(app_state, form_state, grpc_client).await?;
|
||||||
|
match save_outcome {
|
||||||
|
SaveOutcome::NoChange => "No changes to save.".to_string(),
|
||||||
|
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
|
||||||
|
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => "Save not applicable".to_string(),
|
||||||
};
|
};
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
Ok(EventOutcome::Exit(format!("{}. Exiting application.", message)))
|
Ok(EventOutcome::Exit(format!("{}. Exiting application.", message)))
|
||||||
},
|
}
|
||||||
"revert" => {
|
"revert" => {
|
||||||
if app_state.ui.show_login {
|
match &mut router.current {
|
||||||
let message = login_revert(login_state, app_state).await;
|
Page::Login(state) => {
|
||||||
Ok(EventOutcome::Ok(message))
|
let message = login_revert(state, app_state).await;
|
||||||
} else if app_state.ui.show_register {
|
Ok(EventOutcome::Ok(message))
|
||||||
let message = register_revert(register_state, app_state).await;
|
}
|
||||||
Ok(EventOutcome::Ok(message))
|
Page::Register(state) => {
|
||||||
} else {
|
let message = register_revert(state, app_state).await;
|
||||||
let message = form_revert(
|
Ok(EventOutcome::Ok(message))
|
||||||
form_state,
|
}
|
||||||
grpc_client,
|
Page::Form(form_state) => {
|
||||||
).await.context("Form revert x action failed")?;
|
let message = form_revert(form_state, grpc_client)
|
||||||
Ok(EventOutcome::Ok(message))
|
.await
|
||||||
|
.context("Form revert action failed")?;
|
||||||
|
Ok(EventOutcome::Ok(message))
|
||||||
|
}
|
||||||
|
_ => Ok(EventOutcome::Ok("Revert not applicable".into())),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => Ok(EventOutcome::Ok(format!("Core action not handled: {}", action))),
|
_ => Ok(EventOutcome::Ok(format!("Core action not handled: {}", action))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,19 @@
|
|||||||
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use crate::state::pages::form::FormState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::{app::state::AppState, pages::auth::LoginState, pages::auth::RegisterState};
|
|
||||||
use crate::modes::common::commands::CommandHandler;
|
use crate::modes::common::commands::CommandHandler;
|
||||||
use crate::tui::terminal::core::TerminalCore;
|
use crate::tui::terminal::core::TerminalCore;
|
||||||
use crate::tui::functions::common::form::{save, revert};
|
use crate::pages::forms::logic::{save, revert ,SaveOutcome};
|
||||||
use crate::modes::handlers::event::EventOutcome;
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
use crate::tui::functions::common::form::SaveOutcome;
|
use crate::pages::routing::{Router, Page};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
pub async fn handle_command_event(
|
pub async fn handle_command_event(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
login_state: &LoginState,
|
router: &mut Router,
|
||||||
register_state: &RegisterState,
|
|
||||||
command_input: &mut String,
|
command_input: &mut String,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
@@ -26,20 +24,19 @@ pub async fn handle_command_event(
|
|||||||
current_position: &mut u64,
|
current_position: &mut u64,
|
||||||
total_count: u64,
|
total_count: u64,
|
||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
// Exit command mode (via configurable keybinding)
|
// Exit command mode
|
||||||
if config.is_exit_command_mode(key.code, key.modifiers) {
|
if config.is_exit_command_mode(key.code, key.modifiers) {
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
*command_message = "".to_string();
|
*command_message = "".to_string();
|
||||||
return Ok(EventOutcome::Ok("Exited command mode".to_string()));
|
return Ok(EventOutcome::Ok("Exited command mode".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute command (via configurable keybinding, defaults to Enter)
|
// Execute command
|
||||||
if config.is_command_execute(key.code, key.modifiers) {
|
if config.is_command_execute(key.code, key.modifiers) {
|
||||||
return process_command(
|
return process_command(
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
login_state,
|
router,
|
||||||
register_state,
|
|
||||||
command_input,
|
command_input,
|
||||||
command_message,
|
command_message,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
@@ -47,33 +44,31 @@ pub async fn handle_command_event(
|
|||||||
terminal,
|
terminal,
|
||||||
current_position,
|
current_position,
|
||||||
total_count,
|
total_count,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backspace (via configurable keybinding, defaults to Backspace)
|
// Backspace
|
||||||
if config.is_command_backspace(key.code, key.modifiers) {
|
if config.is_command_backspace(key.code, key.modifiers) {
|
||||||
command_input.pop();
|
command_input.pop();
|
||||||
return Ok(EventOutcome::Ok("".to_string()));
|
return Ok(EventOutcome::Ok("".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular character input - accept any character in command mode
|
// Regular character input
|
||||||
if let KeyCode::Char(c) = key.code {
|
if let KeyCode::Char(c) = key.code {
|
||||||
// Accept regular or shifted characters (e.g., 'a' or 'A')
|
|
||||||
if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
|
if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
|
||||||
command_input.push(c);
|
command_input.push(c);
|
||||||
return Ok(EventOutcome::Ok("".to_string()));
|
return Ok(EventOutcome::Ok("".to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore all other keys
|
|
||||||
Ok(EventOutcome::Ok("".to_string()))
|
Ok(EventOutcome::Ok("".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_command(
|
async fn process_command(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
login_state: &LoginState,
|
router: &mut Router,
|
||||||
register_state: &RegisterState,
|
|
||||||
command_input: &mut String,
|
command_input: &mut String,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
@@ -82,27 +77,18 @@ async fn process_command(
|
|||||||
current_position: &mut u64,
|
current_position: &mut u64,
|
||||||
total_count: u64,
|
total_count: u64,
|
||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
// Clone the trimmed command to avoid borrow issues
|
|
||||||
let command = command_input.trim().to_string();
|
let command = command_input.trim().to_string();
|
||||||
if command.is_empty() {
|
if command.is_empty() {
|
||||||
*command_message = "Empty command".to_string();
|
*command_message = "Empty command".to_string();
|
||||||
return Ok(EventOutcome::Ok(command_message.clone()));
|
return Ok(EventOutcome::Ok(command_message.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the action for the command (now checks global and common bindings too)
|
let action = config.get_action_for_command(&command).unwrap_or("unknown");
|
||||||
let action = config.get_action_for_command(&command)
|
|
||||||
.unwrap_or("unknown");
|
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
"force_quit" | "save_and_quit" | "quit" => {
|
"force_quit" | "save_and_quit" | "quit" => {
|
||||||
let (should_exit, message) = command_handler
|
let (should_exit, message) = command_handler
|
||||||
.handle_command(
|
.handle_command(action, terminal, app_state, router)
|
||||||
action,
|
|
||||||
terminal,
|
|
||||||
app_state,
|
|
||||||
login_state,
|
|
||||||
register_state,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
if should_exit {
|
if should_exit {
|
||||||
@@ -110,12 +96,9 @@ async fn process_command(
|
|||||||
} else {
|
} else {
|
||||||
Ok(EventOutcome::Ok(message))
|
Ok(EventOutcome::Ok(message))
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"save" => {
|
"save" => {
|
||||||
let outcome = save(
|
let outcome = save(app_state, grpc_client).await?;
|
||||||
app_state,
|
|
||||||
grpc_client,
|
|
||||||
).await?;
|
|
||||||
let message = match outcome {
|
let message = match outcome {
|
||||||
SaveOutcome::CreatedNew(_) => "New entry created".to_string(),
|
SaveOutcome::CreatedNew(_) => "New entry created".to_string(),
|
||||||
SaveOutcome::UpdatedExisting => "Entry updated".to_string(),
|
SaveOutcome::UpdatedExisting => "Entry updated".to_string(),
|
||||||
@@ -123,15 +106,12 @@ async fn process_command(
|
|||||||
};
|
};
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
Ok(EventOutcome::DataSaved(outcome, message))
|
Ok(EventOutcome::DataSaved(outcome, message))
|
||||||
},
|
}
|
||||||
"revert" => {
|
"revert" => {
|
||||||
let message = revert(
|
let message = revert(app_state, grpc_client).await?;
|
||||||
app_state,
|
|
||||||
grpc_client,
|
|
||||||
).await?;
|
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
Ok(EventOutcome::Ok(message))
|
Ok(EventOutcome::Ok(message))
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let message = format!("Unhandled action: {}", action);
|
let message = format!("Unhandled action: {}", action);
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/modes/common/commands.rs
|
// src/modes/common/commands.rs
|
||||||
use crate::tui::terminal::core::TerminalCore;
|
use crate::tui::terminal::core::TerminalCore;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::{form::FormState, auth::LoginState, auth::RegisterState};
|
use crate::pages::routing::{Router, Page};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
pub struct CommandHandler;
|
pub struct CommandHandler;
|
||||||
@@ -16,11 +16,10 @@ impl CommandHandler {
|
|||||||
action: &str,
|
action: &str,
|
||||||
terminal: &mut TerminalCore,
|
terminal: &mut TerminalCore,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
login_state: &LoginState,
|
router: &Router,
|
||||||
register_state: &RegisterState,
|
|
||||||
) -> Result<(bool, String)> {
|
) -> Result<(bool, String)> {
|
||||||
match action {
|
match action {
|
||||||
"quit" => self.handle_quit(terminal, app_state, login_state, register_state).await,
|
"quit" => self.handle_quit(terminal, app_state, router).await,
|
||||||
"force_quit" => self.handle_force_quit(terminal).await,
|
"force_quit" => self.handle_force_quit(terminal).await,
|
||||||
"save_and_quit" => self.handle_save_quit(terminal).await,
|
"save_and_quit" => self.handle_save_quit(terminal).await,
|
||||||
_ => Ok((false, format!("Unknown command: {}", action))),
|
_ => Ok((false, format!("Unknown command: {}", action))),
|
||||||
@@ -31,18 +30,14 @@ impl CommandHandler {
|
|||||||
&self,
|
&self,
|
||||||
terminal: &mut TerminalCore,
|
terminal: &mut TerminalCore,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
login_state: &LoginState,
|
router: &Router,
|
||||||
register_state: &RegisterState,
|
|
||||||
) -> Result<(bool, String)> {
|
) -> Result<(bool, String)> {
|
||||||
// Use actual unsaved changes state instead of is_saved flag
|
// Use router to check unsaved changes
|
||||||
let has_unsaved = if app_state.ui.show_login {
|
let has_unsaved = match &router.current {
|
||||||
login_state.has_unsaved_changes()
|
Page::Login(state) => state.has_unsaved_changes(),
|
||||||
} else if app_state.ui.show_register {
|
Page::Register(state) => state.has_unsaved_changes(),
|
||||||
register_state.has_unsaved_changes()
|
Page::Form(fs) => fs.has_unsaved_changes,
|
||||||
} else if let Some(fs) = app_state.form_state_mut() {
|
_ => false,
|
||||||
fs.has_unsaved_changes
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if !has_unsaved {
|
if !has_unsaved {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// src/client/modes/general.rs
|
// src/client/modes/general.rs
|
||||||
pub mod navigation;
|
pub mod navigation;
|
||||||
pub mod dialog;
|
|
||||||
pub mod command_navigation;
|
pub mod command_navigation;
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
// src/modes/general/dialog.rs
|
|
||||||
|
|
||||||
use crossterm::event::{Event, KeyCode};
|
|
||||||
use crate::config::binds::config::Config;
|
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
|
||||||
use crate::state::app::{state::AppState, buffer::AppView};
|
|
||||||
use crate::state::app::buffer::BufferState;
|
|
||||||
use crate::state::pages::auth::{LoginState, RegisterState};
|
|
||||||
use crate::state::pages::admin::AdminState;
|
|
||||||
use crate::modes::handlers::event::EventOutcome;
|
|
||||||
use crate::tui::functions::common::{login, register};
|
|
||||||
use crate::tui::functions::common::add_table::handle_delete_selected_columns;
|
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
/// Handles key events specifically when a dialog is active.
|
|
||||||
/// Returns Some(Result<EventOutcome, Error>) if the event was handled (consumed),
|
|
||||||
/// otherwise returns None.
|
|
||||||
pub async fn handle_dialog_event(
|
|
||||||
event: &Event,
|
|
||||||
config: &Config,
|
|
||||||
app_state: &mut AppState,
|
|
||||||
login_state: &mut LoginState,
|
|
||||||
register_state: &mut RegisterState,
|
|
||||||
buffer_state: &mut BufferState,
|
|
||||||
admin_state: &mut AdminState,
|
|
||||||
) -> Option<Result<EventOutcome>> {
|
|
||||||
if let Event::Key(key) = event {
|
|
||||||
// Always allow Esc to dismiss
|
|
||||||
if key.code == KeyCode::Esc {
|
|
||||||
app_state.hide_dialog();
|
|
||||||
return Some(Ok(EventOutcome::Ok("Dialog dismissed".to_string())));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check general bindings for dialog actions
|
|
||||||
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
|
||||||
match action {
|
|
||||||
"move_down" | "next_option" => {
|
|
||||||
let current_index = app_state.ui.dialog.dialog_active_button_index;
|
|
||||||
let num_buttons = app_state.ui.dialog.dialog_buttons.len();
|
|
||||||
if num_buttons > 0 && current_index < num_buttons - 1 {
|
|
||||||
app_state.ui.dialog.dialog_active_button_index += 1;
|
|
||||||
}
|
|
||||||
return Some(Ok(EventOutcome::Ok(String::new())));
|
|
||||||
}
|
|
||||||
"move_up" | "previous_option" => {
|
|
||||||
let current_index = app_state.ui.dialog.dialog_active_button_index;
|
|
||||||
if current_index > 0 {
|
|
||||||
app_state.ui.dialog.dialog_active_button_index -= 1;
|
|
||||||
}
|
|
||||||
return Some(Ok(EventOutcome::Ok(String::new())));
|
|
||||||
}
|
|
||||||
"select" => {
|
|
||||||
let selected_index = app_state.ui.dialog.dialog_active_button_index;
|
|
||||||
let purpose = match app_state.ui.dialog.purpose {
|
|
||||||
Some(p) => p,
|
|
||||||
None => {
|
|
||||||
app_state.hide_dialog();
|
|
||||||
return Some(Ok(EventOutcome::Ok("Internal Error: Dialog context lost".to_string())));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle Dialog Actions Directly Here
|
|
||||||
match purpose {
|
|
||||||
DialogPurpose::LoginSuccess => {
|
|
||||||
match selected_index {
|
|
||||||
0 => { // "Menu" button selected
|
|
||||||
app_state.hide_dialog();
|
|
||||||
let message = login::back_to_main(login_state, app_state, buffer_state).await;
|
|
||||||
return Some(Ok(EventOutcome::Ok(message)));
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
app_state.hide_dialog();
|
|
||||||
return Some(Ok(EventOutcome::Ok("Exiting dialog".to_string())));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
app_state.hide_dialog();
|
|
||||||
return Some(Ok(EventOutcome::Ok("Unknown dialog button selected".to_string())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DialogPurpose::LoginFailed => {
|
|
||||||
match selected_index {
|
|
||||||
0 => { // "OK" button selected
|
|
||||||
app_state.hide_dialog();
|
|
||||||
return Some(Ok(EventOutcome::Ok("Login failed dialog dismissed".to_string())));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
app_state.hide_dialog();
|
|
||||||
return Some(Ok(EventOutcome::Ok("Unknown dialog button selected".to_string())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DialogPurpose::RegisterSuccess => { // Add this arm
|
|
||||||
match selected_index {
|
|
||||||
0 => { // "OK" button for RegisterSuccess
|
|
||||||
app_state.hide_dialog();
|
|
||||||
let message = register::back_to_login(register_state, app_state, buffer_state).await;
|
|
||||||
return Some(Ok(EventOutcome::Ok(message)));
|
|
||||||
}
|
|
||||||
_ => { // Default for RegisterSuccess
|
|
||||||
app_state.hide_dialog();
|
|
||||||
return Some(Ok(EventOutcome::Ok("Unknown dialog button selected".to_string())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DialogPurpose::RegisterFailed => { // Add this arm
|
|
||||||
match selected_index {
|
|
||||||
0 => { // "OK" button for RegisterFailed
|
|
||||||
app_state.hide_dialog(); // Just dismiss
|
|
||||||
return Some(Ok(EventOutcome::Ok("Register failed dialog dismissed".to_string())));
|
|
||||||
}
|
|
||||||
_ => { // Default for RegisterFailed
|
|
||||||
app_state.hide_dialog();
|
|
||||||
return Some(Ok(EventOutcome::Ok("Unknown dialog button selected".to_string())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DialogPurpose::ConfirmDeleteColumns => {
|
|
||||||
match selected_index {
|
|
||||||
0 => { // "Confirm" button selected
|
|
||||||
let outcome_message = handle_delete_selected_columns(&mut admin_state.add_table_state);
|
|
||||||
app_state.hide_dialog();
|
|
||||||
return Some(Ok(EventOutcome::Ok(outcome_message)));
|
|
||||||
}
|
|
||||||
1 => { // "Cancel" button selected
|
|
||||||
app_state.hide_dialog();
|
|
||||||
return Some(Ok(EventOutcome::Ok("Deletion cancelled.".to_string())));
|
|
||||||
}
|
|
||||||
_ => { /* Handle unexpected index */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DialogPurpose::SaveTableSuccess => {
|
|
||||||
match selected_index {
|
|
||||||
0 => { // "OK" button selected
|
|
||||||
app_state.hide_dialog();
|
|
||||||
buffer_state.update_history(AppView::Admin); // Navigate back
|
|
||||||
return Some(Ok(EventOutcome::Ok("Save success dialog dismissed.".to_string())));
|
|
||||||
}
|
|
||||||
_ => { /* Handle unexpected index */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DialogPurpose::SaveLogicSuccess => {
|
|
||||||
match selected_index {
|
|
||||||
0 => { // "OK" button selected
|
|
||||||
app_state.hide_dialog();
|
|
||||||
buffer_state.update_history(AppView::Admin);
|
|
||||||
return Some(Ok(EventOutcome::Ok("Save success dialog dismissed.".to_string())));
|
|
||||||
}
|
|
||||||
_ => { /* Handle unexpected index */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {} // Ignore other general actions when dialog is shown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If it was a key event but not handled above, consume it
|
|
||||||
Some(Ok(EventOutcome::Ok(String::new())))
|
|
||||||
} else {
|
|
||||||
// If it wasn't a key event, consume it too while dialog is active
|
|
||||||
Some(Ok(EventOutcome::Ok(String::new())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,8 @@
|
|||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::form::FormState;
|
use crate::pages::routing::{Router, Page};
|
||||||
use crate::state::pages::auth::LoginState;
|
use crate::pages::forms::FormState;
|
||||||
use crate::state::pages::auth::RegisterState;
|
|
||||||
use crate::state::pages::intro::IntroState;
|
|
||||||
use crate::state::pages::admin::AdminState;
|
|
||||||
use crate::ui::handlers::context::UiContext;
|
use crate::ui::handlers::context::UiContext;
|
||||||
use crate::modes::handlers::event::EventOutcome;
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
use crate::modes::general::command_navigation::{handle_command_navigation_event, NavigationState};
|
use crate::modes::general::command_navigation::{handle_command_navigation_event, NavigationState};
|
||||||
@@ -18,10 +15,7 @@ pub async fn handle_navigation_event(
|
|||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
login_state: &mut LoginState,
|
router: &mut Router,
|
||||||
register_state: &mut RegisterState,
|
|
||||||
intro_state: &mut IntroState,
|
|
||||||
admin_state: &mut AdminState,
|
|
||||||
command_mode: &mut bool,
|
command_mode: &mut bool,
|
||||||
command_input: &mut String,
|
command_input: &mut String,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
@@ -35,19 +29,19 @@ pub async fn handle_navigation_event(
|
|||||||
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
||||||
match action {
|
match action {
|
||||||
"move_up" => {
|
"move_up" => {
|
||||||
move_up(app_state, login_state, register_state, intro_state, admin_state);
|
move_up(app_state, router);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
"move_down" => {
|
"move_down" => {
|
||||||
move_down(app_state, intro_state, admin_state);
|
move_down(app_state, router);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
"next_option" => {
|
"next_option" => {
|
||||||
next_option(app_state, intro_state);
|
next_option(app_state, router);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
"previous_option" => {
|
"previous_option" => {
|
||||||
previous_option(app_state, intro_state);
|
previous_option(app_state, router);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
"next_field" => {
|
"next_field" => {
|
||||||
@@ -67,18 +61,21 @@ pub async fn handle_navigation_event(
|
|||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
"select" => {
|
"select" => {
|
||||||
let (context, index) = if app_state.ui.show_intro {
|
let (context, index) = match &router.current {
|
||||||
(UiContext::Intro, intro_state.selected_option)
|
Page::Intro(state) => (UiContext::Intro, state.selected_option),
|
||||||
} else if app_state.ui.show_login && app_state.ui.focus_outside_canvas {
|
Page::Login(_) if app_state.ui.focus_outside_canvas => {
|
||||||
(UiContext::Login, app_state.focused_button_index)
|
(UiContext::Login, app_state.focused_button_index)
|
||||||
} else if app_state.ui.show_register && app_state.ui.focus_outside_canvas {
|
}
|
||||||
(UiContext::Register, app_state.focused_button_index)
|
Page::Register(_) if app_state.ui.focus_outside_canvas => {
|
||||||
} else if app_state.ui.show_admin {
|
(UiContext::Register, app_state.focused_button_index)
|
||||||
(UiContext::Admin, admin_state.get_selected_index().unwrap_or(0))
|
}
|
||||||
} else if app_state.ui.dialog.dialog_show {
|
Page::Admin(state) => {
|
||||||
(UiContext::Dialog, app_state.ui.dialog.dialog_active_button_index)
|
(UiContext::Admin, state.get_selected_index().unwrap_or(0))
|
||||||
} else {
|
}
|
||||||
return Ok(EventOutcome::Ok("Select (No Action)".to_string()));
|
_ if app_state.ui.dialog.dialog_show => {
|
||||||
|
(UiContext::Dialog, app_state.ui.dialog.dialog_active_button_index)
|
||||||
|
}
|
||||||
|
_ => return Ok(EventOutcome::Ok("Select (No Action)".to_string())),
|
||||||
};
|
};
|
||||||
return Ok(EventOutcome::ButtonSelected { context, index });
|
return Ok(EventOutcome::ButtonSelected { context, index });
|
||||||
}
|
}
|
||||||
@@ -88,60 +85,76 @@ pub async fn handle_navigation_event(
|
|||||||
Ok(EventOutcome::Ok(String::new()))
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_up(app_state: &mut AppState, login_state: &mut LoginState, register_state: &mut RegisterState, intro_state: &mut IntroState, admin_state: &mut AdminState) {
|
pub fn move_up(app_state: &mut AppState, router: &mut Router) {
|
||||||
if app_state.ui.focus_outside_canvas && app_state.ui.show_login || app_state.ui.show_register{
|
match &mut router.current {
|
||||||
if app_state.focused_button_index == 0 {
|
Page::Login(state) if app_state.ui.focus_outside_canvas => {
|
||||||
app_state.ui.focus_outside_canvas = false;
|
if app_state.focused_button_index == 0 {
|
||||||
if app_state.ui.show_login {
|
app_state.ui.focus_outside_canvas = false;
|
||||||
let last_field_index = login_state.field_count().saturating_sub(1);
|
let last_field_index = state.field_count().saturating_sub(1);
|
||||||
login_state.set_current_field(last_field_index);
|
state.set_current_field(last_field_index);
|
||||||
} else {
|
} else {
|
||||||
let last_field_index = register_state.field_count().saturating_sub(1);
|
app_state.focused_button_index =
|
||||||
register_state.set_current_field(last_field_index);
|
app_state.focused_button_index.saturating_sub(1);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
app_state.focused_button_index = app_state.focused_button_index.saturating_sub(1);
|
|
||||||
}
|
}
|
||||||
} else if app_state.ui.show_intro {
|
Page::Register(state) if app_state.ui.focus_outside_canvas => {
|
||||||
intro_state.previous_option();
|
if app_state.focused_button_index == 0 {
|
||||||
} else if app_state.ui.show_admin {
|
app_state.ui.focus_outside_canvas = false;
|
||||||
admin_state.previous();
|
let last_field_index = state.field_count().saturating_sub(1);
|
||||||
}
|
state.set_current_field(last_field_index);
|
||||||
}
|
} else {
|
||||||
|
app_state.focused_button_index =
|
||||||
pub fn move_down(app_state: &mut AppState, intro_state: &mut IntroState, admin_state: &mut AdminState) {
|
app_state.focused_button_index.saturating_sub(1);
|
||||||
if app_state.ui.focus_outside_canvas && app_state.ui.show_login || app_state.ui.show_register {
|
}
|
||||||
let num_general_elements = 2;
|
|
||||||
if app_state.focused_button_index < num_general_elements - 1 {
|
|
||||||
app_state.focused_button_index += 1;
|
|
||||||
}
|
}
|
||||||
} else if app_state.ui.show_intro {
|
Page::Intro(state) => state.previous_option(),
|
||||||
intro_state.next_option();
|
Page::Admin(state) => state.previous(),
|
||||||
} else if app_state.ui.show_admin {
|
_ => {}
|
||||||
admin_state.next();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_option(app_state: &mut AppState, intro_state: &mut IntroState) {
|
pub fn move_down(app_state: &mut AppState, router: &mut Router) {
|
||||||
if app_state.ui.show_intro {
|
match &mut router.current {
|
||||||
intro_state.next_option();
|
Page::Login(_) | Page::Register(_) if app_state.ui.focus_outside_canvas => {
|
||||||
} else {
|
let num_general_elements = 2;
|
||||||
// Get option count from state instead of parameter
|
if app_state.focused_button_index < num_general_elements - 1 {
|
||||||
let option_count = app_state.profile_tree.profiles.len();
|
app_state.focused_button_index += 1;
|
||||||
app_state.focused_button_index = (app_state.focused_button_index + 1) % option_count;
|
}
|
||||||
|
}
|
||||||
|
Page::Intro(state) => state.next_option(),
|
||||||
|
Page::Admin(state) => state.next(),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous_option(app_state: &mut AppState, intro_state: &mut IntroState) {
|
pub fn next_option(app_state: &mut AppState, router: &mut Router) {
|
||||||
if app_state.ui.show_intro {
|
match &mut router.current {
|
||||||
intro_state.previous_option();
|
Page::Intro(state) => state.next_option(),
|
||||||
} else {
|
Page::Admin(_) => {
|
||||||
let option_count = app_state.profile_tree.profiles.len();
|
let option_count = app_state.profile_tree.profiles.len();
|
||||||
app_state.focused_button_index = if app_state.focused_button_index == 0 {
|
if option_count > 0 {
|
||||||
option_count.saturating_sub(1)
|
app_state.focused_button_index =
|
||||||
} else {
|
(app_state.focused_button_index + 1) % option_count;
|
||||||
app_state.focused_button_index - 1
|
}
|
||||||
};
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous_option(app_state: &mut AppState, router: &mut Router) {
|
||||||
|
match &mut router.current {
|
||||||
|
Page::Intro(state) => state.previous_option(),
|
||||||
|
Page::Admin(_) => {
|
||||||
|
let option_count = app_state.profile_tree.profiles.len();
|
||||||
|
if option_count > 0 {
|
||||||
|
app_state.focused_button_index = if app_state.focused_button_index == 0 {
|
||||||
|
option_count.saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
app_state.focused_button_index - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +177,7 @@ pub fn prev_field(form_state: &mut FormState) {
|
|||||||
pub fn handle_enter_command_mode(
|
pub fn handle_enter_command_mode(
|
||||||
command_mode: &mut bool,
|
command_mode: &mut bool,
|
||||||
command_input: &mut String,
|
command_input: &mut String,
|
||||||
command_message: &mut String
|
command_message: &mut String,
|
||||||
) {
|
) {
|
||||||
*command_mode = true;
|
*command_mode = true;
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// src/modes/handlers/event.rs
|
// src/modes/handlers/event.rs
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
use crate::functions::common::buffer;
|
use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list};
|
||||||
|
use crate::sidebar::toggle_sidebar;
|
||||||
|
use crate::search::event::handle_search_palette_event;
|
||||||
use crate::functions::modes::navigation::add_logic_nav;
|
use crate::functions::modes::navigation::add_logic_nav;
|
||||||
use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender;
|
use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender;
|
||||||
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
|
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
|
||||||
@@ -11,42 +13,45 @@ use crate::modes::general::command_navigation::{
|
|||||||
};
|
};
|
||||||
use crate::modes::{
|
use crate::modes::{
|
||||||
common::{command_mode, commands::CommandHandler},
|
common::{command_mode, commands::CommandHandler},
|
||||||
general::{dialog, navigation},
|
general::navigation,
|
||||||
handlers::mode_manager::{AppMode, ModeManager},
|
handlers::mode_manager::{AppMode, ModeManager},
|
||||||
};
|
};
|
||||||
use crate::services::auth::AuthClient;
|
use crate::services::auth::AuthClient;
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use canvas::{FormEditor, AppMode as CanvasMode};
|
use canvas::AppMode as CanvasMode;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
app::{
|
app::{
|
||||||
buffer::{AppView, BufferState},
|
|
||||||
search::SearchState,
|
|
||||||
state::AppState,
|
state::AppState,
|
||||||
},
|
},
|
||||||
pages::{
|
pages::{
|
||||||
admin::AdminState,
|
admin::AdminState,
|
||||||
auth::{AuthState, LoginState, RegisterState},
|
auth::AuthState,
|
||||||
form::FormState,
|
|
||||||
intro::IntroState,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::tui::functions::common::login::LoginResult;
|
use crate::pages::login::LoginState;
|
||||||
use crate::tui::functions::common::register::RegisterResult;
|
use crate::pages::register::RegisterState;
|
||||||
|
use crate::pages::intro::IntroState;
|
||||||
|
use crate::pages::login;
|
||||||
|
use crate::pages::register;
|
||||||
|
use crate::pages::intro;
|
||||||
|
use crate::pages::login::logic::LoginResult;
|
||||||
|
use crate::pages::register::RegisterResult;
|
||||||
|
use crate::pages::routing::{Router, Page};
|
||||||
|
use crate::dialog;
|
||||||
|
use crate::pages::forms::FormState;
|
||||||
|
use crate::pages::forms::logic::{save, revert, SaveOutcome};
|
||||||
|
use crate::search::state::SearchState;
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
functions::common::{form::SaveOutcome, login, register},
|
|
||||||
terminal::core::TerminalCore,
|
terminal::core::TerminalCore,
|
||||||
{admin, intro},
|
admin,
|
||||||
};
|
};
|
||||||
use crate::ui::handlers::context::UiContext;
|
use crate::ui::handlers::context::UiContext;
|
||||||
use crate::ui::handlers::rat_state::UiStateHandler;
|
|
||||||
use canvas::KeyEventOutcome;
|
use canvas::KeyEventOutcome;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use common::proto::komp_ac::search::search_response::Hit;
|
use common::proto::komp_ac::search::search_response::Hit;
|
||||||
use crossterm::event::KeyModifiers;
|
use crossterm::event::{Event, KeyCode};
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent};
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum EventOutcome {
|
pub enum EventOutcome {
|
||||||
@@ -129,219 +134,72 @@ impl EventHandler {
|
|||||||
|
|
||||||
// Helper functions - replace the removed event_helper functions
|
// Helper functions - replace the removed event_helper functions
|
||||||
fn get_current_field_for_state(
|
fn get_current_field_for_state(
|
||||||
app_state: &AppState,
|
router: &Router,
|
||||||
login_state: &LoginState,
|
|
||||||
register_state: &RegisterState,
|
|
||||||
form_state: &FormState,
|
|
||||||
) -> usize {
|
) -> usize {
|
||||||
if app_state.ui.show_login {
|
match &router.current {
|
||||||
login_state.current_field()
|
Page::Login(state) => state.current_field(),
|
||||||
} else if app_state.ui.show_register {
|
Page::Register(state) => state.current_field(),
|
||||||
register_state.current_field()
|
Page::Form(state) => state.current_field(),
|
||||||
} else {
|
_ => 0,
|
||||||
form_state.current_field()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_cursor_pos_for_state(
|
fn get_current_cursor_pos_for_state(
|
||||||
app_state: &AppState,
|
router: &Router,
|
||||||
login_state: &LoginState,
|
|
||||||
register_state: &RegisterState,
|
|
||||||
form_state: &FormState,
|
|
||||||
) -> usize {
|
) -> usize {
|
||||||
if app_state.ui.show_login {
|
match &router.current {
|
||||||
login_state.current_cursor_pos()
|
Page::Login(state) => state.current_cursor_pos(),
|
||||||
} else if app_state.ui.show_register {
|
Page::Register(state) => state.current_cursor_pos(),
|
||||||
register_state.current_cursor_pos()
|
Page::Form(state) => state.current_cursor_pos(),
|
||||||
} else {
|
_ => 0,
|
||||||
form_state.current_cursor_pos()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_has_unsaved_changes_for_state(
|
fn get_has_unsaved_changes_for_state(
|
||||||
app_state: &AppState,
|
router: &Router,
|
||||||
login_state: &LoginState,
|
|
||||||
register_state: &RegisterState,
|
|
||||||
form_state: &FormState,
|
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if app_state.ui.show_login {
|
match &router.current {
|
||||||
login_state.has_unsaved_changes()
|
Page::Login(state) => state.has_unsaved_changes(),
|
||||||
} else if app_state.ui.show_register {
|
Page::Register(state) => state.has_unsaved_changes(),
|
||||||
register_state.has_unsaved_changes()
|
Page::Form(state) => state.has_unsaved_changes(),
|
||||||
} else {
|
_ => false,
|
||||||
form_state.has_unsaved_changes()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_input_for_state<'a>(
|
fn get_current_input_for_state<'a>(
|
||||||
app_state: &AppState,
|
router: &'a Router,
|
||||||
login_state: &'a LoginState,
|
|
||||||
register_state: &'a RegisterState,
|
|
||||||
form_state: &'a FormState,
|
|
||||||
) -> &'a str {
|
) -> &'a str {
|
||||||
if app_state.ui.show_login {
|
match &router.current {
|
||||||
login_state.get_current_input()
|
Page::Login(state) => state.get_current_input(),
|
||||||
} else if app_state.ui.show_register {
|
Page::Register(state) => state.get_current_input(),
|
||||||
register_state.get_current_input()
|
Page::Form(state) => state.get_current_input(),
|
||||||
} else {
|
_ => "",
|
||||||
form_state.get_current_input()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_cursor_pos_for_state(
|
fn set_current_cursor_pos_for_state(
|
||||||
app_state: &AppState,
|
router: &mut Router,
|
||||||
login_state: &mut LoginState,
|
|
||||||
register_state: &mut RegisterState,
|
|
||||||
form_state: &mut FormState,
|
|
||||||
pos: usize,
|
pos: usize,
|
||||||
) {
|
) {
|
||||||
if app_state.ui.show_login {
|
match &mut router.current {
|
||||||
login_state.set_current_cursor_pos(pos);
|
Page::Login(state) => state.set_current_cursor_pos(pos),
|
||||||
} else if app_state.ui.show_register {
|
Page::Register(state) => state.set_current_cursor_pos(pos),
|
||||||
register_state.set_current_cursor_pos(pos);
|
Page::Form(state) => state.set_current_cursor_pos(pos),
|
||||||
} else {
|
_ => {},
|
||||||
form_state.set_current_cursor_pos(pos);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cursor_pos_for_mixed_state(
|
fn get_cursor_pos_for_mixed_state(
|
||||||
app_state: &AppState,
|
router: &Router,
|
||||||
login_state: &LoginState,
|
|
||||||
form_state: &FormState,
|
|
||||||
) -> usize {
|
) -> usize {
|
||||||
if app_state.ui.show_login || app_state.ui.show_register {
|
match &router.current {
|
||||||
login_state.current_cursor_pos()
|
Page::Login(state) => state.current_cursor_pos(),
|
||||||
} else {
|
Page::Register(state) => state.current_cursor_pos(),
|
||||||
form_state.current_cursor_pos()
|
Page::Form(state) => state.current_cursor_pos(),
|
||||||
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function handles state changes.
|
|
||||||
async fn handle_search_palette_event(
|
|
||||||
&mut self,
|
|
||||||
key_event: KeyEvent,
|
|
||||||
app_state: &mut AppState,
|
|
||||||
) -> Result<EventOutcome> {
|
|
||||||
let mut should_close = false;
|
|
||||||
let mut outcome_message = String::new();
|
|
||||||
let mut trigger_search = false;
|
|
||||||
|
|
||||||
// Step 1: Handle search_state logic in a short scope
|
|
||||||
let (maybe_data, maybe_id) = {
|
|
||||||
if let Some(search_state) = app_state.search_state.as_mut() {
|
|
||||||
match key_event.code {
|
|
||||||
KeyCode::Esc => {
|
|
||||||
should_close = true;
|
|
||||||
outcome_message = "Search cancelled".to_string();
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
if let Some(selected_hit) =
|
|
||||||
search_state.results.get(search_state.selected_index)
|
|
||||||
{
|
|
||||||
if let Ok(data) = serde_json::from_str::<
|
|
||||||
std::collections::HashMap<String, String>,
|
|
||||||
>(&selected_hit.content_json)
|
|
||||||
{
|
|
||||||
(Some(data), Some(selected_hit.id))
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Up => {
|
|
||||||
search_state.previous_result();
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
KeyCode::Down => {
|
|
||||||
search_state.next_result();
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
search_state.input.insert(search_state.cursor_position, c);
|
|
||||||
search_state.cursor_position += 1;
|
|
||||||
trigger_search = true;
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
if search_state.cursor_position > 0 {
|
|
||||||
search_state.cursor_position -= 1;
|
|
||||||
search_state.input.remove(search_state.cursor_position);
|
|
||||||
trigger_search = true;
|
|
||||||
}
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
KeyCode::Left => {
|
|
||||||
search_state.cursor_position =
|
|
||||||
search_state.cursor_position.saturating_sub(1);
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
KeyCode::Right => {
|
|
||||||
if search_state.cursor_position < search_state.input.len() {
|
|
||||||
search_state.cursor_position += 1;
|
|
||||||
}
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
_ => (None, None),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Step 2: Now safe to borrow form_state
|
|
||||||
if let (Some(data), Some(id)) = (maybe_data, maybe_id) {
|
|
||||||
if let Some(fs) = app_state.form_state_mut() {
|
|
||||||
let detached_pos = fs.total_count + 2;
|
|
||||||
fs.update_from_response(&data, detached_pos);
|
|
||||||
}
|
|
||||||
should_close = true;
|
|
||||||
outcome_message = format!("Loaded record ID {}", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Trigger async search if needed
|
|
||||||
if trigger_search {
|
|
||||||
if let Some(search_state) = app_state.search_state.as_mut() {
|
|
||||||
search_state.is_loading = true;
|
|
||||||
search_state.results.clear();
|
|
||||||
search_state.selected_index = 0;
|
|
||||||
|
|
||||||
let query = search_state.input.clone();
|
|
||||||
let table_name = search_state.table_name.clone();
|
|
||||||
let sender = self.search_result_sender.clone();
|
|
||||||
let mut grpc_client = self.grpc_client.clone();
|
|
||||||
|
|
||||||
info!("--- 1. Spawning search task for query: '{}' ---", query);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
info!("--- 2. Background task started. ---");
|
|
||||||
match grpc_client.search_table(table_name, query).await {
|
|
||||||
Ok(response) => {
|
|
||||||
info!(
|
|
||||||
"--- 3a. gRPC call successful. Found {} hits. ---",
|
|
||||||
response.hits.len()
|
|
||||||
);
|
|
||||||
let _ = sender.send(response.hits);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("--- 3b. gRPC call failed: {:?} ---", e);
|
|
||||||
let _ = sender.send(vec![]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if should_close {
|
|
||||||
app_state.search_state = None;
|
|
||||||
app_state.ui.show_search_palette = false;
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(EventOutcome::Ok(outcome_message))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn handle_event(
|
pub async fn handle_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -350,22 +208,26 @@ impl EventHandler {
|
|||||||
terminal: &mut TerminalCore,
|
terminal: &mut TerminalCore,
|
||||||
command_handler: &mut CommandHandler,
|
command_handler: &mut CommandHandler,
|
||||||
auth_state: &mut AuthState,
|
auth_state: &mut AuthState,
|
||||||
login_state: &mut LoginState,
|
|
||||||
register_state: &mut RegisterState,
|
|
||||||
intro_state: &mut IntroState,
|
|
||||||
admin_state: &mut AdminState,
|
|
||||||
buffer_state: &mut BufferState,
|
buffer_state: &mut BufferState,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
|
router: &mut Router,
|
||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
if app_state.ui.show_search_palette {
|
if app_state.ui.show_search_palette {
|
||||||
if let Event::Key(key_event) = event {
|
if let Event::Key(key_event) = event {
|
||||||
return self.handle_search_palette_event(key_event, app_state).await;
|
if let Some(message) = handle_search_palette_event(
|
||||||
|
key_event,
|
||||||
|
app_state,
|
||||||
|
&mut self.grpc_client,
|
||||||
|
self.search_result_sender.clone(),
|
||||||
|
).await? {
|
||||||
|
return Ok(EventOutcome::Ok(message));
|
||||||
|
}
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut current_mode =
|
let mut current_mode =
|
||||||
ModeManager::derive_mode(app_state, self, admin_state);
|
ModeManager::derive_mode(app_state, self, router);
|
||||||
|
|
||||||
if current_mode == AppMode::General && self.navigation_state.active {
|
if current_mode == AppMode::General && self.navigation_state.active {
|
||||||
if let Event::Key(key_event) = event {
|
if let Event::Key(key_event) = event {
|
||||||
@@ -379,7 +241,7 @@ impl EventHandler {
|
|||||||
if !self.navigation_state.active {
|
if !self.navigation_state.active {
|
||||||
self.command_message = outcome.get_message_if_ok();
|
self.command_message = outcome.get_message_if_ok();
|
||||||
current_mode =
|
current_mode =
|
||||||
ModeManager::derive_mode(app_state, self, admin_state);
|
ModeManager::derive_mode(app_state, self, router);
|
||||||
}
|
}
|
||||||
app_state.update_mode(current_mode);
|
app_state.update_mode(current_mode);
|
||||||
return Ok(outcome);
|
return Ok(outcome);
|
||||||
@@ -390,25 +252,14 @@ impl EventHandler {
|
|||||||
|
|
||||||
app_state.update_mode(current_mode);
|
app_state.update_mode(current_mode);
|
||||||
|
|
||||||
let current_view = {
|
let current_view = match &router.current {
|
||||||
let ui = &app_state.ui;
|
Page::Intro(_) => AppView::Intro,
|
||||||
if ui.show_intro {
|
Page::Login(_) => AppView::Login,
|
||||||
AppView::Intro
|
Page::Register(_) => AppView::Register,
|
||||||
} else if ui.show_login {
|
Page::Admin(_) => AppView::Admin,
|
||||||
AppView::Login
|
Page::AddLogic(_) => AppView::AddLogic,
|
||||||
} else if ui.show_register {
|
Page::AddTable(_) => AppView::AddTable,
|
||||||
AppView::Register
|
Page::Form(_) => AppView::Form,
|
||||||
} else if ui.show_admin {
|
|
||||||
AppView::Admin
|
|
||||||
} else if ui.show_add_logic {
|
|
||||||
AppView::AddLogic
|
|
||||||
} else if ui.show_add_table {
|
|
||||||
AppView::AddTable
|
|
||||||
} else if ui.show_form {
|
|
||||||
AppView::Form
|
|
||||||
} else {
|
|
||||||
AppView::Scratch
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
buffer_state.update_history(current_view);
|
buffer_state.update_history(current_view);
|
||||||
|
|
||||||
@@ -418,10 +269,8 @@ impl EventHandler {
|
|||||||
&Event::Key(key_event),
|
&Event::Key(key_event),
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
login_state,
|
|
||||||
register_state,
|
|
||||||
buffer_state,
|
buffer_state,
|
||||||
admin_state,
|
router,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -436,7 +285,7 @@ impl EventHandler {
|
|||||||
let key_code = key_event.code;
|
let key_code = key_event.code;
|
||||||
let modifiers = key_event.modifiers;
|
let modifiers = key_event.modifiers;
|
||||||
|
|
||||||
if UiStateHandler::toggle_sidebar(
|
if toggle_sidebar(
|
||||||
&mut app_state.ui,
|
&mut app_state.ui,
|
||||||
config,
|
config,
|
||||||
key_code,
|
key_code,
|
||||||
@@ -452,7 +301,7 @@ impl EventHandler {
|
|||||||
);
|
);
|
||||||
return Ok(EventOutcome::Ok(message));
|
return Ok(EventOutcome::Ok(message));
|
||||||
}
|
}
|
||||||
if UiStateHandler::toggle_buffer_list(
|
if toggle_buffer_list(
|
||||||
&mut app_state.ui,
|
&mut app_state.ui,
|
||||||
config,
|
config,
|
||||||
key_code,
|
key_code,
|
||||||
@@ -477,14 +326,14 @@ impl EventHandler {
|
|||||||
) {
|
) {
|
||||||
match action {
|
match action {
|
||||||
"next_buffer" => {
|
"next_buffer" => {
|
||||||
if buffer::switch_buffer(buffer_state, true) {
|
if switch_buffer(buffer_state, true) {
|
||||||
return Ok(EventOutcome::Ok(
|
return Ok(EventOutcome::Ok(
|
||||||
"Switched to next buffer".to_string(),
|
"Switched to next buffer".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"previous_buffer" => {
|
"previous_buffer" => {
|
||||||
if buffer::switch_buffer(buffer_state, false) {
|
if switch_buffer(buffer_state, false) {
|
||||||
return Ok(EventOutcome::Ok(
|
return Ok(EventOutcome::Ok(
|
||||||
"Switched to previous buffer".to_string(),
|
"Switched to previous buffer".to_string(),
|
||||||
));
|
));
|
||||||
@@ -507,7 +356,7 @@ impl EventHandler {
|
|||||||
config.get_general_action(key_code, modifiers)
|
config.get_general_action(key_code, modifiers)
|
||||||
{
|
{
|
||||||
if action == "open_search" {
|
if action == "open_search" {
|
||||||
if app_state.ui.show_form {
|
if let Page::Form(_) = &router.current {
|
||||||
if let Some(table_name) =
|
if let Some(table_name) =
|
||||||
app_state.current_view_table_name.clone()
|
app_state.current_view_table_name.clone()
|
||||||
{
|
{
|
||||||
@@ -526,51 +375,49 @@ impl EventHandler {
|
|||||||
|
|
||||||
match current_mode {
|
match current_mode {
|
||||||
AppMode::General => {
|
AppMode::General => {
|
||||||
if app_state.ui.show_admin
|
if let Page::Admin(admin_state) = &mut router.current {
|
||||||
&& auth_state.role.as_deref() == Some("admin")
|
if auth_state.role.as_deref() == Some("admin") {
|
||||||
{
|
if admin_nav::handle_admin_navigation(
|
||||||
if admin_nav::handle_admin_navigation(
|
key_event,
|
||||||
key_event,
|
config,
|
||||||
config,
|
app_state,
|
||||||
app_state,
|
admin_state,
|
||||||
admin_state,
|
buffer_state,
|
||||||
buffer_state,
|
&mut self.command_message,
|
||||||
&mut self.command_message,
|
) {
|
||||||
) {
|
return Ok(EventOutcome::Ok(
|
||||||
return Ok(EventOutcome::Ok(
|
self.command_message.clone(),
|
||||||
self.command_message.clone(),
|
));
|
||||||
));
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if app_state.ui.show_add_logic {
|
let client_clone = self.grpc_client.clone();
|
||||||
let client_clone = self.grpc_client.clone();
|
let sender_clone = self.save_logic_result_sender.clone();
|
||||||
let sender_clone = self.save_logic_result_sender.clone();
|
if add_logic_nav::handle_add_logic_navigation(
|
||||||
if add_logic_nav::handle_add_logic_navigation(
|
key_event,
|
||||||
key_event,
|
config,
|
||||||
config,
|
app_state,
|
||||||
app_state,
|
&mut self.is_edit_mode,
|
||||||
&mut admin_state.add_logic_state,
|
buffer_state,
|
||||||
&mut self.is_edit_mode,
|
client_clone,
|
||||||
buffer_state,
|
sender_clone,
|
||||||
client_clone,
|
&mut self.command_message,
|
||||||
sender_clone,
|
router,
|
||||||
&mut self.command_message,
|
) {
|
||||||
) {
|
return Ok(EventOutcome::Ok(
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
self.command_message.clone(),
|
self.command_message.clone(),
|
||||||
));
|
));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if app_state.ui.show_add_table {
|
if let Page::AddTable(add_table_state) = &mut router.current {
|
||||||
let client_clone = self.grpc_client.clone();
|
let client_clone = self.grpc_client.clone();
|
||||||
let sender_clone = self.save_table_result_sender.clone();
|
let sender_clone = self.save_table_result_sender.clone();
|
||||||
if add_table_nav::handle_add_table_navigation(
|
if add_table_nav::handle_add_table_navigation(
|
||||||
key_event,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
&mut admin_state.add_table_state,
|
add_table_state,
|
||||||
client_clone,
|
client_clone,
|
||||||
sender_clone,
|
sender_clone,
|
||||||
&mut self.command_message,
|
&mut self.command_message,
|
||||||
@@ -585,10 +432,7 @@ impl EventHandler {
|
|||||||
key_event,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
login_state,
|
router,
|
||||||
register_state,
|
|
||||||
intro_state,
|
|
||||||
admin_state,
|
|
||||||
&mut self.command_mode,
|
&mut self.command_mode,
|
||||||
&mut self.command_input,
|
&mut self.command_input,
|
||||||
&mut self.command_message,
|
&mut self.command_message,
|
||||||
@@ -603,53 +447,68 @@ impl EventHandler {
|
|||||||
buffer_state,
|
buffer_state,
|
||||||
index,
|
index,
|
||||||
);
|
);
|
||||||
if app_state.ui.show_admin
|
if let Page::Admin(admin_state) = &mut router.current {
|
||||||
&& !app_state
|
if !app_state
|
||||||
.profile_tree
|
.profile_tree
|
||||||
.profiles
|
.profiles
|
||||||
.is_empty()
|
.is_empty()
|
||||||
{
|
{
|
||||||
admin_state
|
admin_state
|
||||||
.profile_list_state
|
.profile_list_state
|
||||||
.select(Some(0));
|
.select(Some(0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
format!("Intro Option {} selected", index)
|
format!("Intro Option {} selected", index)
|
||||||
}
|
}
|
||||||
UiContext::Login => match index {
|
UiContext::Login => {
|
||||||
0 => login::initiate_login(
|
if let Page::Login(login_state) = &mut router.current {
|
||||||
login_state,
|
match index {
|
||||||
app_state,
|
0 => login::initiate_login(
|
||||||
self.auth_client.clone(),
|
login_state,
|
||||||
self.login_result_sender.clone(),
|
app_state,
|
||||||
),
|
self.auth_client.clone(),
|
||||||
1 => login::back_to_main(
|
self.login_result_sender.clone(),
|
||||||
login_state,
|
),
|
||||||
app_state,
|
1 => login::back_to_main(
|
||||||
buffer_state,
|
login_state,
|
||||||
)
|
app_state,
|
||||||
.await,
|
buffer_state,
|
||||||
_ => "Invalid Login Option".to_string(),
|
)
|
||||||
},
|
.await,
|
||||||
UiContext::Register => match index {
|
_ => "Invalid Login Option".to_string(),
|
||||||
0 => register::initiate_registration(
|
}
|
||||||
register_state,
|
} else {
|
||||||
app_state,
|
"Invalid state".to_string()
|
||||||
self.auth_client.clone(),
|
}
|
||||||
self.register_result_sender.clone(),
|
}
|
||||||
),
|
UiContext::Register => {
|
||||||
1 => register::back_to_login(
|
if let Page::Register(register_state) = &mut router.current {
|
||||||
register_state,
|
match index {
|
||||||
app_state,
|
0 => register::initiate_registration(
|
||||||
buffer_state,
|
register_state,
|
||||||
)
|
app_state,
|
||||||
.await,
|
self.auth_client.clone(),
|
||||||
_ => "Invalid Login Option".to_string(),
|
self.register_result_sender.clone(),
|
||||||
},
|
),
|
||||||
|
1 => register::back_to_login(
|
||||||
|
register_state,
|
||||||
|
app_state,
|
||||||
|
buffer_state,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
_ => "Invalid Login Option".to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"Invalid state".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
UiContext::Admin => {
|
UiContext::Admin => {
|
||||||
admin::handle_admin_selection(
|
if let Page::Admin(admin_state) = &router.current {
|
||||||
app_state,
|
admin::handle_admin_selection(
|
||||||
admin_state,
|
app_state,
|
||||||
);
|
admin_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
format!("Admin Option {} selected", index)
|
format!("Admin Option {} selected", index)
|
||||||
}
|
}
|
||||||
UiContext::Dialog => "Internal error: Unexpected dialog state"
|
UiContext::Dialog => "Internal error: Unexpected dialog state"
|
||||||
@@ -663,7 +522,7 @@ impl EventHandler {
|
|||||||
|
|
||||||
AppMode::ReadOnly => {
|
AppMode::ReadOnly => {
|
||||||
// First let the canvas editor try to handle the key
|
// First let the canvas editor try to handle the key
|
||||||
if app_state.ui.show_form {
|
if let Page::Form(_) = &router.current {
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
if let Some(editor) = &mut app_state.form_editor {
|
||||||
let outcome = editor.handle_key_event(key_event);
|
let outcome = editor.handle_key_event(key_event);
|
||||||
let new_mode = AppMode::from(editor.mode());
|
let new_mode = AppMode::from(editor.mode());
|
||||||
@@ -709,10 +568,9 @@ impl EventHandler {
|
|||||||
.handle_core_action(
|
.handle_core_action(
|
||||||
action,
|
action,
|
||||||
auth_state,
|
auth_state,
|
||||||
login_state,
|
|
||||||
register_state,
|
|
||||||
terminal,
|
terminal,
|
||||||
app_state,
|
app_state,
|
||||||
|
router,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -724,7 +582,7 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppMode::Highlight => {
|
AppMode::Highlight => {
|
||||||
if app_state.ui.show_form {
|
if let Page::Form(_) = &router.current {
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
if let Some(editor) = &mut app_state.form_editor {
|
||||||
let outcome = editor.handle_key_event(key_event);
|
let outcome = editor.handle_key_event(key_event);
|
||||||
let new_mode = AppMode::from(editor.mode());
|
let new_mode = AppMode::from(editor.mode());
|
||||||
@@ -761,10 +619,9 @@ impl EventHandler {
|
|||||||
.handle_core_action(
|
.handle_core_action(
|
||||||
action,
|
action,
|
||||||
auth_state,
|
auth_state,
|
||||||
login_state,
|
|
||||||
register_state,
|
|
||||||
terminal,
|
terminal,
|
||||||
app_state,
|
app_state,
|
||||||
|
router,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -773,7 +630,7 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Let the canvas editor handle edit-mode keys
|
// Let the canvas editor handle edit-mode keys
|
||||||
if app_state.ui.show_form {
|
if let Page::Form(_) = &router.current {
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
if let Some(editor) = &mut app_state.form_editor {
|
||||||
let outcome = editor.handle_key_event(key_event);
|
let outcome = editor.handle_key_event(key_event);
|
||||||
let new_mode = AppMode::from(editor.mode());
|
let new_mode = AppMode::from(editor.mode());
|
||||||
@@ -817,7 +674,7 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.is_command_execute(key_code, modifiers) {
|
if config.is_command_execute(key_code, modifiers) {
|
||||||
let (mut current_position, total_count) = if let Some(fs) = app_state.form_state() {
|
let (mut current_position, total_count) = if let Page::Form(fs) = &router.current {
|
||||||
(fs.current_position, fs.total_count)
|
(fs.current_position, fs.total_count)
|
||||||
} else {
|
} else {
|
||||||
(1, 0)
|
(1, 0)
|
||||||
@@ -827,8 +684,7 @@ impl EventHandler {
|
|||||||
key_event,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
login_state,
|
router,
|
||||||
register_state,
|
|
||||||
&mut self.command_input,
|
&mut self.command_input,
|
||||||
&mut self.command_message,
|
&mut self.command_message,
|
||||||
&mut self.grpc_client,
|
&mut self.grpc_client,
|
||||||
@@ -837,7 +693,7 @@ impl EventHandler {
|
|||||||
&mut current_position,
|
&mut current_position,
|
||||||
total_count,
|
total_count,
|
||||||
).await?;
|
).await?;
|
||||||
if let Some(fs) = app_state.form_state_mut() {
|
if let Page::Form(fs) = &mut router.current {
|
||||||
fs.current_position = current_position;
|
fs.current_position = current_position;
|
||||||
}
|
}
|
||||||
self.command_mode = false;
|
self.command_mode = false;
|
||||||
@@ -845,7 +701,7 @@ impl EventHandler {
|
|||||||
let new_mode = ModeManager::derive_mode(
|
let new_mode = ModeManager::derive_mode(
|
||||||
app_state,
|
app_state,
|
||||||
self,
|
self,
|
||||||
admin_state,
|
router,
|
||||||
);
|
);
|
||||||
app_state.update_mode(new_mode);
|
app_state.update_mode(new_mode);
|
||||||
return Ok(outcome);
|
return Ok(outcome);
|
||||||
@@ -867,9 +723,7 @@ impl EventHandler {
|
|||||||
&sequence,
|
&sequence,
|
||||||
) == Some("find_file_palette_toggle")
|
) == Some("find_file_palette_toggle")
|
||||||
{
|
{
|
||||||
if app_state.ui.show_form
|
if matches!(&router.current, Page::Form(_) | Page::Intro(_)) {
|
||||||
|| app_state.ui.show_intro
|
|
||||||
{
|
|
||||||
let mut all_table_paths: Vec<String> =
|
let mut all_table_paths: Vec<String> =
|
||||||
app_state
|
app_state
|
||||||
.profile_tree
|
.profile_tree
|
||||||
@@ -951,15 +805,14 @@ impl EventHandler {
|
|||||||
&mut self,
|
&mut self,
|
||||||
action: &str,
|
action: &str,
|
||||||
auth_state: &mut AuthState,
|
auth_state: &mut AuthState,
|
||||||
login_state: &mut LoginState,
|
|
||||||
register_state: &mut RegisterState,
|
|
||||||
terminal: &mut TerminalCore,
|
terminal: &mut TerminalCore,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
|
router: &mut Router,
|
||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
match action {
|
match action {
|
||||||
"save" => {
|
"save" => {
|
||||||
if app_state.ui.show_login {
|
if let Page::Login(login_state) = &mut router.current {
|
||||||
let message = crate::tui::functions::common::login::save(
|
let message = login::logic::save(
|
||||||
auth_state,
|
auth_state,
|
||||||
login_state,
|
login_state,
|
||||||
&mut self.auth_client,
|
&mut self.auth_client,
|
||||||
@@ -968,8 +821,8 @@ impl EventHandler {
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(EventOutcome::Ok(message))
|
Ok(EventOutcome::Ok(message))
|
||||||
} else {
|
} else {
|
||||||
let save_outcome = if let Some(fs) = app_state.form_state_mut() {
|
let save_outcome = if let Page::Form(_) = &router.current {
|
||||||
crate::tui::functions::common::form::save(
|
save(
|
||||||
app_state,
|
app_state,
|
||||||
&mut self.grpc_client,
|
&mut self.grpc_client,
|
||||||
)
|
)
|
||||||
@@ -995,8 +848,8 @@ impl EventHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
"save_and_quit" => {
|
"save_and_quit" => {
|
||||||
let message = if app_state.ui.show_login {
|
let message = if let Page::Login(login_state) = &mut router.current {
|
||||||
crate::tui::functions::common::login::save(
|
login::logic::save(
|
||||||
auth_state,
|
auth_state,
|
||||||
login_state,
|
login_state,
|
||||||
&mut self.auth_client,
|
&mut self.auth_client,
|
||||||
@@ -1004,7 +857,7 @@ impl EventHandler {
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
let save_outcome = crate::tui::functions::common::form::save(
|
let save_outcome = save(
|
||||||
app_state,
|
app_state,
|
||||||
&mut self.grpc_client,
|
&mut self.grpc_client,
|
||||||
).await?;
|
).await?;
|
||||||
@@ -1024,18 +877,17 @@ impl EventHandler {
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
"revert" => {
|
"revert" => {
|
||||||
let message = if app_state.ui.show_login {
|
let message = if let Page::Login(login_state) = &mut router.current {
|
||||||
crate::tui::functions::common::login::revert(login_state, app_state)
|
login::logic::revert(login_state, app_state).await
|
||||||
.await
|
} else if let Page::Register(register_state) = &mut router.current {
|
||||||
} else if app_state.ui.show_register {
|
register::revert(
|
||||||
crate::tui::functions::common::register::revert(
|
|
||||||
register_state,
|
register_state,
|
||||||
app_state,
|
app_state,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
if let Some(fs) = app_state.form_state_mut() {
|
if let Page::Form(_) = &router.current {
|
||||||
crate::tui::functions::common::form::revert(
|
revert(
|
||||||
app_state,
|
app_state,
|
||||||
&mut self.grpc_client,
|
&mut self.grpc_client,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::modes::handlers::event::EventHandler;
|
use crate::modes::handlers::event::EventHandler;
|
||||||
use crate::state::pages::add_logic::AddLogicFocus;
|
use crate::state::pages::add_logic::AddLogicFocus;
|
||||||
use crate::state::pages::admin::AdminState;
|
use crate::pages::routing::{Router, Page};
|
||||||
use canvas::AppMode as CanvasMode;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AppMode {
|
pub enum AppMode {
|
||||||
@@ -29,11 +28,11 @@ impl From<canvas::AppMode> for AppMode {
|
|||||||
pub struct ModeManager;
|
pub struct ModeManager;
|
||||||
|
|
||||||
impl ModeManager {
|
impl ModeManager {
|
||||||
/// Determine current mode based on app state
|
/// Determine current mode based on app state + router
|
||||||
pub fn derive_mode(
|
pub fn derive_mode(
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
event_handler: &EventHandler,
|
event_handler: &EventHandler,
|
||||||
admin_state: &AdminState,
|
router: &Router,
|
||||||
) -> AppMode {
|
) -> AppMode {
|
||||||
// Navigation palette always forces General
|
// Navigation palette always forces General
|
||||||
if event_handler.navigation_state.active {
|
if event_handler.navigation_state.active {
|
||||||
@@ -45,16 +44,17 @@ impl ModeManager {
|
|||||||
return AppMode::Command;
|
return AppMode::Command;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always trust the FormEditor when a form is active
|
match &router.current {
|
||||||
if app_state.ui.show_form && !app_state.ui.focus_outside_canvas {
|
// --- Form view ---
|
||||||
if let Some(editor) = &app_state.form_editor {
|
Page::Form(_) if !app_state.ui.focus_outside_canvas => {
|
||||||
return AppMode::from(editor.mode());
|
if let Some(editor) = &app_state.form_editor {
|
||||||
|
return AppMode::from(editor.mode());
|
||||||
|
}
|
||||||
|
AppMode::General
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- Non-form views (add_logic, add_table, etc.) ---
|
// --- AddLogic view ---
|
||||||
if app_state.ui.show_add_logic {
|
Page::AddLogic(state) => match state.current_focus {
|
||||||
match admin_state.add_logic_state.current_focus {
|
|
||||||
AddLogicFocus::InputLogicName
|
AddLogicFocus::InputLogicName
|
||||||
| AddLogicFocus::InputTargetColumn
|
| AddLogicFocus::InputTargetColumn
|
||||||
| AddLogicFocus::InputDescription => {
|
| AddLogicFocus::InputDescription => {
|
||||||
@@ -65,26 +65,30 @@ impl ModeManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => AppMode::General,
|
_ => AppMode::General,
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- AddTable view ---
|
||||||
|
Page::AddTable(_) => {
|
||||||
|
if app_state.ui.focus_outside_canvas {
|
||||||
|
AppMode::General
|
||||||
|
} else if event_handler.is_edit_mode {
|
||||||
|
AppMode::Edit
|
||||||
|
} else {
|
||||||
|
AppMode::ReadOnly
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if app_state.ui.show_add_table {
|
|
||||||
if app_state.ui.focus_outside_canvas {
|
// --- Login/Register views ---
|
||||||
AppMode::General
|
Page::Login(_) | Page::Register(_) => {
|
||||||
} else if event_handler.is_edit_mode {
|
if event_handler.is_edit_mode {
|
||||||
AppMode::Edit
|
AppMode::Edit
|
||||||
} else {
|
} else {
|
||||||
AppMode::ReadOnly
|
AppMode::ReadOnly
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if app_state.ui.show_login
|
|
||||||
|| app_state.ui.show_register
|
// --- Everything else (Intro, Admin, etc.) ---
|
||||||
{
|
_ => AppMode::General,
|
||||||
// login/register still use the old flag
|
|
||||||
if event_handler.is_edit_mode {
|
|
||||||
AppMode::Edit
|
|
||||||
} else {
|
|
||||||
AppMode::ReadOnly
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AppMode::General
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,4 +7,3 @@ pub mod canvas;
|
|||||||
pub use handlers::*;
|
pub use handlers::*;
|
||||||
pub use general::*;
|
pub use general::*;
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
pub use canvas::*;
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// src/tui/functions/common/form.rs
|
// src/pages/forms/logic.rs
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
|
use crate::pages::forms::FormState;
|
||||||
use crate::utils::data_converter;
|
use crate::utils::data_converter;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -21,7 +22,6 @@ pub async fn save(
|
|||||||
return Ok(SaveOutcome::NoChange);
|
return Ok(SaveOutcome::NoChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy out what we need before dropping the mutable borrow
|
|
||||||
let profile_name = fs.profile_name.clone();
|
let profile_name = fs.profile_name.clone();
|
||||||
let table_name = fs.table_name.clone();
|
let table_name = fs.table_name.clone();
|
||||||
let fields = fs.fields.clone();
|
let fields = fs.fields.clone();
|
||||||
@@ -75,7 +75,7 @@ pub async fn save(
|
|||||||
} else {
|
} else {
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"Cannot update record: ID is 0, but not classified as new entry."
|
"Cannot update record: ID is 0, but not classified as new entry."
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let response = grpc_client
|
let response = grpc_client
|
||||||
@@ -106,7 +106,7 @@ pub async fn revert(
|
|||||||
if let Some(fs) = app_state.form_state_mut() {
|
if let Some(fs) = app_state.form_state_mut() {
|
||||||
if fs.id == 0
|
if fs.id == 0
|
||||||
|| (fs.total_count > 0 && fs.current_position > fs.total_count)
|
|| (fs.total_count > 0 && fs.current_position > fs.total_count)
|
||||||
|| (fs.total_count == 0 && fs.current_position == 1)
|
|| (fs.total_count == 0 && fs.current_position == 1)
|
||||||
{
|
{
|
||||||
let old_total_count = fs.total_count;
|
let old_total_count = fs.total_count;
|
||||||
fs.reset_to_empty();
|
fs.reset_to_empty();
|
||||||
@@ -136,8 +136,8 @@ pub async fn revert(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context(format!(
|
.context(format!(
|
||||||
"Failed to get table data by position {} for table {}.{}",
|
"Failed to get table data by position {} for table {}.{}",
|
||||||
fs.current_position, fs.profile_name, fs.table_name
|
fs.current_position, fs.profile_name, fs.table_name
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
fs.update_from_response(&response.data, fs.current_position);
|
fs.update_from_response(&response.data, fs.current_position);
|
||||||
@@ -146,3 +146,37 @@ pub async fn revert(
|
|||||||
Ok("Nothing to revert".to_string())
|
Ok("Nothing to revert".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_action(
|
||||||
|
action: &str,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
_grpc_client: &mut GrpcClient,
|
||||||
|
ideal_cursor_column: &mut usize,
|
||||||
|
) -> Result<String> {
|
||||||
|
if form_state.has_unsaved_changes() {
|
||||||
|
return Ok(
|
||||||
|
"Unsaved changes. Save (Ctrl+S) or Revert (Ctrl+R) before navigating."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_count = form_state.total_count;
|
||||||
|
|
||||||
|
match action {
|
||||||
|
"previous_entry" => {
|
||||||
|
if form_state.current_position > 1 {
|
||||||
|
form_state.current_position -= 1;
|
||||||
|
*ideal_cursor_column = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"next_entry" => {
|
||||||
|
if form_state.current_position <= total_count {
|
||||||
|
form_state.current_position += 1;
|
||||||
|
*ideal_cursor_column = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Err(anyhow!("Unknown form action: {}", action)),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
9
client/src/pages/forms/mod.rs
Normal file
9
client/src/pages/forms/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// src/pages/forms/mod.rs
|
||||||
|
|
||||||
|
pub mod ui;
|
||||||
|
pub mod state;
|
||||||
|
pub mod logic;
|
||||||
|
|
||||||
|
pub use ui::*;
|
||||||
|
pub use state::*;
|
||||||
|
pub use logic::*;
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
// src/state/pages/form.rs
|
// src/pages/forms/state.rs
|
||||||
|
|
||||||
use crate::config::colors::themes::Theme;
|
use canvas::{DataProvider, AppMode};
|
||||||
use canvas::{DataProvider, AppMode, EditorState, FormEditor};
|
|
||||||
use common::proto::komp_ac::search::search_response::Hit;
|
use common::proto::komp_ac::search::search_response::Hit;
|
||||||
use ratatui::layout::Rect;
|
|
||||||
use ratatui::Frame;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn json_value_to_string(value: &serde_json::Value) -> String {
|
fn json_value_to_string(value: &serde_json::Value) -> String {
|
||||||
@@ -24,7 +21,7 @@ pub struct FieldDefinition {
|
|||||||
pub link_target_table: Option<String>,
|
pub link_target_table: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FormState {
|
pub struct FormState {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub profile_name: String,
|
pub profile_name: String,
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
// src/components/form/form.rs
|
// src/pages/forms/ui.rs
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::form::FormState;
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||||
style::Style,
|
style::Style,
|
||||||
widgets::{Block, Borders, Paragraph},
|
widgets::{Block, Borders, Paragraph},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use canvas::canvas::HighlightState;
|
use crate::pages::forms::FormState;
|
||||||
use canvas::{
|
use canvas::{
|
||||||
render_canvas, render_suggestions_dropdown, DefaultCanvasTheme, FormEditor,
|
render_canvas, render_suggestions_dropdown, DefaultCanvasTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn render_form(
|
pub fn render_form_page(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/tui/functions/intro.rs
|
// src/tui/functions/intro.rs
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::app::buffer::{AppView, BufferState};
|
use crate::buffer::state::{AppView, BufferState};
|
||||||
|
|
||||||
/// Handles intro screen selection by updating view history and managing focus state.
|
/// Handles intro screen selection by updating view history and managing focus state.
|
||||||
/// 0: Continue (restores last form or default)
|
/// 0: Continue (restores last form or default)
|
||||||
9
client/src/pages/intro/mod.rs
Normal file
9
client/src/pages/intro/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// src/pages/intro/mod.rs
|
||||||
|
|
||||||
|
pub mod state;
|
||||||
|
pub mod ui;
|
||||||
|
pub mod logic;
|
||||||
|
|
||||||
|
pub use state::*;
|
||||||
|
pub use ui::render_intro;
|
||||||
|
pub use logic::*;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/components/intro/intro.rs
|
// src/pages/intro/ui.rs
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::Style,
|
style::Style,
|
||||||
@@ -8,7 +8,7 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::pages::intro::IntroState;
|
use crate::pages::intro::IntroState;
|
||||||
|
|
||||||
pub fn render_intro(f: &mut Frame, intro_state: &IntroState, area: Rect, theme: &Theme) {
|
pub fn render_intro(f: &mut Frame, intro_state: &IntroState, area: Rect, theme: &Theme) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
@@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
use crate::services::auth::AuthClient;
|
use crate::services::auth::AuthClient;
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::state::pages::auth::LoginState;
|
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::app::buffer::{AppView, BufferState};
|
use crate::buffer::state::{AppView, BufferState};
|
||||||
use crate::config::storage::storage::{StoredAuthData, save_auth_data};
|
use crate::config::storage::storage::{StoredAuthData, save_auth_data};
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use common::proto::komp_ac::auth::LoginResponse;
|
use common::proto::komp_ac::auth::LoginResponse;
|
||||||
|
use crate::pages::login::LoginState;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{info, error};
|
use tracing::{info, error};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LoginResult {
|
pub enum LoginResult {
|
||||||
@@ -237,3 +238,15 @@ pub fn handle_login_result(
|
|||||||
login_state.current_cursor_pos = 0;
|
login_state.current_cursor_pos = 0;
|
||||||
true // Request redraw as dialog content changed
|
true // Request redraw as dialog content changed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_action(action: &str,) -> Result<String> {
|
||||||
|
match action {
|
||||||
|
"previous_entry" => {
|
||||||
|
Ok("Previous entry at tui/functions/login.rs not implemented".into())
|
||||||
|
}
|
||||||
|
"next_entry" => {
|
||||||
|
Ok("Next entry at tui/functions/login.rs not implemented".into())
|
||||||
|
}
|
||||||
|
_ => Err(anyhow!("Unknown login action: {}", action))
|
||||||
|
}
|
||||||
|
}
|
||||||
9
client/src/pages/login/mod.rs
Normal file
9
client/src/pages/login/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// src/pages/login/mod.rs
|
||||||
|
|
||||||
|
pub mod state;
|
||||||
|
pub mod ui;
|
||||||
|
pub mod logic;
|
||||||
|
|
||||||
|
pub use state::*;
|
||||||
|
pub use ui::render_login;
|
||||||
|
pub use logic::*;
|
||||||
121
client/src/pages/login/state.rs
Normal file
121
client/src/pages/login/state.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// src/pages/login/state.rs
|
||||||
|
|
||||||
|
use canvas::{AppMode, DataProvider};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LoginState {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
pub current_field: usize,
|
||||||
|
pub current_cursor_pos: usize,
|
||||||
|
pub has_unsaved_changes: bool,
|
||||||
|
pub login_request_pending: bool,
|
||||||
|
pub app_mode: AppMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoginState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
username: String::new(),
|
||||||
|
password: String::new(),
|
||||||
|
error_message: None,
|
||||||
|
current_field: 0,
|
||||||
|
current_cursor_pos: 0,
|
||||||
|
has_unsaved_changes: false,
|
||||||
|
login_request_pending: false,
|
||||||
|
app_mode: AppMode::Edit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
app_mode: AppMode::Edit,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_field(&self) -> usize {
|
||||||
|
self.current_field
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_cursor_pos(&self) -> usize {
|
||||||
|
self.current_cursor_pos
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_field(&mut self, index: usize) {
|
||||||
|
if index < 2 {
|
||||||
|
self.current_field = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
|
self.current_cursor_pos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_input(&self) -> &str {
|
||||||
|
match self.current_field {
|
||||||
|
0 => &self.username,
|
||||||
|
1 => &self.password,
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_input_mut(&mut self) -> &mut String {
|
||||||
|
match self.current_field {
|
||||||
|
0 => &mut self.username,
|
||||||
|
1 => &mut self.password,
|
||||||
|
_ => panic!("Invalid current_field index in LoginState"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_mode(&self) -> AppMode {
|
||||||
|
self.app_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_unsaved_changes(&self) -> bool {
|
||||||
|
self.has_unsaved_changes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||||
|
self.has_unsaved_changes = changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement DataProvider for LoginState
|
||||||
|
impl DataProvider for LoginState {
|
||||||
|
fn field_count(&self) -> usize {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_name(&self, index: usize) -> &str {
|
||||||
|
match index {
|
||||||
|
0 => "Username/Email",
|
||||||
|
1 => "Password",
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_value(&self, index: usize) -> &str {
|
||||||
|
match index {
|
||||||
|
0 => &self.username,
|
||||||
|
1 => &self.password,
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_field_value(&mut self, index: usize, value: String) {
|
||||||
|
match index {
|
||||||
|
0 => self.username = value,
|
||||||
|
1 => self.password = value,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_suggestions(&self, _field_index: usize) -> bool {
|
||||||
|
false // Login form doesn't support suggestions
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
// src/components/auth/login.rs
|
// src/pages/login/ui.rs
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::colors::themes::Theme,
|
config::colors::themes::Theme,
|
||||||
state::pages::auth::LoginState,
|
|
||||||
components::common::dialog,
|
|
||||||
state::app::state::AppState,
|
state::app::state::AppState,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@@ -18,6 +16,8 @@ use canvas::{
|
|||||||
render_suggestions_dropdown,
|
render_suggestions_dropdown,
|
||||||
DefaultCanvasTheme,
|
DefaultCanvasTheme,
|
||||||
};
|
};
|
||||||
|
use crate::pages::login::LoginState;
|
||||||
|
use crate::dialog;
|
||||||
|
|
||||||
pub fn render_login(
|
pub fn render_login(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
7
client/src/pages/mod.rs
Normal file
7
client/src/pages/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// src/pages/mod.rs
|
||||||
|
|
||||||
|
pub mod routing;
|
||||||
|
pub mod intro;
|
||||||
|
pub mod login;
|
||||||
|
pub mod register;
|
||||||
|
pub mod forms;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
// src/tui/functions/common/register.rs
|
// src/pages/register/logic.rs
|
||||||
|
|
||||||
use crate::services::auth::AuthClient;
|
use crate::services::auth::AuthClient;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
pages::auth::RegisterState,
|
|
||||||
app::state::AppState,
|
app::state::AppState,
|
||||||
};
|
};
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use crate::state::app::buffer::{AppView, BufferState};
|
use crate::buffer::state::{AppView, BufferState};
|
||||||
use common::proto::komp_ac::auth::AuthResponse;
|
use common::proto::komp_ac::auth::AuthResponse;
|
||||||
|
use crate::pages::register::RegisterState;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
11
client/src/pages/register/mod.rs
Normal file
11
client/src/pages/register/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// src/pages/register/mod.rs
|
||||||
|
|
||||||
|
// pub mod state;
|
||||||
|
pub mod ui;
|
||||||
|
pub mod state;
|
||||||
|
pub mod logic;
|
||||||
|
|
||||||
|
// pub use state::*;
|
||||||
|
pub use ui::render_register;
|
||||||
|
pub use logic::*;
|
||||||
|
pub use state::*;
|
||||||
184
client/src/pages/register/state.rs
Normal file
184
client/src/pages/register/state.rs
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
// src/pages/register/state.rs
|
||||||
|
|
||||||
|
use canvas::{DataProvider, AppMode};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref AVAILABLE_ROLES: Vec<String> = vec![
|
||||||
|
"admin".to_string(),
|
||||||
|
"moderator".to_string(),
|
||||||
|
"accountant".to_string(),
|
||||||
|
"viewer".to_string(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the state of the Registration form UI
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RegisterState {
|
||||||
|
pub username: String,
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
pub password_confirmation: String,
|
||||||
|
pub role: String,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
pub current_field: usize,
|
||||||
|
pub current_cursor_pos: usize,
|
||||||
|
pub has_unsaved_changes: bool,
|
||||||
|
pub app_mode: AppMode,
|
||||||
|
pub role_suggestions: Vec<String>,
|
||||||
|
pub role_suggestions_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RegisterState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
username: String::new(),
|
||||||
|
email: String::new(),
|
||||||
|
password: String::new(),
|
||||||
|
password_confirmation: String::new(),
|
||||||
|
role: String::new(),
|
||||||
|
error_message: None,
|
||||||
|
current_field: 0,
|
||||||
|
current_cursor_pos: 0,
|
||||||
|
has_unsaved_changes: false,
|
||||||
|
app_mode: AppMode::Edit,
|
||||||
|
role_suggestions: AVAILABLE_ROLES.clone(),
|
||||||
|
role_suggestions_active: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegisterState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
app_mode: AppMode::Edit,
|
||||||
|
role_suggestions: AVAILABLE_ROLES.clone(),
|
||||||
|
role_suggestions_active: false,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_field(&self) -> usize {
|
||||||
|
self.current_field
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_cursor_pos(&self) -> usize {
|
||||||
|
self.current_cursor_pos
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_field(&mut self, index: usize) {
|
||||||
|
if index < 5 {
|
||||||
|
self.current_field = index;
|
||||||
|
|
||||||
|
if index == 4 {
|
||||||
|
self.activate_role_suggestions();
|
||||||
|
} else {
|
||||||
|
self.deactivate_role_suggestions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
|
self.current_cursor_pos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_input(&self) -> &str {
|
||||||
|
match self.current_field {
|
||||||
|
0 => &self.username,
|
||||||
|
1 => &self.email,
|
||||||
|
2 => &self.password,
|
||||||
|
3 => &self.password_confirmation,
|
||||||
|
4 => &self.role,
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_input_mut(&mut self, index: usize) -> &mut String {
|
||||||
|
match index {
|
||||||
|
0 => &mut self.username,
|
||||||
|
1 => &mut self.email,
|
||||||
|
2 => &mut self.password,
|
||||||
|
3 => &mut self.password_confirmation,
|
||||||
|
4 => &mut self.role,
|
||||||
|
_ => panic!("Invalid current_field index in RegisterState"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_mode(&self) -> AppMode {
|
||||||
|
self.app_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_role_suggestions(&mut self) {
|
||||||
|
self.role_suggestions_active = true;
|
||||||
|
let current_input = self.role.to_lowercase();
|
||||||
|
self.role_suggestions = AVAILABLE_ROLES
|
||||||
|
.iter()
|
||||||
|
.filter(|role| role.to_lowercase().contains(¤t_input))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deactivate_role_suggestions(&mut self) {
|
||||||
|
self.role_suggestions_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_role_suggestions_active(&self) -> bool {
|
||||||
|
self.role_suggestions_active
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_role_suggestions(&self) -> &[String] {
|
||||||
|
&self.role_suggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_unsaved_changes(&self) -> bool {
|
||||||
|
self.has_unsaved_changes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||||
|
self.has_unsaved_changes = changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataProvider for RegisterState {
|
||||||
|
fn field_count(&self) -> usize {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_name(&self, index: usize) -> &str {
|
||||||
|
match index {
|
||||||
|
0 => "Username",
|
||||||
|
1 => "Email (Optional)",
|
||||||
|
2 => "Password (Optional)",
|
||||||
|
3 => "Confirm Password",
|
||||||
|
4 => "Role (Optional)",
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_value(&self, index: usize) -> &str {
|
||||||
|
match index {
|
||||||
|
0 => &self.username,
|
||||||
|
1 => &self.email,
|
||||||
|
2 => &self.password,
|
||||||
|
3 => &self.password_confirmation,
|
||||||
|
4 => &self.role,
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_field_value(&mut self, index: usize, value: String) {
|
||||||
|
match index {
|
||||||
|
0 => self.username = value,
|
||||||
|
1 => self.email = value,
|
||||||
|
2 => self.password = value,
|
||||||
|
3 => self.password_confirmation = value,
|
||||||
|
4 => self.role = value,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_suggestions(&self, field_index: usize) -> bool {
|
||||||
|
field_index == 4 // only Role field supports suggestions
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
// src/components/auth/register.rs
|
// src/pages/register/ui.rs
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::colors::themes::Theme,
|
config::colors::themes::Theme,
|
||||||
state::pages::auth::RegisterState,
|
|
||||||
components::common::dialog,
|
|
||||||
state::app::state::AppState,
|
state::app::state::AppState,
|
||||||
modes::handlers::mode_manager::AppMode,
|
modes::handlers::mode_manager::AppMode,
|
||||||
};
|
};
|
||||||
@@ -13,7 +11,9 @@ use ratatui::{
|
|||||||
widgets::{Block, BorderType, Borders, Paragraph},
|
widgets::{Block, BorderType, Borders, Paragraph},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use canvas::{FormEditor, render_canvas_default, render_canvas, render_suggestions_dropdown, DefaultCanvasTheme};
|
use crate::dialog;
|
||||||
|
use crate::pages::register::RegisterState;
|
||||||
|
use canvas::{FormEditor, render_canvas, render_suggestions_dropdown, DefaultCanvasTheme};
|
||||||
|
|
||||||
pub fn render_register(
|
pub fn render_register(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
5
client/src/pages/routing/mod.rs
Normal file
5
client/src/pages/routing/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// src/pages/routing/mod.rs
|
||||||
|
|
||||||
|
pub mod router;
|
||||||
|
|
||||||
|
pub use router::{Page, Router};
|
||||||
38
client/src/pages/routing/router.rs
Normal file
38
client/src/pages/routing/router.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// src/pages/routing/router.rs
|
||||||
|
use crate::state::pages::{
|
||||||
|
admin::AdminState,
|
||||||
|
auth::AuthState,
|
||||||
|
add_logic::AddLogicState,
|
||||||
|
add_table::AddTableState,
|
||||||
|
};
|
||||||
|
use crate::pages::forms::FormState;
|
||||||
|
use crate::pages::login::LoginState;
|
||||||
|
use crate::pages::register::RegisterState;
|
||||||
|
use crate::pages::intro::IntroState;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Page {
|
||||||
|
Intro(IntroState),
|
||||||
|
Login(LoginState),
|
||||||
|
Register(RegisterState),
|
||||||
|
Admin(AdminState),
|
||||||
|
AddLogic(AddLogicState),
|
||||||
|
AddTable(AddTableState),
|
||||||
|
Form(FormState),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Router {
|
||||||
|
pub current: Page,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Router {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
current: Page::Intro(IntroState::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn navigate(&mut self, page: Page) {
|
||||||
|
self.current = page;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
client/src/search/event.rs
Normal file
106
client/src/search/event.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// src/search/event.rs
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
use crate::services::grpc_client::GrpcClient;
|
||||||
|
use common::proto::komp_ac::search::search_response::Hit;
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tracing::{error, info};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub async fn handle_search_palette_event(
|
||||||
|
key_event: crossterm::event::KeyEvent,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
grpc_client: &mut GrpcClient,
|
||||||
|
search_result_sender: mpsc::UnboundedSender<Vec<Hit>>,
|
||||||
|
) -> Result<Option<String>> {
|
||||||
|
let mut should_close = false;
|
||||||
|
let mut outcome_message = None;
|
||||||
|
let mut trigger_search = false;
|
||||||
|
|
||||||
|
if let Some(search_state) = app_state.search_state.as_mut() {
|
||||||
|
match key_event.code {
|
||||||
|
KeyCode::Esc => {
|
||||||
|
should_close = true;
|
||||||
|
outcome_message = Some("Search cancelled".to_string());
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
// Step 1: Extract the data we need while holding the borrow
|
||||||
|
let maybe_data = search_state
|
||||||
|
.results
|
||||||
|
.get(search_state.selected_index)
|
||||||
|
.map(|hit| (hit.id, hit.content_json.clone()));
|
||||||
|
|
||||||
|
// Step 2: Process outside the borrow
|
||||||
|
if let Some((id, content_json)) = maybe_data {
|
||||||
|
if let Ok(data) = serde_json::from_str::<HashMap<String, String>>(&content_json) {
|
||||||
|
if let Some(fs) = app_state.form_state_mut() {
|
||||||
|
let detached_pos = fs.total_count + 2;
|
||||||
|
fs.update_from_response(&data, detached_pos);
|
||||||
|
}
|
||||||
|
should_close = true;
|
||||||
|
outcome_message = Some(format!("Loaded record ID {}", id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Up => search_state.previous_result(),
|
||||||
|
KeyCode::Down => search_state.next_result(),
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
search_state.input.insert(search_state.cursor_position, c);
|
||||||
|
search_state.cursor_position += 1;
|
||||||
|
trigger_search = true;
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
if search_state.cursor_position > 0 {
|
||||||
|
search_state.cursor_position -= 1;
|
||||||
|
search_state.input.remove(search_state.cursor_position);
|
||||||
|
trigger_search = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
search_state.cursor_position =
|
||||||
|
search_state.cursor_position.saturating_sub(1);
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
if search_state.cursor_position < search_state.input.len() {
|
||||||
|
search_state.cursor_position += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if trigger_search {
|
||||||
|
if let Some(search_state) = app_state.search_state.as_mut() {
|
||||||
|
search_state.is_loading = true;
|
||||||
|
search_state.results.clear();
|
||||||
|
search_state.selected_index = 0;
|
||||||
|
|
||||||
|
let query = search_state.input.clone();
|
||||||
|
let table_name = search_state.table_name.clone();
|
||||||
|
let sender = search_result_sender.clone();
|
||||||
|
let mut grpc_client = grpc_client.clone();
|
||||||
|
|
||||||
|
info!("Spawning search task for query: '{}'", query);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match grpc_client.search_table(table_name, query).await {
|
||||||
|
Ok(response) => {
|
||||||
|
let _ = sender.send(response.hits);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Search failed: {:?}", e);
|
||||||
|
let _ = sender.send(vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if should_close {
|
||||||
|
app_state.search_state = None;
|
||||||
|
app_state.ui.show_search_palette = false;
|
||||||
|
app_state.ui.focus_outside_canvas = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(outcome_message)
|
||||||
|
}
|
||||||
31
client/src/search/grpc.rs
Normal file
31
client/src/search/grpc.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// src/search/grpc.rs
|
||||||
|
|
||||||
|
use common::proto::komp_ac::search::{
|
||||||
|
searcher_client::SearcherClient, SearchRequest, SearchResponse,
|
||||||
|
};
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
/// Internal search gRPC wrapper
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SearchGrpc {
|
||||||
|
client: SearcherClient<Channel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchGrpc {
|
||||||
|
pub fn new(channel: Channel) -> Self {
|
||||||
|
Self {
|
||||||
|
client: SearcherClient::new(channel),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_table(
|
||||||
|
&mut self,
|
||||||
|
table_name: String,
|
||||||
|
query: String,
|
||||||
|
) -> Result<SearchResponse> {
|
||||||
|
let request = tonic::Request::new(SearchRequest { table_name, query });
|
||||||
|
let response = self.client.search_table(request).await?;
|
||||||
|
Ok(response.into_inner())
|
||||||
|
}
|
||||||
|
}
|
||||||
9
client/src/search/mod.rs
Normal file
9
client/src/search/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// src/search/mod.rs
|
||||||
|
|
||||||
|
pub mod state;
|
||||||
|
pub mod ui;
|
||||||
|
pub mod event;
|
||||||
|
pub mod grpc;
|
||||||
|
|
||||||
|
pub use ui::*;
|
||||||
|
pub use grpc::SearchGrpc;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/state/app/search.rs
|
// src/search/state.rs
|
||||||
|
|
||||||
use common::proto::komp_ac::search::search_response::Hit;
|
use common::proto::komp_ac::search::search_response::Hit;
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/components/common/search_palette.rs
|
// src/search/ui.rs
|
||||||
|
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::search::SearchState;
|
use crate::search::state::SearchState;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
style::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
@@ -22,9 +22,8 @@ use common::proto::komp_ac::tables_data::{
|
|||||||
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
|
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
|
||||||
PutTableDataResponse,
|
PutTableDataResponse,
|
||||||
};
|
};
|
||||||
use common::proto::komp_ac::search::{
|
use crate::search::SearchGrpc;
|
||||||
searcher_client::SearcherClient, SearchRequest, SearchResponse,
|
use common::proto::komp_ac::search::SearchResponse;
|
||||||
};
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tonic::transport::Channel;
|
use tonic::transport::Channel;
|
||||||
@@ -36,7 +35,7 @@ pub struct GrpcClient {
|
|||||||
table_definition_client: TableDefinitionClient<Channel>,
|
table_definition_client: TableDefinitionClient<Channel>,
|
||||||
table_script_client: TableScriptClient<Channel>,
|
table_script_client: TableScriptClient<Channel>,
|
||||||
tables_data_client: TablesDataClient<Channel>,
|
tables_data_client: TablesDataClient<Channel>,
|
||||||
search_client: SearcherClient<Channel>,
|
search_client: SearchGrpc,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GrpcClient {
|
impl GrpcClient {
|
||||||
@@ -52,7 +51,7 @@ impl GrpcClient {
|
|||||||
TableDefinitionClient::new(channel.clone());
|
TableDefinitionClient::new(channel.clone());
|
||||||
let table_script_client = TableScriptClient::new(channel.clone());
|
let table_script_client = TableScriptClient::new(channel.clone());
|
||||||
let tables_data_client = TablesDataClient::new(channel.clone());
|
let tables_data_client = TablesDataClient::new(channel.clone());
|
||||||
let search_client = SearcherClient::new(channel.clone());
|
let search_client = SearchGrpc::new(channel.clone());
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
table_structure_client,
|
table_structure_client,
|
||||||
@@ -247,11 +246,6 @@ impl GrpcClient {
|
|||||||
table_name: String,
|
table_name: String,
|
||||||
query: String,
|
query: String,
|
||||||
) -> Result<SearchResponse> {
|
) -> Result<SearchResponse> {
|
||||||
let request = tonic::Request::new(SearchRequest { table_name, query });
|
self.search_client.search_table(table_name, query).await
|
||||||
let response = self
|
|
||||||
.search_client
|
|
||||||
.search_table(request)
|
|
||||||
.await?;
|
|
||||||
Ok(response.into_inner())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::add_logic::AddLogicState;
|
use crate::state::pages::add_logic::AddLogicState;
|
||||||
use crate::state::pages::form::{FieldDefinition, FormState};
|
use crate::pages::forms::logic::SaveOutcome;
|
||||||
use crate::tui::functions::common::form::SaveOutcome;
|
|
||||||
use crate::utils::columns::filter_user_columns;
|
use crate::utils::columns::filter_user_columns;
|
||||||
|
use crate::pages::forms::{FieldDefinition, FormState};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|||||||
21
client/src/sidebar/logic.rs
Normal file
21
client/src/sidebar/logic.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// src/sidebar/state.rs
|
||||||
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::state::app::state::UiState;
|
||||||
|
|
||||||
|
pub fn toggle_sidebar(
|
||||||
|
ui_state: &mut UiState,
|
||||||
|
config: &Config,
|
||||||
|
key: KeyCode,
|
||||||
|
modifiers: KeyModifiers,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(action) =
|
||||||
|
config.get_action_for_key_in_mode(&config.keybindings.common, key, modifiers)
|
||||||
|
{
|
||||||
|
if action == "toggle_sidebar" {
|
||||||
|
ui_state.show_sidebar = !ui_state.show_sidebar;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
7
client/src/sidebar/mod.rs
Normal file
7
client/src/sidebar/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// src/sidebar/mod.rs
|
||||||
|
|
||||||
|
pub mod ui;
|
||||||
|
pub mod logic;
|
||||||
|
|
||||||
|
pub use ui::{calculate_sidebar_layout, render_sidebar};
|
||||||
|
pub use logic::toggle_sidebar;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/components/handlers/sidebar.rs
|
// src/sidebar/ui.rs
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
widgets::{Block, List, ListItem},
|
widgets::{Block, List, ListItem},
|
||||||
layout::{Rect, Direction, Layout, Constraint},
|
layout::{Rect, Direction, Layout, Constraint},
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// src/state/app.rs
|
// src/state/app.rs
|
||||||
|
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod buffer;
|
|
||||||
pub mod search;
|
|
||||||
|
|||||||
@@ -5,28 +5,18 @@ use common::proto::komp_ac::table_definition::ProfileTreeResponse;
|
|||||||
// NEW: Import the types we need for the cache
|
// NEW: Import the types we need for the cache
|
||||||
use common::proto::komp_ac::table_structure::TableStructureResponse;
|
use common::proto::komp_ac::table_structure::TableStructureResponse;
|
||||||
use crate::modes::handlers::mode_manager::AppMode;
|
use crate::modes::handlers::mode_manager::AppMode;
|
||||||
use crate::state::app::search::SearchState;
|
use crate::search::state::SearchState;
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use crate::state::pages::form::FormState;
|
|
||||||
use crate::config::binds::Config;
|
use crate::config::binds::Config;
|
||||||
|
use crate::pages::forms::FormState;
|
||||||
use canvas::FormEditor;
|
use canvas::FormEditor;
|
||||||
|
use crate::dialog::DialogState;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
// --- DialogState and UiState are unchanged ---
|
|
||||||
pub struct DialogState {
|
|
||||||
pub dialog_show: bool,
|
|
||||||
pub dialog_title: String,
|
|
||||||
pub dialog_message: String,
|
|
||||||
pub dialog_buttons: Vec<String>,
|
|
||||||
pub dialog_active_button_index: usize,
|
|
||||||
pub purpose: Option<DialogPurpose>,
|
|
||||||
pub is_loading: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UiState {
|
pub struct UiState {
|
||||||
pub show_sidebar: bool,
|
pub show_sidebar: bool,
|
||||||
pub show_buffer_list: bool,
|
pub show_buffer_list: bool,
|
||||||
@@ -109,84 +99,6 @@ impl AppState {
|
|||||||
self.current_view_table_name = Some(table_name);
|
self.current_view_table_name = Some(table_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_dialog(
|
|
||||||
&mut self,
|
|
||||||
title: &str,
|
|
||||||
message: &str,
|
|
||||||
buttons: Vec<String>,
|
|
||||||
purpose: DialogPurpose,
|
|
||||||
) {
|
|
||||||
self.ui.dialog.dialog_title = title.to_string();
|
|
||||||
self.ui.dialog.dialog_message = message.to_string();
|
|
||||||
self.ui.dialog.dialog_buttons = buttons;
|
|
||||||
self.ui.dialog.dialog_active_button_index = 0;
|
|
||||||
self.ui.dialog.purpose = Some(purpose);
|
|
||||||
self.ui.dialog.is_loading = false;
|
|
||||||
self.ui.dialog.dialog_show = true;
|
|
||||||
self.ui.focus_outside_canvas = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show_loading_dialog(&mut self, title: &str, message: &str) {
|
|
||||||
self.ui.dialog.dialog_title = title.to_string();
|
|
||||||
self.ui.dialog.dialog_message = message.to_string();
|
|
||||||
self.ui.dialog.dialog_buttons.clear();
|
|
||||||
self.ui.dialog.dialog_active_button_index = 0;
|
|
||||||
self.ui.dialog.purpose = None;
|
|
||||||
self.ui.dialog.is_loading = true;
|
|
||||||
self.ui.dialog.dialog_show = true;
|
|
||||||
self.ui.focus_outside_canvas = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_dialog_content(
|
|
||||||
&mut self,
|
|
||||||
message: &str,
|
|
||||||
buttons: Vec<String>,
|
|
||||||
purpose: DialogPurpose,
|
|
||||||
) {
|
|
||||||
if self.ui.dialog.dialog_show {
|
|
||||||
self.ui.dialog.dialog_message = message.to_string();
|
|
||||||
self.ui.dialog.dialog_buttons = buttons;
|
|
||||||
self.ui.dialog.dialog_active_button_index = 0;
|
|
||||||
self.ui.dialog.purpose = Some(purpose);
|
|
||||||
self.ui.dialog.is_loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hide_dialog(&mut self) {
|
|
||||||
self.ui.dialog.dialog_show = false;
|
|
||||||
self.ui.dialog.dialog_title.clear();
|
|
||||||
self.ui.dialog.dialog_message.clear();
|
|
||||||
self.ui.dialog.dialog_buttons.clear();
|
|
||||||
self.ui.dialog.dialog_active_button_index = 0;
|
|
||||||
self.ui.dialog.purpose = None;
|
|
||||||
self.ui.focus_outside_canvas = false;
|
|
||||||
self.ui.dialog.is_loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_dialog_button(&mut self) {
|
|
||||||
if !self.ui.dialog.dialog_buttons.is_empty() {
|
|
||||||
let next_index = (self.ui.dialog.dialog_active_button_index + 1)
|
|
||||||
% self.ui.dialog.dialog_buttons.len();
|
|
||||||
self.ui.dialog.dialog_active_button_index = next_index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn previous_dialog_button(&mut self) {
|
|
||||||
if !self.ui.dialog.dialog_buttons.is_empty() {
|
|
||||||
let len = self.ui.dialog.dialog_buttons.len();
|
|
||||||
let prev_index =
|
|
||||||
(self.ui.dialog.dialog_active_button_index + len - 1) % len;
|
|
||||||
self.ui.dialog.dialog_active_button_index = prev_index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_active_dialog_button_label(&self) -> Option<&str> {
|
|
||||||
self.ui.dialog
|
|
||||||
.dialog_buttons
|
|
||||||
.get(self.ui.dialog.dialog_active_button_index)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_form_editor(&mut self, form_state: FormState, config: &Config) {
|
pub fn init_form_editor(&mut self, form_state: FormState, config: &Config) {
|
||||||
let mut editor = FormEditor::new(form_state);
|
let mut editor = FormEditor::new(form_state);
|
||||||
editor.set_keymap(config.build_canvas_keymap()); // inject keymap
|
editor.set_keymap(config.build_canvas_keymap()); // inject keymap
|
||||||
@@ -229,17 +141,3 @@ impl Default for UiState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DialogState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
dialog_show: false,
|
|
||||||
dialog_title: String::new(),
|
|
||||||
dialog_message: String::new(),
|
|
||||||
dialog_buttons: Vec::new(),
|
|
||||||
dialog_active_button_index: 0,
|
|
||||||
purpose: None,
|
|
||||||
is_loading: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// src/state/pages.rs
|
// src/state/pages.rs
|
||||||
|
|
||||||
pub mod form;
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod intro;
|
|
||||||
pub mod add_table;
|
pub mod add_table;
|
||||||
pub mod add_logic;
|
pub mod add_logic;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/state/pages/add_table.rs
|
// src/state/pages/add_table.rs
|
||||||
|
|
||||||
use canvas::{DataProvider, CanvasAction, AppMode};
|
use canvas::{DataProvider, AppMode};
|
||||||
use ratatui::widgets::TableState;
|
use ratatui::widgets::TableState;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
// src/state/pages/auth.rs
|
// src/state/pages/auth.rs
|
||||||
use canvas::{DataProvider, AppMode, SuggestionItem};
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
lazy_static! {
|
use canvas::{DataProvider, AppMode};
|
||||||
pub static ref AVAILABLE_ROLES: Vec<String> = vec![
|
|
||||||
"admin".to_string(),
|
|
||||||
"moderator".to_string(),
|
|
||||||
"accountant".to_string(),
|
|
||||||
"viewer".to_string(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the authenticated session state
|
/// Represents the authenticated session state
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -20,307 +11,8 @@ pub struct AuthState {
|
|||||||
pub decoded_username: Option<String>,
|
pub decoded_username: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the state of the Login form UI
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct LoginState {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
pub error_message: Option<String>,
|
|
||||||
pub current_field: usize,
|
|
||||||
pub current_cursor_pos: usize,
|
|
||||||
pub has_unsaved_changes: bool,
|
|
||||||
pub login_request_pending: bool,
|
|
||||||
pub app_mode: AppMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LoginState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
username: String::new(),
|
|
||||||
password: String::new(),
|
|
||||||
error_message: None,
|
|
||||||
current_field: 0,
|
|
||||||
current_cursor_pos: 0,
|
|
||||||
has_unsaved_changes: false,
|
|
||||||
login_request_pending: false,
|
|
||||||
app_mode: AppMode::Edit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the state of the Registration form UI
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RegisterState {
|
|
||||||
pub username: String,
|
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
pub password_confirmation: String,
|
|
||||||
pub role: String,
|
|
||||||
pub error_message: Option<String>,
|
|
||||||
pub current_field: usize,
|
|
||||||
pub current_cursor_pos: usize,
|
|
||||||
pub has_unsaved_changes: bool,
|
|
||||||
pub app_mode: AppMode,
|
|
||||||
// Keep role suggestions for later integration
|
|
||||||
pub role_suggestions: Vec<String>,
|
|
||||||
pub role_suggestions_active: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RegisterState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
username: String::new(),
|
|
||||||
email: String::new(),
|
|
||||||
password: String::new(),
|
|
||||||
password_confirmation: String::new(),
|
|
||||||
role: String::new(),
|
|
||||||
error_message: None,
|
|
||||||
current_field: 0,
|
|
||||||
current_cursor_pos: 0,
|
|
||||||
has_unsaved_changes: false,
|
|
||||||
app_mode: AppMode::Edit,
|
|
||||||
role_suggestions: AVAILABLE_ROLES.clone(),
|
|
||||||
role_suggestions_active: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthState {
|
impl AuthState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoginState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
app_mode: AppMode::Edit,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy method compatibility
|
|
||||||
pub fn current_field(&self) -> usize {
|
|
||||||
self.current_field
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_cursor_pos(&self) -> usize {
|
|
||||||
self.current_cursor_pos
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_current_field(&mut self, index: usize) {
|
|
||||||
if index < 2 {
|
|
||||||
self.current_field = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_current_cursor_pos(&mut self, pos: usize) {
|
|
||||||
self.current_cursor_pos = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_current_input(&self) -> &str {
|
|
||||||
match self.current_field {
|
|
||||||
0 => &self.username,
|
|
||||||
1 => &self.password,
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_current_input_mut(&mut self) -> &mut String {
|
|
||||||
match self.current_field {
|
|
||||||
0 => &mut self.username,
|
|
||||||
1 => &mut self.password,
|
|
||||||
_ => panic!("Invalid current_field index in LoginState"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_mode(&self) -> AppMode {
|
|
||||||
self.app_mode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing methods that used to come from CanvasState trait
|
|
||||||
pub fn has_unsaved_changes(&self) -> bool {
|
|
||||||
self.has_unsaved_changes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_has_unsaved_changes(&mut self, changed: bool) {
|
|
||||||
self.has_unsaved_changes = changed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RegisterState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
app_mode: AppMode::Edit,
|
|
||||||
role_suggestions: AVAILABLE_ROLES.clone(),
|
|
||||||
role_suggestions_active: false,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy method compatibility
|
|
||||||
pub fn current_field(&self) -> usize {
|
|
||||||
self.current_field
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_cursor_pos(&self) -> usize {
|
|
||||||
self.current_cursor_pos
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_current_field(&mut self, index: usize) {
|
|
||||||
if index < 5 {
|
|
||||||
self.current_field = index;
|
|
||||||
|
|
||||||
// Auto-activate role suggestions when moving to role field (index 4)
|
|
||||||
if index == 4 {
|
|
||||||
self.activate_role_suggestions();
|
|
||||||
} else {
|
|
||||||
self.deactivate_role_suggestions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_current_cursor_pos(&mut self, pos: usize) {
|
|
||||||
self.current_cursor_pos = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_current_input(&self) -> &str {
|
|
||||||
match self.current_field {
|
|
||||||
0 => &self.username,
|
|
||||||
1 => &self.email,
|
|
||||||
2 => &self.password,
|
|
||||||
3 => &self.password_confirmation,
|
|
||||||
4 => &self.role,
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_current_input_mut(&mut self) -> &mut String {
|
|
||||||
match self.current_field {
|
|
||||||
0 => &mut self.username,
|
|
||||||
1 => &mut self.email,
|
|
||||||
2 => &mut self.password,
|
|
||||||
3 => &mut self.password_confirmation,
|
|
||||||
4 => &mut self.role,
|
|
||||||
_ => panic!("Invalid current_field index in RegisterState"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_mode(&self) -> AppMode {
|
|
||||||
self.app_mode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role suggestions management
|
|
||||||
pub fn activate_role_suggestions(&mut self) {
|
|
||||||
self.role_suggestions_active = true;
|
|
||||||
// Filter suggestions based on current input
|
|
||||||
let current_input = self.role.to_lowercase();
|
|
||||||
self.role_suggestions = AVAILABLE_ROLES
|
|
||||||
.iter()
|
|
||||||
.filter(|role| role.to_lowercase().contains(¤t_input))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deactivate_role_suggestions(&mut self) {
|
|
||||||
self.role_suggestions_active = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_role_suggestions_active(&self) -> bool {
|
|
||||||
self.role_suggestions_active
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_role_suggestions(&self) -> &[String] {
|
|
||||||
&self.role_suggestions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing methods that used to come from CanvasState trait
|
|
||||||
pub fn has_unsaved_changes(&self) -> bool {
|
|
||||||
self.has_unsaved_changes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_has_unsaved_changes(&mut self, changed: bool) {
|
|
||||||
self.has_unsaved_changes = changed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Implement DataProvider for LoginState
|
|
||||||
impl DataProvider for LoginState {
|
|
||||||
fn field_count(&self) -> usize {
|
|
||||||
2
|
|
||||||
}
|
|
||||||
|
|
||||||
fn field_name(&self, index: usize) -> &str {
|
|
||||||
match index {
|
|
||||||
0 => "Username/Email",
|
|
||||||
1 => "Password",
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn field_value(&self, index: usize) -> &str {
|
|
||||||
match index {
|
|
||||||
0 => &self.username,
|
|
||||||
1 => &self.password,
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_field_value(&mut self, index: usize, value: String) {
|
|
||||||
match index {
|
|
||||||
0 => self.username = value,
|
|
||||||
1 => self.password = value,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self.has_unsaved_changes = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supports_suggestions(&self, _field_index: usize) -> bool {
|
|
||||||
false // Login form doesn't support suggestions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Implement DataProvider for RegisterState
|
|
||||||
impl DataProvider for RegisterState {
|
|
||||||
fn field_count(&self) -> usize {
|
|
||||||
5
|
|
||||||
}
|
|
||||||
|
|
||||||
fn field_name(&self, index: usize) -> &str {
|
|
||||||
match index {
|
|
||||||
0 => "Username",
|
|
||||||
1 => "Email (Optional)",
|
|
||||||
2 => "Password (Optional)",
|
|
||||||
3 => "Confirm Password",
|
|
||||||
4 => "Role (Optional)",
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn field_value(&self, index: usize) -> &str {
|
|
||||||
match index {
|
|
||||||
0 => &self.username,
|
|
||||||
1 => &self.email,
|
|
||||||
2 => &self.password,
|
|
||||||
3 => &self.password_confirmation,
|
|
||||||
4 => &self.role,
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_field_value(&mut self, index: usize, value: String) {
|
|
||||||
match index {
|
|
||||||
0 => self.username = value,
|
|
||||||
1 => self.email = value,
|
|
||||||
2 => self.password = value,
|
|
||||||
3 => self.password_confirmation = value,
|
|
||||||
4 => self.role = value,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self.has_unsaved_changes = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supports_suggestions(&self, field_index: usize) -> bool {
|
|
||||||
field_index == 4 // only Role field supports suggestions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
// src/tui/functions.rs
|
// src/tui/functions.rs
|
||||||
|
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod intro;
|
|
||||||
pub mod login;
|
|
||||||
pub mod form;
|
|
||||||
pub mod common;
|
pub mod common;
|
||||||
|
|
||||||
pub use admin::*;
|
pub use admin::*;
|
||||||
pub use intro::*;
|
|
||||||
pub use form::*;
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
// src/tui/functions/common.rs
|
// src/tui/functions/common.rs
|
||||||
|
|
||||||
pub mod form;
|
|
||||||
pub mod login;
|
|
||||||
pub mod logout;
|
pub mod logout;
|
||||||
pub mod register;
|
|
||||||
pub mod add_table;
|
pub mod add_table;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
use crate::config::storage::delete_auth_data;
|
use crate::config::storage::delete_auth_data;
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::app::buffer::{AppView, BufferState};
|
use crate::buffer::state::{AppView, BufferState};
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
// src/tui/functions/form.rs
|
|
||||||
use crate::state::pages::form::FormState;
|
|
||||||
use crate::services::grpc_client::GrpcClient;
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
|
|
||||||
pub async fn handle_action(
|
|
||||||
action: &str,
|
|
||||||
form_state: &mut FormState,
|
|
||||||
_grpc_client: &mut GrpcClient,
|
|
||||||
ideal_cursor_column: &mut usize,
|
|
||||||
) -> Result<String> {
|
|
||||||
if form_state.has_unsaved_changes() {
|
|
||||||
return Ok(
|
|
||||||
"Unsaved changes. Save (Ctrl+S) or Revert (Ctrl+R) before navigating."
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let total_count = form_state.total_count;
|
|
||||||
|
|
||||||
match action {
|
|
||||||
"previous_entry" => {
|
|
||||||
// Only decrement if the current position is greater than the first record.
|
|
||||||
// This prevents wrapping from 1 to total_count.
|
|
||||||
// It also correctly handles moving from "New Entry" (total_count + 1) to the last record.
|
|
||||||
if form_state.current_position > 1 {
|
|
||||||
form_state.current_position -= 1;
|
|
||||||
*ideal_cursor_column = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"next_entry" => {
|
|
||||||
// Only increment if the current position is not yet at the "New Entry" stage.
|
|
||||||
// The "New Entry" position is total_count + 1.
|
|
||||||
// This allows moving from the last record to "New Entry", but stops there.
|
|
||||||
if form_state.current_position <= total_count {
|
|
||||||
form_state.current_position += 1;
|
|
||||||
*ideal_cursor_column = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => return Err(anyhow!("Unknown form action: {}", action)),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(String::new())
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// src/tui/functions/login.rs
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
|
|
||||||
pub async fn handle_action(action: &str,) -> Result<String> {
|
|
||||||
match action {
|
|
||||||
"previous_entry" => {
|
|
||||||
Ok("Previous entry at tui/functions/login.rs not implemented".into())
|
|
||||||
}
|
|
||||||
"next_entry" => {
|
|
||||||
Ok("Next entry at tui/functions/login.rs not implemented".into())
|
|
||||||
}
|
|
||||||
_ => Err(anyhow!("Unknown login action: {}", action))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod rat_state;
|
|
||||||
pub mod context;
|
pub mod context;
|
||||||
|
|
||||||
pub use ui::run_ui;
|
pub use ui::run_ui;
|
||||||
pub use rat_state::*;
|
|
||||||
pub use context::*;
|
pub use context::*;
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
// client/src/ui/handlers/rat_state.rs
|
|
||||||
use crossterm::event::{KeyCode, KeyModifiers};
|
|
||||||
use crate::config::binds::config::Config;
|
|
||||||
use crate::state::app::state::UiState;
|
|
||||||
|
|
||||||
pub struct UiStateHandler;
|
|
||||||
|
|
||||||
impl UiStateHandler {
|
|
||||||
pub fn toggle_sidebar(
|
|
||||||
ui_state: &mut UiState,
|
|
||||||
config: &Config,
|
|
||||||
key: KeyCode,
|
|
||||||
modifiers: KeyModifiers,
|
|
||||||
) -> bool {
|
|
||||||
if let Some(action) = config.get_action_for_key_in_mode(&config.keybindings.common, key, modifiers) {
|
|
||||||
if action == "toggle_sidebar" {
|
|
||||||
ui_state.show_sidebar = !ui_state.show_sidebar;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_buffer_list(
|
|
||||||
ui_state: &mut UiState,
|
|
||||||
config: &Config,
|
|
||||||
key: KeyCode,
|
|
||||||
modifiers: KeyModifiers,
|
|
||||||
) -> bool {
|
|
||||||
if let Some(action) = config.get_common_action(key, modifiers) {
|
|
||||||
if action == "toggle_buffer_list" {
|
|
||||||
ui_state.show_buffer_list = !ui_state.show_buffer_list;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,42 +3,36 @@
|
|||||||
use crate::components::{
|
use crate::components::{
|
||||||
admin::add_logic::render_add_logic,
|
admin::add_logic::render_add_logic,
|
||||||
admin::render_add_table,
|
admin::render_add_table,
|
||||||
auth::{login::render_login, register::render_register},
|
|
||||||
common::dialog::render_dialog,
|
|
||||||
common::find_file_palette,
|
|
||||||
common::search_palette::render_search_palette,
|
|
||||||
handlers::sidebar::{self, calculate_sidebar_layout},
|
|
||||||
intro::intro::render_intro,
|
|
||||||
render_background,
|
render_background,
|
||||||
render_buffer_list,
|
|
||||||
render_command_line,
|
|
||||||
render_status_line,
|
|
||||||
};
|
};
|
||||||
|
use crate::pages::login::render_login;
|
||||||
|
use crate::pages::register::render_register;
|
||||||
|
use crate::pages::intro::render_intro;
|
||||||
|
use crate::bottom_panel::{
|
||||||
|
command_line::render_command_line,
|
||||||
|
status_line::render_status_line,
|
||||||
|
find_file_palette,
|
||||||
|
};
|
||||||
|
use crate::sidebar::{calculate_sidebar_layout, render_sidebar};
|
||||||
|
use crate::buffer::render_buffer_list;
|
||||||
|
use crate::search::render_search_palette;
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::modes::general::command_navigation::NavigationState;
|
use crate::modes::general::command_navigation::NavigationState;
|
||||||
use crate::state::app::buffer::BufferState;
|
use crate::buffer::state::BufferState;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::state::pages::admin::AdminState;
|
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::state::pages::auth::LoginState;
|
use crate::bottom_panel::layout::{bottom_panel_constraints, render_bottom_panel};
|
||||||
use crate::state::pages::auth::RegisterState;
|
|
||||||
use crate::state::pages::form::FormState;
|
|
||||||
use crate::state::pages::intro::IntroState;
|
|
||||||
use crate::components::render_form;
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
use crate::pages::routing::{Router, Page};
|
||||||
|
use crate::dialog::render_dialog;
|
||||||
|
use crate::pages::forms::render_form_page;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn render_ui(
|
pub fn render_ui(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
form_state: &mut FormState,
|
router: &mut Router,
|
||||||
auth_state: &mut AuthState,
|
|
||||||
login_state: &LoginState,
|
|
||||||
register_state: &RegisterState,
|
|
||||||
intro_state: &IntroState,
|
|
||||||
admin_state: &mut AdminState,
|
|
||||||
buffer_state: &BufferState,
|
buffer_state: &BufferState,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
is_event_handler_edit_mode: bool,
|
is_event_handler_edit_mode: bool,
|
||||||
@@ -52,38 +46,16 @@ pub fn render_ui(
|
|||||||
) {
|
) {
|
||||||
render_background(f, f.area(), theme);
|
render_background(f, f.area(), theme);
|
||||||
|
|
||||||
// --- START DYNAMIC LAYOUT LOGIC ---
|
// Layout: optional buffer list + main content + bottom panel
|
||||||
let mut status_line_height = 1;
|
|
||||||
#[cfg(feature = "ui-debug")]
|
|
||||||
{
|
|
||||||
if let Some(debug_state) = &app_state.debug_state {
|
|
||||||
if debug_state.is_error {
|
|
||||||
status_line_height = 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// --- END DYNAMIC LAYOUT LOGIC ---
|
|
||||||
|
|
||||||
const PALETTE_OPTIONS_HEIGHT_FOR_LAYOUT: u16 = 15;
|
|
||||||
|
|
||||||
let mut bottom_area_constraints: Vec<Constraint> = vec![Constraint::Length(status_line_height)];
|
|
||||||
let command_palette_area_height = if navigation_state.active {
|
|
||||||
1 + PALETTE_OPTIONS_HEIGHT_FOR_LAYOUT
|
|
||||||
} else if event_handler_command_mode_active {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
if command_palette_area_height > 0 {
|
|
||||||
bottom_area_constraints.push(Constraint::Length(command_palette_area_height));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut main_layout_constraints = vec![Constraint::Min(1)];
|
let mut main_layout_constraints = vec![Constraint::Min(1)];
|
||||||
if app_state.ui.show_buffer_list {
|
if app_state.ui.show_buffer_list {
|
||||||
main_layout_constraints.insert(0, Constraint::Length(1));
|
main_layout_constraints.insert(0, Constraint::Length(1));
|
||||||
}
|
}
|
||||||
main_layout_constraints.extend(bottom_area_constraints);
|
main_layout_constraints.extend(bottom_panel_constraints(
|
||||||
|
app_state,
|
||||||
|
navigation_state,
|
||||||
|
event_handler_command_mode_active,
|
||||||
|
));
|
||||||
|
|
||||||
let root_chunks = Layout::default()
|
let root_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
@@ -102,144 +74,98 @@ pub fn render_ui(
|
|||||||
let main_content_area = root_chunks[chunk_idx];
|
let main_content_area = root_chunks[chunk_idx];
|
||||||
chunk_idx += 1;
|
chunk_idx += 1;
|
||||||
|
|
||||||
let status_line_area = root_chunks[chunk_idx];
|
// Page rendering is now fully router-driven
|
||||||
chunk_idx += 1;
|
match &mut router.current {
|
||||||
|
Page::Intro(state) => render_intro(f, state, main_content_area, theme),
|
||||||
let command_render_area = if command_palette_area_height > 0 {
|
Page::Login(state) => render_login(
|
||||||
if root_chunks.len() > chunk_idx {
|
|
||||||
Some(root_chunks[chunk_idx])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if app_state.ui.show_intro {
|
|
||||||
render_intro(f, intro_state, main_content_area, theme);
|
|
||||||
} else if app_state.ui.show_register {
|
|
||||||
render_register(
|
|
||||||
f,
|
f,
|
||||||
main_content_area,
|
main_content_area,
|
||||||
theme,
|
theme,
|
||||||
register_state,
|
state,
|
||||||
app_state,
|
app_state,
|
||||||
register_state.current_field() < 4, // Now using CanvasState trait method
|
state.current_field() < 2,
|
||||||
);
|
),
|
||||||
} else if app_state.ui.show_add_table {
|
Page::Register(state) => render_register(
|
||||||
render_add_table(
|
|
||||||
f,
|
f,
|
||||||
main_content_area,
|
main_content_area,
|
||||||
theme,
|
theme,
|
||||||
|
state,
|
||||||
app_state,
|
app_state,
|
||||||
&mut admin_state.add_table_state,
|
state.current_field() < 4,
|
||||||
is_event_handler_edit_mode,
|
),
|
||||||
);
|
Page::Admin(state) => crate::components::admin::admin_panel::render_admin_panel(
|
||||||
} else if app_state.ui.show_add_logic {
|
|
||||||
render_add_logic(
|
|
||||||
f,
|
|
||||||
main_content_area,
|
|
||||||
theme,
|
|
||||||
app_state,
|
|
||||||
&mut admin_state.add_logic_state,
|
|
||||||
is_event_handler_edit_mode,
|
|
||||||
);
|
|
||||||
} else if app_state.ui.show_login {
|
|
||||||
render_login(
|
|
||||||
f,
|
|
||||||
main_content_area,
|
|
||||||
theme,
|
|
||||||
login_state,
|
|
||||||
app_state,
|
|
||||||
login_state.current_field() < 2, // Now using CanvasState trait method
|
|
||||||
);
|
|
||||||
} else if app_state.ui.show_admin {
|
|
||||||
crate::components::admin::admin_panel::render_admin_panel(
|
|
||||||
f,
|
f,
|
||||||
app_state,
|
app_state,
|
||||||
auth_state,
|
&mut AuthState::default(), // TODO: later move AuthState into Router
|
||||||
admin_state,
|
state,
|
||||||
main_content_area,
|
main_content_area,
|
||||||
theme,
|
theme,
|
||||||
&app_state.profile_tree,
|
&app_state.profile_tree,
|
||||||
&app_state.selected_profile,
|
&app_state.selected_profile,
|
||||||
);
|
),
|
||||||
} else if app_state.ui.show_form {
|
Page::AddLogic(state) => render_add_logic(
|
||||||
let (sidebar_area, form_actual_area) =
|
f,
|
||||||
calculate_sidebar_layout(app_state.ui.show_sidebar, main_content_area);
|
main_content_area,
|
||||||
if let Some(sidebar_rect) = sidebar_area {
|
theme,
|
||||||
sidebar::render_sidebar(
|
app_state,
|
||||||
|
state,
|
||||||
|
is_event_handler_edit_mode,
|
||||||
|
),
|
||||||
|
Page::AddTable(state) => render_add_table(
|
||||||
|
f,
|
||||||
|
main_content_area,
|
||||||
|
theme,
|
||||||
|
app_state,
|
||||||
|
state,
|
||||||
|
is_event_handler_edit_mode,
|
||||||
|
),
|
||||||
|
Page::Form(state) => {
|
||||||
|
let (sidebar_area, form_actual_area) =
|
||||||
|
calculate_sidebar_layout(app_state.ui.show_sidebar, main_content_area);
|
||||||
|
if let Some(sidebar_rect) = sidebar_area {
|
||||||
|
render_sidebar(
|
||||||
|
f,
|
||||||
|
sidebar_rect,
|
||||||
|
theme,
|
||||||
|
&app_state.profile_tree,
|
||||||
|
&app_state.selected_profile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let available_width = form_actual_area.width;
|
||||||
|
let form_render_area = if available_width >= 80 {
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Min(0), Constraint::Length(80), Constraint::Min(0)])
|
||||||
|
.split(form_actual_area)[1]
|
||||||
|
} else {
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(available_width),
|
||||||
|
Constraint::Min(0),
|
||||||
|
])
|
||||||
|
.split(form_actual_area)[1]
|
||||||
|
};
|
||||||
|
|
||||||
|
render_form_page(
|
||||||
f,
|
f,
|
||||||
sidebar_rect,
|
form_render_area,
|
||||||
|
app_state,
|
||||||
|
state,
|
||||||
|
app_state.current_view_table_name.as_deref().unwrap_or(""),
|
||||||
theme,
|
theme,
|
||||||
&app_state.profile_tree,
|
state.total_count,
|
||||||
&app_state.selected_profile,
|
state.current_position,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let available_width = form_actual_area.width;
|
|
||||||
let form_render_area = if available_width >= 80 {
|
|
||||||
Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([Constraint::Min(0), Constraint::Length(80), Constraint::Min(0)])
|
|
||||||
.split(form_actual_area)[1]
|
|
||||||
} else {
|
|
||||||
Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Min(0),
|
|
||||||
Constraint::Length(available_width),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(form_actual_area)[1]
|
|
||||||
};
|
|
||||||
|
|
||||||
render_form(
|
|
||||||
f,
|
|
||||||
form_render_area,
|
|
||||||
app_state,
|
|
||||||
form_state,
|
|
||||||
app_state.current_view_table_name.as_deref().unwrap_or(""),
|
|
||||||
theme,
|
|
||||||
form_state.total_count,
|
|
||||||
form_state.current_position,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global overlays (not tied to a page)
|
||||||
if let Some(area) = buffer_list_area {
|
if let Some(area) = buffer_list_area {
|
||||||
render_buffer_list(f, area, theme, buffer_state, app_state);
|
render_buffer_list(f, area, theme, buffer_state, app_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
render_status_line(
|
|
||||||
f,
|
|
||||||
status_line_area,
|
|
||||||
current_dir,
|
|
||||||
theme,
|
|
||||||
is_event_handler_edit_mode,
|
|
||||||
current_fps,
|
|
||||||
app_state,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(palette_or_command_area) = command_render_area {
|
|
||||||
if navigation_state.active {
|
|
||||||
find_file_palette::render_find_file_palette(
|
|
||||||
f,
|
|
||||||
palette_or_command_area,
|
|
||||||
theme,
|
|
||||||
navigation_state,
|
|
||||||
);
|
|
||||||
} else if event_handler_command_mode_active {
|
|
||||||
render_command_line(
|
|
||||||
f,
|
|
||||||
palette_or_command_area,
|
|
||||||
event_handler_command_input,
|
|
||||||
true,
|
|
||||||
theme,
|
|
||||||
event_handler_command_message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This block now correctly handles drawing popups over any view.
|
|
||||||
if app_state.ui.show_search_palette {
|
if app_state.ui.show_search_palette {
|
||||||
if let Some(search_state) = &app_state.search_state {
|
if let Some(search_state) = &app_state.search_state {
|
||||||
render_search_palette(f, f.area(), theme, search_state);
|
render_search_palette(f, f.area(), theme, search_state);
|
||||||
@@ -256,4 +182,19 @@ pub fn render_ui(
|
|||||||
app_state.ui.dialog.is_loading,
|
app_state.ui.dialog.is_loading,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_bottom_panel(
|
||||||
|
f,
|
||||||
|
&root_chunks,
|
||||||
|
&mut chunk_idx,
|
||||||
|
current_dir,
|
||||||
|
theme,
|
||||||
|
is_event_handler_edit_mode,
|
||||||
|
current_fps,
|
||||||
|
app_state,
|
||||||
|
navigation_state,
|
||||||
|
event_handler_command_input,
|
||||||
|
event_handler_command_mode_active,
|
||||||
|
event_handler_command_message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,23 +8,24 @@ use crate::config::storage::storage::load_auth_data;
|
|||||||
use crate::modes::common::commands::CommandHandler;
|
use crate::modes::common::commands::CommandHandler;
|
||||||
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
||||||
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
||||||
use crate::state::pages::form::{FormState, FieldDefinition};
|
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::state::pages::auth::LoginState;
|
use crate::pages::register::RegisterState;
|
||||||
use crate::state::pages::auth::RegisterState;
|
|
||||||
use crate::state::pages::admin::AdminState;
|
use crate::state::pages::admin::AdminState;
|
||||||
use crate::state::pages::admin::AdminFocus;
|
use crate::state::pages::admin::AdminFocus;
|
||||||
use crate::state::pages::intro::IntroState;
|
use crate::pages::intro::IntroState;
|
||||||
use crate::state::app::buffer::BufferState;
|
use crate::pages::forms::{FormState, FieldDefinition};
|
||||||
use crate::state::app::buffer::AppView;
|
use crate::pages::routing::{Router, Page};
|
||||||
|
use crate::buffer::state::BufferState;
|
||||||
|
use crate::buffer::state::AppView;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::tui::terminal::{EventReader, TerminalCore};
|
use crate::tui::terminal::{EventReader, TerminalCore};
|
||||||
use crate::ui::handlers::render::render_ui;
|
use crate::ui::handlers::render::render_ui;
|
||||||
use crate::tui::functions::common::login::LoginResult;
|
use crate::pages::login;
|
||||||
use crate::tui::functions::common::register::RegisterResult;
|
use crate::pages::register;
|
||||||
|
use crate::pages::login::LoginResult;
|
||||||
|
use crate::pages::login::LoginState;
|
||||||
|
use crate::pages::register::RegisterResult;
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
use crate::ui::handlers::context::DialogPurpose;
|
||||||
use crate::tui::functions::common::login;
|
|
||||||
use crate::tui::functions::common::register;
|
|
||||||
use crate::utils::columns::filter_user_columns;
|
use crate::utils::columns::filter_user_columns;
|
||||||
use canvas::keymap::KeyEventOutcome;
|
use canvas::keymap::KeyEventOutcome;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
@@ -32,7 +33,8 @@ use crossterm::cursor::SetCursorStyle;
|
|||||||
use crossterm::event as crossterm_event;
|
use crossterm::event as crossterm_event;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use std::time::{Instant, Duration};
|
use std::time::Instant;
|
||||||
|
use std::time::Duration;
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
use crate::state::app::state::DebugState;
|
use crate::state::app::state::DebugState;
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
@@ -67,6 +69,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
let mut register_state = RegisterState::default();
|
let mut register_state = RegisterState::default();
|
||||||
let mut intro_state = IntroState::default();
|
let mut intro_state = IntroState::default();
|
||||||
let mut admin_state = AdminState::default();
|
let mut admin_state = AdminState::default();
|
||||||
|
let mut router = Router::new();
|
||||||
let mut buffer_state = BufferState::default();
|
let mut buffer_state = BufferState::default();
|
||||||
let mut app_state = AppState::new().context("Failed to create initial app state")?;
|
let mut app_state = AppState::new().context("Failed to create initial app state")?;
|
||||||
|
|
||||||
@@ -196,7 +199,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
event_processed = true;
|
event_processed = true;
|
||||||
|
|
||||||
if let crossterm_event::Event::Key(key_event) = &event {
|
if let crossterm_event::Event::Key(key_event) = &event {
|
||||||
if app_state.ui.show_form {
|
if let Page::Form(_) = &router.current {
|
||||||
if let Some(editor) = app_state.form_editor.as_mut() {
|
if let Some(editor) = app_state.form_editor.as_mut() {
|
||||||
match editor.handle_key_event(*key_event) {
|
match editor.handle_key_event(*key_event) {
|
||||||
KeyEventOutcome::Consumed(Some(msg)) => {
|
KeyEventOutcome::Consumed(Some(msg)) => {
|
||||||
@@ -229,12 +232,9 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
&mut terminal,
|
&mut terminal,
|
||||||
&mut command_handler,
|
&mut command_handler,
|
||||||
&mut auth_state,
|
&mut auth_state,
|
||||||
&mut login_state,
|
|
||||||
&mut register_state,
|
|
||||||
&mut intro_state,
|
|
||||||
&mut admin_state,
|
|
||||||
&mut buffer_state,
|
&mut buffer_state,
|
||||||
&mut app_state,
|
&mut app_state,
|
||||||
|
&mut router,
|
||||||
).await;
|
).await;
|
||||||
let mut should_exit = false;
|
let mut should_exit = false;
|
||||||
match event_outcome_result {
|
match event_outcome_result {
|
||||||
@@ -340,17 +340,10 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(active_view) = buffer_state.get_active_view() {
|
if let Some(active_view) = buffer_state.get_active_view() {
|
||||||
app_state.ui.show_intro = false;
|
|
||||||
app_state.ui.show_login = false;
|
|
||||||
app_state.ui.show_register = false;
|
|
||||||
app_state.ui.show_admin = false;
|
|
||||||
app_state.ui.show_add_table = false;
|
|
||||||
app_state.ui.show_add_logic = false;
|
|
||||||
app_state.ui.show_form = false;
|
|
||||||
match active_view {
|
match active_view {
|
||||||
AppView::Intro => app_state.ui.show_intro = true,
|
AppView::Intro => router.navigate(Page::Intro(intro_state.clone())),
|
||||||
AppView::Login => app_state.ui.show_login = true,
|
AppView::Login => router.navigate(Page::Login(login_state.clone())),
|
||||||
AppView::Register => app_state.ui.show_register = true,
|
AppView::Register => router.navigate(Page::Register(register_state.clone())),
|
||||||
AppView::Admin => {
|
AppView::Admin => {
|
||||||
info!("Active view is Admin, refreshing profile tree...");
|
info!("Active view is Admin, refreshing profile tree...");
|
||||||
match grpc_client.get_profile_tree().await {
|
match grpc_client.get_profile_tree().await {
|
||||||
@@ -359,37 +352,43 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to refresh profile tree for Admin panel: {}", e);
|
error!("Failed to refresh profile tree for Admin panel: {}", e);
|
||||||
event_handler.command_message = format!("Error refreshing admin data: {}", e);
|
event_handler.command_message =
|
||||||
|
format!("Error refreshing admin data: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app_state.ui.show_admin = true;
|
|
||||||
let profile_names = app_state.profile_tree.profiles.iter()
|
let profile_names = app_state.profile_tree.profiles.iter()
|
||||||
.map(|p| p.name.clone())
|
.map(|p| p.name.clone())
|
||||||
.collect();
|
.collect();
|
||||||
admin_state.set_profiles(profile_names);
|
admin_state.set_profiles(profile_names);
|
||||||
|
|
||||||
if admin_state.current_focus == AdminFocus::default() ||
|
if admin_state.current_focus == AdminFocus::default()
|
||||||
!matches!(admin_state.current_focus,
|
|| !matches!(admin_state.current_focus,
|
||||||
AdminFocus::InsideProfilesList |
|
AdminFocus::InsideProfilesList |
|
||||||
AdminFocus::Tables | AdminFocus::InsideTablesList |
|
AdminFocus::Tables | AdminFocus::InsideTablesList |
|
||||||
AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3) {
|
AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3)
|
||||||
|
{
|
||||||
admin_state.current_focus = AdminFocus::ProfilesPane;
|
admin_state.current_focus = AdminFocus::ProfilesPane;
|
||||||
}
|
}
|
||||||
if admin_state.profile_list_state.selected().is_none() && !app_state.profile_tree.profiles.is_empty() {
|
if admin_state.profile_list_state.selected().is_none()
|
||||||
|
&& !app_state.profile_tree.profiles.is_empty()
|
||||||
|
{
|
||||||
admin_state.profile_list_state.select(Some(0));
|
admin_state.profile_list_state.select(Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.navigate(Page::Admin(admin_state.clone()));
|
||||||
|
}
|
||||||
|
AppView::AddTable => router.navigate(Page::AddTable(admin_state.add_table_state.clone())),
|
||||||
|
AppView::AddLogic => router.navigate(Page::AddLogic(admin_state.add_logic_state.clone())),
|
||||||
|
AppView::Form => {
|
||||||
|
if let Some(form_state) = app_state.form_state().cloned() {
|
||||||
|
router.navigate(Page::Form(form_state));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AppView::AddTable => app_state.ui.show_add_table = true,
|
|
||||||
AppView::AddLogic => app_state.ui.show_add_logic = true,
|
|
||||||
AppView::Form => app_state.ui.show_form = true,
|
|
||||||
AppView::Scratch => {}
|
AppView::Scratch => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with the rest of the function...
|
if let Page::Form(_) = &router.current {
|
||||||
// (The rest remains the same, but now CanvasState trait methods are available)
|
|
||||||
|
|
||||||
if app_state.ui.show_form {
|
|
||||||
let current_view_profile = app_state.current_view_profile_name.clone();
|
let current_view_profile = app_state.current_view_profile_name.clone();
|
||||||
let current_view_table = app_state.current_view_table_name.clone();
|
let current_view_table = app_state.current_view_table_name.clone();
|
||||||
|
|
||||||
@@ -475,18 +474,21 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
// Now we can use CanvasState methods like get_current_input(), current_field(), etc.
|
// Now we can use CanvasState methods like get_current_input(), current_field(), etc.
|
||||||
|
|
||||||
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
||||||
if app_state.ui.show_add_logic {
|
if let Page::AddLogic(state) = &mut router.current {
|
||||||
if admin_state.add_logic_state.profile_name == profile_name &&
|
if state.profile_name == profile_name
|
||||||
admin_state.add_logic_state.selected_table_name.as_deref() == Some(table_name.as_str()) {
|
&& state.selected_table_name.as_deref() == Some(table_name.as_str())
|
||||||
|
{
|
||||||
info!("Fetching table structure for {}.{}", profile_name, table_name);
|
info!("Fetching table structure for {}.{}", profile_name, table_name);
|
||||||
let fetch_message = UiService::initialize_add_logic_table_data(
|
let fetch_message = UiService::initialize_add_logic_table_data(
|
||||||
&mut grpc_client,
|
&mut grpc_client,
|
||||||
&mut admin_state.add_logic_state,
|
state,
|
||||||
&app_state.profile_tree,
|
&app_state.profile_tree,
|
||||||
).await.unwrap_or_else(|e| {
|
)
|
||||||
error!("Error initializing add_logic_table_data: {}", e);
|
.await
|
||||||
format!("Error fetching table structure: {}", e)
|
.unwrap_or_else(|e| {
|
||||||
});
|
error!("Error initializing add_logic_table_data: {}", e);
|
||||||
|
format!("Error fetching table structure: {}", e)
|
||||||
|
});
|
||||||
|
|
||||||
if !fetch_message.contains("Error") && !fetch_message.contains("Warning") {
|
if !fetch_message.contains("Error") && !fetch_message.contains("Warning") {
|
||||||
info!("{}", fetch_message);
|
info!("{}", fetch_message);
|
||||||
@@ -496,10 +498,11 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
} else {
|
} else {
|
||||||
error!(
|
error!(
|
||||||
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, but add_logic_state is for {}.{:?}",
|
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, but AddLogic state is for {}.{:?}",
|
||||||
profile_name, table_name,
|
profile_name,
|
||||||
admin_state.add_logic_state.profile_name,
|
table_name,
|
||||||
admin_state.add_logic_state.selected_table_name
|
state.profile_name,
|
||||||
|
state.selected_table_name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -510,21 +513,21 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() {
|
if let Page::AddLogic(state) = &mut router.current {
|
||||||
if app_state.ui.show_add_logic {
|
if let Some(table_name) = state.script_editor_awaiting_column_autocomplete.clone() {
|
||||||
let profile_name = admin_state.add_logic_state.profile_name.clone();
|
let profile_name = state.profile_name.clone();
|
||||||
|
|
||||||
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
|
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
|
||||||
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
|
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
|
||||||
Ok(columns) => {
|
Ok(columns) => {
|
||||||
admin_state.add_logic_state.set_columns_for_table_autocomplete(columns.clone());
|
state.set_columns_for_table_autocomplete(columns.clone());
|
||||||
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
|
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
|
||||||
event_handler.command_message = format!("Columns for '{}' loaded. Select a column.", table_name);
|
event_handler.command_message = format!("Columns for '{}' loaded. Select a column.", table_name);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
|
error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
|
||||||
admin_state.add_logic_state.script_editor_awaiting_column_autocomplete = None;
|
state.script_editor_awaiting_column_autocomplete = None;
|
||||||
admin_state.add_logic_state.deactivate_script_editor_autocomplete();
|
state.deactivate_script_editor_autocomplete();
|
||||||
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
|
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,59 +541,75 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
let position_changed = current_position != position_before_event;
|
let position_changed = current_position != position_before_event;
|
||||||
let mut position_logic_needs_redraw = false;
|
let mut position_logic_needs_redraw = false;
|
||||||
|
|
||||||
if app_state.ui.show_form && !table_just_switched {
|
if let Page::Form(form_state) = &mut router.current {
|
||||||
if position_changed && !event_handler.is_edit_mode {
|
if !table_just_switched {
|
||||||
position_logic_needs_redraw = true;
|
if position_changed && !event_handler.is_edit_mode {
|
||||||
|
position_logic_needs_redraw = true;
|
||||||
|
|
||||||
if let Some(form_state) = app_state.form_state_mut() {
|
if let Some(form_state) = app_state.form_state_mut() {
|
||||||
if form_state.current_position > form_state.total_count {
|
if form_state.current_position > form_state.total_count {
|
||||||
form_state.reset_to_empty();
|
form_state.reset_to_empty();
|
||||||
event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name);
|
event_handler.command_message = format!(
|
||||||
} else {
|
"New entry for {}.{}",
|
||||||
match UiService::load_table_data_by_position(&mut grpc_client, form_state).await {
|
form_state.profile_name,
|
||||||
Ok(load_message) => {
|
form_state.table_name
|
||||||
if event_handler.command_message.is_empty() || !load_message.starts_with("Error") {
|
);
|
||||||
event_handler.command_message = load_message;
|
} else {
|
||||||
|
match UiService::load_table_data_by_position(&mut grpc_client, form_state).await {
|
||||||
|
Ok(load_message) => {
|
||||||
|
if event_handler.command_message.is_empty()
|
||||||
|
|| !load_message.starts_with("Error")
|
||||||
|
{
|
||||||
|
event_handler.command_message = load_message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
event_handler.command_message =
|
||||||
|
format!("Error loading data: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
event_handler.command_message = format!("Error loading data: {}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let current_input_after_load_str = form_state.get_current_input();
|
let current_input_after_load_str = form_state.get_current_input();
|
||||||
let current_input_len_after_load = current_input_after_load_str.chars().count();
|
let current_input_len_after_load =
|
||||||
let max_cursor_pos = if current_input_len_after_load > 0 {
|
current_input_after_load_str.chars().count();
|
||||||
current_input_len_after_load.saturating_sub(1)
|
let max_cursor_pos = if current_input_len_after_load > 0 {
|
||||||
} else {
|
current_input_len_after_load.saturating_sub(1)
|
||||||
0
|
} else {
|
||||||
};
|
0
|
||||||
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
};
|
||||||
}
|
form_state.current_cursor_pos =
|
||||||
} else if !position_changed && !event_handler.is_edit_mode {
|
event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||||||
if let Some(form_state) = app_state.form_state_mut() {
|
}
|
||||||
let current_input_str = form_state.get_current_input();
|
} else if !position_changed && !event_handler.is_edit_mode {
|
||||||
let current_input_len = current_input_str.chars().count();
|
if let Some(form_state) = app_state.form_state_mut() {
|
||||||
let max_cursor_pos = if current_input_len > 0 {
|
let current_input_str = form_state.get_current_input();
|
||||||
current_input_len.saturating_sub(1)
|
let current_input_len = current_input_str.chars().count();
|
||||||
} else {
|
let max_cursor_pos = if current_input_len > 0 {
|
||||||
0
|
current_input_len.saturating_sub(1)
|
||||||
};
|
} else {
|
||||||
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
0
|
||||||
|
};
|
||||||
|
form_state.current_cursor_pos =
|
||||||
|
event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if app_state.ui.show_register {
|
} else if let Page::Register(state) = &mut router.current {
|
||||||
if !event_handler.is_edit_mode {
|
if !event_handler.is_edit_mode {
|
||||||
let current_input = register_state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
let max_cursor_pos =
|
||||||
register_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
||||||
|
state.current_cursor_pos =
|
||||||
|
event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||||||
}
|
}
|
||||||
} else if app_state.ui.show_login {
|
} else if let Page::Login(state) = &mut router.current {
|
||||||
if !event_handler.is_edit_mode {
|
if !event_handler.is_edit_mode {
|
||||||
let current_input = login_state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
let max_cursor_pos =
|
||||||
login_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
|
||||||
|
state.current_cursor_pos =
|
||||||
|
event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,7 +640,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if event_processed || needs_redraw || position_changed {
|
if event_processed || needs_redraw || position_changed {
|
||||||
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
|
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router);
|
||||||
match current_mode {
|
match current_mode {
|
||||||
AppMode::Edit => { terminal.show_cursor()?; }
|
AppMode::Edit => { terminal.show_cursor()?; }
|
||||||
AppMode::Highlight => { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.show_cursor()?; }
|
AppMode::Highlight => { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.show_cursor()?; }
|
||||||
@@ -653,12 +672,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
let mut temp_form_state = form_state_clone.clone();
|
let mut temp_form_state = form_state_clone.clone();
|
||||||
render_ui(
|
render_ui(
|
||||||
f,
|
f,
|
||||||
&mut temp_form_state,
|
&mut router,
|
||||||
&mut auth_state,
|
|
||||||
&login_state,
|
|
||||||
®ister_state,
|
|
||||||
&intro_state,
|
|
||||||
&mut admin_state,
|
|
||||||
&buffer_state,
|
&buffer_state,
|
||||||
&theme,
|
&theme,
|
||||||
event_handler.is_edit_mode,
|
event_handler.is_edit_mode,
|
||||||
|
|||||||
1
server/.gitignore
vendored
Normal file
1
server/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
docs-prod/
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[book]
|
|
||||||
authors = ["Priec"]
|
|
||||||
language = "en"
|
|
||||||
src = "src"
|
|
||||||
title = "Server API Documentation"
|
|
||||||
|
|
||||||
[output.html.search]
|
|
||||||
enable = true
|
|
||||||
limit-results = 30
|
|
||||||
teaser-word-count = 30
|
|
||||||
use-boolean-and = true
|
|
||||||
boost-title = 2
|
|
||||||
boost-hierarchy = 1
|
|
||||||
boost-paragraph = 1
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
This file makes sure that Github Pages doesn't process mdBook's output.
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
<!DOCTYPE HTML>
|
|
||||||
<html lang="en" class="light sidebar-visible" dir="ltr">
|
|
||||||
<head>
|
|
||||||
<!-- Book generated using mdBook -->
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Page not found - Server API Documentation</title>
|
|
||||||
<base href="/">
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Custom HTML head -->
|
|
||||||
|
|
||||||
<meta name="description" content="">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="theme-color" content="#ffffff">
|
|
||||||
|
|
||||||
<link rel="icon" href="favicon.svg">
|
|
||||||
<link rel="shortcut icon" href="favicon.png">
|
|
||||||
<link rel="stylesheet" href="css/variables.css">
|
|
||||||
<link rel="stylesheet" href="css/general.css">
|
|
||||||
<link rel="stylesheet" href="css/chrome.css">
|
|
||||||
<link rel="stylesheet" href="css/print.css" media="print">
|
|
||||||
|
|
||||||
<!-- Fonts -->
|
|
||||||
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
|
||||||
<link rel="stylesheet" href="fonts/fonts.css">
|
|
||||||
|
|
||||||
<!-- Highlight.js Stylesheets -->
|
|
||||||
<link rel="stylesheet" id="highlight-css" href="highlight.css">
|
|
||||||
<link rel="stylesheet" id="tomorrow-night-css" href="tomorrow-night.css">
|
|
||||||
<link rel="stylesheet" id="ayu-highlight-css" href="ayu-highlight.css">
|
|
||||||
|
|
||||||
<!-- Custom theme stylesheets -->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Provide site root and default themes to javascript -->
|
|
||||||
<script>
|
|
||||||
const path_to_root = "";
|
|
||||||
const default_light_theme = "light";
|
|
||||||
const default_dark_theme = "navy";
|
|
||||||
</script>
|
|
||||||
<!-- Start loading toc.js asap -->
|
|
||||||
<script src="toc.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="body-container">
|
|
||||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
|
||||||
<script>
|
|
||||||
try {
|
|
||||||
let theme = localStorage.getItem('mdbook-theme');
|
|
||||||
let sidebar = localStorage.getItem('mdbook-sidebar');
|
|
||||||
|
|
||||||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
|
||||||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
|
||||||
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
|
||||||
}
|
|
||||||
} catch (e) { }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
|
||||||
<script>
|
|
||||||
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
|
|
||||||
let theme;
|
|
||||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
|
||||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
|
||||||
const html = document.documentElement;
|
|
||||||
html.classList.remove('light')
|
|
||||||
html.classList.add(theme);
|
|
||||||
html.classList.add("js");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
|
||||||
|
|
||||||
<!-- Hide / unhide sidebar before it is displayed -->
|
|
||||||
<script>
|
|
||||||
let sidebar = null;
|
|
||||||
const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
|
||||||
if (document.body.clientWidth >= 1080) {
|
|
||||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
|
||||||
sidebar = sidebar || 'visible';
|
|
||||||
} else {
|
|
||||||
sidebar = 'hidden';
|
|
||||||
}
|
|
||||||
sidebar_toggle.checked = sidebar === 'visible';
|
|
||||||
html.classList.remove('sidebar-visible');
|
|
||||||
html.classList.add("sidebar-" + sidebar);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
|
||||||
<!-- populated by js -->
|
|
||||||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
|
||||||
<noscript>
|
|
||||||
<iframe class="sidebar-iframe-outer" src="toc.html"></iframe>
|
|
||||||
</noscript>
|
|
||||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
|
||||||
<div class="sidebar-resize-indicator"></div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id="page-wrapper" class="page-wrapper">
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<div id="menu-bar-hover-placeholder"></div>
|
|
||||||
<div id="menu-bar" class="menu-bar sticky">
|
|
||||||
<div class="left-buttons">
|
|
||||||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
|
||||||
<i class="fa fa-bars"></i>
|
|
||||||
</label>
|
|
||||||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
|
||||||
<i class="fa fa-paint-brush"></i>
|
|
||||||
</button>
|
|
||||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</button></li>
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
|
||||||
</ul>
|
|
||||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
|
||||||
<i class="fa fa-search"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 class="menu-title">Server API Documentation</h1>
|
|
||||||
|
|
||||||
<div class="right-buttons">
|
|
||||||
<a href="print.html" title="Print this book" aria-label="Print this book">
|
|
||||||
<i id="print-button" class="fa fa-print"></i>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="search-wrapper" class="hidden">
|
|
||||||
<form id="searchbar-outer" class="searchbar-outer">
|
|
||||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
|
||||||
</form>
|
|
||||||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
|
||||||
<div id="searchresults-header" class="searchresults-header"></div>
|
|
||||||
<ul id="searchresults">
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
|
||||||
<script>
|
|
||||||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
|
||||||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
|
||||||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
|
||||||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="content" class="content">
|
|
||||||
<main>
|
|
||||||
<h1 id="document-not-found-404"><a class="header" href="#document-not-found-404">Document not found (404)</a></h1>
|
|
||||||
<p>This URL is invalid, sorry. Please use the navigation bar or search to continue.</p>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
|
||||||
<!-- Mobile navigation buttons -->
|
|
||||||
|
|
||||||
|
|
||||||
<div style="clear: both"></div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
|
||||||
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Livereload script (if served using the cli tool) -->
|
|
||||||
<script>
|
|
||||||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
||||||
const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
|
|
||||||
const socket = new WebSocket(wsAddress);
|
|
||||||
socket.onmessage = function (event) {
|
|
||||||
if (event.data === "reload") {
|
|
||||||
socket.close();
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onbeforeunload = function() {
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.playground_copyable = true;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<script src="elasticlunr.min.js"></script>
|
|
||||||
<script src="mark.min.js"></script>
|
|
||||||
<script src="searcher.js"></script>
|
|
||||||
|
|
||||||
<script src="clipboard.min.js"></script>
|
|
||||||
<script src="highlight.js"></script>
|
|
||||||
<script src="book.js"></script>
|
|
||||||
|
|
||||||
<!-- Custom JS scripts -->
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
Based off of the Ayu theme
|
|
||||||
Original by Dempfi (https://github.com/dempfi/ayu)
|
|
||||||
*/
|
|
||||||
|
|
||||||
.hljs {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
background: #191f26;
|
|
||||||
color: #e6e1cf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-comment,
|
|
||||||
.hljs-quote {
|
|
||||||
color: #5c6773;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-variable,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hljs-attribute,
|
|
||||||
.hljs-attr,
|
|
||||||
.hljs-regexp,
|
|
||||||
.hljs-link,
|
|
||||||
.hljs-selector-id,
|
|
||||||
.hljs-selector-class {
|
|
||||||
color: #ff7733;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-number,
|
|
||||||
.hljs-meta,
|
|
||||||
.hljs-builtin-name,
|
|
||||||
.hljs-literal,
|
|
||||||
.hljs-type,
|
|
||||||
.hljs-params {
|
|
||||||
color: #ffee99;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-bullet {
|
|
||||||
color: #b8cc52;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-title,
|
|
||||||
.hljs-built_in,
|
|
||||||
.hljs-section {
|
|
||||||
color: #ffb454;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-selector-tag,
|
|
||||||
.hljs-symbol {
|
|
||||||
color: #ff7733;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-name {
|
|
||||||
color: #36a3d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-tag {
|
|
||||||
color: #00568d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-emphasis {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-addition {
|
|
||||||
color: #91b362;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-deletion {
|
|
||||||
color: #d96c75;
|
|
||||||
}
|
|
||||||
@@ -1,769 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/* global default_theme, default_dark_theme, default_light_theme, hljs, ClipboardJS */
|
|
||||||
|
|
||||||
// Fix back button cache problem
|
|
||||||
window.onunload = function() { };
|
|
||||||
|
|
||||||
// Global variable, shared between modules
|
|
||||||
function playground_text(playground, hidden = true) {
|
|
||||||
const code_block = playground.querySelector('code');
|
|
||||||
|
|
||||||
if (window.ace && code_block.classList.contains('editable')) {
|
|
||||||
const editor = window.ace.edit(code_block);
|
|
||||||
return editor.getValue();
|
|
||||||
} else if (hidden) {
|
|
||||||
return code_block.textContent;
|
|
||||||
} else {
|
|
||||||
return code_block.innerText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(function codeSnippets() {
|
|
||||||
function fetch_with_timeout(url, options, timeout = 6000) {
|
|
||||||
return Promise.race([
|
|
||||||
fetch(url, options),
|
|
||||||
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const playgrounds = Array.from(document.querySelectorAll('.playground'));
|
|
||||||
if (playgrounds.length > 0) {
|
|
||||||
fetch_with_timeout('https://play.rust-lang.org/meta/crates', {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
mode: 'cors',
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(response => {
|
|
||||||
// get list of crates available in the rust playground
|
|
||||||
const playground_crates = response.crates.map(item => item['id']);
|
|
||||||
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_crate_list_update(playground_block, playground_crates) {
|
|
||||||
// update the play buttons after receiving the response
|
|
||||||
update_play_button(playground_block, playground_crates);
|
|
||||||
|
|
||||||
// and install on change listener to dynamically update ACE editors
|
|
||||||
if (window.ace) {
|
|
||||||
const code_block = playground_block.querySelector('code');
|
|
||||||
if (code_block.classList.contains('editable')) {
|
|
||||||
const editor = window.ace.edit(code_block);
|
|
||||||
editor.addEventListener('change', () => {
|
|
||||||
update_play_button(playground_block, playground_crates);
|
|
||||||
});
|
|
||||||
// add Ctrl-Enter command to execute rust code
|
|
||||||
editor.commands.addCommand({
|
|
||||||
name: 'run',
|
|
||||||
bindKey: {
|
|
||||||
win: 'Ctrl-Enter',
|
|
||||||
mac: 'Ctrl-Enter',
|
|
||||||
},
|
|
||||||
exec: _editor => run_rust_code(playground_block),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates the visibility of play button based on `no_run` class and
|
|
||||||
// used crates vs ones available on https://play.rust-lang.org
|
|
||||||
function update_play_button(pre_block, playground_crates) {
|
|
||||||
const play_button = pre_block.querySelector('.play-button');
|
|
||||||
|
|
||||||
// skip if code is `no_run`
|
|
||||||
if (pre_block.querySelector('code').classList.contains('no_run')) {
|
|
||||||
play_button.classList.add('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get list of `extern crate`'s from snippet
|
|
||||||
const txt = playground_text(pre_block);
|
|
||||||
const re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
|
||||||
const snippet_crates = [];
|
|
||||||
let item;
|
|
||||||
// eslint-disable-next-line no-cond-assign
|
|
||||||
while (item = re.exec(txt)) {
|
|
||||||
snippet_crates.push(item[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if all used crates are available on play.rust-lang.org
|
|
||||||
const all_available = snippet_crates.every(function(elem) {
|
|
||||||
return playground_crates.indexOf(elem) > -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (all_available) {
|
|
||||||
play_button.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
play_button.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function run_rust_code(code_block) {
|
|
||||||
let result_block = code_block.querySelector('.result');
|
|
||||||
if (!result_block) {
|
|
||||||
result_block = document.createElement('code');
|
|
||||||
result_block.className = 'result hljs language-bash';
|
|
||||||
|
|
||||||
code_block.append(result_block);
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = playground_text(code_block);
|
|
||||||
const classes = code_block.querySelector('code').classList;
|
|
||||||
let edition = '2015';
|
|
||||||
classes.forEach(className => {
|
|
||||||
if (className.startsWith('edition')) {
|
|
||||||
edition = className.slice(7);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const params = {
|
|
||||||
version: 'stable',
|
|
||||||
optimize: '0',
|
|
||||||
code: text,
|
|
||||||
edition: edition,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (text.indexOf('#![feature') !== -1) {
|
|
||||||
params.version = 'nightly';
|
|
||||||
}
|
|
||||||
|
|
||||||
result_block.innerText = 'Running...';
|
|
||||||
|
|
||||||
fetch_with_timeout('https://play.rust-lang.org/evaluate.json', {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
mode: 'cors',
|
|
||||||
body: JSON.stringify(params),
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(response => {
|
|
||||||
if (response.result.trim() === '') {
|
|
||||||
result_block.innerText = 'No output';
|
|
||||||
result_block.classList.add('result-no-output');
|
|
||||||
} else {
|
|
||||||
result_block.innerText = response.result;
|
|
||||||
result_block.classList.remove('result-no-output');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => result_block.innerText = 'Playground Communication: ' + error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Syntax highlighting Configuration
|
|
||||||
hljs.configure({
|
|
||||||
tabReplace: ' ', // 4 spaces
|
|
||||||
languages: [], // Languages used for auto-detection
|
|
||||||
});
|
|
||||||
|
|
||||||
const code_nodes = Array
|
|
||||||
.from(document.querySelectorAll('code'))
|
|
||||||
// Don't highlight `inline code` blocks in headers.
|
|
||||||
.filter(function(node) {
|
|
||||||
return !node.parentElement.classList.contains('header');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.ace) {
|
|
||||||
// language-rust class needs to be removed for editable
|
|
||||||
// blocks or highlightjs will capture events
|
|
||||||
code_nodes
|
|
||||||
.filter(function(node) {
|
|
||||||
return node.classList.contains('editable');
|
|
||||||
})
|
|
||||||
.forEach(function(block) {
|
|
||||||
block.classList.remove('language-rust');
|
|
||||||
});
|
|
||||||
|
|
||||||
code_nodes
|
|
||||||
.filter(function(node) {
|
|
||||||
return !node.classList.contains('editable');
|
|
||||||
})
|
|
||||||
.forEach(function(block) {
|
|
||||||
hljs.highlightBlock(block);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
code_nodes.forEach(function(block) {
|
|
||||||
hljs.highlightBlock(block);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adding the hljs class gives code blocks the color css
|
|
||||||
// even if highlighting doesn't apply
|
|
||||||
code_nodes.forEach(function(block) {
|
|
||||||
block.classList.add('hljs');
|
|
||||||
});
|
|
||||||
|
|
||||||
Array.from(document.querySelectorAll('code.hljs')).forEach(function(block) {
|
|
||||||
|
|
||||||
const lines = Array.from(block.querySelectorAll('.boring'));
|
|
||||||
// If no lines were hidden, return
|
|
||||||
if (!lines.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
block.classList.add('hide-boring');
|
|
||||||
|
|
||||||
const buttons = document.createElement('div');
|
|
||||||
buttons.className = 'buttons';
|
|
||||||
buttons.innerHTML = '<button class="fa fa-eye" title="Show hidden lines" \
|
|
||||||
aria-label="Show hidden lines"></button>';
|
|
||||||
|
|
||||||
// add expand button
|
|
||||||
const pre_block = block.parentNode;
|
|
||||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
|
||||||
|
|
||||||
pre_block.querySelector('.buttons').addEventListener('click', function(e) {
|
|
||||||
if (e.target.classList.contains('fa-eye')) {
|
|
||||||
e.target.classList.remove('fa-eye');
|
|
||||||
e.target.classList.add('fa-eye-slash');
|
|
||||||
e.target.title = 'Hide lines';
|
|
||||||
e.target.setAttribute('aria-label', e.target.title);
|
|
||||||
|
|
||||||
block.classList.remove('hide-boring');
|
|
||||||
} else if (e.target.classList.contains('fa-eye-slash')) {
|
|
||||||
e.target.classList.remove('fa-eye-slash');
|
|
||||||
e.target.classList.add('fa-eye');
|
|
||||||
e.target.title = 'Show hidden lines';
|
|
||||||
e.target.setAttribute('aria-label', e.target.title);
|
|
||||||
|
|
||||||
block.classList.add('hide-boring');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.playground_copyable) {
|
|
||||||
Array.from(document.querySelectorAll('pre code')).forEach(function(block) {
|
|
||||||
const pre_block = block.parentNode;
|
|
||||||
if (!pre_block.classList.contains('playground')) {
|
|
||||||
let buttons = pre_block.querySelector('.buttons');
|
|
||||||
if (!buttons) {
|
|
||||||
buttons = document.createElement('div');
|
|
||||||
buttons.className = 'buttons';
|
|
||||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
const clipButton = document.createElement('button');
|
|
||||||
clipButton.className = 'clip-button';
|
|
||||||
clipButton.title = 'Copy to clipboard';
|
|
||||||
clipButton.setAttribute('aria-label', clipButton.title);
|
|
||||||
clipButton.innerHTML = '<i class="tooltiptext"></i>';
|
|
||||||
|
|
||||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process playground code blocks
|
|
||||||
Array.from(document.querySelectorAll('.playground')).forEach(function(pre_block) {
|
|
||||||
// Add play button
|
|
||||||
let buttons = pre_block.querySelector('.buttons');
|
|
||||||
if (!buttons) {
|
|
||||||
buttons = document.createElement('div');
|
|
||||||
buttons.className = 'buttons';
|
|
||||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
const runCodeButton = document.createElement('button');
|
|
||||||
runCodeButton.className = 'fa fa-play play-button';
|
|
||||||
runCodeButton.hidden = true;
|
|
||||||
runCodeButton.title = 'Run this code';
|
|
||||||
runCodeButton.setAttribute('aria-label', runCodeButton.title);
|
|
||||||
|
|
||||||
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
|
||||||
runCodeButton.addEventListener('click', () => {
|
|
||||||
run_rust_code(pre_block);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.playground_copyable) {
|
|
||||||
const copyCodeClipboardButton = document.createElement('button');
|
|
||||||
copyCodeClipboardButton.className = 'clip-button';
|
|
||||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
|
||||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
|
||||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
|
||||||
|
|
||||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
const code_block = pre_block.querySelector('code');
|
|
||||||
if (window.ace && code_block.classList.contains('editable')) {
|
|
||||||
const undoChangesButton = document.createElement('button');
|
|
||||||
undoChangesButton.className = 'fa fa-history reset-button';
|
|
||||||
undoChangesButton.title = 'Undo changes';
|
|
||||||
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
|
|
||||||
|
|
||||||
buttons.insertBefore(undoChangesButton, buttons.firstChild);
|
|
||||||
|
|
||||||
undoChangesButton.addEventListener('click', function() {
|
|
||||||
const editor = window.ace.edit(code_block);
|
|
||||||
editor.setValue(editor.originalCode);
|
|
||||||
editor.clearSelection();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function themes() {
|
|
||||||
const html = document.querySelector('html');
|
|
||||||
const themeToggleButton = document.getElementById('theme-toggle');
|
|
||||||
const themePopup = document.getElementById('theme-list');
|
|
||||||
const themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
|
|
||||||
const themeIds = [];
|
|
||||||
themePopup.querySelectorAll('button.theme').forEach(function(el) {
|
|
||||||
themeIds.push(el.id);
|
|
||||||
});
|
|
||||||
const stylesheets = {
|
|
||||||
ayuHighlight: document.querySelector('#ayu-highlight-css'),
|
|
||||||
tomorrowNight: document.querySelector('#tomorrow-night-css'),
|
|
||||||
highlight: document.querySelector('#highlight-css'),
|
|
||||||
};
|
|
||||||
|
|
||||||
function showThemes() {
|
|
||||||
themePopup.style.display = 'block';
|
|
||||||
themeToggleButton.setAttribute('aria-expanded', true);
|
|
||||||
themePopup.querySelector('button#' + get_theme()).focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateThemeSelected() {
|
|
||||||
themePopup.querySelectorAll('.theme-selected').forEach(function(el) {
|
|
||||||
el.classList.remove('theme-selected');
|
|
||||||
});
|
|
||||||
const selected = get_saved_theme() ?? 'default_theme';
|
|
||||||
let element = themePopup.querySelector('button#' + selected);
|
|
||||||
if (element === null) {
|
|
||||||
// Fall back in case there is no "Default" item.
|
|
||||||
element = themePopup.querySelector('button#' + get_theme());
|
|
||||||
}
|
|
||||||
element.classList.add('theme-selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideThemes() {
|
|
||||||
themePopup.style.display = 'none';
|
|
||||||
themeToggleButton.setAttribute('aria-expanded', false);
|
|
||||||
themeToggleButton.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_saved_theme() {
|
|
||||||
let theme = null;
|
|
||||||
try {
|
|
||||||
theme = localStorage.getItem('mdbook-theme');
|
|
||||||
} catch (e) {
|
|
||||||
// ignore error.
|
|
||||||
}
|
|
||||||
return theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
function delete_saved_theme() {
|
|
||||||
localStorage.removeItem('mdbook-theme');
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_theme() {
|
|
||||||
const theme = get_saved_theme();
|
|
||||||
if (theme === null || theme === undefined || !themeIds.includes(theme)) {
|
|
||||||
if (typeof default_dark_theme === 'undefined') {
|
|
||||||
// A customized index.hbs might not define this, so fall back to
|
|
||||||
// old behavior of determining the default on page load.
|
|
||||||
return default_theme;
|
|
||||||
}
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
? default_dark_theme
|
|
||||||
: default_light_theme;
|
|
||||||
} else {
|
|
||||||
return theme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let previousTheme = default_theme;
|
|
||||||
function set_theme(theme, store = true) {
|
|
||||||
let ace_theme;
|
|
||||||
|
|
||||||
if (theme === 'coal' || theme === 'navy') {
|
|
||||||
stylesheets.ayuHighlight.disabled = true;
|
|
||||||
stylesheets.tomorrowNight.disabled = false;
|
|
||||||
stylesheets.highlight.disabled = true;
|
|
||||||
|
|
||||||
ace_theme = 'ace/theme/tomorrow_night';
|
|
||||||
} else if (theme === 'ayu') {
|
|
||||||
stylesheets.ayuHighlight.disabled = false;
|
|
||||||
stylesheets.tomorrowNight.disabled = true;
|
|
||||||
stylesheets.highlight.disabled = true;
|
|
||||||
ace_theme = 'ace/theme/tomorrow_night';
|
|
||||||
} else {
|
|
||||||
stylesheets.ayuHighlight.disabled = true;
|
|
||||||
stylesheets.tomorrowNight.disabled = true;
|
|
||||||
stylesheets.highlight.disabled = false;
|
|
||||||
ace_theme = 'ace/theme/dawn';
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
|
|
||||||
}, 1);
|
|
||||||
|
|
||||||
if (window.ace && window.editors) {
|
|
||||||
window.editors.forEach(function(editor) {
|
|
||||||
editor.setTheme(ace_theme);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (store) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem('mdbook-theme', theme);
|
|
||||||
} catch (e) {
|
|
||||||
// ignore error.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html.classList.remove(previousTheme);
|
|
||||||
html.classList.add(theme);
|
|
||||||
previousTheme = theme;
|
|
||||||
updateThemeSelected();
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
query.onchange = function() {
|
|
||||||
set_theme(get_theme(), false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set theme.
|
|
||||||
set_theme(get_theme(), false);
|
|
||||||
|
|
||||||
themeToggleButton.addEventListener('click', function() {
|
|
||||||
if (themePopup.style.display === 'block') {
|
|
||||||
hideThemes();
|
|
||||||
} else {
|
|
||||||
showThemes();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
themePopup.addEventListener('click', function(e) {
|
|
||||||
let theme;
|
|
||||||
if (e.target.className === 'theme') {
|
|
||||||
theme = e.target.id;
|
|
||||||
} else if (e.target.parentElement.className === 'theme') {
|
|
||||||
theme = e.target.parentElement.id;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (theme === 'default_theme' || theme === null) {
|
|
||||||
delete_saved_theme();
|
|
||||||
set_theme(get_theme(), false);
|
|
||||||
} else {
|
|
||||||
set_theme(theme);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
themePopup.addEventListener('focusout', function(e) {
|
|
||||||
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
|
|
||||||
if (!!e.relatedTarget &&
|
|
||||||
!themeToggleButton.contains(e.relatedTarget) &&
|
|
||||||
!themePopup.contains(e.relatedTarget)
|
|
||||||
) {
|
|
||||||
hideThemes();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should not be needed, but it works around an issue on macOS & iOS:
|
|
||||||
// https://github.com/rust-lang/mdBook/issues/628
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (themePopup.style.display === 'block' &&
|
|
||||||
!themeToggleButton.contains(e.target) &&
|
|
||||||
!themePopup.contains(e.target)
|
|
||||||
) {
|
|
||||||
hideThemes();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!themePopup.contains(e.target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let li;
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Escape':
|
|
||||||
e.preventDefault();
|
|
||||||
hideThemes();
|
|
||||||
break;
|
|
||||||
case 'ArrowUp':
|
|
||||||
e.preventDefault();
|
|
||||||
li = document.activeElement.parentElement;
|
|
||||||
if (li && li.previousElementSibling) {
|
|
||||||
li.previousElementSibling.querySelector('button').focus();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'ArrowDown':
|
|
||||||
e.preventDefault();
|
|
||||||
li = document.activeElement.parentElement;
|
|
||||||
if (li && li.nextElementSibling) {
|
|
||||||
li.nextElementSibling.querySelector('button').focus();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'Home':
|
|
||||||
e.preventDefault();
|
|
||||||
themePopup.querySelector('li:first-child button').focus();
|
|
||||||
break;
|
|
||||||
case 'End':
|
|
||||||
e.preventDefault();
|
|
||||||
themePopup.querySelector('li:last-child button').focus();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function sidebar() {
|
|
||||||
const body = document.querySelector('body');
|
|
||||||
const sidebar = document.getElementById('sidebar');
|
|
||||||
const sidebarLinks = document.querySelectorAll('#sidebar a');
|
|
||||||
const sidebarToggleButton = document.getElementById('sidebar-toggle');
|
|
||||||
const sidebarToggleAnchor = document.getElementById('sidebar-toggle-anchor');
|
|
||||||
const sidebarResizeHandle = document.getElementById('sidebar-resize-handle');
|
|
||||||
let firstContact = null;
|
|
||||||
|
|
||||||
function showSidebar() {
|
|
||||||
body.classList.remove('sidebar-hidden');
|
|
||||||
body.classList.add('sidebar-visible');
|
|
||||||
Array.from(sidebarLinks).forEach(function(link) {
|
|
||||||
link.setAttribute('tabIndex', 0);
|
|
||||||
});
|
|
||||||
sidebarToggleButton.setAttribute('aria-expanded', true);
|
|
||||||
sidebar.setAttribute('aria-hidden', false);
|
|
||||||
try {
|
|
||||||
localStorage.setItem('mdbook-sidebar', 'visible');
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideSidebar() {
|
|
||||||
body.classList.remove('sidebar-visible');
|
|
||||||
body.classList.add('sidebar-hidden');
|
|
||||||
Array.from(sidebarLinks).forEach(function(link) {
|
|
||||||
link.setAttribute('tabIndex', -1);
|
|
||||||
});
|
|
||||||
sidebarToggleButton.setAttribute('aria-expanded', false);
|
|
||||||
sidebar.setAttribute('aria-hidden', true);
|
|
||||||
try {
|
|
||||||
localStorage.setItem('mdbook-sidebar', 'hidden');
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle sidebar
|
|
||||||
sidebarToggleAnchor.addEventListener('change', function sidebarToggle() {
|
|
||||||
if (sidebarToggleAnchor.checked) {
|
|
||||||
const current_width = parseInt(
|
|
||||||
document.documentElement.style.getPropertyValue('--sidebar-target-width'), 10);
|
|
||||||
if (current_width < 150) {
|
|
||||||
document.documentElement.style.setProperty('--sidebar-target-width', '150px');
|
|
||||||
}
|
|
||||||
showSidebar();
|
|
||||||
} else {
|
|
||||||
hideSidebar();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
|
|
||||||
|
|
||||||
function initResize() {
|
|
||||||
window.addEventListener('mousemove', resize, false);
|
|
||||||
window.addEventListener('mouseup', stopResize, false);
|
|
||||||
body.classList.add('sidebar-resizing');
|
|
||||||
}
|
|
||||||
function resize(e) {
|
|
||||||
let pos = e.clientX - sidebar.offsetLeft;
|
|
||||||
if (pos < 20) {
|
|
||||||
hideSidebar();
|
|
||||||
} else {
|
|
||||||
if (body.classList.contains('sidebar-hidden')) {
|
|
||||||
showSidebar();
|
|
||||||
}
|
|
||||||
pos = Math.min(pos, window.innerWidth - 100);
|
|
||||||
document.documentElement.style.setProperty('--sidebar-target-width', pos + 'px');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//on mouseup remove windows functions mousemove & mouseup
|
|
||||||
function stopResize() {
|
|
||||||
body.classList.remove('sidebar-resizing');
|
|
||||||
window.removeEventListener('mousemove', resize, false);
|
|
||||||
window.removeEventListener('mouseup', stopResize, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('touchstart', function(e) {
|
|
||||||
firstContact = {
|
|
||||||
x: e.touches[0].clientX,
|
|
||||||
time: Date.now(),
|
|
||||||
};
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
document.addEventListener('touchmove', function(e) {
|
|
||||||
if (!firstContact) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const curX = e.touches[0].clientX;
|
|
||||||
const xDiff = curX - firstContact.x,
|
|
||||||
tDiff = Date.now() - firstContact.time;
|
|
||||||
|
|
||||||
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
|
|
||||||
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300)) {
|
|
||||||
showSidebar();
|
|
||||||
} else if (xDiff < 0 && curX < 300) {
|
|
||||||
hideSidebar();
|
|
||||||
}
|
|
||||||
|
|
||||||
firstContact = null;
|
|
||||||
}
|
|
||||||
}, { passive: true });
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function chapterNavigation() {
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (window.search && window.search.hasFocus()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const html = document.querySelector('html');
|
|
||||||
|
|
||||||
function next() {
|
|
||||||
const nextButton = document.querySelector('.nav-chapters.next');
|
|
||||||
if (nextButton) {
|
|
||||||
window.location.href = nextButton.href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function prev() {
|
|
||||||
const previousButton = document.querySelector('.nav-chapters.previous');
|
|
||||||
if (previousButton) {
|
|
||||||
window.location.href = previousButton.href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowRight':
|
|
||||||
e.preventDefault();
|
|
||||||
if (html.dir === 'rtl') {
|
|
||||||
prev();
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'ArrowLeft':
|
|
||||||
e.preventDefault();
|
|
||||||
if (html.dir === 'rtl') {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
prev();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function clipboard() {
|
|
||||||
const clipButtons = document.querySelectorAll('.clip-button');
|
|
||||||
|
|
||||||
function hideTooltip(elem) {
|
|
||||||
elem.firstChild.innerText = '';
|
|
||||||
elem.className = 'clip-button';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showTooltip(elem, msg) {
|
|
||||||
elem.firstChild.innerText = msg;
|
|
||||||
elem.className = 'clip-button tooltipped';
|
|
||||||
}
|
|
||||||
|
|
||||||
const clipboardSnippets = new ClipboardJS('.clip-button', {
|
|
||||||
text: function(trigger) {
|
|
||||||
hideTooltip(trigger);
|
|
||||||
const playground = trigger.closest('pre');
|
|
||||||
return playground_text(playground, false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Array.from(clipButtons).forEach(function(clipButton) {
|
|
||||||
clipButton.addEventListener('mouseout', function(e) {
|
|
||||||
hideTooltip(e.currentTarget);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
clipboardSnippets.on('success', function(e) {
|
|
||||||
e.clearSelection();
|
|
||||||
showTooltip(e.trigger, 'Copied!');
|
|
||||||
});
|
|
||||||
|
|
||||||
clipboardSnippets.on('error', function(e) {
|
|
||||||
showTooltip(e.trigger, 'Clipboard error!');
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function scrollToTop() {
|
|
||||||
const menuTitle = document.querySelector('.menu-title');
|
|
||||||
|
|
||||||
menuTitle.addEventListener('click', function() {
|
|
||||||
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function controllMenu() {
|
|
||||||
const menu = document.getElementById('menu-bar');
|
|
||||||
|
|
||||||
(function controllPosition() {
|
|
||||||
let scrollTop = document.scrollingElement.scrollTop;
|
|
||||||
let prevScrollTop = scrollTop;
|
|
||||||
const minMenuY = -menu.clientHeight - 50;
|
|
||||||
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
|
|
||||||
menu.style.top = scrollTop + 'px';
|
|
||||||
// Same as parseInt(menu.style.top.slice(0, -2), but faster
|
|
||||||
let topCache = menu.style.top.slice(0, -2);
|
|
||||||
menu.classList.remove('sticky');
|
|
||||||
let stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
|
|
||||||
document.addEventListener('scroll', function() {
|
|
||||||
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
|
|
||||||
// `null` means that it doesn't need to be updated
|
|
||||||
let nextSticky = null;
|
|
||||||
let nextTop = null;
|
|
||||||
const scrollDown = scrollTop > prevScrollTop;
|
|
||||||
const menuPosAbsoluteY = topCache - scrollTop;
|
|
||||||
if (scrollDown) {
|
|
||||||
nextSticky = false;
|
|
||||||
if (menuPosAbsoluteY > 0) {
|
|
||||||
nextTop = prevScrollTop;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (menuPosAbsoluteY > 0) {
|
|
||||||
nextSticky = true;
|
|
||||||
} else if (menuPosAbsoluteY < minMenuY) {
|
|
||||||
nextTop = prevScrollTop + minMenuY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nextSticky === true && stickyCache === false) {
|
|
||||||
menu.classList.add('sticky');
|
|
||||||
stickyCache = true;
|
|
||||||
} else if (nextSticky === false && stickyCache === true) {
|
|
||||||
menu.classList.remove('sticky');
|
|
||||||
stickyCache = false;
|
|
||||||
}
|
|
||||||
if (nextTop !== null) {
|
|
||||||
menu.style.top = nextTop + 'px';
|
|
||||||
topCache = nextTop;
|
|
||||||
}
|
|
||||||
prevScrollTop = scrollTop;
|
|
||||||
}, { passive: true });
|
|
||||||
})();
|
|
||||||
(function controllBorder() {
|
|
||||||
function updateBorder() {
|
|
||||||
if (menu.offsetTop === 0) {
|
|
||||||
menu.classList.remove('bordered');
|
|
||||||
} else {
|
|
||||||
menu.classList.add('bordered');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateBorder();
|
|
||||||
document.addEventListener('scroll', updateBorder, { passive: true });
|
|
||||||
})();
|
|
||||||
})();
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
<!DOCTYPE HTML>
|
|
||||||
<html lang="en" class="light sidebar-visible" dir="ltr">
|
|
||||||
<head>
|
|
||||||
<!-- Book generated using mdBook -->
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Chapter 1 - Server API Documentation</title>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Custom HTML head -->
|
|
||||||
|
|
||||||
<meta name="description" content="">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="theme-color" content="#ffffff">
|
|
||||||
|
|
||||||
<link rel="icon" href="favicon.svg">
|
|
||||||
<link rel="shortcut icon" href="favicon.png">
|
|
||||||
<link rel="stylesheet" href="css/variables.css">
|
|
||||||
<link rel="stylesheet" href="css/general.css">
|
|
||||||
<link rel="stylesheet" href="css/chrome.css">
|
|
||||||
<link rel="stylesheet" href="css/print.css" media="print">
|
|
||||||
|
|
||||||
<!-- Fonts -->
|
|
||||||
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
|
||||||
<link rel="stylesheet" href="fonts/fonts.css">
|
|
||||||
|
|
||||||
<!-- Highlight.js Stylesheets -->
|
|
||||||
<link rel="stylesheet" id="highlight-css" href="highlight.css">
|
|
||||||
<link rel="stylesheet" id="tomorrow-night-css" href="tomorrow-night.css">
|
|
||||||
<link rel="stylesheet" id="ayu-highlight-css" href="ayu-highlight.css">
|
|
||||||
|
|
||||||
<!-- Custom theme stylesheets -->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Provide site root and default themes to javascript -->
|
|
||||||
<script>
|
|
||||||
const path_to_root = "";
|
|
||||||
const default_light_theme = "light";
|
|
||||||
const default_dark_theme = "navy";
|
|
||||||
</script>
|
|
||||||
<!-- Start loading toc.js asap -->
|
|
||||||
<script src="toc.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="body-container">
|
|
||||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
|
||||||
<script>
|
|
||||||
try {
|
|
||||||
let theme = localStorage.getItem('mdbook-theme');
|
|
||||||
let sidebar = localStorage.getItem('mdbook-sidebar');
|
|
||||||
|
|
||||||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
|
||||||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
|
||||||
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
|
||||||
}
|
|
||||||
} catch (e) { }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
|
||||||
<script>
|
|
||||||
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
|
|
||||||
let theme;
|
|
||||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
|
||||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
|
||||||
const html = document.documentElement;
|
|
||||||
html.classList.remove('light')
|
|
||||||
html.classList.add(theme);
|
|
||||||
html.classList.add("js");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
|
||||||
|
|
||||||
<!-- Hide / unhide sidebar before it is displayed -->
|
|
||||||
<script>
|
|
||||||
let sidebar = null;
|
|
||||||
const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
|
||||||
if (document.body.clientWidth >= 1080) {
|
|
||||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
|
||||||
sidebar = sidebar || 'visible';
|
|
||||||
} else {
|
|
||||||
sidebar = 'hidden';
|
|
||||||
}
|
|
||||||
sidebar_toggle.checked = sidebar === 'visible';
|
|
||||||
html.classList.remove('sidebar-visible');
|
|
||||||
html.classList.add("sidebar-" + sidebar);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
|
||||||
<!-- populated by js -->
|
|
||||||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
|
||||||
<noscript>
|
|
||||||
<iframe class="sidebar-iframe-outer" src="toc.html"></iframe>
|
|
||||||
</noscript>
|
|
||||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
|
||||||
<div class="sidebar-resize-indicator"></div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id="page-wrapper" class="page-wrapper">
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<div id="menu-bar-hover-placeholder"></div>
|
|
||||||
<div id="menu-bar" class="menu-bar sticky">
|
|
||||||
<div class="left-buttons">
|
|
||||||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
|
||||||
<i class="fa fa-bars"></i>
|
|
||||||
</label>
|
|
||||||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
|
||||||
<i class="fa fa-paint-brush"></i>
|
|
||||||
</button>
|
|
||||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</button></li>
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
|
||||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
|
||||||
</ul>
|
|
||||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
|
||||||
<i class="fa fa-search"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 class="menu-title">Server API Documentation</h1>
|
|
||||||
|
|
||||||
<div class="right-buttons">
|
|
||||||
<a href="print.html" title="Print this book" aria-label="Print this book">
|
|
||||||
<i id="print-button" class="fa fa-print"></i>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="search-wrapper" class="hidden">
|
|
||||||
<form id="searchbar-outer" class="searchbar-outer">
|
|
||||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
|
||||||
</form>
|
|
||||||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
|
||||||
<div id="searchresults-header" class="searchresults-header"></div>
|
|
||||||
<ul id="searchresults">
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
|
||||||
<script>
|
|
||||||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
|
||||||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
|
||||||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
|
||||||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="content" class="content">
|
|
||||||
<main>
|
|
||||||
<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
|
||||||
<!-- Mobile navigation buttons -->
|
|
||||||
|
|
||||||
|
|
||||||
<div style="clear: both"></div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
|
||||||
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Livereload script (if served using the cli tool) -->
|
|
||||||
<script>
|
|
||||||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
||||||
const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
|
|
||||||
const socket = new WebSocket(wsAddress);
|
|
||||||
socket.onmessage = function (event) {
|
|
||||||
if (event.data === "reload") {
|
|
||||||
socket.close();
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onbeforeunload = function() {
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.playground_copyable = true;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<script src="elasticlunr.min.js"></script>
|
|
||||||
<script src="mark.min.js"></script>
|
|
||||||
<script src="searcher.js"></script>
|
|
||||||
|
|
||||||
<script src="clipboard.min.js"></script>
|
|
||||||
<script src="highlight.js"></script>
|
|
||||||
<script src="book.js"></script>
|
|
||||||
|
|
||||||
<!-- Custom JS scripts -->
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
7
server/docs-prod/book/clipboard.min.js
vendored
7
server/docs-prod/book/clipboard.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,643 +0,0 @@
|
|||||||
/* CSS for UI elements (a.k.a. chrome) */
|
|
||||||
|
|
||||||
html {
|
|
||||||
scrollbar-color: var(--scrollbar) var(--bg);
|
|
||||||
}
|
|
||||||
#searchresults a,
|
|
||||||
.content a:link,
|
|
||||||
a:visited,
|
|
||||||
a > .hljs {
|
|
||||||
color: var(--links);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
body-container is necessary because mobile browsers don't seem to like
|
|
||||||
overflow-x on the body tag when there is a <meta name="viewport"> tag.
|
|
||||||
*/
|
|
||||||
#body-container {
|
|
||||||
/*
|
|
||||||
This is used when the sidebar pushes the body content off the side of
|
|
||||||
the screen on small screens. Without it, dragging on mobile Safari
|
|
||||||
will want to reposition the viewport in a weird way.
|
|
||||||
*/
|
|
||||||
overflow-x: clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Menu Bar */
|
|
||||||
|
|
||||||
#menu-bar,
|
|
||||||
#menu-bar-hover-placeholder {
|
|
||||||
z-index: 101;
|
|
||||||
margin: auto calc(0px - var(--page-padding));
|
|
||||||
}
|
|
||||||
#menu-bar {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
background-color: var(--bg);
|
|
||||||
border-block-end-color: var(--bg);
|
|
||||||
border-block-end-width: 1px;
|
|
||||||
border-block-end-style: solid;
|
|
||||||
}
|
|
||||||
#menu-bar.sticky,
|
|
||||||
#menu-bar-hover-placeholder:hover + #menu-bar,
|
|
||||||
#menu-bar:hover,
|
|
||||||
html.sidebar-visible #menu-bar {
|
|
||||||
position: -webkit-sticky;
|
|
||||||
position: sticky;
|
|
||||||
top: 0 !important;
|
|
||||||
}
|
|
||||||
#menu-bar-hover-placeholder {
|
|
||||||
position: sticky;
|
|
||||||
position: -webkit-sticky;
|
|
||||||
top: 0;
|
|
||||||
height: var(--menu-bar-height);
|
|
||||||
}
|
|
||||||
#menu-bar.bordered {
|
|
||||||
border-block-end-color: var(--table-border-color);
|
|
||||||
}
|
|
||||||
#menu-bar i, #menu-bar .icon-button {
|
|
||||||
position: relative;
|
|
||||||
padding: 0 8px;
|
|
||||||
z-index: 10;
|
|
||||||
line-height: var(--menu-bar-height);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.5s;
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 420px) {
|
|
||||||
#menu-bar i, #menu-bar .icon-button {
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
.icon-button i {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-buttons {
|
|
||||||
margin: 0 15px;
|
|
||||||
}
|
|
||||||
.right-buttons a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-buttons {
|
|
||||||
display: flex;
|
|
||||||
margin: 0 5px;
|
|
||||||
}
|
|
||||||
html:not(.js) .left-buttons button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-title {
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: 200;
|
|
||||||
font-size: 2.4rem;
|
|
||||||
line-height: var(--menu-bar-height);
|
|
||||||
text-align: center;
|
|
||||||
margin: 0;
|
|
||||||
flex: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.menu-title {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-bar,
|
|
||||||
.menu-bar:visited,
|
|
||||||
.nav-chapters,
|
|
||||||
.nav-chapters:visited,
|
|
||||||
.mobile-nav-chapters,
|
|
||||||
.mobile-nav-chapters:visited,
|
|
||||||
.menu-bar .icon-button,
|
|
||||||
.menu-bar a i {
|
|
||||||
color: var(--icons);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-bar i:hover,
|
|
||||||
.menu-bar .icon-button:hover,
|
|
||||||
.nav-chapters:hover,
|
|
||||||
.mobile-nav-chapters i:hover {
|
|
||||||
color: var(--icons-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nav Icons */
|
|
||||||
|
|
||||||
.nav-chapters {
|
|
||||||
font-size: 2.5em;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: 0;
|
|
||||||
max-width: 150px;
|
|
||||||
min-width: 90px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
transition: color 0.5s, background-color 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-chapters:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: var(--theme-hover);
|
|
||||||
transition: background-color 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-wrapper {
|
|
||||||
margin-block-start: 50px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav-chapters {
|
|
||||||
font-size: 2.5em;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
width: 90px;
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: var(--sidebar-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Only Firefox supports flow-relative values */
|
|
||||||
.previous { float: left; }
|
|
||||||
[dir=rtl] .previous { float: right; }
|
|
||||||
|
|
||||||
/* Only Firefox supports flow-relative values */
|
|
||||||
.next {
|
|
||||||
float: right;
|
|
||||||
right: var(--page-padding);
|
|
||||||
}
|
|
||||||
[dir=rtl] .next {
|
|
||||||
float: left;
|
|
||||||
right: unset;
|
|
||||||
left: var(--page-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Use the correct buttons for RTL layouts*/
|
|
||||||
[dir=rtl] .previous i.fa-angle-left:before {content:"\f105";}
|
|
||||||
[dir=rtl] .next i.fa-angle-right:before { content:"\f104"; }
|
|
||||||
|
|
||||||
@media only screen and (max-width: 1080px) {
|
|
||||||
.nav-wide-wrapper { display: none; }
|
|
||||||
.nav-wrapper { display: block; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sidebar-visible */
|
|
||||||
@media only screen and (max-width: 1380px) {
|
|
||||||
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; }
|
|
||||||
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
|
|
||||||
:not(pre) > .hljs {
|
|
||||||
display: inline;
|
|
||||||
padding: 0.1em 0.3em;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(pre):not(a) > .hljs {
|
|
||||||
color: var(--inline-code-color);
|
|
||||||
overflow-x: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover > .hljs {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
pre > .buttons {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100;
|
|
||||||
right: 0px;
|
|
||||||
top: 2px;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 2px 0px;
|
|
||||||
|
|
||||||
color: var(--sidebar-fg);
|
|
||||||
cursor: pointer;
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
transition: visibility 0.1s linear, opacity 0.1s linear;
|
|
||||||
}
|
|
||||||
pre:hover > .buttons {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
pre > .buttons :hover {
|
|
||||||
color: var(--sidebar-active);
|
|
||||||
border-color: var(--icons-hover);
|
|
||||||
background-color: var(--theme-hover);
|
|
||||||
}
|
|
||||||
pre > .buttons i {
|
|
||||||
margin-inline-start: 8px;
|
|
||||||
}
|
|
||||||
pre > .buttons button {
|
|
||||||
cursor: inherit;
|
|
||||||
margin: 0px 5px;
|
|
||||||
padding: 4px 4px 3px 5px;
|
|
||||||
font-size: 23px;
|
|
||||||
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-color: var(--icons);
|
|
||||||
background-color: var(--theme-popup-bg);
|
|
||||||
transition: 100ms;
|
|
||||||
transition-property: color,border-color,background-color;
|
|
||||||
color: var(--icons);
|
|
||||||
}
|
|
||||||
|
|
||||||
pre > .buttons button.clip-button {
|
|
||||||
padding: 2px 4px 0px 6px;
|
|
||||||
}
|
|
||||||
pre > .buttons button.clip-button::before {
|
|
||||||
/* clipboard image from octicons (https://github.com/primer/octicons/tree/v2.0.0) MIT license
|
|
||||||
*/
|
|
||||||
content: url('data:image/svg+xml,<svg width="21" height="20" viewBox="0 0 24 25" \
|
|
||||||
xmlns="http://www.w3.org/2000/svg" aria-label="Copy to clipboard">\
|
|
||||||
<path d="M18 20h2v3c0 1-1 2-2 2H2c-.998 0-2-1-2-2V5c0-.911.755-1.667 1.667-1.667h5A3.323 3.323 0 \
|
|
||||||
0110 0a3.323 3.323 0 013.333 3.333h5C19.245 3.333 20 4.09 20 5v8.333h-2V9H2v14h16v-3zM3 \
|
|
||||||
7h14c0-.911-.793-1.667-1.75-1.667H13.5c-.957 0-1.75-.755-1.75-1.666C11.75 2.755 10.957 2 10 \
|
|
||||||
2s-1.75.755-1.75 1.667c0 .911-.793 1.666-1.75 1.666H4.75C3.793 5.333 3 6.09 3 7z"/>\
|
|
||||||
<path d="M4 19h6v2H4zM12 11H4v2h8zM4 17h4v-2H4zM15 15v-3l-4.5 4.5L15 21v-3l8.027-.032L23 15z"/>\
|
|
||||||
</svg>');
|
|
||||||
filter: var(--copy-button-filter);
|
|
||||||
}
|
|
||||||
pre > .buttons button.clip-button:hover::before {
|
|
||||||
filter: var(--copy-button-filter-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (pointer: coarse) {
|
|
||||||
pre > .buttons button {
|
|
||||||
/* On mobile, make it easier to tap buttons. */
|
|
||||||
padding: 0.3rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-resize-indicator {
|
|
||||||
/* Hide resize indicator on devices with limited accuracy */
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pre > code {
|
|
||||||
display: block;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FIXME: ACE editors overlap their buttons because ACE does absolute
|
|
||||||
positioning within the code block which breaks padding. The only solution I
|
|
||||||
can think of is to move the padding to the outer pre tag (or insert a div
|
|
||||||
wrapper), but that would require fixing a whole bunch of CSS rules.
|
|
||||||
*/
|
|
||||||
.hljs.ace_editor {
|
|
||||||
padding: 0rem 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre > .result {
|
|
||||||
margin-block-start: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search */
|
|
||||||
|
|
||||||
#searchresults a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
mark {
|
|
||||||
border-radius: 2px;
|
|
||||||
padding-block-start: 0;
|
|
||||||
padding-block-end: 1px;
|
|
||||||
padding-inline-start: 3px;
|
|
||||||
padding-inline-end: 3px;
|
|
||||||
margin-block-start: 0;
|
|
||||||
margin-block-end: -1px;
|
|
||||||
margin-inline-start: -3px;
|
|
||||||
margin-inline-end: -3px;
|
|
||||||
background-color: var(--search-mark-bg);
|
|
||||||
transition: background-color 300ms linear;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
mark.fade-out {
|
|
||||||
background-color: rgba(0,0,0,0) !important;
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchbar-outer {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: auto;
|
|
||||||
max-width: var(--content-max-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
#searchbar {
|
|
||||||
width: 100%;
|
|
||||||
margin-block-start: 5px;
|
|
||||||
margin-block-end: 0;
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: auto;
|
|
||||||
padding: 10px 16px;
|
|
||||||
transition: box-shadow 300ms ease-in-out;
|
|
||||||
border: 1px solid var(--searchbar-border-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: var(--searchbar-bg);
|
|
||||||
color: var(--searchbar-fg);
|
|
||||||
}
|
|
||||||
#searchbar:focus,
|
|
||||||
#searchbar.active {
|
|
||||||
box-shadow: 0 0 3px var(--searchbar-shadow-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchresults-header {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1em;
|
|
||||||
padding-block-start: 18px;
|
|
||||||
padding-block-end: 0;
|
|
||||||
padding-inline-start: 5px;
|
|
||||||
padding-inline-end: 0;
|
|
||||||
color: var(--searchresults-header-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchresults-outer {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: auto;
|
|
||||||
max-width: var(--content-max-width);
|
|
||||||
border-block-end: 1px dashed var(--searchresults-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul#searchresults {
|
|
||||||
list-style: none;
|
|
||||||
padding-inline-start: 20px;
|
|
||||||
}
|
|
||||||
ul#searchresults li {
|
|
||||||
margin: 10px 0px;
|
|
||||||
padding: 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
ul#searchresults li.focus {
|
|
||||||
background-color: var(--searchresults-li-bg);
|
|
||||||
}
|
|
||||||
ul#searchresults span.teaser {
|
|
||||||
display: block;
|
|
||||||
clear: both;
|
|
||||||
margin-block-start: 5px;
|
|
||||||
margin-block-end: 0;
|
|
||||||
margin-inline-start: 20px;
|
|
||||||
margin-inline-end: 0;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
ul#searchresults span.teaser em {
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: var(--sidebar-width);
|
|
||||||
font-size: 0.875em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
overscroll-behavior-y: contain;
|
|
||||||
background-color: var(--sidebar-bg);
|
|
||||||
color: var(--sidebar-fg);
|
|
||||||
}
|
|
||||||
.sidebar-iframe-inner {
|
|
||||||
--padding: 10px;
|
|
||||||
|
|
||||||
background-color: var(--sidebar-bg);
|
|
||||||
padding: var(--padding);
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
color: var(--sidebar-fg);
|
|
||||||
min-height: calc(100vh - var(--padding) * 2);
|
|
||||||
}
|
|
||||||
.sidebar-iframe-outer {
|
|
||||||
border: none;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
[dir=rtl] .sidebar { left: unset; right: 0; }
|
|
||||||
.sidebar-resizing {
|
|
||||||
-moz-user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
html:not(.sidebar-resizing) .sidebar {
|
|
||||||
transition: transform 0.3s; /* Animation: slide away */
|
|
||||||
}
|
|
||||||
.sidebar code {
|
|
||||||
line-height: 2em;
|
|
||||||
}
|
|
||||||
.sidebar .sidebar-scrollbox {
|
|
||||||
overflow-y: auto;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 10px 10px;
|
|
||||||
}
|
|
||||||
.sidebar .sidebar-resize-handle {
|
|
||||||
position: absolute;
|
|
||||||
cursor: col-resize;
|
|
||||||
width: 0;
|
|
||||||
right: calc(var(--sidebar-resize-indicator-width) * -1);
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-resize-handle .sidebar-resize-indicator {
|
|
||||||
width: 100%;
|
|
||||||
height: 12px;
|
|
||||||
background-color: var(--icons);
|
|
||||||
margin-inline-start: var(--sidebar-resize-indicator-space);
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir=rtl] .sidebar .sidebar-resize-handle {
|
|
||||||
left: calc(var(--sidebar-resize-indicator-width) * -1);
|
|
||||||
right: unset;
|
|
||||||
}
|
|
||||||
.js .sidebar .sidebar-resize-handle {
|
|
||||||
cursor: col-resize;
|
|
||||||
width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space));
|
|
||||||
}
|
|
||||||
/* sidebar-hidden */
|
|
||||||
#sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
|
||||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
[dir=rtl] #sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
|
||||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
|
||||||
}
|
|
||||||
.sidebar::-webkit-scrollbar {
|
|
||||||
background: var(--sidebar-bg);
|
|
||||||
}
|
|
||||||
.sidebar::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--scrollbar);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sidebar-visible */
|
|
||||||
#sidebar-toggle-anchor:checked ~ .page-wrapper {
|
|
||||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
|
||||||
}
|
|
||||||
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
|
|
||||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
|
||||||
}
|
|
||||||
@media only screen and (min-width: 620px) {
|
|
||||||
#sidebar-toggle-anchor:checked ~ .page-wrapper {
|
|
||||||
transform: none;
|
|
||||||
margin-inline-start: calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width));
|
|
||||||
}
|
|
||||||
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter {
|
|
||||||
list-style: none outside none;
|
|
||||||
padding-inline-start: 0;
|
|
||||||
line-height: 2.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter ol {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter li {
|
|
||||||
display: flex;
|
|
||||||
color: var(--sidebar-non-existant);
|
|
||||||
}
|
|
||||||
.chapter li a {
|
|
||||||
display: block;
|
|
||||||
padding: 0;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--sidebar-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter li a:hover {
|
|
||||||
color: var(--sidebar-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter li a.active {
|
|
||||||
color: var(--sidebar-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter li > a.toggle {
|
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
margin-inline-start: auto;
|
|
||||||
padding: 0 10px;
|
|
||||||
user-select: none;
|
|
||||||
opacity: 0.68;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter li > a.toggle div {
|
|
||||||
transition: transform 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* collapse the section */
|
|
||||||
.chapter li:not(.expanded) + li > ol {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter li.chapter-item {
|
|
||||||
line-height: 1.5em;
|
|
||||||
margin-block-start: 0.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter li.expanded > a.toggle div {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
width: 100%;
|
|
||||||
height: 3px;
|
|
||||||
margin: 5px 0px;
|
|
||||||
}
|
|
||||||
.chapter .spacer {
|
|
||||||
background-color: var(--sidebar-spacer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
|
||||||
.chapter li a { padding: 5px 0; }
|
|
||||||
.spacer { margin: 10px 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
list-style: none outside none;
|
|
||||||
padding-inline-start: 20px;
|
|
||||||
line-height: 1.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme Menu Popup */
|
|
||||||
|
|
||||||
.theme-popup {
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
top: var(--menu-bar-height);
|
|
||||||
z-index: 1000;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.7em;
|
|
||||||
color: var(--fg);
|
|
||||||
background: var(--theme-popup-bg);
|
|
||||||
border: 1px solid var(--theme-popup-border);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
display: none;
|
|
||||||
/* Don't let the children's background extend past the rounded corners. */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
[dir=rtl] .theme-popup { left: unset; right: 10px; }
|
|
||||||
.theme-popup .default {
|
|
||||||
color: var(--icons);
|
|
||||||
}
|
|
||||||
.theme-popup .theme {
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 2px 20px;
|
|
||||||
line-height: 25px;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: start;
|
|
||||||
cursor: pointer;
|
|
||||||
color: inherit;
|
|
||||||
background: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
.theme-popup .theme:hover {
|
|
||||||
background-color: var(--theme-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-selected::before {
|
|
||||||
display: inline-block;
|
|
||||||
content: "✓";
|
|
||||||
margin-inline-start: -14px;
|
|
||||||
width: 14px;
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
/* Base styles and content styles */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Browser default font-size is 16px, this way 1 rem = 10px */
|
|
||||||
font-size: 62.5%;
|
|
||||||
color-scheme: var(--color-scheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-family: "Open Sans", sans-serif;
|
|
||||||
color: var(--fg);
|
|
||||||
background-color: var(--bg);
|
|
||||||
text-size-adjust: none;
|
|
||||||
-webkit-text-size-adjust: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: var(--mono-font) !important;
|
|
||||||
font-size: var(--code-font-size);
|
|
||||||
direction: ltr !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* make long words/inline code not x overflow */
|
|
||||||
main {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* make wide tables scroll if they overflow */
|
|
||||||
.table-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Don't change font size in headers. */
|
|
||||||
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
|
||||||
font-size: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left { float: left; }
|
|
||||||
.right { float: right; }
|
|
||||||
.boring { opacity: 0.6; }
|
|
||||||
.hide-boring .boring { display: none; }
|
|
||||||
.hidden { display: none !important; }
|
|
||||||
|
|
||||||
h2, h3 { margin-block-start: 2.5em; }
|
|
||||||
h4, h5 { margin-block-start: 2em; }
|
|
||||||
|
|
||||||
.header + .header h3,
|
|
||||||
.header + .header h4,
|
|
||||||
.header + .header h5 {
|
|
||||||
margin-block-start: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1:target::before,
|
|
||||||
h2:target::before,
|
|
||||||
h3:target::before,
|
|
||||||
h4:target::before,
|
|
||||||
h5:target::before,
|
|
||||||
h6:target::before {
|
|
||||||
display: inline-block;
|
|
||||||
content: "»";
|
|
||||||
margin-inline-start: -30px;
|
|
||||||
width: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* This is broken on Safari as of version 14, but is fixed
|
|
||||||
in Safari Technology Preview 117 which I think will be Safari 14.2.
|
|
||||||
https://bugs.webkit.org/show_bug.cgi?id=218076
|
|
||||||
*/
|
|
||||||
:target {
|
|
||||||
/* Safari does not support logical properties */
|
|
||||||
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
|
||||||
outline: 0;
|
|
||||||
padding: 0 var(--page-padding);
|
|
||||||
margin-block-start: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
|
|
||||||
}
|
|
||||||
.page-wrapper {
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-color: var(--bg);
|
|
||||||
}
|
|
||||||
.no-js .page-wrapper,
|
|
||||||
.js:not(.sidebar-resizing) .page-wrapper {
|
|
||||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
|
||||||
}
|
|
||||||
[dir=rtl] .js:not(.sidebar-resizing) .page-wrapper {
|
|
||||||
transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0 5px 50px 5px;
|
|
||||||
}
|
|
||||||
.content main {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: auto;
|
|
||||||
max-width: var(--content-max-width);
|
|
||||||
}
|
|
||||||
.content p { line-height: 1.45em; }
|
|
||||||
.content ol { line-height: 1.45em; }
|
|
||||||
.content ul { line-height: 1.45em; }
|
|
||||||
.content a { text-decoration: none; }
|
|
||||||
.content a:hover { text-decoration: underline; }
|
|
||||||
.content img, .content video { max-width: 100%; }
|
|
||||||
.content .header:link,
|
|
||||||
.content .header:visited {
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
.content .header:link,
|
|
||||||
.content .header:visited:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
margin: 0 auto;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
table td {
|
|
||||||
padding: 3px 20px;
|
|
||||||
border: 1px var(--table-border-color) solid;
|
|
||||||
}
|
|
||||||
table thead {
|
|
||||||
background: var(--table-header-bg);
|
|
||||||
}
|
|
||||||
table thead td {
|
|
||||||
font-weight: 700;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
table thead th {
|
|
||||||
padding: 3px 20px;
|
|
||||||
}
|
|
||||||
table thead tr {
|
|
||||||
border: 1px var(--table-header-bg) solid;
|
|
||||||
}
|
|
||||||
/* Alternate background colors for rows */
|
|
||||||
table tbody tr:nth-child(2n) {
|
|
||||||
background: var(--table-alternate-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 0 20px;
|
|
||||||
color: var(--fg);
|
|
||||||
background-color: var(--quote-bg);
|
|
||||||
border-block-start: .1em solid var(--quote-border);
|
|
||||||
border-block-end: .1em solid var(--quote-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
margin: 20px;
|
|
||||||
padding: 0 20px;
|
|
||||||
border-inline-start: 2px solid var(--warning-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning:before {
|
|
||||||
position: absolute;
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
margin-inline-start: calc(-1.5rem - 21px);
|
|
||||||
content: "ⓘ";
|
|
||||||
text-align: center;
|
|
||||||
background-color: var(--bg);
|
|
||||||
color: var(--warning-border);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote .warning:before {
|
|
||||||
background-color: var(--quote-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
kbd {
|
|
||||||
background-color: var(--table-border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
border: solid 1px var(--theme-popup-border);
|
|
||||||
box-shadow: inset 0 -1px 0 var(--theme-hover);
|
|
||||||
display: inline-block;
|
|
||||||
font-size: var(--code-font-size);
|
|
||||||
font-family: var(--mono-font);
|
|
||||||
line-height: 10px;
|
|
||||||
padding: 4px 5px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
sup {
|
|
||||||
/* Set the line-height for superscript and footnote references so that there
|
|
||||||
isn't an awkward space appearing above lines that contain the footnote.
|
|
||||||
|
|
||||||
See https://github.com/rust-lang/mdBook/pull/2443#discussion_r1813773583
|
|
||||||
for an explanation.
|
|
||||||
*/
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footnote-definition {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
/* The default spacing for a list is a little too large. */
|
|
||||||
.footnote-definition ul,
|
|
||||||
.footnote-definition ol {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
.footnote-definition > li {
|
|
||||||
/* Required to position the ::before target */
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.footnote-definition > li:target {
|
|
||||||
scroll-margin-top: 50vh;
|
|
||||||
}
|
|
||||||
.footnote-reference:target {
|
|
||||||
scroll-margin-top: 50vh;
|
|
||||||
}
|
|
||||||
/* Draws a border around the footnote (including the marker) when it is selected.
|
|
||||||
TODO: If there are multiple linkbacks, highlight which one you just came
|
|
||||||
from so you know which one to click.
|
|
||||||
*/
|
|
||||||
.footnote-definition > li:target::before {
|
|
||||||
border: 2px solid var(--footnote-highlight);
|
|
||||||
border-radius: 6px;
|
|
||||||
position: absolute;
|
|
||||||
top: -8px;
|
|
||||||
right: -8px;
|
|
||||||
bottom: -8px;
|
|
||||||
left: -32px;
|
|
||||||
pointer-events: none;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
/* Pulses the footnote reference so you can quickly see where you left off reading.
|
|
||||||
This could use some improvement.
|
|
||||||
*/
|
|
||||||
@media not (prefers-reduced-motion) {
|
|
||||||
.footnote-reference:target {
|
|
||||||
animation: fn-highlight 0.8s;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fn-highlight {
|
|
||||||
from {
|
|
||||||
background-color: var(--footnote-highlight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltiptext {
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #333;
|
|
||||||
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
|
||||||
left: -8px; /* Half of the width of the icon */
|
|
||||||
top: -35px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 5px 8px;
|
|
||||||
margin: 5px;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
.tooltipped .tooltiptext {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter li.part-title {
|
|
||||||
color: var(--sidebar-fg);
|
|
||||||
margin: 5px 0px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-no-output {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user