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
This commit is contained in:
2025-12-01 00:04:51 +01:00
parent e7f575ca47
commit cb35269308
5 changed files with 225 additions and 161 deletions

View File

@@ -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<CrosstermBackend<io::Stdout>> = 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::<Event>();
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> = event_tx.clone();
thread::spawn(move || {
handle_input_events(tx_to_input_events);
});
let ast: Parse<Root> = Root::parse(&content);
let tx_to_background_events: Sender<Event> = 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<Event>) {
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<Event>) {
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<Event>) -> 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(),
"<C>".blue().bold(),
" Quit ".into(),
"<Q>".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);
}
}

68
src/nix.rs Normal file
View File

@@ -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<SyntaxNode> = 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
}

76
src/test.nix Normal file
View File

@@ -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: microcompositor 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: terminalbased 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: opensource 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: privacyfriendly 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
};
}