From 2dc736b02e48369469adf88354ad77dfdea7b697 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Wed, 17 Dec 2025 16:40:34 -0700 Subject: [PATCH] Add tree thing --- unshell-gui/src/app/mod.rs | 6 +- .../src/blobs/mod.rs | 0 unshell-gui/src/interface/config.rs | 42 +++++ unshell-gui/src/interface/mod.rs | 150 ++++++++++++++++++ unshell-gui/src/lib.rs | 2 + unshell-server/TODO.txt | 3 + unshell-server/src/api/app.rs | 3 + unshell-server/src/config/interface.rs | 49 ++++++ unshell-server/src/config/mod.rs | 4 +- unshell-server/src/lib.rs | 1 + unshell-server/src/server/mod.rs | 8 +- unshell-server/src/server/tree2.rs | 120 ++++++++++++++ 12 files changed, 385 insertions(+), 3 deletions(-) rename unshell-server/src/server/manager.rs => unshell-gui/src/blobs/mod.rs (100%) create mode 100644 unshell-gui/src/interface/config.rs create mode 100644 unshell-gui/src/interface/mod.rs create mode 100644 unshell-server/TODO.txt create mode 100644 unshell-server/src/config/interface.rs create mode 100644 unshell-server/src/server/tree2.rs diff --git a/unshell-gui/src/app/mod.rs b/unshell-gui/src/app/mod.rs index 91037b1..98db3b7 100644 --- a/unshell-gui/src/app/mod.rs +++ b/unshell-gui/src/app/mod.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use crate::{ app::windows::WindowWrapper, auth::Auth, config::Config, flowchart::FlowChart, - log_viewer::LogViewer, payload_config::PayloadConfig, + interface::InterfaceWindow, log_viewer::LogViewer, payload_config::PayloadConfig, }; pub use app::TemplateApp; use egui_tiles::{TileId, Tree}; @@ -20,6 +20,7 @@ pub struct AppState { pub config: Config, pub payload_config: PayloadConfig, pub log_viewer: LogViewer, + pub interface: InterfaceWindow, } impl AppState { @@ -29,6 +30,7 @@ impl AppState { (AppWindow::PayloadConfig, "Payload Config"), (AppWindow::Config, "Config"), (AppWindow::LogViewer, "Log Viewer"), + (AppWindow::InterfaceWindow, "Tree Test"), ]) .iter() .enumerate() @@ -103,6 +105,7 @@ pub enum AppWindow { Config, PayloadConfig, LogViewer, + InterfaceWindow, } impl AppWindow { @@ -112,6 +115,7 @@ impl AppWindow { AppWindow::Config => state.config.update(&mut state.auth, ui), AppWindow::PayloadConfig => state.payload_config.update(ui), AppWindow::LogViewer => state.log_viewer.update(&mut state.auth, ui), + AppWindow::InterfaceWindow => state.interface.update(&mut state.auth, ui), } } diff --git a/unshell-server/src/server/manager.rs b/unshell-gui/src/blobs/mod.rs similarity index 100% rename from unshell-server/src/server/manager.rs rename to unshell-gui/src/blobs/mod.rs diff --git a/unshell-gui/src/interface/config.rs b/unshell-gui/src/interface/config.rs new file mode 100644 index 0000000..313d1a5 --- /dev/null +++ b/unshell-gui/src/interface/config.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +use serde_json::Value; + +#[derive(Clone, serde::Deserialize, serde::Serialize)] +pub enum Tree2Repr { + File(String), + Folder(Vec), +} + +// #[derive(Clone, serde::Deserialize, serde::Serialize)] +// pub struct InterfaceWrapper { +// pub name: String, +// pub interface: Interface, +// } + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub enum ConfigStructField { + Header(String), + Text(String), + String { + // Default value of string edit in struct + #[serde(default)] + default: String, + max_length: Option, + // Display string edit as password + #[serde(default)] + protected: Option, + }, + Integer { + // Default value of integer in struct + #[serde(default)] + default: i32, + min: Option, + max: Option, + }, + // Checkbox + // Dropdown + // Collapsing header + // Slider + // ... +} diff --git a/unshell-gui/src/interface/mod.rs b/unshell-gui/src/interface/mod.rs new file mode 100644 index 0000000..1bbdbbb --- /dev/null +++ b/unshell-gui/src/interface/mod.rs @@ -0,0 +1,150 @@ +mod config; + +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use crate::{auth::Auth, interface::config::Tree2Repr}; + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct InterfaceWindow { + path: PathBuf, + + #[serde(skip)] + state: Arc>, +} + +pub struct InterfaceWindowState { + is_request: bool, + is_error: bool, + branch: Option, +} + +impl InterfaceWindow { + pub fn update(&mut self, auth: &mut Auth, ui: &mut egui::Ui) { + ui.heading("Interface"); + ui.label(self.path.to_string_lossy()); + + { + if !self.path.eq("/") && ui.button("Go up").clicked() { + self.go_up(); + self.state.lock().unwrap().branch = None; + } + + let state_lock = self.state.lock().unwrap(); + + let (is_request, is_error, is_some) = ( + state_lock.is_request, + state_lock.is_error, + state_lock.branch.is_some(), + ); + + drop(state_lock); + + if is_request { + ui.spinner(); + } else if is_error { + self.reset_path(); + let mut state_lock = self.state.lock().unwrap(); + + state_lock.is_error = false; + state_lock.is_request = false; + state_lock.branch = None; + + drop(state_lock) + } else if !is_some { + { + let mut state_lock = self.state.lock().unwrap(); + + state_lock.is_request = true; + + drop(state_lock) + } + + let state_clone = self.state.clone(); + auth.get( + &format!("/api/interface{}", self.path.display()), + move |response: Result| { + let mut state_lock = state_clone.lock().unwrap(); + + match response { + Ok(item) => { + state_lock.branch = Some(item); + } + Err(_) => { + state_lock.is_error = true; + } + } + + state_lock.is_request = false; + + drop(state_lock); + }, + ); + } else { + let state_clone = self.state.clone(); + + let mut state_lock = state_clone.lock().unwrap(); + + let branch = (state_lock.branch).as_ref().unwrap(); + + let clear = match branch { + Tree2Repr::File(file) => { + ui.label(&format!("File {}", file)); + false + } + Tree2Repr::Folder(items) => { + let mut clear = false; + for item in items { + if ui.button(&format!("Item {}", item)).clicked() { + self.go_down(item.clone()); + clear = true; + break; + } + } + clear + } + _ => false, + }; + + if clear { + state_lock.branch = None; + } + + drop(state_lock) + } + } + } + + fn reset_path(&mut self) { + self.path = PathBuf::from("/"); + } + + fn go_up(&mut self) { + self.path = PathBuf::from(self.path.parent().unwrap()); + } + + fn go_down(&mut self, path: String) { + self.path = self.path.join(path); + } +} + +impl Default for InterfaceWindow { + fn default() -> Self { + Self { + path: "/".into(), + state: Arc::new(Mutex::new(InterfaceWindowState::default())), + } + } +} + +impl Default for InterfaceWindowState { + fn default() -> Self { + Self { + is_request: false, + branch: None, + is_error: false, + } + } +} diff --git a/unshell-gui/src/lib.rs b/unshell-gui/src/lib.rs index 80ce4d7..854ceec 100644 --- a/unshell-gui/src/lib.rs +++ b/unshell-gui/src/lib.rs @@ -5,8 +5,10 @@ extern crate log; pub mod app; mod auth; +mod blobs; mod config; mod flowchart; +mod interface; mod log_viewer; mod payload_config; diff --git a/unshell-server/TODO.txt b/unshell-server/TODO.txt new file mode 100644 index 0000000..5baef54 --- /dev/null +++ b/unshell-server/TODO.txt @@ -0,0 +1,3 @@ +GET /api/interface/*/some_config -> Returns the interface and struct at that location + +GET /api/interface/*/some_folder -> Returns the list of sub-folders and interfaces at that location diff --git a/unshell-server/src/api/app.rs b/unshell-server/src/api/app.rs index 0a34381..6162ff8 100644 --- a/unshell-server/src/api/app.rs +++ b/unshell-server/src/api/app.rs @@ -53,6 +53,9 @@ pub async fn start_api(address: &str, server: Server) { router = route_get!(router, "/api/keys/{*path}", Server::all_tree_keys_api); router = route_get!(router, "/api/values/{*path}", Server::all_tree_values_api); + router = route_get!(router, "/api/interface/", Server::get_tree2_root); + router = route_get!(router, "/api/interface/{*path}", Server::get_tree2); + // router = route_get_log(router); axum::serve(listener, router.with_state(server)) diff --git a/unshell-server/src/config/interface.rs b/unshell-server/src/config/interface.rs new file mode 100644 index 0000000..038d642 --- /dev/null +++ b/unshell-server/src/config/interface.rs @@ -0,0 +1,49 @@ +use std::collections::HashMap; + +use serde_json::Value; + +use crate::config::ConfigStructField; + +#[derive(Clone, serde::Deserialize, serde::Serialize)] +pub enum Interface { + Sub(HashMap), + Struct { + fields: HashMap, + value: HashMap, + }, +} + +#[derive(Clone, serde::Deserialize, serde::Serialize)] +pub struct InterfaceWrapper { + pub name: String, + pub interface: Interface, +} + +impl InterfaceWrapper { + pub fn get_path(&self, elements: &Vec<&str>, depth: usize) -> Result { + if depth == elements.len() { + return Ok(self.clone()); + } + + let element = elements[depth]; + + match &self.interface { + Interface::Sub(interface_wrappers) => { + if let Some(interface) = interface_wrappers.get(element) { + interface.get_path(elements, depth) + } else { + Err("Invalid Path".into()) + } + } + + _ => Err("Invalid Path".into()), + } + } +} + +pub fn get_test_interface() -> InterfaceWrapper { + InterfaceWrapper { + name: "Root Interface".into(), + interface: Interface::Sub(HashMap::new()), + } +} diff --git a/unshell-server/src/config/mod.rs b/unshell-server/src/config/mod.rs index cd05852..8fd0470 100644 --- a/unshell-server/src/config/mod.rs +++ b/unshell-server/src/config/mod.rs @@ -1,6 +1,8 @@ mod blob; +// pub mod interface; pub use blob::Blob; +// pub use interface::InterfaceWrapper; use std::{ collections::HashMap, @@ -37,7 +39,7 @@ struct BuildConfig { } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -enum ConfigStructField { +pub enum ConfigStructField { Header(String), Text(String), String { diff --git a/unshell-server/src/lib.rs b/unshell-server/src/lib.rs index 6958764..2403ed8 100644 --- a/unshell-server/src/lib.rs +++ b/unshell-server/src/lib.rs @@ -5,6 +5,7 @@ mod config; pub mod logger; mod modules; mod server; + pub use api::app::start_api; pub use server::Server; diff --git a/unshell-server/src/server/mod.rs b/unshell-server/src/server/mod.rs index 2c9cd0d..7869353 100644 --- a/unshell-server/src/server/mod.rs +++ b/unshell-server/src/server/mod.rs @@ -1,14 +1,18 @@ use std::{error::Error, path::PathBuf}; +use crate::server::tree2::Tree2; + mod blobs; mod database; -mod manager; +mod tree2; #[derive(Clone)] pub struct Server { pub component_configs: Vec, + // pub interface: InterfaceWrapper, // pub manager: Arc>, pub db: sled::Db, + pub tree: Tree2, } impl Server { @@ -23,6 +27,8 @@ impl Server { component_configs, // manager: Manager::start(&SERVER_CONFIG, Vec::new()), db: sled::open(database)?, + tree: Tree2::default(), + // interface: get_test_interface(), }) } } diff --git a/unshell-server/src/server/tree2.rs b/unshell-server/src/server/tree2.rs new file mode 100644 index 0000000..c74a1e3 --- /dev/null +++ b/unshell-server/src/server/tree2.rs @@ -0,0 +1,120 @@ +use std::collections::HashMap; + +use axum::{ + Extension, Json, + extract::{Path, State}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use unshell_lib::debug; + +use crate::{Server, api::CurrentUser}; + +#[derive(Clone)] +enum Tree2Branch { + File(String), + Folder(String, HashMap), +} + +#[derive(Clone, Serialize, Deserialize)] +enum Tree2Repr { + File(String), + Folder(Vec), +} + +#[derive(Clone)] +pub struct Tree2 { + root: Tree2Branch, +} + +impl Tree2Branch { + pub fn get_path(&self, elements: &Vec<&str>, depth: usize) -> Result<&Tree2Branch, String> { + if depth == elements.len() { + return Ok(self); + } + + let element = elements[depth]; + + if let Tree2Branch::Folder(_, hash_map) = self { + if let Some(branch) = hash_map.get(element) { + branch.get_path(elements, depth + 1) + } else { + Err("Invalid Path".into()) + } + } else { + return Err("This is a folder, not a file".into()); + } + } + + pub fn to_repr(&self) -> Tree2Repr { + match self { + Tree2Branch::File(name) => Tree2Repr::File(name.clone()), + Tree2Branch::Folder(_, hash_map) => { + Tree2Repr::Folder(hash_map.keys().cloned().collect::>()) + } + } + } +} + +impl Tree2 { + fn get(&self, path: &str) -> Result { + let elements = if path.is_empty() { + Vec::new() + } else { + path.split("/").collect::>() + }; + + // let elements = path.split("/").collect::>(); + + let branch = self.root.get_path(&elements, 0)?; + Ok(branch.to_repr()) + } +} + +impl Default for Tree2 { + fn default() -> Self { + Self { + root: Tree2Branch::Folder( + "ROOT".into(), + HashMap::from([ + ("File A".into(), Tree2Branch::File("A".into())), + ("File B".into(), Tree2Branch::File("B".into())), + ( + "Folder C".into(), + Tree2Branch::Folder( + "A".into(), + HashMap::from([ + ("File A".into(), Tree2Branch::File("A".into())), + ("File B".into(), Tree2Branch::File("B".into())), + ]), + ), + ), + ]), + ), + } + } +} + +impl Server { + pub async fn get_tree2( + State(server): State, + Path(path): Path, + Extension(_): Extension, + ) -> Json { + debug!("GET /api/interface/{}", path); + + let result = (|| -> Result<_, String> { + let interface = server.tree.get(&path)?; + + Ok(interface) + })(); + + Json(serde_json::to_value(result).unwrap()) + } + pub async fn get_tree2_root( + State(server): State, + Extension(extension): Extension, + ) -> Json { + Self::get_tree2(State(server), Path("".into()), Extension(extension)).await + } +}