feat: Add logic for runtime tab creation based on JSON data

This commit is contained in:
Liam 2024-08-10 19:52:49 -07:00
parent 548cfa8e36
commit c18c76c694
No known key found for this signature in database
5 changed files with 235 additions and 164 deletions

40
Cargo.lock generated
View File

@ -420,6 +420,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.6.5" version = "0.6.5"
@ -644,6 +650,38 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.205"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.205"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serial" name = "serial"
version = "0.4.0" version = "0.4.0"
@ -844,6 +882,8 @@ dependencies = [
"oneshot", "oneshot",
"portable-pty", "portable-pty",
"ratatui", "ratatui",
"serde",
"serde_json",
"tempdir", "tempdir",
"tui-term", "tui-term",
] ]

View File

@ -14,6 +14,8 @@ ratatui = "0.27.0"
tui-term = "0.1.12" tui-term = "0.1.12"
include_dir = "0.7.4" include_dir = "0.7.4"
tempdir = "0.3.7" tempdir = "0.3.7"
serde_json = "1.0.122"
serde = { version = "1.0.205", features = ["derive"] }
[[bin]] [[bin]]
name = "linutil" name = "linutil"

View File

@ -13,7 +13,7 @@ use ratatui::{
}; };
use std::{ use std::{
io::Write, io::Write,
path::Path, path::{Path, PathBuf},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread::JoinHandle, thread::JoinHandle,
}; };
@ -22,10 +22,10 @@ use tui_term::{
widget::PseudoTerminal, widget::PseudoTerminal,
}; };
#[derive(Clone)] #[derive(Clone, Hash, Eq, PartialEq)]
pub enum Command { pub enum Command {
Raw(&'static str), Raw(String),
LocalFile(&'static str), LocalFile(PathBuf),
None, // Directory None, // Directory
} }

View File

@ -2,7 +2,7 @@ use crate::{
float::{Float, FloatContent}, float::{Float, FloatContent},
floating_text::FloatingText, floating_text::FloatingText,
running_command::{Command, RunningCommand}, running_command::{Command, RunningCommand},
tabs::{ListNode, TABS}, tabs::{ListNode, Tab},
theme::Theme, theme::Theme,
}; };
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
@ -23,6 +23,8 @@ pub struct AppState {
temp_path: PathBuf, temp_path: PathBuf,
/// Currently focused area /// Currently focused area
focus: Focus, focus: Focus,
/// List of tabs
tabs: Vec<Tab>,
/// Current tab /// Current tab
current_tab: ListState, current_tab: ListState,
/// Current search query /// Current search query
@ -52,11 +54,13 @@ struct ListEntry {
impl AppState { impl AppState {
pub fn new(theme: Theme, temp_path: PathBuf) -> Self { pub fn new(theme: Theme, temp_path: PathBuf) -> Self {
let root_id = TABS[0].tree.root().id(); let tabs = crate::tabs::get_tabs(&temp_path, true);
let root_id = tabs[0].tree.root().id();
let mut state = Self { let mut state = Self {
theme, theme,
temp_path, temp_path,
focus: Focus::List, focus: Focus::List,
tabs,
current_tab: ListState::default().with_selected(Some(0)), current_tab: ListState::default().with_selected(Some(0)),
search_query: String::new(), search_query: String::new(),
items: vec![], items: vec![],
@ -67,7 +71,8 @@ impl AppState {
state state
} }
pub fn draw(&mut self, frame: &mut Frame) { pub fn draw(&mut self, frame: &mut Frame) {
let longest_tab_display_len = TABS let longest_tab_display_len = self
.tabs
.iter() .iter()
.map(|tab| tab.name.len() + self.theme.tab_icon.len()) .map(|tab| tab.name.len() + self.theme.tab_icon.len())
.max() .max()
@ -85,7 +90,11 @@ impl AppState {
.constraints([Constraint::Length(3), Constraint::Min(1)]) .constraints([Constraint::Length(3), Constraint::Min(1)])
.split(horizontal[0]); .split(horizontal[0]);
let tabs = TABS.iter().map(|tab| tab.name).collect::<Vec<_>>(); let tabs = self
.tabs
.iter()
.map(|tab| tab.name.as_str())
.collect::<Vec<_>>();
let tab_hl_style = if let Focus::TabList = self.focus { let tab_hl_style = if let Focus::TabList = self.focus {
Style::default().reversed().fg(self.theme.tab_color) Style::default().reversed().fg(self.theme.tab_color)
@ -186,7 +195,7 @@ impl AppState {
self.focus = Focus::List self.focus = Focus::List
} }
KeyCode::Char('j') | KeyCode::Down KeyCode::Char('j') | KeyCode::Down
if self.current_tab.selected().unwrap() + 1 < TABS.len() => if self.current_tab.selected().unwrap() + 1 < self.tabs.len() =>
{ {
self.current_tab.select_next(); self.current_tab.select_next();
self.refresh_tab(); self.refresh_tab();
@ -220,7 +229,7 @@ impl AppState {
} }
pub fn update_items(&mut self) { pub fn update_items(&mut self) {
if self.search_query.is_empty() { if self.search_query.is_empty() {
let curr = TABS[self.current_tab.selected().unwrap()] let curr = self.tabs[self.current_tab.selected().unwrap()]
.tree .tree
.get(*self.visit_stack.last().unwrap()) .get(*self.visit_stack.last().unwrap())
.unwrap(); .unwrap();
@ -237,7 +246,7 @@ impl AppState {
self.items.clear(); self.items.clear();
let query_lower = self.search_query.to_lowercase(); let query_lower = self.search_query.to_lowercase();
for tab in TABS.iter() { for tab in self.tabs.iter() {
let mut stack = vec![tab.tree.root().id()]; let mut stack = vec![tab.tree.root().id()];
while let Some(node_id) = stack.pop() { while let Some(node_id) = stack.pop() {
let node = tab.tree.get(node_id).unwrap(); let node = tab.tree.get(node_id).unwrap();
@ -255,7 +264,7 @@ impl AppState {
stack.extend(node.children().map(|child| child.id())); stack.extend(node.children().map(|child| child.id()));
} }
} }
self.items.sort_by(|a, b| a.node.name.cmp(b.node.name)); self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name));
} }
} }
/// Checks ehther the current tree node is the root node (can we go up the tree or no) /// Checks ehther the current tree node is the root node (can we go up the tree or no)
@ -319,7 +328,10 @@ impl AppState {
self.update_items(); self.update_items();
} }
fn refresh_tab(&mut self) { fn refresh_tab(&mut self) {
self.visit_stack = vec![TABS[self.current_tab.selected().unwrap()].tree.root().id()]; self.visit_stack = vec![self.tabs[self.current_tab.selected().unwrap()]
.tree
.root()
.id()];
self.selection.select(Some(0)); self.selection.select(Some(0));
self.update_items(); self.update_items();
} }

View File

@ -1,162 +1,179 @@
use std::sync::LazyLock;
use ego_tree::{tree, Tree};
use crate::running_command::Command; use crate::running_command::Command;
use ego_tree::{tree, NodeId, Tree};
use serde::Deserialize;
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
sync::LazyLock,
};
#[derive(Deserialize)]
struct ScriptInfo {
ui_path: Vec<String>,
#[serde(default)]
description: String,
#[serde(default)]
preconditions: Option<Vec<Precondition>>,
#[serde(default)]
command: Option<String>,
}
impl ScriptInfo {
fn is_supported(&self) -> bool {
self.preconditions.as_deref().map_or(true, |preconditions| {
preconditions.iter().all(
|Precondition {
matches,
data,
values,
}| {
match data {
SystemDataType::Environment(var_name) => std::env::var(var_name)
.map_or(false, |var| values.contains(&var) == *matches),
SystemDataType::File(path) => {
std::fs::read_to_string(path).map_or(false, |data| {
values
.iter()
.any(|matching_value| data.contains(matching_value))
== *matches
})
}
}
},
)
})
}
}
#[derive(Deserialize)]
struct Precondition {
// If true, the data must be contained within the list of values.
// Otherwise, the data must not be contained within the list of values
matches: bool,
data: SystemDataType,
values: Vec<String>,
}
#[derive(Deserialize)]
enum SystemDataType {
#[serde(rename = "environment")]
Environment(String),
#[serde(rename = "file")]
File(PathBuf),
}
#[derive(Hash, Eq, PartialEq)]
pub struct Tab { pub struct Tab {
pub name: &'static str, pub name: String,
pub tree: Tree<ListNode>, pub tree: Tree<ListNode>,
} }
#[derive(Clone)] #[derive(Clone, Hash, Eq, PartialEq)]
pub struct ListNode { pub struct ListNode {
pub name: &'static str, pub name: String,
pub command: Command, pub command: Command,
} }
pub static TABS: LazyLock<Vec<Tab>> = LazyLock::new(|| { pub fn get_tabs(command_dir: &Path, validate: bool) -> Vec<Tab> {
vec![ let scripts = get_script_list(command_dir);
Tab {
name: "System Setup", let mut paths: HashMap<Vec<String>, (String, NodeId)> = HashMap::new();
tree: tree!(ListNode { let mut tabs: Vec<Tab> = Vec::new();
name: "root",
command: Command::None, for (json_file, script) in scripts {
} => { let json_text = std::fs::read_to_string(&json_file).unwrap();
ListNode { let script_info: ScriptInfo =
name: "Full System Update", serde_json::from_str(&json_text).expect("Unexpected JSON input");
command: Command::LocalFile("system-update.sh"), if validate && !script_info.is_supported() {
}, continue;
ListNode { }
name: "Build Prerequisites", if script_info.ui_path.len() < 2 {
command: Command::LocalFile("system-setup/1-compile-setup.sh"), panic!(
}, "UI path must contain a tab. Ensure that {} has correct data",
ListNode { json_file.display()
name: "Gaming Dependencies", );
command: Command::LocalFile("system-setup/2-gaming-setup.sh"), }
}, let command = match script_info.command {
ListNode { Some(command) => Command::Raw(command),
name: "Global Theme", None if script.exists() => Command::LocalFile(script),
command: Command::LocalFile("system-setup/3-global-theme.sh"), _ => panic!(
}, "Command not specified & matching script does not exist for JSON {}",
ListNode { json_file.display()
name: "Remove Snaps", ),
command: Command::LocalFile("system-setup/4-remove-snaps.sh"), };
for path_index in 1..script_info.ui_path.len() {
let path = script_info.ui_path[..path_index].to_vec();
if !paths.contains_key(&path) {
let tab_name = script_info.ui_path[0].clone();
if path_index == 1 {
let tab = Tab {
name: tab_name.clone(),
tree: Tree::new(ListNode {
name: "root".to_string(),
command: Command::None,
}),
};
let root_id = tab.tree.root().id();
tabs.push(tab);
paths.insert(path, (tab_name, root_id));
} else {
let parent_path = &script_info.ui_path[..path_index - 1];
let (tab, parent_id) = paths.get(parent_path).unwrap();
let tab = tabs
.iter_mut()
.find(|Tab { name, .. }| name == tab)
.unwrap();
let mut parent = tab.tree.get_mut(*parent_id).unwrap();
let new_node = ListNode {
name: script_info.ui_path[path_index - 1].clone(),
command: Command::None,
};
let new_id = parent.append(new_node).id();
paths.insert(path, (tab_name, new_id));
} }
}), }
}, }
Tab { let (tab, parent_id) = paths
name: "Applications Setup", .get(&script_info.ui_path[..script_info.ui_path.len() - 1])
tree: tree!(ListNode { .unwrap();
name: "root", let tab = tabs
command: Command::None, .iter_mut()
} => { .find(|Tab { name, .. }| name == tab)
ListNode { .unwrap();
name: "Alacritty", let mut parent = tab.tree.get_mut(*parent_id).unwrap();
command: Command::LocalFile("applications-setup/alacritty-setup.sh"),
}, let command = ListNode {
ListNode { name: script_info.ui_path.last().unwrap().clone(),
name: "Bash Prompt", command,
command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\""), };
}, parent.append(command);
ListNode { }
name: "DWM-Titus", if tabs.is_empty() {
command: Command::LocalFile("applications-setup/dwmtitus-setup.sh") panic!("No tabs found.");
}, }
ListNode { tabs
name: "Kitty", }
command: Command::LocalFile("applications-setup/kitty-setup.sh")
}, fn get_script_list(directory: &Path) -> Vec<(PathBuf, PathBuf)> {
ListNode { let mut entries = std::fs::read_dir(directory)
name: "Neovim", .expect("Command directory does not exist.")
command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/neovim/main/setup.sh)\""), .flatten()
}, .collect::<Vec<_>>();
ListNode { entries.sort_by_key(|d| d.path());
name: "Rofi",
command: Command::LocalFile("applications-setup/rofi-setup.sh"), entries
}, .into_iter()
ListNode { .filter_map(|entry| {
name: "ZSH Prompt", let path = entry.path();
command: Command::LocalFile("applications-setup/zsh-setup.sh"), // Recursively iterate through directories
} if entry.file_type().map_or(false, |f| f.is_dir()) {
}), Some(get_script_list(&path))
}, } else {
Tab { let is_json = path.extension().map_or(false, |ext| ext == "json");
name: "Security", let script = path.with_extension("sh");
tree: tree!(ListNode { (is_json).then_some(vec![(path, script)])
name: "root", }
command: Command::None, })
} => { .flatten()
ListNode { .collect()
name: "Firewall Baselines (CTT)", }
command: Command::LocalFile("security/firewall-baselines.sh"),
}
}),
},
Tab {
name: "Utilities",
tree: tree!(ListNode {
name: "root",
command: Command::None,
} => {
ListNode {
name: "Wifi Manager",
command: Command::LocalFile("utils/wifi-control.sh"),
},
ListNode {
name: "Bluetooth Manager",
command: Command::LocalFile("utils/bluetooth-control.sh"),
},
ListNode {
name: "MonitorControl(xorg)",
command: Command::None,
} => {
ListNode {
name: "Set Resolution",
command: Command::LocalFile("utils/monitor-control/set_resolutions.sh"),
},
ListNode {
name: "Duplicate Displays",
command: Command::LocalFile("utils/monitor-control/duplicate_displays.sh"),
},
ListNode {
name: "Extend Displays",
command: Command::LocalFile("utils/monitor-control/extend_displays.sh"),
},
ListNode {
name: "Auto Detect Displays",
command: Command::LocalFile("utils/monitor-control/auto_detect_displays.sh"),
},
ListNode {
name: "Enable Monitor",
command: Command::LocalFile("utils/monitor-control/enable_monitor.sh"),
},
ListNode {
name: "Disable Monitor",
command: Command::LocalFile("utils/monitor-control/disable_monitor.sh"),
},
ListNode {
name: "Set Primary Monitor",
command: Command::LocalFile("utils/monitor-control/set_primary_monitor.sh"),
},
ListNode {
name: "Change Orientation",
command: Command::LocalFile("utils/monitor-control/change_orientation.sh"),
},
ListNode {
name: "Manage Arrangement",
command: Command::LocalFile("utils/monitor-control/manage_arrangement.sh"),
},
ListNode {
name: "Scale Monitors",
command: Command::LocalFile("utils/monitor-control/scale_monitor.sh"),
},
ListNode {
name: "Reset Scaling",
command: Command::LocalFile("utils/monitor-control/reset_scaling.sh"),
}
},
}),
},
]
});