mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2024-11-05 13:15:21 +00:00
feat: Add logic for runtime tab creation based on JSON data
This commit is contained in:
parent
548cfa8e36
commit
c18c76c694
40
Cargo.lock
generated
40
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
30
src/state.rs
30
src/state.rs
|
@ -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();
|
||||
}
|
||||
|
|
311
src/tabs.rs
311
src/tabs.rs
|
@ -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;
|
||||
}
|
||||
}),
|
||||
},
|
||||
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"),
|
||||
if script_info.ui_path.len() < 2 {
|
||||
panic!(
|
||||
"UI path must contain a tab. Ensure that {} has correct data",
|
||||
json_file.display()
|
||||
);
|
||||
}
|
||||
}),
|
||||
},
|
||||
Tab {
|
||||
name: "Security",
|
||||
tree: tree!(ListNode {
|
||||
name: "root",
|
||||
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,
|
||||
} => {
|
||||
ListNode {
|
||||
name: "Firewall Baselines (CTT)",
|
||||
command: Command::LocalFile("security/firewall-baselines.sh"),
|
||||
}),
|
||||
};
|
||||
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: "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()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user