diff --git a/Cargo.lock b/Cargo.lock index 137f179..8a7dd9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,62 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bitflags" version = "2.10.0" @@ -35,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" @@ -58,6 +160,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" @@ -201,13 +309,21 @@ 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", ] +[[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" @@ -253,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" @@ -313,7 +435,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]] @@ -328,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" @@ -405,6 +542,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 +695,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" @@ -565,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 abd0dc5..ef9d477 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +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/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. diff --git a/default.nix b/default.nix index a9cac24..b49369f 100644 --- a/default.nix +++ b/default.nix @@ -5,8 +5,20 @@ }: rustPlatform.buildRustPackage { name = "garandos-tui"; + pname = "garandos-tui"; + version = "0.1.0"; + src = ./.; # buildInputs = [ ]; nativeBuildInputs = [pkg-config]; - cargoHash = lib.fakeHash; + 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 91e1a5c..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"; @@ -32,10 +32,6 @@ cargo = rustToolchain; rustc = rustToolchain; }; - - cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); - packageVersion = cargoToml.package.version; - # ... in { packages.${system} = { default = @@ -44,11 +40,56 @@ develop = naerskLib.buildPackage { name = "garandos-tui"; src = ./.; - # buildInputs = with pkgs; []; nativeBuildInputs = with pkgs; [pkg-config]; }; }; + 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; + 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 +98,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 new file mode 100644 index 0000000..a876590 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,295 @@ +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, + buffer::Buffer, + layout::{Constraint, Layout, Rect}, + style::{Color, Stylize}, + text::{Line, Span}, + widgets::{Block, Borders, 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, + pub selected_index: usize, + pub last_action_status: String, +} + +pub enum Event { + Input(KeyEvent), +} + +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)?, + } + 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; + } + // 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') || 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 { + self.selected_index = self.selected_index.saturating_sub(1); + } + // ↓ - scroll w dół + else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Down { + 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/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(); + + 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() + }; + } + + 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()); + + let new_modules: Vec = + 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()); + + (new_node, new_ast, new_modules, new_module_value) + } + None => ( + node.clone(), + Root::parse(&node.to_string().as_str()), + Vec::new(), + fallback_module_value, + ), + }; + + ( + modules.to_vec(), + new_node, + new_ast, + new_modules, + new_module_value, + ) + } +} + +impl Widget for &App { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let vertical_layout: Layout = Layout::vertical([ + Constraint::Percentage(4), + Constraint::Percentage(91), + Constraint::Percentage(5), + ]); + + let [title_area, content_area, footer_area] = vertical_layout.areas(area); + + { + 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/Space".bold().white()), + Span::from(" to toggle, "), + Span::from("1/2 or ←/→".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); + } + } +} + +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; + } + } + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..7e1e48c --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,29 @@ +use crate::nix::{NixModules, build_nix_modules}; +use clap::Parser; +use std::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 fn get_modules() -> NixModules { + let args: Cli = Cli::parse(); + build_nix_modules(args.sf, args.hf) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..027a671 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod app; +pub mod cli; +pub mod nix; diff --git a/src/main.rs b/src/main.rs index 699dc96..4b8526a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,29 @@ -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 garandos_tui::{ + app::{App, Event, handle_input_events}, + cli::get_modules, + nix::{ConfigSource, NixModules}, }; +use ratatui::{Terminal, prelude::CrosstermBackend}; use std::{ - io, + io::{Result, Stdout}, sync::mpsc::{self, Sender}, thread, - time::Duration, }; -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_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, + selected_index: 0_usize, + last_action_status: "".to_string(), }; let (event_tx, event_rx) = mpsc::channel::(); @@ -32,136 +33,7 @@ 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 app_result: Result<()> = 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; - } - } - } -} - -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, - ); - } -} diff --git a/src/nix.rs b/src/nix.rs new file mode 100644 index 0000000..086db49 --- /dev/null +++ b/src/nix.rs @@ -0,0 +1,250 @@ +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, + pub value: bool, + pub source: ConfigSource, +} + +#[derive(PartialEq, Clone)] +pub enum ConfigSource { + System, + 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(), + "", + "".to_string(), + ConfigSource::System, + ); + let home_modules: Vec = + collect_nix_options(&home_ast.syntax(), "", "".to_string(), ConfigSource::Home); + + NixModules { + system_path, + system_ast, + system_modules, + home_path, + home_ast, + home_modules, + } +} + +pub fn get_nix_value_by_path(node: &SyntaxNode, query_path: &str) -> Option { + let options: Vec = + collect_nix_options(node, "", "".to_string(), ConfigSource::System); + + let map: HashMap<&str, bool> = options + .iter() + .map(|option| (option.path.as_str(), option.value)) + .collect(); + + map.get(query_path).copied() +} + +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() + } +} + +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 children: Vec> = node.children_with_tokens().collect(); + + 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(); + current_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( + &grand_node, + &new_path, + current_category.clone(), + current_source.clone(), + )); + } + _ => {} + } + } + } + + 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 bool_value: bool = value_node.text() == "true"; + result.push(ConfigOption { + category: current_category.clone(), + path: full_path, + value: bool_value, + source: current_source.clone(), + }); + } + } + + NodeOrToken::Node(child_node) => { + result.extend(collect_nix_options( + child_node, + current_path, + current_category.clone(), + current_source.clone(), + )); + } + + _ => {} + } + } + + result +}