mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2024-11-05 13:15:21 +00:00
feat: Update tabs.rs to use tab data
This commit is contained in:
parent
d07946fc41
commit
e73c978d95
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -672,9 +672,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.122"
|
version = "1.0.124"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
|
checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
|
@ -14,8 +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"] }
|
serde = { version = "1.0.205", features = ["derive"] }
|
||||||
|
serde_json = "1.0.124"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "linutil"
|
name = "linutil"
|
||||||
|
|
|
@ -7,7 +7,6 @@ use ratatui::{
|
||||||
widgets::{Block, Borders, List},
|
widgets::{Block, Borders, List},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub struct FloatingText {
|
pub struct FloatingText {
|
||||||
text: Vec<String>,
|
text: Vec<String>,
|
||||||
|
@ -19,7 +18,7 @@ impl FloatingText {
|
||||||
Self { text, scroll: 0 }
|
Self { text, scroll: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_command(command: &Command, mut full_path: PathBuf) -> Option<Self> {
|
pub fn from_command(command: &Command) -> Option<Self> {
|
||||||
let lines = match command {
|
let lines = match command {
|
||||||
Command::Raw(cmd) => {
|
Command::Raw(cmd) => {
|
||||||
// Reconstruct the line breaks and file formatting after the
|
// Reconstruct the line breaks and file formatting after the
|
||||||
|
@ -27,9 +26,8 @@ impl FloatingText {
|
||||||
cmd.lines().map(|line| line.to_string()).collect()
|
cmd.lines().map(|line| line.to_string()).collect()
|
||||||
}
|
}
|
||||||
Command::LocalFile(file_path) => {
|
Command::LocalFile(file_path) => {
|
||||||
full_path.push(file_path);
|
let file_contents = std::fs::read_to_string(file_path)
|
||||||
let file_contents = std::fs::read_to_string(&full_path)
|
.map_err(|_| format!("File not found: {:?}", file_path))
|
||||||
.map_err(|_| format!("File not found: {:?}", &full_path))
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
file_contents.lines().map(|line| line.to_string()).collect()
|
file_contents.lines().map(|line| line.to_string()).collect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ fn main() -> std::io::Result<()> {
|
||||||
.extract(temp_dir.path())
|
.extract(temp_dir.path())
|
||||||
.expect("Failed to extract the saved directory");
|
.expect("Failed to extract the saved directory");
|
||||||
|
|
||||||
let mut state = AppState::new(theme, temp_dir.path().to_owned());
|
let mut state = AppState::new(theme, temp_dir.path(), args.override_validation);
|
||||||
|
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
stdout().execute(EnterAlternateScreen)?;
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
|
@ -13,7 +13,7 @@ use ratatui::{
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
io::Write,
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::PathBuf,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
};
|
};
|
||||||
|
@ -126,7 +126,7 @@ impl FloatContent for RunningCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunningCommand {
|
impl RunningCommand {
|
||||||
pub fn new(command: Command, temp_path: &Path) -> Self {
|
pub fn new(command: Command) -> Self {
|
||||||
let pty_system = NativePtySystem::default();
|
let pty_system = NativePtySystem::default();
|
||||||
|
|
||||||
// Build the command based on the provided Command enum variant
|
// Build the command based on the provided Command enum variant
|
||||||
|
@ -142,8 +142,6 @@ impl RunningCommand {
|
||||||
Command::None => panic!("Command::None was treated as a command"),
|
Command::None => panic!("Command::None was treated as a command"),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.cwd(temp_path);
|
|
||||||
|
|
||||||
// Open a pseudo-terminal with initial size
|
// Open a pseudo-terminal with initial size
|
||||||
let pair = pty_system
|
let pair = pty_system
|
||||||
.openpty(PtySize {
|
.openpty(PtySize {
|
||||||
|
|
13
src/state.rs
13
src/state.rs
|
@ -14,13 +14,11 @@ use ratatui::{
|
||||||
widgets::{Block, Borders, List, ListState, Paragraph},
|
widgets::{Block, Borders, List, ListState, Paragraph},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::Path;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
/// Selected theme
|
/// Selected theme
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
/// Path to the root of the unpacked files in /tmp
|
|
||||||
temp_path: PathBuf,
|
|
||||||
/// Currently focused area
|
/// Currently focused area
|
||||||
focus: Focus,
|
focus: Focus,
|
||||||
/// List of tabs
|
/// List of tabs
|
||||||
|
@ -53,12 +51,11 @@ struct ListEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(theme: Theme, temp_path: PathBuf) -> Self {
|
pub fn new(theme: Theme, temp_path: &Path, override_validation: bool) -> Self {
|
||||||
let tabs = crate::tabs::get_tabs(&temp_path, true);
|
let tabs = crate::tabs::get_tabs(temp_path, !override_validation);
|
||||||
let root_id = tabs[0].tree.root().id();
|
let root_id = tabs[0].tree.root().id();
|
||||||
let mut state = Self {
|
let mut state = Self {
|
||||||
theme,
|
theme,
|
||||||
temp_path,
|
|
||||||
focus: Focus::List,
|
focus: Focus::List,
|
||||||
tabs,
|
tabs,
|
||||||
current_tab: ListState::default().with_selected(Some(0)),
|
current_tab: ListState::default().with_selected(Some(0)),
|
||||||
|
@ -304,14 +301,14 @@ impl AppState {
|
||||||
}
|
}
|
||||||
fn enable_preview(&mut self) {
|
fn enable_preview(&mut self) {
|
||||||
if let Some(command) = self.get_selected_command(false) {
|
if let Some(command) = self.get_selected_command(false) {
|
||||||
if let Some(preview) = FloatingText::from_command(&command, self.temp_path.clone()) {
|
if let Some(preview) = FloatingText::from_command(&command) {
|
||||||
self.spawn_float(preview, 80, 80);
|
self.spawn_float(preview, 80, 80);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn handle_enter(&mut self) {
|
fn handle_enter(&mut self) {
|
||||||
if let Some(cmd) = self.get_selected_command(true) {
|
if let Some(cmd) = self.get_selected_command(true) {
|
||||||
let command = RunningCommand::new(cmd, &self.temp_path);
|
let command = RunningCommand::new(cmd);
|
||||||
self.spawn_float(command, 80, 80);
|
self.spawn_float(command, 80, 80);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
208
src/tabs.rs
208
src/tabs.rs
|
@ -1,28 +1,36 @@
|
||||||
use crate::running_command::Command;
|
use crate::running_command::Command;
|
||||||
use ego_tree::{NodeId, Tree};
|
use ego_tree::{NodeMut, Tree};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::path::{Path, PathBuf};
|
||||||
collections::HashMap,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct ScriptInfo {
|
struct TabEntry {
|
||||||
// Path to the script file in the UI, formatted as an array of directory names (first of which being the tab)
|
name: String,
|
||||||
ui_path: Vec<String>,
|
data: Vec<Entry>,
|
||||||
#[allow(dead_code)]
|
|
||||||
#[serde(default)]
|
|
||||||
// Description: Currently unused field, should be added in the future
|
|
||||||
description: String,
|
|
||||||
#[serde(default)]
|
|
||||||
// Requirements that must be met for the script to be displayed
|
|
||||||
preconditions: Option<Vec<Precondition>>,
|
|
||||||
#[serde(default)]
|
|
||||||
// Optional command. This is used for adding "raw" commands to the UI.
|
|
||||||
command: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptInfo {
|
#[derive(Deserialize)]
|
||||||
|
enum Entry {
|
||||||
|
#[serde(rename = "directory")]
|
||||||
|
Directory(EntryData<Vec<Entry>>),
|
||||||
|
#[serde(rename = "command")]
|
||||||
|
Command(EntryData<String>),
|
||||||
|
#[serde(rename = "script")]
|
||||||
|
Script(EntryData<PathBuf>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct EntryData<T> {
|
||||||
|
name: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[serde(default)]
|
||||||
|
description: String,
|
||||||
|
data: T,
|
||||||
|
#[serde(default)]
|
||||||
|
preconditions: Option<Vec<Precondition>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> EntryData<T> {
|
||||||
fn is_supported(&self) -> bool {
|
fn is_supported(&self) -> bool {
|
||||||
self.preconditions.as_deref().map_or(true, |preconditions| {
|
self.preconditions.as_deref().map_or(true, |preconditions| {
|
||||||
preconditions.iter().all(
|
preconditions.iter().all(
|
||||||
|
@ -79,107 +87,79 @@ pub struct ListNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_tabs(command_dir: &Path, validate: bool) -> Vec<Tab> {
|
pub fn get_tabs(command_dir: &Path, validate: bool) -> Vec<Tab> {
|
||||||
let scripts = get_script_list(command_dir);
|
let tab_files =
|
||||||
|
std::fs::read_to_string(command_dir.join("tabs.json")).expect("Failed to read tabs.json");
|
||||||
|
let tab_files: Vec<PathBuf> =
|
||||||
|
serde_json::from_str(&tab_files).expect("Failed to parse tabs.json");
|
||||||
|
let tabs = tab_files.into_iter().map(|path| {
|
||||||
|
let file = command_dir.join(&path);
|
||||||
|
let directory = file.parent().unwrap().to_owned();
|
||||||
|
let data =
|
||||||
|
std::fs::read_to_string(command_dir.join(path)).expect("Failed to read tab data");
|
||||||
|
let mut tab_data: TabEntry = serde_json::from_str(&data).expect("Failed to parse tab data");
|
||||||
|
|
||||||
let mut paths: HashMap<Vec<String>, (String, NodeId)> = HashMap::new();
|
if validate {
|
||||||
let mut tabs: Vec<Tab> = Vec::new();
|
filter_entries(&mut tab_data.data);
|
||||||
|
}
|
||||||
|
(tab_data, directory)
|
||||||
|
});
|
||||||
|
|
||||||
for (json_file, script) in scripts {
|
let tabs: Vec<Tab> = tabs
|
||||||
let json_text = std::fs::read_to_string(&json_file).unwrap();
|
.map(|(TabEntry { name, data }, directory)| {
|
||||||
let script_info: ScriptInfo =
|
let mut tree = Tree::new(ListNode {
|
||||||
serde_json::from_str(&json_text).expect("Unexpected JSON input");
|
name: "root".to_string(),
|
||||||
if validate && !script_info.is_supported() {
|
command: Command::None,
|
||||||
continue;
|
});
|
||||||
}
|
let mut root = tree.root_mut();
|
||||||
if script_info.ui_path.len() < 2 {
|
create_directory(data, &mut root, &directory);
|
||||||
panic!(
|
Tab { name, tree }
|
||||||
"UI path must contain a tab. Ensure that {} has correct data",
|
})
|
||||||
json_file.display()
|
.collect();
|
||||||
);
|
|
||||||
}
|
|
||||||
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];
|
|
||||||
// Create tabs and directories which don't yet exist
|
|
||||||
if !paths.contains_key(path) {
|
|
||||||
let path = path.to_vec();
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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() {
|
if tabs.is_empty() {
|
||||||
panic!("No tabs found.");
|
panic!("No tabs found");
|
||||||
}
|
}
|
||||||
tabs
|
tabs
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_script_list(directory: &Path) -> Vec<(PathBuf, PathBuf)> {
|
fn filter_entries(entries: &mut Vec<Entry>) {
|
||||||
let mut entries = std::fs::read_dir(directory)
|
entries.retain_mut(|entry| match entry {
|
||||||
.expect("Command directory does not exist.")
|
Entry::Script(entry) => entry.is_supported(),
|
||||||
.flatten()
|
Entry::Command(entry) => entry.is_supported(),
|
||||||
.collect::<Vec<_>>();
|
Entry::Directory(entry) if !entry.is_supported() => false,
|
||||||
entries.sort_by_key(|d| d.path());
|
Entry::Directory(entry) => {
|
||||||
|
filter_entries(&mut entry.data);
|
||||||
entries
|
!entry.data.is_empty()
|
||||||
.into_iter()
|
}
|
||||||
.filter_map(|entry| {
|
});
|
||||||
let path = entry.path();
|
}
|
||||||
// Recursively iterate through directories
|
|
||||||
if entry.file_type().map_or(false, |f| f.is_dir()) {
|
fn create_directory(data: Vec<Entry>, node: &mut NodeMut<ListNode>, command_dir: &Path) {
|
||||||
Some(get_script_list(&path))
|
for entry in data {
|
||||||
} else {
|
match entry {
|
||||||
let is_json = path.extension().map_or(false, |ext| ext == "json");
|
Entry::Directory(entry) => {
|
||||||
let script = path.with_extension("sh");
|
let mut node = node.append(ListNode {
|
||||||
is_json.then_some(vec![(path, script)])
|
name: entry.name,
|
||||||
}
|
command: Command::None,
|
||||||
})
|
});
|
||||||
.flatten()
|
create_directory(entry.data, &mut node, command_dir);
|
||||||
.collect()
|
}
|
||||||
|
Entry::Command(entry) => {
|
||||||
|
node.append(ListNode {
|
||||||
|
name: entry.name,
|
||||||
|
command: Command::Raw(entry.data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Entry::Script(entry) => {
|
||||||
|
let dir = command_dir.join(entry.data);
|
||||||
|
if !dir.exists() {
|
||||||
|
panic!("Script {} does not exist", dir.display());
|
||||||
|
}
|
||||||
|
node.append(ListNode {
|
||||||
|
name: entry.name,
|
||||||
|
command: Command::LocalFile(dir),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user