From cb3526930809e4318b0c42d3c63f52ba8c969ae2 Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Mon, 1 Dec 2025 00:04:51 +0100 Subject: [PATCH 1/9] Add Nix configuration parser and option extraction Introduce rnix dependency to parse Nix files and collect boolean options with their full attribute paths from a test configuration file --- Cargo.lock | 64 ++++++++++++++++++- Cargo.toml | 1 + src/main.rs | 177 +++++---------------------------------------------- src/nix.rs | 68 ++++++++++++++++++++ src/test.nix | 76 ++++++++++++++++++++++ 5 files changed, 225 insertions(+), 161 deletions(-) create mode 100644 src/nix.rs create mode 100644 src/test.nix diff --git a/Cargo.lock b/Cargo.lock index 137f179..3b73717 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bitflags" version = "2.10.0" @@ -58,6 +64,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + [[package]] name = "crossterm" version = "0.28.1" @@ -206,8 +218,15 @@ version = "0.1.0" dependencies = [ "crossterm 0.29.0", "ratatui", + "rnix", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -313,7 +332,16 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", ] [[package]] @@ -405,6 +433,34 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rnix" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f15e00b0ab43abd70d50b6f8cd021290028f9b7fdd7cdfa6c35997173bc1ba9" +dependencies = [ + "rowan", +] + +[[package]] +name = "rowan" +version = "0.15.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4f1e4a001f863f41ea8d0e6a0c34b356d5b733db50dadab3efef640bafb779b" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "memoffset", + "rustc-hash", + "text-size", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.38.44" @@ -530,6 +586,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + [[package]] name = "unicode-ident" version = "1.0.22" diff --git a/Cargo.toml b/Cargo.toml index abd0dc5..ea495e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2024" [dependencies] crossterm = "0.29.0" ratatui = "0.29.0" +rnix = "0.12.0" diff --git a/src/main.rs b/src/main.rs index 699dc96..696b5bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,167 +1,24 @@ -use crossterm::{ - event, - event::{KeyCode, KeyEvent, KeyEventKind}, -}; -use ratatui::{ - DefaultTerminal, Frame, Terminal, - prelude::{Buffer, Constraint, CrosstermBackend, Layout, Rect, Stylize}, - style::{Color, Style}, - symbols::border, - text::Line, - widgets::{Block, Gauge, Widget}, -}; -use std::{ - io, - sync::mpsc::{self, Sender}, - thread, - time::Duration, -}; +use rnix::{Parse, Root}; +use std::fs; -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, - }; +mod nix; - let (event_tx, event_rx) = mpsc::channel::(); +fn main() { + let content: String = + fs::read_to_string("src/test.nix").expect("Nie można odczytać pliku src/test.nix"); - let tx_to_input_events: Sender = event_tx.clone(); - thread::spawn(move || { - handle_input_events(tx_to_input_events); - }); + let ast: Parse = Root::parse(&content); - let tx_to_background_events: Sender = event_tx.clone(); - thread::spawn(move || { - run_background_thread(tx_to_background_events); - }); - - let app_result: Result<(), io::Error> = app.run(&mut terminal, event_rx); - - ratatui::restore(); - app_result -} - -enum Event { - Input(KeyEvent), - Progress(f64), -} - -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; - } + if !ast.errors().is_empty() { + eprintln!("Błędy parsowania:"); + for error in ast.errors() { + eprintln!(" - {}", error); } - } -} - -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 { - exit: bool, - progress_bar_color: Color, - background_progress: f64, -} - -impl App { - 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, - ); + eprintln!(); + } + + let options: Vec<(String, bool)> = nix::collect_nix_options_with_path(&ast.syntax(), ""); + for (path, value) in options { + println!("{} = {};", path, value); } } diff --git a/src/nix.rs b/src/nix.rs new file mode 100644 index 0000000..cfb3f79 --- /dev/null +++ b/src/nix.rs @@ -0,0 +1,68 @@ +use rnix::{NodeOrToken, SyntaxKind, SyntaxNode}; + +pub fn collect_nix_options_with_path(node: &SyntaxNode, current_path: &str) -> Vec<(String, bool)> { + let mut result: Vec<(String, bool)> = Vec::new(); + + for child in node.children_with_tokens() { + if let NodeOrToken::Node(child_node) = child { + match child_node.kind() { + SyntaxKind::NODE_ATTRPATH_VALUE => { + let mut attr_path: String = String::new(); + let mut value_node: Option = None; + + for grand in child_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 text: String = grand_node.text().to_string(); + if text == "true" || text == "false" { + value_node = Some(grand_node); + } + } + SyntaxKind::NODE_ATTR_SET => { + let new_path: String = if current_path.is_empty() { + attr_path.clone() + } else { + format!("{}.{}", current_path, attr_path) + }; + + result.extend(collect_nix_options_with_path( + &grand_node, + &new_path, + )); + } + _ => {} + } + } + } + + if let Some(value_node) = value_node { + let full_path: String = if current_path.is_empty() { + attr_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; + } + + result.push((full_path, bool_value)); + } + } + + _ => { + result.extend(collect_nix_options_with_path(&child_node, current_path)); + } + } + } + } + + result +} diff --git a/src/test.nix b/src/test.nix new file mode 100644 index 0000000..d3f98b4 --- /dev/null +++ b/src/test.nix @@ -0,0 +1,76 @@ +_: { + /* + Container & Packaging + */ + docker.enable = true; # Docker: container runtime and management + flatpak = { + enable = true; # Flatpak: universal packaging system for Linux + packages = { + sober = false; # Roblox client + warehouse = true; # Flatpak manager + flatseal = true; # Flatpak permissions manager + }; + }; + + /* + Gaming + */ + gamemode.enable = true; # GameMode: optimizes system performance for gaming + gamescope.enable = false; # Gamescope: micro‑compositor for games + steam.enable = true; # Steam: platform for buying and playing games + + packages = { + /* + Container & Packaging + */ + distrobox.enable = false; # Distrobox: containerized development environments + lazydocker.enable = false; # Lazydocker: simple TUI for Docker + + /* + Gaming + */ + prismlauncher.enable = false; # Prism Launcher: Minecraft modded launcher + spaceCadetPinball.enable = true; # SpaceCadet Pinball: classic pinball game + ttySolitaire.enable = true; # TTY Solitaire: terminal‑based solitaire game + + /* + Development Tools + */ + exercism.enable = true; # Exercism: coding practice platform + lazygit.enable = false; # Lazygit: simple TUI for Git + opencode.enable = true; # OpenCode: tools for coding and development + jan.enable = true; # Jan: AI chat UI + + /* + Communication & Collaboration + */ + mattermost.enable = true; # Mattermost: open‑source Slack alternative + slack.enable = true; # Slack: team communication and collaboration tool + tutanota.enable = true; # Tutanota: secure email client + + /* + Productivity / Knowledge Management + */ + bitwarden.enable = false; # Bitwarden: password manager (desktop) + iotas.enable = true; # Iotas: lightweight notes manager + logseq.enable = false; # Logseq: knowledge base and outliner + + /* + Media & Graphics + */ + affinity.enable = false; # Affinity: professional graphics suite + eyeOfGnome.enable = true; # Eye of GNOME: image viewer + freetube.enable = false; # FreeTube: privacy‑friendly YouTube client + gimp.enable = false; # GIMP: GNU Image Manipulation Program + kdenlive.enable = false; # Kdenlive: video editing software + plex.enable = true; # Plex: media player and server client + + /* + Utilities / Misc + */ + eddieAirVPN.enable = true; # Eddie AirVPN: VPN client + galculator.enable = true; # Galculator: simple calculator + gedit.enable = false; # Gedit: GNOME text editor + winboat.enable = false; # Winboat: Windows remote desktop via RDP + }; +} From e6e1695084ae6b2101fe8a106e424cca7e6c1171 Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Mon, 1 Dec 2025 00:43:23 +0100 Subject: [PATCH 2/9] Add support for category comments in Nix option collection The nix module now parses `/* */` comments as categories and associates them with subsequent options. The output format has been updated to include the category prefix before the option path. The internal state is maintained during traversal to ensure correct category propagation through nested structures. --- src/main.rs | 6 +-- src/nix.rs | 144 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 90 insertions(+), 60 deletions(-) diff --git a/src/main.rs b/src/main.rs index 696b5bc..1de2e75 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,8 +17,8 @@ fn main() { eprintln!(); } - let options: Vec<(String, bool)> = nix::collect_nix_options_with_path(&ast.syntax(), ""); - for (path, value) in options { - println!("{} = {};", path, value); + let options: Vec<(String, String, bool)> = nix::collect_nix_options(&ast.syntax()); + for (category, path, value) in options { + println!("{}: {} = {};", category, path, value); } } diff --git a/src/nix.rs b/src/nix.rs index cfb3f79..fd650ba 100644 --- a/src/nix.rs +++ b/src/nix.rs @@ -1,66 +1,96 @@ -use rnix::{NodeOrToken, SyntaxKind, SyntaxNode}; +use rnix::{NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken}; -pub fn collect_nix_options_with_path(node: &SyntaxNode, current_path: &str) -> Vec<(String, bool)> { - let mut result: Vec<(String, bool)> = Vec::new(); +pub fn collect_nix_options(node: &SyntaxNode) -> Vec<(String, String, bool)> { + collect_nix_options_with_path(node, "", "") +} - for child in node.children_with_tokens() { - if let NodeOrToken::Node(child_node) = child { - match child_node.kind() { - SyntaxKind::NODE_ATTRPATH_VALUE => { - let mut attr_path: String = String::new(); - let mut value_node: Option = None; +fn collect_nix_options_with_path( + node: &SyntaxNode, + current_path: &str, + current_category: &str, +) -> Vec<(String, String, bool)> { + let mut result: Vec<(String, String, bool)> = Vec::new(); + let mut category: String = current_category.to_string(); - for grand in child_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 text: String = grand_node.text().to_string(); - if text == "true" || text == "false" { - value_node = Some(grand_node); - } - } - SyntaxKind::NODE_ATTR_SET => { - let new_path: String = if current_path.is_empty() { - attr_path.clone() - } else { - format!("{}.{}", current_path, attr_path) - }; + let children: Vec> = node.children_with_tokens().collect(); - result.extend(collect_nix_options_with_path( - &grand_node, - &new_path, - )); - } - _ => {} - } - } - } - - if let Some(value_node) = value_node { - let full_path: String = if current_path.is_empty() { - attr_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; - } - - result.push((full_path, bool_value)); - } - } - - _ => { - result.extend(collect_nix_options_with_path(&child_node, current_path)); + for i in 0..children.len() { + match &children[i] { + NodeOrToken::Token(token) if token.kind() == SyntaxKind::TOKEN_COMMENT => { + let text: &str = token.text(); + if text.starts_with("/*") && text.ends_with("*/") { + let content: String = text + .trim_start_matches("/*") + .trim_end_matches("*/") + .trim() + .to_string(); + category = content; } } + + NodeOrToken::Node(child_node) + if child_node.kind() == SyntaxKind::NODE_ATTRPATH_VALUE => + { + let mut attr_path: String = String::new(); + let mut value_node: Option = None; + + for grand in child_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 text: String = grand_node.text().to_string(); + if text == "true" || text == "false" { + value_node = Some(grand_node); + } + } + SyntaxKind::NODE_ATTR_SET => { + let new_path: String = if current_path.is_empty() { + attr_path.clone() + } else { + format!("{}.{}", current_path, attr_path) + }; + + result.extend(collect_nix_options_with_path( + &grand_node, + &new_path, + &category, + )); + } + _ => {} + } + } + } + + if let Some(value_node) = value_node { + let full_path: String = if current_path.is_empty() { + attr_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; + } + + result.push((category.clone(), full_path, bool_value)); + } + } + + NodeOrToken::Node(child_node) => { + result.extend(collect_nix_options_with_path( + child_node, + current_path, + &category, + )); + } + + _ => {} } } From 21d6d7997f1f16aeb656c02c6e9f739890db5103 Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Mon, 1 Dec 2025 18:56:10 +0100 Subject: [PATCH 3/9] 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 }; }; From 19e820ca936303c5ecfd3df4b7aeb69ce09d774f Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Thu, 4 Dec 2025 23:16:45 +0100 Subject: [PATCH 4/9] Add clap dependency and update app for CLI integration Update Cargo files to add clap and related dependencies, restructure App to handle configuration from CLI, and update nix module to support config source tracking. Add new cli module and adjust main to initialize modules at startup. --- Cargo.lock | 117 +++++++++++++++++++++- Cargo.toml | 3 +- src/app.rs | 231 ++++++++++++++++++++++---------------------- src/cli.rs | 64 ++++++++++++ src/lib.rs | 1 + src/main.rs | 77 ++++----------- src/nix.rs | 117 +++++++++++----------- src/placeholder.txt | 224 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 599 insertions(+), 235 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/placeholder.txt diff --git a/Cargo.lock b/Cargo.lock index 3b73717..8a7dd9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,56 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -41,6 +91,52 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "compact_str" version = "0.8.1" @@ -213,9 +309,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "garandos-tui" +name = "garandos_tui" version = "0.1.0" dependencies = [ + "clap", "crossterm 0.29.0", "ratatui", "rnix", @@ -272,6 +369,12 @@ dependencies = [ "syn", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -356,6 +459,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "parking_lot" version = "0.12.5" @@ -627,6 +736,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index ea495e3..ef9d477 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ [package] -name = "garandos-tui" +name = "garandos_tui" version = "0.1.0" edition = "2024" [dependencies] +clap = { version = "4.5.53", features = ["derive"] } crossterm = "0.29.0" ratatui = "0.29.0" rnix = "0.12.0" diff --git a/src/app.rs b/src/app.rs index f1682fa..5f2e749 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,20 +1,124 @@ -use crossterm::{ - event, - event::{KeyCode, KeyEvent, KeyEventKind}, -}; +use crate::nix::{ConfigOption, ConfigSource}; +use crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind}; use ratatui::{ DefaultTerminal, Frame, - prelude::{Buffer, Constraint, Layout, Rect, Stylize}, - style::{Color, Style}, - symbols::border, - text::Line, - widgets::{Block, Gauge, Widget}, + buffer::Buffer, + layout::{Constraint, Layout, Rect}, + style::Stylize, + text::{Line, Span}, + widgets::Widget, }; -use std::{io, sync::mpsc, thread, time::Duration}; +use std::{ + io::Result, + sync::mpsc::{self, Receiver}, +}; + +pub struct App { + pub exit: bool, + pub system_modules: Vec, + pub home_modules: Vec, + pub current_file: ConfigSource, + pub selected_index: usize, + pub last_action_status: String, +} pub enum Event { Input(KeyEvent), - Progress(f64), + EditFile, +} + +impl App { + pub fn run(&mut self, terminal: &mut DefaultTerminal, rx: Receiver) -> 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::EditFile => self.handle_file_edit_events()?, + _ => {} + } + 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) -> Result<()> { + // q - wyjście z programu + if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('q') { + self.exit = true; + } + // ↑ - scroll w górę + else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Up { + self.selected_index = self.selected_index.saturating_sub(1); + } + // ↓ - scroll w dół + else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Down { + self.selected_index = self.selected_index.saturating_add(1); + } + // ENTER - zmień wartość boolen danej opcji + else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Enter { + } + // 1 - zmień plik na ConfigSource::System (pomiń wykonywaniej, jeżeli już jest wybrany) + else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('1') { + if self.current_file != ConfigSource::System { + self.current_file = ConfigSource::System; + } else { + self.current_file = ConfigSource::Home; + } + } + // 2 - zmień plik na ConfigSource::Home (pomiń wykonywaniej, jeżeli już jest wybrany) + else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('2') { + if self.current_file != ConfigSource::Home { + self.current_file = ConfigSource::Home; + } else { + self.current_file = ConfigSource::System; + } + } + + Ok(()) + } +} + +impl Widget for &App { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let vertical_layout: Layout = Layout::vertical([ + Constraint::Percentage(10), + Constraint::Percentage(80), + Constraint::Percentage(10), + ]); + + let [title_area, content_area, footer_area] = vertical_layout.areas(area); + + let (system_file, home_file) = match self.current_file { + ConfigSource::System => ("(System)".blue().bold(), "Home".reset()), + ConfigSource::Home => ("System".reset(), "(Home)".blue().bold()), + }; + let navbar: Line<'_> = Line::from(vec![ + Span::from("GarandOS TUI".bold().italic().yellow()), + Span::from(" * ".reset()), + system_file, + Span::from(" [1]".reset()), + Span::from(" * ".reset()), + home_file, + Span::from(" [2]".reset()), + Span::from(" *".reset()), + Span::from(" Quit".reset()), + Span::from(" [q]".reset()), + ]) + .left_aligned(); + + navbar.render(title_area, buf); + } } pub fn handle_input_events(tx: mpsc::Sender) { @@ -33,108 +137,3 @@ pub fn handle_input_events(tx: mpsc::Sender) { } } } - -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/cli.rs b/src/cli.rs new file mode 100644 index 0000000..fe15034 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,64 @@ +use crate::nix::{ConfigOption, ConfigSource, collect_nix_options}; +use clap::Parser; +use rnix::{Parse, Root}; +use std::{fs, path::PathBuf}; + +#[derive(Parser, Debug)] +#[command( + version, + about = "Potrzebne pliki znajdziesz w ~/garandos/hosts//", + long_about = "Potrzebne pliki znajdziesz w ~/garandos/hosts//" +)] +pub struct Cli { + #[arg( + long, + help = "Ścieżka do pliku system-modules.nix", + value_name = "SYSTEM_MODULES" + )] + pub sf: PathBuf, + #[arg( + long, + help = "Ścieżka do pliku home-modules.nix", + value_name = "HOME_MODULES" + )] + pub hf: PathBuf, +} + +pub struct NixModules { + pub system_modules: Vec, + pub home_modules: Vec, +} + +pub fn get_modules() -> NixModules { + let args: Cli = Cli::parse(); + + let system_file: String = fs::read_to_string(&args.sf).expect("Failed to read system file"); + let home_file: String = fs::read_to_string(&args.hf).expect("Failed to read home file"); + + let system_ast: Parse = get_ast(&system_file); + let home_ast: Parse = get_ast(&home_file); + + let system_modules: Vec = + collect_nix_options(&system_ast.syntax(), "", ConfigSource::System); + let home_modules: Vec = + collect_nix_options(&home_ast.syntax(), "", ConfigSource::Home); + + NixModules { + system_modules, + home_modules, + } +} + +fn get_ast(file: &str) -> Parse { + let ast: Parse = Root::parse(&file); + + if !ast.errors().is_empty() { + eprintln!("Błędy parsowania:"); + for error in ast.errors() { + eprintln!(" - {}", error); + } + panic!("Błąd z parsowaniem pliku .nix") + } + + ast +} diff --git a/src/lib.rs b/src/lib.rs index f959912..027a671 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,3 @@ pub mod app; +pub mod cli; pub mod nix; diff --git a/src/main.rs b/src/main.rs index e40a64e..7fb5ead 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,25 @@ 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}, + app::{App, Event, handle_input_events}, + cli::{NixModules, get_modules}, + nix::ConfigSource, }; -use ratatui::{Terminal, prelude::CrosstermBackend, style::Color}; -// use rnix::{Parse, Root, SyntaxNode}; -// use std::fs; +use ratatui::{Terminal, prelude::CrosstermBackend}; use std::{ - io, + io::{Result, Stdout}, sync::mpsc::{self, Sender}, thread, }; -fn main() -> io::Result<()> { - let mut terminal: Terminal> = ratatui::init(); +fn main() -> Result<()> { + let nix_modules: NixModules = get_modules(); + let mut terminal: Terminal> = ratatui::init(); let mut app: App = App { exit: false, - progress_bar_color: Color::Green, - background_progress: 0_f64, + system_modules: nix_modules.system_modules, + home_modules: nix_modules.home_modules, + current_file: ConfigSource::System, + selected_index: 0_usize, + last_action_status: "".to_string(), }; let (event_tx, event_rx) = mpsc::channel::(); @@ -26,56 +29,12 @@ fn main() -> io::Result<()> { handle_input_events(tx_to_input_events); }); - let tx_to_background_events: Sender = event_tx.clone(); - thread::spawn(move || { - run_background_thread(tx_to_background_events); - }); - - let app_result: Result<(), io::Error> = app.run(&mut terminal, event_rx); + // let tx_to_file_edit_events: Sender = event_tx.clone(); + // thread::spawn(move || { + // // handle_file_edit_events(tx_to_file_edit_events); + // }); + let app_result: Result<()> = 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 51a16df..f15d361 100644 --- a/src/nix.rs +++ b/src/nix.rs @@ -1,46 +1,26 @@ 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, "", "") +pub struct ConfigOption { + pub category: String, + pub path: String, + pub value: bool, + pub source: ConfigSource, +} + +#[derive(PartialEq)] +pub enum ConfigSource { + System, + Home, +} + +impl Clone for ConfigSource { + fn clone(&self) -> Self { + match self { + ConfigSource::System => ConfigSource::System, + ConfigSource::Home => ConfigSource::Home, + } + } } /// Pobiera wartość logiczną opcji Nix wskazanej pełną ścieżką. @@ -72,11 +52,11 @@ pub fn collect_nix_options(node: &SyntaxNode) -> Vec<(String, String, bool)> { /// 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 options: Vec = collect_nix_options(node, "", ConfigSource::System); let map: HashMap<&str, bool> = options .iter() - .map(|(_, path, val)| (path.as_str(), *val)) + .map(|option| (option.path.as_str(), option.value)) .collect(); map.get(query_path).copied() @@ -199,10 +179,6 @@ pub fn toggle_bool_at_path(node: &SyntaxNode, query_path: &str) -> SyntaxNode { /// 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. @@ -213,15 +189,35 @@ pub fn toggle_bool_at_path(node: &SyntaxNode, query_path: &str) -> SyntaxNode { /// /// # Zwraca /// -/// `Vec<(String, String, bool)>` – wektor trójek `(kategoria, pełna_ścieżka, -/// wartość_bool)`. -fn collect_nix_options_with_path( +/// `Vec` – wektor opcji z kategorią, pełną ścieżką i wartością bool. +/// +/// # 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(), "", ConfigSource::System); +/// assert_eq!(options.len(), 2); +/// assert!(options.iter().any(|option| option.path == "services.ssh.enable" && option.value)); +/// assert!(options.iter().any(|option| option.path == "services.httpd.enable" && !option.value)); +/// ``` +pub fn collect_nix_options( node: &SyntaxNode, current_path: &str, - current_category: &str, -) -> Vec<(String, String, bool)> { - let mut result: Vec<(String, String, bool)> = Vec::new(); - let mut category: String = current_category.to_string(); + current_source: ConfigSource, +) -> Vec { + let mut result: Vec = Vec::new(); + let mut category: String = String::new(); let children: Vec> = node.children_with_tokens().collect(); @@ -263,10 +259,10 @@ fn collect_nix_options_with_path( } else { format!("{}.{}", current_path, attr_path) }; - result.extend(collect_nix_options_with_path( + result.extend(collect_nix_options( &grand_node, &new_path, - &category, + current_source.clone(), )); } _ => {} @@ -281,15 +277,20 @@ fn collect_nix_options_with_path( format!("{}.{}", current_path, attr_path) }; let bool_value: bool = value_node.text() == "true"; - result.push((category.clone(), full_path, bool_value)); + result.push(ConfigOption { + category: category.clone(), + path: full_path, + value: bool_value, + source: current_source.clone(), + }); } } NodeOrToken::Node(child_node) => { - result.extend(collect_nix_options_with_path( + result.extend(collect_nix_options( child_node, current_path, - &category, + current_source.clone(), )); } diff --git a/src/placeholder.txt b/src/placeholder.txt new file mode 100644 index 0000000..adc3b8b --- /dev/null +++ b/src/placeholder.txt @@ -0,0 +1,224 @@ +// main.rs +use clap::Parser; +use garandos_tui::{ + cli::Cli, + // app::{App, Event, handle_input_events, run_background_thread}, + nix::{ + ConfigOption, ConfigSource, 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, +// }; + +// 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, +// }; + +// let (event_tx, event_rx) = mpsc::channel::(); + +// let tx_to_input_events: Sender = event_tx.clone(); +// thread::spawn(move || { +// handle_input_events(tx_to_input_events); +// }); + +// let tx_to_background_events: Sender = event_tx.clone(); +// thread::spawn(move || { +// run_background_thread(tx_to_background_events); +// }); + +// let app_result: Result<(), io::Error> = app.run(&mut terminal, event_rx); + +// ratatui::restore(); +// app_result +// } + +// fn main() { +// let args: Cli = Cli::parse(); + +// let system_modules_content: String = +// fs::read_to_string(args.system_file).expect("Nie można odczytać pliku {args.system_file}"); + +// let home_modules_content: String = +// fs::read_to_string(args.home_file).expect("Nie można odczytać pliku {args.home_file}"); + +// let ast: Parse = Root::parse(&home_modules_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 = collect_nix_options(&node, "", ConfigSource::System); + +// const OPTION: &str = "flatpak.enable"; + +// println!("Options:"); +// for option in options { +// // if option.path == OPTION { +// println!("{} = {};", option.path, option.value); +// // } +// } + +// // if let Some(value) = get_nix_value_by_path(&node, OPTION) { +// // println!("\n{OPTION}: {}", value); +// // } + +// // let new_node: SyntaxNode = toggle_bool_at_path(&node, OPTION); +// // let new_options: Vec = collect_nix_options(&new_node, "", ConfigSource::System); + +// // println!("\nNew Options:"); +// // for option in new_options { +// // if option.path == OPTION { +// // println!("{} = {};", option.path, option.value); +// // } +// // } + +// // fs::write("src/test.nix", new_node.to_string()).expect("Nie można zapisać tego pliku"); +// } + +// app.rs + +// use core::option::Option; +// 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 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; +// } +// } +// } + +// 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, +// ); +// } +// } From 6c50da8c1876dfee2074df34369ef7226fb51384 Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Sat, 6 Dec 2025 17:21:44 +0100 Subject: [PATCH 5/9] Refactor Nix handling and add toggle UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce `NixModules` struct containing file paths, parsed ASTs, and option lists. - Add `parse_nix` and `build_nix_modules` helpers for centralized parsing. - Update CLI to use `build_nix_modules` instead of manual parsing. - Extend `App` with system/home paths and ASTs, enabling in‑place editing. - Implement boolean toggle on Enter: update AST, rewrite file, refresh modules, and report action. - Simplify event handling, adding shortcuts `1`/`2` to switch files. - Revise UI layout, introduce footer with usage hints and last‑action status. - Adjust imports and remove obsolete code across modules. --- src/app.rs | 159 +++++++++++++++++++++++++++++++++++++++------------- src/cli.rs | 41 +------------- src/main.rs | 8 ++- src/nix.rs | 51 ++++++++++++++--- 4 files changed, 171 insertions(+), 88 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5f2e749..2313521 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,6 @@ -use crate::nix::{ConfigOption, ConfigSource}; +use crate::nix::{ + ConfigOption, ConfigSource, collect_nix_options, get_nix_value_by_path, toggle_bool_at_path, +}; use crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind}; use ratatui::{ DefaultTerminal, Frame, @@ -6,15 +8,22 @@ use ratatui::{ layout::{Constraint, Layout, Rect}, style::Stylize, text::{Line, Span}, - widgets::Widget, + widgets::{Paragraph, Widget}, }; +use rnix::{Parse, Root, SyntaxNode}; use std::{ + fs, io::Result, + path::PathBuf, sync::mpsc::{self, Receiver}, }; pub struct App { pub exit: bool, + pub system_path: PathBuf, + pub home_path: PathBuf, + pub system_ast: Parse, + pub home_ast: Parse, pub system_modules: Vec, pub home_modules: Vec, pub current_file: ConfigSource, @@ -24,7 +33,7 @@ pub struct App { pub enum Event { Input(KeyEvent), - EditFile, + // EditFile, } impl App { @@ -37,7 +46,7 @@ impl App { match event { Event::Input(key_event) => self.handle_key_event(key_event)?, // Event::EditFile => self.handle_file_edit_events()?, - _ => {} + // _ => {} } terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?; } @@ -54,6 +63,14 @@ impl App { if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('q') { self.exit = true; } + // 1 - zmień plik na ConfigSource::System (pomiń wykonywaniej, jeżeli już jest wybrany) + else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('1') { + self.current_file = ConfigSource::System; + } + // 2 - zmień plik na ConfigSource::Home (pomiń wykonywaniej, jeżeli już jest wybrany) + else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('2') { + self.current_file = ConfigSource::Home; + } // ↑ - scroll w górę else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Up { self.selected_index = self.selected_index.saturating_sub(1); @@ -64,21 +81,53 @@ impl App { } // ENTER - zmień wartość boolen danej opcji else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Enter { - } - // 1 - zmień plik na ConfigSource::System (pomiń wykonywaniej, jeżeli już jest wybrany) - else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('1') { - if self.current_file != ConfigSource::System { - self.current_file = ConfigSource::System; - } else { - self.current_file = ConfigSource::Home; - } - } - // 2 - zmień plik na ConfigSource::Home (pomiń wykonywaniej, jeżeli już jest wybrany) - else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('2') { - if self.current_file != ConfigSource::Home { - self.current_file = ConfigSource::Home; - } else { - self.current_file = ConfigSource::System; + let modules: &Vec = match self.current_file { + ConfigSource::System => &self.system_modules.clone(), + ConfigSource::Home => &self.home_modules.clone(), + }; + let node: &SyntaxNode = match self.current_file { + ConfigSource::System => &self.system_ast.syntax(), + ConfigSource::Home => &self.home_ast.syntax(), + }; + if let Some(module) = modules.get(self.selected_index) { + let new_node: SyntaxNode = toggle_bool_at_path(node, module.path.as_str()); + + let new_ast: Parse = Root::parse(&new_node.to_string().as_str()); + + let new_modules: Vec = + collect_nix_options(&new_node, "", self.current_file.clone()); + + let new_module_value: bool = + if let Some(value) = get_nix_value_by_path(&new_node, module.path.as_str()) { + value + } else { + module.value.clone() + }; + + self.last_action_status = if let Some(module) = modules.get(self.selected_index) { + match self.current_file { + ConfigSource::System => { + self.system_modules = new_modules; + self.system_ast = new_ast; + fs::write(&self.system_path, new_node.to_string()) + .expect("Nie można zapisać pliku modułów systemowych"); + } + ConfigSource::Home => { + self.home_modules = new_modules; + self.home_ast = new_ast; + fs::write(&self.home_path, new_node.to_string()) + .expect("Nie można zapisać pliku modułów domowych"); + } + }; + format!( + "{} = {} -> {}", + module.path.clone(), + module.value.clone(), + new_module_value + ) + } else { + "Nothing changed".to_string() + }; } } @@ -92,32 +141,62 @@ impl Widget for &App { Self: Sized, { let vertical_layout: Layout = Layout::vertical([ - Constraint::Percentage(10), - Constraint::Percentage(80), - Constraint::Percentage(10), + Constraint::Percentage(5), + Constraint::Percentage(90), + Constraint::Percentage(5), ]); let [title_area, content_area, footer_area] = vertical_layout.areas(area); - let (system_file, home_file) = match self.current_file { - ConfigSource::System => ("(System)".blue().bold(), "Home".reset()), - ConfigSource::Home => ("System".reset(), "(Home)".blue().bold()), - }; - let navbar: Line<'_> = Line::from(vec![ - Span::from("GarandOS TUI".bold().italic().yellow()), - Span::from(" * ".reset()), - system_file, - Span::from(" [1]".reset()), - Span::from(" * ".reset()), - home_file, - Span::from(" [2]".reset()), - Span::from(" *".reset()), - Span::from(" Quit".reset()), - Span::from(" [q]".reset()), - ]) - .left_aligned(); + { + let (system_file, home_file) = match self.current_file { + ConfigSource::System => ("(System)".blue().bold(), "Home".reset()), + ConfigSource::Home => ("System".reset(), "(Home)".blue().bold()), + }; + let title: Line<'_> = Line::from(vec![ + Span::from("GarandOS TUI".bold().italic().yellow()), + Span::from(" * ".reset()), + system_file, + Span::from(" [1]".reset()), + Span::from(" * ".reset()), + home_file, + Span::from(" [2]".reset()), + Span::from(" *".reset()), + Span::from(" Quit".reset()), + Span::from(" [q]".reset()), + ]) + .left_aligned(); - navbar.render(title_area, buf); + title.render(title_area, buf); + } + + { + let footer_line1: Line<'_> = Line::from(vec![ + Span::from("Use "), + Span::from("↑/↓".italic().green()), + Span::from(" to navigate, "), + Span::from("Enter".bold().white()), + Span::from(" to toggle, "), + Span::from("1/2".bold().blue()), + Span::from(" to switch file, "), + Span::from("q".bold().red()), + Span::from(" to quit."), + ]); + let last_action: Span<'_> = if self.last_action_status.is_empty() { + Span::from("Nothing changed") + } else { + self.last_action_status.clone().into() + }; + let footer_line2: Line<'_> = Line::from(vec![ + Span::from("Last action: "), + last_action.italic().green(), + Span::from("."), + ]); + let footer: Paragraph<'_> = + Paragraph::new(vec![footer_line1, footer_line2]).left_aligned(); + + footer.render(footer_area, buf); + } } } diff --git a/src/cli.rs b/src/cli.rs index fe15034..7e1e48c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,6 @@ -use crate::nix::{ConfigOption, ConfigSource, collect_nix_options}; +use crate::nix::{NixModules, build_nix_modules}; use clap::Parser; -use rnix::{Parse, Root}; -use std::{fs, path::PathBuf}; +use std::path::PathBuf; #[derive(Parser, Debug)] #[command( @@ -24,41 +23,7 @@ pub struct Cli { pub hf: PathBuf, } -pub struct NixModules { - pub system_modules: Vec, - pub home_modules: Vec, -} - pub fn get_modules() -> NixModules { let args: Cli = Cli::parse(); - - let system_file: String = fs::read_to_string(&args.sf).expect("Failed to read system file"); - let home_file: String = fs::read_to_string(&args.hf).expect("Failed to read home file"); - - let system_ast: Parse = get_ast(&system_file); - let home_ast: Parse = get_ast(&home_file); - - let system_modules: Vec = - collect_nix_options(&system_ast.syntax(), "", ConfigSource::System); - let home_modules: Vec = - collect_nix_options(&home_ast.syntax(), "", ConfigSource::Home); - - NixModules { - system_modules, - home_modules, - } -} - -fn get_ast(file: &str) -> Parse { - let ast: Parse = Root::parse(&file); - - if !ast.errors().is_empty() { - eprintln!("Błędy parsowania:"); - for error in ast.errors() { - eprintln!(" - {}", error); - } - panic!("Błąd z parsowaniem pliku .nix") - } - - ast + build_nix_modules(args.sf, args.hf) } diff --git a/src/main.rs b/src/main.rs index 7fb5ead..e43dd73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use garandos_tui::{ app::{App, Event, handle_input_events}, - cli::{NixModules, get_modules}, - nix::ConfigSource, + cli::get_modules, + nix::{ConfigSource, NixModules}, }; use ratatui::{Terminal, prelude::CrosstermBackend}; use std::{ @@ -15,6 +15,10 @@ fn main() -> Result<()> { let mut terminal: Terminal> = ratatui::init(); let mut app: App = App { exit: false, + system_path: nix_modules.system_path, + home_path: nix_modules.home_path, + system_ast: nix_modules.system_ast, + home_ast: nix_modules.home_ast, system_modules: nix_modules.system_modules, home_modules: nix_modules.home_modules, current_file: ConfigSource::System, diff --git a/src/nix.rs b/src/nix.rs index f15d361..4c5ccb2 100644 --- a/src/nix.rs +++ b/src/nix.rs @@ -1,6 +1,16 @@ -use rnix::{NodeOrToken, Root, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize}; -use std::collections::HashMap; +use rnix::{NodeOrToken, Parse, Root, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize}; +use std::{collections::HashMap, fs, path::PathBuf}; +pub struct NixModules { + pub system_path: PathBuf, + pub system_ast: Parse, + pub system_modules: Vec, + pub home_path: PathBuf, + pub home_ast: Parse, + pub home_modules: Vec, +} + +#[derive(Clone)] pub struct ConfigOption { pub category: String, pub path: String, @@ -8,18 +18,43 @@ pub struct ConfigOption { pub source: ConfigSource, } -#[derive(PartialEq)] +#[derive(PartialEq, Clone)] pub enum ConfigSource { System, Home, } -impl Clone for ConfigSource { - fn clone(&self) -> Self { - match self { - ConfigSource::System => ConfigSource::System, - ConfigSource::Home => ConfigSource::Home, +pub fn parse_nix(src: &str) -> Parse { + let ast: Parse = Root::parse(src); + if !ast.errors().is_empty() { + eprintln!("Błędy parsowania:"); + for err in ast.errors() { + eprintln!(" - {}", err); } + panic!("Błąd parsowania pliku .nix"); + } + ast +} + +pub fn build_nix_modules(system_path: PathBuf, home_path: PathBuf) -> NixModules { + let system_src: String = fs::read_to_string(&system_path).expect("Failed to read system file"); + let home_src: String = fs::read_to_string(&home_path).expect("Failed to read home file"); + + let system_ast: Parse = parse_nix(&system_src); + let home_ast: Parse = parse_nix(&home_src); + + let system_modules: Vec = + collect_nix_options(&system_ast.syntax(), "", ConfigSource::System); + let home_modules: Vec = + collect_nix_options(&home_ast.syntax(), "", ConfigSource::Home); + + NixModules { + system_path, + system_ast, + system_modules, + home_path, + home_ast, + home_modules, } } From 09faba4f7c711436c64ecbb0c45aa791d064ffed Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Sat, 6 Dec 2025 20:06:40 +0100 Subject: [PATCH 6/9] Extract option context method for toggle handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed the commented‑out EditFile event and its match arm. Added get_selected_option_context to centralize module retrieval, AST updates, and file writes, simplifying the Enter‑key handling logic. --- src/app.rs | 117 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 43 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2313521..7411fd7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -33,7 +33,6 @@ pub struct App { pub enum Event { Input(KeyEvent), - // EditFile, } impl App { @@ -45,8 +44,6 @@ impl App { }; match event { Event::Input(key_event) => self.handle_key_event(key_event)?, - // Event::EditFile => self.handle_file_edit_events()?, - // _ => {} } terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?; } @@ -81,15 +78,64 @@ impl App { } // ENTER - zmień wartość boolen danej opcji else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Enter { - let modules: &Vec = match self.current_file { - ConfigSource::System => &self.system_modules.clone(), - ConfigSource::Home => &self.home_modules.clone(), + let (modules, new_node, new_ast, new_modules, new_module_value) = + self.get_selected_option_context(); + + self.last_action_status = if let Some(module) = modules.get(self.selected_index) { + match self.current_file { + ConfigSource::System => { + self.system_modules = new_modules; + self.system_ast = new_ast; + fs::write(&self.system_path, new_node.to_string()) + .expect("Nie można zapisać pliku modułów systemowych"); + } + ConfigSource::Home => { + self.home_modules = new_modules; + self.home_ast = new_ast; + fs::write(&self.home_path, new_node.to_string()) + .expect("Nie można zapisać pliku modułów domowych"); + } + }; + format!( + "{} = {} -> {}", + module.path.clone(), + module.value.clone(), + new_module_value + ) + } else { + "Nothing changed".to_string() }; - let node: &SyntaxNode = match self.current_file { - ConfigSource::System => &self.system_ast.syntax(), - ConfigSource::Home => &self.home_ast.syntax(), - }; - if let Some(module) = modules.get(self.selected_index) { + } + + Ok(()) + } + + fn get_selected_option_context( + &self, + ) -> ( + Vec, + SyntaxNode, + Parse, + Vec, + bool, + ) { + let modules: &Vec = match self.current_file { + ConfigSource::System => &self.system_modules, + ConfigSource::Home => &self.home_modules, + }; + let node: &SyntaxNode = match self.current_file { + ConfigSource::System => &self.system_ast.syntax(), + ConfigSource::Home => &self.home_ast.syntax(), + }; + let fallback_module_value: bool = modules + .get(self.selected_index) + .map(|m| m.value.clone()) + .unwrap_or(false); + + let (new_node, new_ast, new_modules, new_module_value) = match modules + .get(self.selected_index) + { + Some(module) => { let new_node: SyntaxNode = toggle_bool_at_path(node, module.path.as_str()); let new_ast: Parse = Root::parse(&new_node.to_string().as_str()); @@ -97,41 +143,26 @@ impl App { let new_modules: Vec = collect_nix_options(&new_node, "", self.current_file.clone()); - let new_module_value: bool = - if let Some(value) = get_nix_value_by_path(&new_node, module.path.as_str()) { - value - } else { - module.value.clone() - }; + let new_module_value: bool = get_nix_value_by_path(&new_node, module.path.as_str()) + .unwrap_or_else(|| module.value.clone()); - self.last_action_status = if let Some(module) = modules.get(self.selected_index) { - match self.current_file { - ConfigSource::System => { - self.system_modules = new_modules; - self.system_ast = new_ast; - fs::write(&self.system_path, new_node.to_string()) - .expect("Nie można zapisać pliku modułów systemowych"); - } - ConfigSource::Home => { - self.home_modules = new_modules; - self.home_ast = new_ast; - fs::write(&self.home_path, new_node.to_string()) - .expect("Nie można zapisać pliku modułów domowych"); - } - }; - format!( - "{} = {} -> {}", - module.path.clone(), - module.value.clone(), - new_module_value - ) - } else { - "Nothing changed".to_string() - }; + (new_node, new_ast, new_modules, new_module_value) } - } + None => ( + node.clone(), + Root::parse(&node.to_string().as_str()), + Vec::new(), + fallback_module_value, + ), + }; - Ok(()) + ( + modules.to_vec(), + new_node, + new_ast, + new_modules, + new_module_value, + ) } } From ba24e36c7a8cae9a5a62c5994f45a0e1aaf83769 Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Sat, 6 Dec 2025 22:17:45 +0100 Subject: [PATCH 7/9] Add NixOS module and improve UI navigation and rendering The flake now includes a NixOS module that enables integration with system configuration. The TUI has been enhanced with better file switching using arrow keys, space bar support for toggling values, improved scrolling logic, styled category headings, and visual selection feedback. Documentation comments were streamlined and placeholder files removed. --- default.nix | 3 +- flake.nix | 50 ++++++++-- src/app.rs | 108 +++++++++++++++------ src/main.rs | 5 + src/nix.rs | 115 +++-------------------- src/placeholder.txt | 224 -------------------------------------------- src/test.nix | 76 --------------- 7 files changed, 140 insertions(+), 441 deletions(-) delete mode 100644 src/placeholder.txt delete mode 100644 src/test.nix diff --git a/default.nix b/default.nix index a9cac24..b6fb6c5 100644 --- a/default.nix +++ b/default.nix @@ -1,5 +1,4 @@ { - lib, rustPlatform, pkg-config, }: @@ -8,5 +7,5 @@ rustPlatform.buildRustPackage { src = ./.; # buildInputs = [ ]; nativeBuildInputs = [pkg-config]; - cargoHash = lib.fakeHash; + cargoHash = "sha256-cFAkKwgLzj6Hr2pq7W/1Ps1G3yKzgEam/qV6p31gadA="; } diff --git a/flake.nix b/flake.nix index 91e1a5c..510c6a2 100644 --- a/flake.nix +++ b/flake.nix @@ -23,7 +23,11 @@ nixpkgs, naersk, fenix, + lib, + cfg, }: let + inherit (lib) mkOption mkIf types; + system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; @@ -32,10 +36,6 @@ cargo = rustToolchain; rustc = rustToolchain; }; - - cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); - packageVersion = cargoToml.package.version; - # ... in { packages.${system} = { default = @@ -44,11 +44,49 @@ develop = naerskLib.buildPackage { name = "garandos-tui"; src = ./.; - # buildInputs = with pkgs; []; nativeBuildInputs = with pkgs; [pkg-config]; }; }; + nixosModules.garandos-tui = { + options.programs.garandos-tui = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable the Garandos TUI module"; + }; + package = mkOption { + type = types.package; + default = self.packages.${system}.default; + defaultText = "self.packages.${system}.default"; + description = "The Garandos TUI package"; + }; + systemModulesFilePath = mkOption { + type = types.path; + default = ""; + example = "/home/\${username}/garandos/hosts/\${hostname}/system-modules.nix"; + description = "The path to the Host's system modules file"; + }; + homeModulesFilePath = mkOption { + type = types.path; + default = ""; + example = "/home/\${username}/garandos/hosts/\${hostname}/home-modules.nix"; + description = "The path to the Host's home modules file"; + }; + }; + config = mkIf (cfg.enable && cfg.systemModulesFilePath != "" && cfg.homeModulesFilePath != "") { + environment.systemPackages = [ + (pkgs.writeScriptBin "garandos-tui" '' + #!${pkgs.runtimeShell} + exec ${cfg.package}/bin/garandos_tui \ + --sf '${cfg.systemModulesFilePath}' \ + --hf '${cfg.homeModulesFilePath}' \ + "$@" + '') + ]; + }; + }; + devShells.${system}.default = pkgs.mkShell { env.RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; buildInputs = [ @@ -57,8 +95,6 @@ nativeBuildInputs = with pkgs; [pkg-config]; shellHook = '' - echo "garandos-tui v${packageVersion}" - echo "" echo "Commands:" echo " nix build - Build production version" echo " nix run - Run production version" diff --git a/src/app.rs b/src/app.rs index 7411fd7..a876590 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,9 +6,9 @@ use ratatui::{ DefaultTerminal, Frame, buffer::Buffer, layout::{Constraint, Layout, Rect}, - style::Stylize, + style::{Color, Stylize}, text::{Line, Span}, - widgets::{Paragraph, Widget}, + widgets::{Block, Borders, Paragraph, Widget}, }; use rnix::{Parse, Root, SyntaxNode}; use std::{ @@ -60,13 +60,19 @@ impl App { if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('q') { self.exit = true; } - // 1 - zmień plik na ConfigSource::System (pomiń wykonywaniej, jeżeli już jest wybrany) - else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('1') { + // 1/← - zmień plik na ConfigSource::System (pomiń wykonywaniej, jeżeli już jest wybrany) + else if key_event.kind == KeyEventKind::Press + && (key_event.code == KeyCode::Char('1') || key_event.code == KeyCode::Left) + { self.current_file = ConfigSource::System; + self.selected_index = 0_usize; } - // 2 - zmień plik na ConfigSource::Home (pomiń wykonywaniej, jeżeli już jest wybrany) - else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('2') { + // 2/→ - zmień plik na ConfigSource::Home (pomiń wykonywaniej, jeżeli już jest wybrany) + else if key_event.kind == KeyEventKind::Press + && (key_event.code == KeyCode::Char('2') || key_event.code == KeyCode::Right) + { self.current_file = ConfigSource::Home; + self.selected_index = 0_usize; } // ↑ - scroll w górę else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Up { @@ -74,10 +80,16 @@ impl App { } // ↓ - scroll w dół else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Down { - self.selected_index = self.selected_index.saturating_add(1); + let max_index = match self.current_file { + ConfigSource::System => self.system_modules.len() - 1, + ConfigSource::Home => self.home_modules.len() - 1, + }; + self.selected_index = self.selected_index.saturating_add(1).min(max_index); } - // ENTER - zmień wartość boolen danej opcji - else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Enter { + // ENTER/SPACE - zmień wartość boolen danej opcji + else if key_event.kind == KeyEventKind::Press + && (key_event.code == KeyCode::Enter || key_event.code == KeyCode::Char(' ')) + { let (modules, new_node, new_ast, new_modules, new_module_value) = self.get_selected_option_context(); @@ -141,7 +153,7 @@ impl App { let new_ast: Parse = Root::parse(&new_node.to_string().as_str()); let new_modules: Vec = - collect_nix_options(&new_node, "", self.current_file.clone()); + collect_nix_options(&new_node, "", "".to_string(), self.current_file.clone()); let new_module_value: bool = get_nix_value_by_path(&new_node, module.path.as_str()) .unwrap_or_else(|| module.value.clone()); @@ -172,43 +184,77 @@ impl Widget for &App { Self: Sized, { let vertical_layout: Layout = Layout::vertical([ - Constraint::Percentage(5), - Constraint::Percentage(90), + Constraint::Percentage(4), + Constraint::Percentage(91), Constraint::Percentage(5), ]); let [title_area, content_area, footer_area] = vertical_layout.areas(area); { - let (system_file, home_file) = match self.current_file { - ConfigSource::System => ("(System)".blue().bold(), "Home".reset()), - ConfigSource::Home => ("System".reset(), "(Home)".blue().bold()), - }; - let title: Line<'_> = Line::from(vec![ - Span::from("GarandOS TUI".bold().italic().yellow()), - Span::from(" * ".reset()), - system_file, - Span::from(" [1]".reset()), - Span::from(" * ".reset()), - home_file, - Span::from(" [2]".reset()), - Span::from(" *".reset()), - Span::from(" Quit".reset()), - Span::from(" [q]".reset()), - ]) - .left_aligned(); + let title: Line<'_> = + Line::from("GarandOS TUI".bold().italic().yellow()).left_aligned(); title.render(title_area, buf); } + { + let modules: &Vec = match self.current_file { + ConfigSource::System => &self.system_modules, + ConfigSource::Home => &self.home_modules, + }; + + let mut lines: Vec = Vec::new(); + let mut last_category: String = String::new(); + + for (idx, module) in modules.iter().enumerate() { + if module.category != last_category { + lines.push("".into()); + let cat_line = Line::from(vec![ + Span::raw(format!("/* {} */", module.category)) + .dark_gray() + .italic(), + ]); + lines.push(cat_line); + last_category = module.category.clone(); + } + + let checkbox: &'static str = if module.value { "☑" } else { "☐" }; + + let mut line: Line<'_> = + Line::from(vec![Span::raw(format!("{} {}", checkbox, module.path))]); + + if idx == self.selected_index { + line = line.style(Color::Yellow) + } + + lines.push(line); + } + + let block_title: &'static str = match self.current_file { + ConfigSource::System => "[ System Configuration ]", + ConfigSource::Home => "[ Home Configuration ]", + }; + + let content: Paragraph<'_> = Paragraph::new(lines) + .left_aligned() + .block( + Block::default() + .borders(Borders::ALL) + .title(block_title.blue().bold()), + ) + .scroll((self.selected_index as u16, 0)); + content.render(content_area, buf); + } + { let footer_line1: Line<'_> = Line::from(vec![ Span::from("Use "), Span::from("↑/↓".italic().green()), Span::from(" to navigate, "), - Span::from("Enter".bold().white()), + Span::from("Enter/Space".bold().white()), Span::from(" to toggle, "), - Span::from("1/2".bold().blue()), + Span::from("1/2 or ←/→".bold().blue()), Span::from(" to switch file, "), Span::from("q".bold().red()), Span::from(" to quit."), diff --git a/src/main.rs b/src/main.rs index e43dd73..0f0cddc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,11 @@ use std::{ fn main() -> Result<()> { let nix_modules: NixModules = get_modules(); + + for module in &nix_modules.system_modules { + println!("{} - ({})", module.category, module.path); + } + let mut terminal: Terminal> = ratatui::init(); let mut app: App = App { exit: false, diff --git a/src/nix.rs b/src/nix.rs index 4c5ccb2..086db49 100644 --- a/src/nix.rs +++ b/src/nix.rs @@ -43,10 +43,14 @@ pub fn build_nix_modules(system_path: PathBuf, home_path: PathBuf) -> NixModules let system_ast: Parse = parse_nix(&system_src); let home_ast: Parse = parse_nix(&home_src); - let system_modules: Vec = - collect_nix_options(&system_ast.syntax(), "", ConfigSource::System); + let system_modules: Vec = collect_nix_options( + &system_ast.syntax(), + "", + "".to_string(), + ConfigSource::System, + ); let home_modules: Vec = - collect_nix_options(&home_ast.syntax(), "", ConfigSource::Home); + collect_nix_options(&home_ast.syntax(), "", "".to_string(), ConfigSource::Home); NixModules { system_path, @@ -58,36 +62,9 @@ pub fn build_nix_modules(system_path: PathBuf, home_path: PathBuf) -> NixModules } } -/// 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 = collect_nix_options(node, "", ConfigSource::System); + let options: Vec = + collect_nix_options(node, "", "".to_string(), ConfigSource::System); let map: HashMap<&str, bool> = options .iter() @@ -97,36 +74,6 @@ pub fn get_nix_value_by_path(node: &SyntaxNode, query_path: &str) -> Option SyntaxNode { let src: String = node.text().to_string(); @@ -210,49 +157,13 @@ pub fn toggle_bool_at_path(node: &SyntaxNode, query_path: &str) -> SyntaxNode { } } -/// 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). -/// -/// # 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` – wektor opcji z kategorią, pełną ścieżką i wartością bool. -/// -/// # 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(), "", ConfigSource::System); -/// assert_eq!(options.len(), 2); -/// assert!(options.iter().any(|option| option.path == "services.ssh.enable" && option.value)); -/// assert!(options.iter().any(|option| option.path == "services.httpd.enable" && !option.value)); -/// ``` pub fn collect_nix_options( node: &SyntaxNode, current_path: &str, + mut current_category: String, current_source: ConfigSource, ) -> Vec { let mut result: Vec = Vec::new(); - let mut category: String = String::new(); let children: Vec> = node.children_with_tokens().collect(); @@ -266,7 +177,7 @@ pub fn collect_nix_options( .trim_end_matches("*/") .trim() .to_string(); - category = content; + current_category = content; } } @@ -297,6 +208,7 @@ pub fn collect_nix_options( result.extend(collect_nix_options( &grand_node, &new_path, + current_category.clone(), current_source.clone(), )); } @@ -313,7 +225,7 @@ pub fn collect_nix_options( }; let bool_value: bool = value_node.text() == "true"; result.push(ConfigOption { - category: category.clone(), + category: current_category.clone(), path: full_path, value: bool_value, source: current_source.clone(), @@ -325,6 +237,7 @@ pub fn collect_nix_options( result.extend(collect_nix_options( child_node, current_path, + current_category.clone(), current_source.clone(), )); } diff --git a/src/placeholder.txt b/src/placeholder.txt deleted file mode 100644 index adc3b8b..0000000 --- a/src/placeholder.txt +++ /dev/null @@ -1,224 +0,0 @@ -// main.rs -use clap::Parser; -use garandos_tui::{ - cli::Cli, - // app::{App, Event, handle_input_events, run_background_thread}, - nix::{ - ConfigOption, ConfigSource, 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, -// }; - -// 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, -// }; - -// let (event_tx, event_rx) = mpsc::channel::(); - -// let tx_to_input_events: Sender = event_tx.clone(); -// thread::spawn(move || { -// handle_input_events(tx_to_input_events); -// }); - -// let tx_to_background_events: Sender = event_tx.clone(); -// thread::spawn(move || { -// run_background_thread(tx_to_background_events); -// }); - -// let app_result: Result<(), io::Error> = app.run(&mut terminal, event_rx); - -// ratatui::restore(); -// app_result -// } - -// fn main() { -// let args: Cli = Cli::parse(); - -// let system_modules_content: String = -// fs::read_to_string(args.system_file).expect("Nie można odczytać pliku {args.system_file}"); - -// let home_modules_content: String = -// fs::read_to_string(args.home_file).expect("Nie można odczytać pliku {args.home_file}"); - -// let ast: Parse = Root::parse(&home_modules_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 = collect_nix_options(&node, "", ConfigSource::System); - -// const OPTION: &str = "flatpak.enable"; - -// println!("Options:"); -// for option in options { -// // if option.path == OPTION { -// println!("{} = {};", option.path, option.value); -// // } -// } - -// // if let Some(value) = get_nix_value_by_path(&node, OPTION) { -// // println!("\n{OPTION}: {}", value); -// // } - -// // let new_node: SyntaxNode = toggle_bool_at_path(&node, OPTION); -// // let new_options: Vec = collect_nix_options(&new_node, "", ConfigSource::System); - -// // println!("\nNew Options:"); -// // for option in new_options { -// // if option.path == OPTION { -// // println!("{} = {};", option.path, option.value); -// // } -// // } - -// // fs::write("src/test.nix", new_node.to_string()).expect("Nie można zapisać tego pliku"); -// } - -// app.rs - -// use core::option::Option; -// 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 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; -// } -// } -// } - -// 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/test.nix b/src/test.nix deleted file mode 100644 index 72ffe37..0000000 --- a/src/test.nix +++ /dev/null @@ -1,76 +0,0 @@ -_: { - /* - Container & Packaging - */ - docker.enable = true; # Docker: container runtime and management - flatpak = { - enable = true; # Flatpak: universal packaging system for Linux - packages = { - sober.enable = false; # Roblox client - warehouse.enable = true; # Flatpak manager - flatseal.enable = true; # Flatpak permissions manager - }; - }; - - /* - Gaming - */ - gamemode.enable = true; # GameMode: optimizes system performance for gaming - gamescope.enable = false; # Gamescope: micro‑compositor for games - steam.enable = true; # Steam: platform for buying and playing games - - packages = { - /* - Container & Packaging - */ - distrobox.enable = false; # Distrobox: containerized development environments - lazydocker.enable = false; # Lazydocker: simple TUI for Docker - - /* - Gaming - */ - prismlauncher.enable = false; # Prism Launcher: Minecraft modded launcher - spaceCadetPinball.enable = true; # SpaceCadet Pinball: classic pinball game - ttySolitaire.enable = true; # TTY Solitaire: terminal‑based solitaire game - - /* - Development Tools - */ - exercism.enable = true; # Exercism: coding practice platform - lazygit.enable = false; # Lazygit: simple TUI for Git - opencode.enable = true; # OpenCode: tools for coding and development - jan.enable = true; # Jan: AI chat UI - - /* - Communication & Collaboration - */ - mattermost.enable = true; # Mattermost: open‑source Slack alternative - slack.enable = true; # Slack: team communication and collaboration tool - tutanota.enable = true; # Tutanota: secure email client - - /* - Productivity / Knowledge Management - */ - bitwarden.enable = false; # Bitwarden: password manager (desktop) - iotas.enable = true; # Iotas: lightweight notes manager - logseq.enable = false; # Logseq: knowledge base and outliner - - /* - Media & Graphics - */ - affinity.enable = false; # Affinity: professional graphics suite - eyeOfGnome.enable = true; # Eye of GNOME: image viewer - freetube.enable = false; # FreeTube: privacy‑friendly YouTube client - gimp.enable = false; # GIMP: GNU Image Manipulation Program - kdenlive.enable = false; # Kdenlive: video editing software - plex.enable = true; # Plex: media player and server client - - /* - Utilities / Misc - */ - eddieAirVPN.enable = true; # Eddie AirVPN: VPN client - galculator.enable = true; # Galculator: simple calculator - gedit.enable = false; # Gedit: GNOME text editor - winboat.enable = false; # Winboat: Windows remote desktop via RDP - }; -} From 8940198d02d87be8eb5128fb5fdc345aa4eb2f12 Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Sat, 6 Dec 2025 23:06:45 +0100 Subject: [PATCH 8/9] poprawienie flake'a i dodanie licencji --- default.nix | 13 +++++++++++++ flake.nix | 13 ++++++++----- src/main.rs | 10 ---------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/default.nix b/default.nix index b6fb6c5..b49369f 100644 --- a/default.nix +++ b/default.nix @@ -1,11 +1,24 @@ { + lib, rustPlatform, pkg-config, }: rustPlatform.buildRustPackage { name = "garandos-tui"; + pname = "garandos-tui"; + version = "0.1.0"; + src = ./.; # buildInputs = [ ]; nativeBuildInputs = [pkg-config]; cargoHash = "sha256-cFAkKwgLzj6Hr2pq7W/1Ps1G3yKzgEam/qV6p31gadA="; + + meta = { + description = "TUI for managing GarandsOS' hosts enabled modules"; + homepage = "https://gitea.garandplg.com/GarandPLG/garandos-tui"; + license = lib.licenses.mit; + maintainers = [ + "Garand_PLG" + ]; + }; } diff --git a/flake.nix b/flake.nix index 510c6a2..2049a04 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "TUI for managing GarandsOS' hosts configuration"; + description = "TUI for managing GarandsOS' hosts enabled modules"; inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; @@ -23,11 +23,7 @@ nixpkgs, naersk, fenix, - lib, - cfg, }: let - inherit (lib) mkOption mkIf types; - system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; @@ -49,6 +45,13 @@ }; nixosModules.garandos-tui = { + config, + lib, + ... + }: let + inherit (lib) mkOption mkIf types; + cfg = config.programs.garandos-tui; + in { options.programs.garandos-tui = { enable = mkOption { type = types.bool; diff --git a/src/main.rs b/src/main.rs index 0f0cddc..4b8526a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,11 +12,6 @@ use std::{ fn main() -> Result<()> { let nix_modules: NixModules = get_modules(); - - for module in &nix_modules.system_modules { - println!("{} - ({})", module.category, module.path); - } - let mut terminal: Terminal> = ratatui::init(); let mut app: App = App { exit: false, @@ -38,11 +33,6 @@ fn main() -> Result<()> { handle_input_events(tx_to_input_events); }); - // let tx_to_file_edit_events: Sender = event_tx.clone(); - // thread::spawn(move || { - // // handle_file_edit_events(tx_to_file_edit_events); - // }); - let app_result: Result<()> = app.run(&mut terminal, event_rx); ratatui::restore(); app_result From 29d82736297792a662b5c52720354bbfb87a4d87 Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Sat, 6 Dec 2025 23:07:18 +0100 Subject: [PATCH 9/9] . --- LICENCE | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 LICENCE diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..0b93610 --- /dev/null +++ b/LICENCE @@ -0,0 +1,19 @@ +Copyright (c) 2025 GarandPLG + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE.