From 21d6d7997f1f16aeb656c02c6e9f739890db5103 Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Mon, 1 Dec 2025 18:56:10 +0100 Subject: [PATCH] Add TUI application and Nix option helpers Introduce a terminal UI with a progress bar and key controls, and add utilities for collecting and querying boolean Nix options. The toggle function also allows flipping a boolean value in the source. --- src/app.rs | 140 ++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/main.rs | 93 ++++++++++++++++----- src/nix.rs | 223 ++++++++++++++++++++++++++++++++++++++++++++++++--- src/test.nix | 6 +- 5 files changed, 433 insertions(+), 31 deletions(-) create mode 100644 src/app.rs create mode 100644 src/lib.rs diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..f1682fa --- /dev/null +++ b/src/app.rs @@ -0,0 +1,140 @@ +use crossterm::{ + event, + event::{KeyCode, KeyEvent, KeyEventKind}, +}; +use ratatui::{ + DefaultTerminal, Frame, + prelude::{Buffer, Constraint, Layout, Rect, Stylize}, + style::{Color, Style}, + symbols::border, + text::Line, + widgets::{Block, Gauge, Widget}, +}; +use std::{io, sync::mpsc, thread, time::Duration}; + +pub enum Event { + Input(KeyEvent), + Progress(f64), +} + +pub fn handle_input_events(tx: mpsc::Sender) { + loop { + match event::read() { + Ok(ev) => { + if let event::Event::Key(key_event) = ev { + if tx.send(Event::Input(key_event)).is_err() { + break; + } + } + } + Err(_) => { + continue; + } + } + } +} + +pub fn run_background_thread(tx: mpsc::Sender) { + let mut progress: f64 = 0_f64; + const INCREMENT: f64 = 0.01_f64; + loop { + thread::sleep(Duration::from_millis(100)); + progress += INCREMENT; + progress = progress.min(1_f64); + if tx.send(Event::Progress(progress)).is_err() { + break; + } + } +} + +pub struct App { + pub exit: bool, + pub progress_bar_color: Color, + pub background_progress: f64, +} + +impl App { + pub fn run( + &mut self, + terminal: &mut DefaultTerminal, + rx: mpsc::Receiver, + ) -> io::Result<()> { + while !self.exit { + let event: Event = match rx.recv() { + Ok(ev) => ev, + Err(_) => break, + }; + match event { + Event::Input(key_event) => self.handle_key_event(key_event)?, + Event::Progress(progress) => self.background_progress = progress, + } + terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?; + } + + Ok(()) + } + + fn draw(&self, frame: &mut Frame<'_>) { + frame.render_widget(self, frame.area()); + } + + fn handle_key_event(&mut self, key_event: KeyEvent) -> io::Result<()> { + if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('q') { + self.exit = true; + } else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('c') { + if self.progress_bar_color == Color::Green { + self.progress_bar_color = Color::Red; + } else { + self.progress_bar_color = Color::Green; + } + } + Ok(()) + } +} + +impl Widget for &App { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let vertical_layout: Layout = + Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]); + let [title_area, gauge_area] = vertical_layout.areas(area); + + Line::from("Process overview") + .bold() + .render(title_area, buf); + + let instructions: Line<'_> = Line::from(vec![ + " Change color ".into(), + "".blue().bold(), + " Quit ".into(), + "".blue().bold(), + ]) + .centered(); + + let block: Block<'_> = Block::bordered() + .title(Line::from(" Background processes ")) + .title_bottom(instructions) + .border_set(border::THICK); + + let progress_bar: Gauge<'_> = Gauge::default() + .gauge_style(Style::default().fg(self.progress_bar_color)) + .block(block) + .label(format!( + "Process Bar: {:.2}%", + self.background_progress * 100_f64 + )) + .ratio(self.background_progress); + + progress_bar.render( + Rect { + x: gauge_area.left(), + y: gauge_area.top(), + width: gauge_area.width, + height: 3, + }, + buf, + ); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f959912 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod app; +pub mod nix; diff --git a/src/main.rs b/src/main.rs index 1de2e75..e40a64e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,81 @@ -use rnix::{Parse, Root}; -use std::fs; +use garandos_tui::{ + app::{App, Event, handle_input_events, run_background_thread}, + // nix::{collect_nix_options, get_nix_value_by_path, toggle_bool_at_path}, +}; +use ratatui::{Terminal, prelude::CrosstermBackend, style::Color}; +// use rnix::{Parse, Root, SyntaxNode}; +// use std::fs; +use std::{ + io, + sync::mpsc::{self, Sender}, + thread, +}; -mod nix; +fn main() -> io::Result<()> { + let mut terminal: Terminal> = ratatui::init(); + let mut app: App = App { + exit: false, + progress_bar_color: Color::Green, + background_progress: 0_f64, + }; -fn main() { - let content: String = - fs::read_to_string("src/test.nix").expect("Nie można odczytać pliku src/test.nix"); + let (event_tx, event_rx) = mpsc::channel::(); - let ast: Parse = Root::parse(&content); + let tx_to_input_events: Sender = event_tx.clone(); + thread::spawn(move || { + handle_input_events(tx_to_input_events); + }); - if !ast.errors().is_empty() { - eprintln!("Błędy parsowania:"); - for error in ast.errors() { - eprintln!(" - {}", error); - } - eprintln!(); - } + let tx_to_background_events: Sender = event_tx.clone(); + thread::spawn(move || { + run_background_thread(tx_to_background_events); + }); - let options: Vec<(String, String, bool)> = nix::collect_nix_options(&ast.syntax()); - for (category, path, value) in options { - println!("{}: {} = {};", category, path, value); - } + let app_result: Result<(), io::Error> = app.run(&mut terminal, event_rx); + + ratatui::restore(); + app_result } + +// fn main() { +// let content: String = +// fs::read_to_string("src/test.nix").expect("Nie można odczytać pliku src/test.nix"); + +// let ast: Parse = Root::parse(&content); + +// if !ast.errors().is_empty() { +// eprintln!("Błędy parsowania:"); +// for error in ast.errors() { +// eprintln!(" - {}", error); +// } +// eprintln!(); +// } + +// let node: SyntaxNode = ast.syntax(); +// let options: Vec<(String, String, bool)> = collect_nix_options(&node); + +// const OPTION: &str = "flatpak.enable"; + +// println!("Options:"); +// for (_, path, value) in options { +// if path == OPTION { +// println!("{} = {};", path, value); +// } +// } + +// // if let Some(value) = get_nix_value_by_path(&node, OPTION) { +// // println!("Flatseal ma wartość: {}", value); +// // } + +// let new_node: SyntaxNode = toggle_bool_at_path(&node, OPTION); +// let new_options: Vec<(String, String, bool)> = collect_nix_options(&new_node); + +// println!("\nNew Options:"); +// for (_, path, value) in new_options { +// if path == OPTION { +// println!("{} = {};", path, value); +// } +// } + +// fs::write("src/test.nix", new_node.to_string()).expect("Nie można zapisać tego pliku"); +// } diff --git a/src/nix.rs b/src/nix.rs index fd650ba..51a16df 100644 --- a/src/nix.rs +++ b/src/nix.rs @@ -1,9 +1,220 @@ -use rnix::{NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken}; +use rnix::{NodeOrToken, Root, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize}; +use std::collections::HashMap; +/// Zbiera wszystkie opcje typu `bool` z drzewa składniowego Nix. +/// +/// Jest to niewielka funkcja‑wrapper, która deleguje właściwe zebranie opcji do +/// `collect_nix_options_with_path`, przekazując jako początkową ścieżkę i kategorię +/// pusty ciąg znaków. +/// +/// # Argumenty +/// +/// * `node` – węzeł korzenia drzewa składniowego, od którego rozpoczynamy przeszukiwanie. +/// +/// # Zwraca +/// +/// `Vec<(String, String, bool)>`, gdzie: +/// - pierwszy element (`String`) to nazwa kategorii (pobrana z najbliższego +/// blokowego komentarza `/* … */`); +/// - drugi element (`String`) to pełna ścieżka do opcji (np. `services.ssh.enable`); +/// - trzeci element (`bool`) to wartość logiczna tej opcji. +/// +/// # Przykład +/// +/// ``` +/// use rnix::{Root, SyntaxNode}; +/// // przykładowe źródło Nix +/// let src = r#" +/// /* Services */ +/// services.ssh.enable = true; +/// services.httpd.enable = false; +/// "#; +/// +/// // parsujemy źródło +/// let ast = Root::parse(src); +/// +/// // zbieramy wszystkie opcje bool +/// let options = collect_nix_options(&ast.syntax()); +/// assert_eq!(options.len(), 2); +/// assert!(options.iter().any(|(_, path, val)| path == "services.ssh.enable" && *val)); +/// assert!(options.iter().any(|(_, path, val)| path == "services.httpd.enable" && !*val)); +/// ``` pub fn collect_nix_options(node: &SyntaxNode) -> Vec<(String, String, bool)> { collect_nix_options_with_path(node, "", "") } +/// Pobiera wartość logiczną opcji Nix wskazanej pełną ścieżką. +/// +/// # Argumenty +/// +/// * `node` – korzeń drzewa składniowego Nix (`SyntaxNode`), uzyskany np. +/// z `Root::parse(src).syntax()`. +/// * `query_path` – pełna ścieżka do opcji, np. `services.ssh.enable`. +/// +/// # Zwraca +/// +/// `Some(true)` lub `Some(false)` jeśli opcja istnieje, w przeciwnym razie `None`. +/// +/// # Przykład +/// +/// ``` +/// use rnix::{Root, SyntaxNode}; +/// let src = r#"services.ssh.enable = true;"#; +/// let parse = Root::parse(src); +/// let root: SyntaxNode = parse.node(); +/// +/// // zwraca Some(true) +/// let val = get_nix_value_by_path(&root, "services.ssh.enable"); +/// assert_eq!(val, Some(true)); +/// +/// // nieistniejąca opcja → None +/// let missing = get_nix_value_by_path(&root, "services.httpd.enable"); +/// assert_eq!(missing, None); +/// ``` +pub fn get_nix_value_by_path(node: &SyntaxNode, query_path: &str) -> Option { + let options: Vec<(String, String, bool)> = collect_nix_options(node); + + let map: HashMap<&str, bool> = options + .iter() + .map(|(_, path, val)| (path.as_str(), *val)) + .collect(); + + map.get(query_path).copied() +} + +/// Przełącza (toggle) wartość logiczną opcji w drzewie Nix określonej przez `query_path`. +/// +/// Funkcja odczytuje tekst źródłowy z podanego węzła, znajduje zakres tekstowy +/// odpowiadający wartości boolean i zamienia ją na przeciwną (`true` ↔ `false`). +/// Zwraca nowy węzeł składniowy (`SyntaxNode`) reprezentujący zmodyfikowany +/// kod Nix. Jeśli podana ścieżka nie zostanie znaleziona, zwracany jest węzeł +/// niezmieniony. +/// +/// # Argumenty +/// +/// * `node` – korzeń drzewa składniowego Nix. +/// * `query_path` – pełna ścieżka do opcji, której wartość ma zostać przełączona. +/// +/// # Zwraca +/// +/// `SyntaxNode` będący korzeniem drzewa składniowego po zastosowaniu zmiany. +/// +/// # Przykład +/// +/// ``` +/// use rnix::{Root, SyntaxNode}; +/// let src = r#"services.ssh.enable = true;"#; +/// let parse = Root::parse(src); +/// let root: SyntaxNode = parse.syntax(); +/// +/// // przełączamy wartość +/// let new_root = toggle_bool_at_path(&root, "services.ssh.enable"); +/// let new_src = new_root.text(); +/// assert!(new_src.contains("services.ssh.enable = false")); +/// ``` +pub fn toggle_bool_at_path(node: &SyntaxNode, query_path: &str) -> SyntaxNode { + let src: String = node.text().to_string(); + + fn walk( + syntax_node: &SyntaxNode, + cur_path: &str, + query_path: &str, + ) -> Option<(rnix::TextRange, bool)> { + let children: Vec> = + syntax_node.children_with_tokens().collect(); + + for child in children { + match child { + NodeOrToken::Node(attr_val_node) + if attr_val_node.kind() == SyntaxKind::NODE_ATTRPATH_VALUE => + { + let mut attr_path: String = String::new(); + let mut bool_node: Option = None; + + for grand in attr_val_node.children_with_tokens() { + if let NodeOrToken::Node(grand_node) = grand { + match grand_node.kind() { + SyntaxKind::NODE_ATTRPATH => { + attr_path = grand_node.text().to_string(); + } + SyntaxKind::NODE_IDENT | SyntaxKind::NODE_LITERAL => { + let txt = grand_node.text(); + if txt == "true" || txt == "false" { + bool_node = Some(grand_node); + } + } + SyntaxKind::NODE_ATTR_SET => { + let next_path: String = if cur_path.is_empty() { + attr_path.clone() + } else { + format!("{}.{}", cur_path, attr_path) + }; + if let Some(res) = walk(&grand_node, &next_path, query_path) { + return Some(res); + } + } + _ => {} + } + } + } + + if let Some(bool_node) = bool_node { + let full_path: String = if cur_path.is_empty() { + attr_path.clone() + } else { + format!("{}.{}", cur_path, attr_path) + }; + if full_path == query_path { + let range: TextRange = bool_node.text_range(); + let current_is_true: bool = bool_node.text() == "true"; + return Some((range, current_is_true)); + } + } + } + + NodeOrToken::Node(inner) => { + if let Some(res) = walk(&inner, cur_path, query_path) { + return Some(res); + } + } + + _ => {} + } + } + None + } + + if let Some((range, current_is_true)) = walk(&node, "", query_path) { + let start: usize = >::into(range.start()); + let end: usize = >::into(range.end()); + let mut new_src: String = src.clone(); + new_src.replace_range(start..end, if current_is_true { "false" } else { "true" }); + Root::parse(&new_src).syntax() + } else { + Root::parse(&src).syntax() + } +} + +/// Rekurencyjnie przegląda drzewo składniowe Nix i zbiera wszystkie opcje +/// typu `bool`, uwzględniając aktualną ścieżkę oraz kategorię (pobrane z +/// najbliższego blokowego komentarza). +/// +/// Funkcja jest wewnętrzną implementacją używaną przez `collect_nix_options`. +/// Nie jest częścią publicznego API, ale jej zachowanie jest opisane poniżej, +/// aby ułatwić utrzymanie kodu. +/// +/// # Argumenty +/// +/// * `node` – bieżący węzeł drzewa składniowego. +/// * `current_path` – dotychczasowa ścieżka do bieżącego węzła (pusty ciąg dla +/// węzła korzenia). +/// * `current_category` – nazwa kategorii pochodząca z najbliższego komentarza +/// blokowego nad węzłem. +/// +/// # Zwraca +/// +/// `Vec<(String, String, bool)>` – wektor trójek `(kategoria, pełna_ścieżka, +/// wartość_bool)`. fn collect_nix_options_with_path( node: &SyntaxNode, current_path: &str, @@ -52,7 +263,6 @@ fn collect_nix_options_with_path( } else { format!("{}.{}", current_path, attr_path) }; - result.extend(collect_nix_options_with_path( &grand_node, &new_path, @@ -70,14 +280,7 @@ fn collect_nix_options_with_path( } else { format!("{}.{}", current_path, attr_path) }; - - let value_text: String = value_node.text().to_string(); - let mut bool_value: bool = false; - - if value_text == "true" { - bool_value = true; - } - + let bool_value: bool = value_node.text() == "true"; result.push((category.clone(), full_path, bool_value)); } } diff --git a/src/test.nix b/src/test.nix index d3f98b4..72ffe37 100644 --- a/src/test.nix +++ b/src/test.nix @@ -6,9 +6,9 @@ _: { flatpak = { enable = true; # Flatpak: universal packaging system for Linux packages = { - sober = false; # Roblox client - warehouse = true; # Flatpak manager - flatseal = true; # Flatpak permissions manager + sober.enable = false; # Roblox client + warehouse.enable = true; # Flatpak manager + flatseal.enable = true; # Flatpak permissions manager }; };