Refactor Nix handling and add toggle UI

- 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.
This commit is contained in:
2025-12-06 17:21:44 +01:00
parent 19e820ca93
commit 6c50da8c18
4 changed files with 171 additions and 88 deletions

View File

@@ -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 crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind};
use ratatui::{ use ratatui::{
DefaultTerminal, Frame, DefaultTerminal, Frame,
@@ -6,15 +8,22 @@ use ratatui::{
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
style::Stylize, style::Stylize,
text::{Line, Span}, text::{Line, Span},
widgets::Widget, widgets::{Paragraph, Widget},
}; };
use rnix::{Parse, Root, SyntaxNode};
use std::{ use std::{
fs,
io::Result, io::Result,
path::PathBuf,
sync::mpsc::{self, Receiver}, sync::mpsc::{self, Receiver},
}; };
pub struct App { pub struct App {
pub exit: bool, pub exit: bool,
pub system_path: PathBuf,
pub home_path: PathBuf,
pub system_ast: Parse<Root>,
pub home_ast: Parse<Root>,
pub system_modules: Vec<ConfigOption>, pub system_modules: Vec<ConfigOption>,
pub home_modules: Vec<ConfigOption>, pub home_modules: Vec<ConfigOption>,
pub current_file: ConfigSource, pub current_file: ConfigSource,
@@ -24,7 +33,7 @@ pub struct App {
pub enum Event { pub enum Event {
Input(KeyEvent), Input(KeyEvent),
EditFile, // EditFile,
} }
impl App { impl App {
@@ -37,7 +46,7 @@ impl App {
match event { match event {
Event::Input(key_event) => self.handle_key_event(key_event)?, Event::Input(key_event) => self.handle_key_event(key_event)?,
// Event::EditFile => self.handle_file_edit_events()?, // Event::EditFile => self.handle_file_edit_events()?,
_ => {} // _ => {}
} }
terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?; 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') { if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('q') {
self.exit = true; 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ę // ↑ - scroll w górę
else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Up { else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Up {
self.selected_index = self.selected_index.saturating_sub(1); self.selected_index = self.selected_index.saturating_sub(1);
@@ -64,21 +81,53 @@ impl App {
} }
// ENTER - zmień wartość boolen danej opcji // ENTER - zmień wartość boolen danej opcji
else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Enter { else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Enter {
} let modules: &Vec<ConfigOption> = match self.current_file {
// 1 - zmień plik na ConfigSource::System (pomiń wykonywaniej, jeżeli już jest wybrany) ConfigSource::System => &self.system_modules.clone(),
else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('1') { ConfigSource::Home => &self.home_modules.clone(),
if self.current_file != ConfigSource::System { };
self.current_file = ConfigSource::System; let node: &SyntaxNode = match self.current_file {
} else { ConfigSource::System => &self.system_ast.syntax(),
self.current_file = ConfigSource::Home; ConfigSource::Home => &self.home_ast.syntax(),
} };
} if let Some(module) = modules.get(self.selected_index) {
// 2 - zmień plik na ConfigSource::Home (pomiń wykonywaniej, jeżeli już jest wybrany) let new_node: SyntaxNode = toggle_bool_at_path(node, module.path.as_str());
else if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Char('2') {
if self.current_file != ConfigSource::Home { let new_ast: Parse<Root> = Root::parse(&new_node.to_string().as_str());
self.current_file = ConfigSource::Home;
} else { let new_modules: Vec<ConfigOption> =
self.current_file = ConfigSource::System; 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, Self: Sized,
{ {
let vertical_layout: Layout = Layout::vertical([ let vertical_layout: Layout = Layout::vertical([
Constraint::Percentage(10), Constraint::Percentage(5),
Constraint::Percentage(80), Constraint::Percentage(90),
Constraint::Percentage(10), Constraint::Percentage(5),
]); ]);
let [title_area, content_area, footer_area] = vertical_layout.areas(area); 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()), let (system_file, home_file) = match self.current_file {
ConfigSource::Home => ("System".reset(), "(Home)".blue().bold()), 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()), let title: Line<'_> = Line::from(vec![
Span::from(" * ".reset()), Span::from("GarandOS TUI".bold().italic().yellow()),
system_file, Span::from(" * ".reset()),
Span::from(" [1]".reset()), system_file,
Span::from(" * ".reset()), Span::from(" [1]".reset()),
home_file, Span::from(" * ".reset()),
Span::from(" [2]".reset()), home_file,
Span::from(" *".reset()), Span::from(" [2]".reset()),
Span::from(" Quit".reset()), Span::from(" *".reset()),
Span::from(" [q]".reset()), Span::from(" Quit".reset()),
]) Span::from(" [q]".reset()),
.left_aligned(); ])
.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);
}
} }
} }

View File

@@ -1,7 +1,6 @@
use crate::nix::{ConfigOption, ConfigSource, collect_nix_options}; use crate::nix::{NixModules, build_nix_modules};
use clap::Parser; use clap::Parser;
use rnix::{Parse, Root}; use std::path::PathBuf;
use std::{fs, path::PathBuf};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command( #[command(
@@ -24,41 +23,7 @@ pub struct Cli {
pub hf: PathBuf, pub hf: PathBuf,
} }
pub struct NixModules {
pub system_modules: Vec<ConfigOption>,
pub home_modules: Vec<ConfigOption>,
}
pub fn get_modules() -> NixModules { pub fn get_modules() -> NixModules {
let args: Cli = Cli::parse(); let args: Cli = Cli::parse();
build_nix_modules(args.sf, args.hf)
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<Root> = get_ast(&system_file);
let home_ast: Parse<Root> = get_ast(&home_file);
let system_modules: Vec<ConfigOption> =
collect_nix_options(&system_ast.syntax(), "", ConfigSource::System);
let home_modules: Vec<ConfigOption> =
collect_nix_options(&home_ast.syntax(), "", ConfigSource::Home);
NixModules {
system_modules,
home_modules,
}
}
fn get_ast(file: &str) -> Parse<Root> {
let ast: Parse<Root> = 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
} }

View File

@@ -1,7 +1,7 @@
use garandos_tui::{ use garandos_tui::{
app::{App, Event, handle_input_events}, app::{App, Event, handle_input_events},
cli::{NixModules, get_modules}, cli::get_modules,
nix::ConfigSource, nix::{ConfigSource, NixModules},
}; };
use ratatui::{Terminal, prelude::CrosstermBackend}; use ratatui::{Terminal, prelude::CrosstermBackend};
use std::{ use std::{
@@ -15,6 +15,10 @@ fn main() -> Result<()> {
let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init(); let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init();
let mut app: App = App { let mut app: App = App {
exit: false, 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, system_modules: nix_modules.system_modules,
home_modules: nix_modules.home_modules, home_modules: nix_modules.home_modules,
current_file: ConfigSource::System, current_file: ConfigSource::System,

View File

@@ -1,6 +1,16 @@
use rnix::{NodeOrToken, Root, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize}; use rnix::{NodeOrToken, Parse, Root, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize};
use std::collections::HashMap; use std::{collections::HashMap, fs, path::PathBuf};
pub struct NixModules {
pub system_path: PathBuf,
pub system_ast: Parse<Root>,
pub system_modules: Vec<ConfigOption>,
pub home_path: PathBuf,
pub home_ast: Parse<Root>,
pub home_modules: Vec<ConfigOption>,
}
#[derive(Clone)]
pub struct ConfigOption { pub struct ConfigOption {
pub category: String, pub category: String,
pub path: String, pub path: String,
@@ -8,18 +18,43 @@ pub struct ConfigOption {
pub source: ConfigSource, pub source: ConfigSource,
} }
#[derive(PartialEq)] #[derive(PartialEq, Clone)]
pub enum ConfigSource { pub enum ConfigSource {
System, System,
Home, Home,
} }
impl Clone for ConfigSource { pub fn parse_nix(src: &str) -> Parse<Root> {
fn clone(&self) -> Self { let ast: Parse<Root> = Root::parse(src);
match self { if !ast.errors().is_empty() {
ConfigSource::System => ConfigSource::System, eprintln!("Błędy parsowania:");
ConfigSource::Home => ConfigSource::Home, 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<Root> = parse_nix(&system_src);
let home_ast: Parse<Root> = parse_nix(&home_src);
let system_modules: Vec<ConfigOption> =
collect_nix_options(&system_ast.syntax(), "", ConfigSource::System);
let home_modules: Vec<ConfigOption> =
collect_nix_options(&home_ast.syntax(), "", ConfigSource::Home);
NixModules {
system_path,
system_ast,
system_modules,
home_path,
home_ast,
home_modules,
} }
} }