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",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
version = "0.6.5"
@ -644,6 +650,38 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "serial"
version = "0.4.0"
@ -844,6 +882,8 @@ dependencies = [
"oneshot",
"portable-pty",
"ratatui",
"serde",
"serde_json",
"tempdir",
"tui-term",
]

View File

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

View File

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

View File

@ -2,7 +2,7 @@ use crate::{
float::{Float, FloatContent},
floating_text::FloatingText,
running_command::{Command, RunningCommand},
tabs::{ListNode, TABS},
tabs::{ListNode, Tab},
theme::Theme,
};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
@ -23,6 +23,8 @@ pub struct AppState {
temp_path: PathBuf,
/// Currently focused area
focus: Focus,
/// List of tabs
tabs: Vec<Tab>,
/// Current tab
current_tab: ListState,
/// Current search query
@ -52,11 +54,13 @@ struct ListEntry {
impl AppState {
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 {
theme,
temp_path,
focus: Focus::List,
tabs,
current_tab: ListState::default().with_selected(Some(0)),
search_query: String::new(),
items: vec![],
@ -67,7 +71,8 @@ impl AppState {
state
}
pub fn draw(&mut self, frame: &mut Frame) {
let longest_tab_display_len = TABS
let longest_tab_display_len = self
.tabs
.iter()
.map(|tab| tab.name.len() + self.theme.tab_icon.len())
.max()
@ -85,7 +90,11 @@ impl AppState {
.constraints([Constraint::Length(3), Constraint::Min(1)])
.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 {
Style::default().reversed().fg(self.theme.tab_color)
@ -186,7 +195,7 @@ impl AppState {
self.focus = Focus::List
}
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.refresh_tab();
@ -220,7 +229,7 @@ impl AppState {
}
pub fn update_items(&mut self) {
if self.search_query.is_empty() {
let curr = TABS[self.current_tab.selected().unwrap()]
let curr = self.tabs[self.current_tab.selected().unwrap()]
.tree
.get(*self.visit_stack.last().unwrap())
.unwrap();
@ -237,7 +246,7 @@ impl AppState {
self.items.clear();
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()];
while let Some(node_id) = stack.pop() {
let node = tab.tree.get(node_id).unwrap();
@ -255,7 +264,7 @@ impl AppState {
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)
@ -319,7 +328,10 @@ impl AppState {
self.update_items();
}
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.update_items();
}

View File

@ -1,162 +1,179 @@
use std::sync::LazyLock;
use ego_tree::{tree, Tree};
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 name: &'static str,
pub name: String,
pub tree: Tree<ListNode>,
}
#[derive(Clone)]
#[derive(Clone, Hash, Eq, PartialEq)]
pub struct ListNode {
pub name: &'static str,
pub name: String,
pub command: Command,
}
pub static TABS: LazyLock<Vec<Tab>> = LazyLock::new(|| {
vec![
Tab {
name: "System Setup",
tree: tree!(ListNode {
name: "root",
command: Command::None,
} => {
ListNode {
name: "Full System Update",
command: Command::LocalFile("system-update.sh"),
},
ListNode {
name: "Build Prerequisites",
command: Command::LocalFile("system-setup/1-compile-setup.sh"),
},
ListNode {
name: "Gaming Dependencies",
command: Command::LocalFile("system-setup/2-gaming-setup.sh"),
},
ListNode {
name: "Global Theme",
command: Command::LocalFile("system-setup/3-global-theme.sh"),
},
ListNode {
name: "Remove Snaps",
command: Command::LocalFile("system-setup/4-remove-snaps.sh"),
pub fn get_tabs(command_dir: &Path, validate: bool) -> Vec<Tab> {
let scripts = get_script_list(command_dir);
let mut paths: HashMap<Vec<String>, (String, NodeId)> = HashMap::new();
let mut tabs: Vec<Tab> = Vec::new();
for (json_file, script) in scripts {
let json_text = std::fs::read_to_string(&json_file).unwrap();
let script_info: ScriptInfo =
serde_json::from_str(&json_text).expect("Unexpected JSON input");
if validate && !script_info.is_supported() {
continue;
}
if script_info.ui_path.len() < 2 {
panic!(
"UI path must contain a tab. Ensure that {} has correct data",
json_file.display()
);
}
let command = match script_info.command {
Some(command) => Command::Raw(command),
None if script.exists() => Command::LocalFile(script),
_ => panic!(
"Command not specified & matching script does not exist for JSON {}",
json_file.display()
),
};
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 {
name: "Applications Setup",
tree: tree!(ListNode {
name: "root",
command: Command::None,
} => {
ListNode {
name: "Alacritty",
command: Command::LocalFile("applications-setup/alacritty-setup.sh"),
},
ListNode {
name: "Bash Prompt",
command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\""),
},
ListNode {
name: "DWM-Titus",
command: Command::LocalFile("applications-setup/dwmtitus-setup.sh")
},
ListNode {
name: "Kitty",
command: Command::LocalFile("applications-setup/kitty-setup.sh")
},
ListNode {
name: "Neovim",
command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/neovim/main/setup.sh)\""),
},
ListNode {
name: "Rofi",
command: Command::LocalFile("applications-setup/rofi-setup.sh"),
},
ListNode {
name: "ZSH Prompt",
command: Command::LocalFile("applications-setup/zsh-setup.sh"),
}
}),
},
Tab {
name: "Security",
tree: tree!(ListNode {
name: "root",
command: Command::None,
} => {
ListNode {
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"),
}
},
}),
},
]
});
}
}
let (tab, parent_id) = paths
.get(&script_info.ui_path[..script_info.ui_path.len() - 1])
.unwrap();
let tab = tabs
.iter_mut()
.find(|Tab { name, .. }| name == tab)
.unwrap();
let mut parent = tab.tree.get_mut(*parent_id).unwrap();
let command = ListNode {
name: script_info.ui_path.last().unwrap().clone(),
command,
};
parent.append(command);
}
if tabs.is_empty() {
panic!("No tabs found.");
}
tabs
}
fn get_script_list(directory: &Path) -> Vec<(PathBuf, PathBuf)> {
let mut entries = std::fs::read_dir(directory)
.expect("Command directory does not exist.")
.flatten()
.collect::<Vec<_>>();
entries.sort_by_key(|d| d.path());
entries
.into_iter()
.filter_map(|entry| {
let path = entry.path();
// Recursively iterate through directories
if entry.file_type().map_or(false, |f| f.is_dir()) {
Some(get_script_list(&path))
} else {
let is_json = path.extension().map_or(false, |ext| ext == "json");
let script = path.with_extension("sh");
(is_json).then_some(vec![(path, script)])
}
})
.flatten()
.collect()
}