Compare commits
4 Commits
19e820ca93
...
8940198d02
| Author | SHA1 | Date | |
|---|---|---|---|
| 8940198d02 | |||
| ba24e36c7a | |||
| 09faba4f7c | |||
| 6c50da8c18 |
14
default.nix
14
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"
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
55
flake.nix
55
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"
|
||||
|
||||
244
src/app.rs
244
src/app.rs
@@ -1,20 +1,29 @@
|
||||
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,
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
style::{Color, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::Widget,
|
||||
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<Root>,
|
||||
pub home_ast: Parse<Root>,
|
||||
pub system_modules: Vec<ConfigOption>,
|
||||
pub home_modules: Vec<ConfigOption>,
|
||||
pub current_file: ConfigSource,
|
||||
@@ -24,7 +33,6 @@ pub struct App {
|
||||
|
||||
pub enum Event {
|
||||
Input(KeyEvent),
|
||||
EditFile,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -36,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))?;
|
||||
}
|
||||
@@ -54,36 +60,122 @@ 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') || 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 {
|
||||
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();
|
||||
|
||||
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");
|
||||
}
|
||||
// 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;
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
"Nothing changed".to_string()
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_selected_option_context(
|
||||
&self,
|
||||
) -> (
|
||||
Vec<ConfigOption>,
|
||||
SyntaxNode,
|
||||
Parse<Root>,
|
||||
Vec<ConfigOption>,
|
||||
bool,
|
||||
) {
|
||||
let modules: &Vec<ConfigOption> = 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> = Root::parse(&new_node.to_string().as_str());
|
||||
|
||||
let new_modules: Vec<ConfigOption> =
|
||||
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 {
|
||||
@@ -92,32 +184,96 @@ impl Widget for &App {
|
||||
Self: Sized,
|
||||
{
|
||||
let vertical_layout: Layout = Layout::vertical([
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
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 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 title: Line<'_> =
|
||||
Line::from("GarandOS TUI".bold().italic().yellow()).left_aligned();
|
||||
|
||||
navbar.render(title_area, buf);
|
||||
title.render(title_area, buf);
|
||||
}
|
||||
|
||||
{
|
||||
let modules: &Vec<ConfigOption> = match self.current_file {
|
||||
ConfigSource::System => &self.system_modules,
|
||||
ConfigSource::Home => &self.home_modules,
|
||||
};
|
||||
|
||||
let mut lines: Vec<Line> = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
41
src/cli.rs
41
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<ConfigOption>,
|
||||
pub home_modules: Vec<ConfigOption>,
|
||||
}
|
||||
|
||||
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<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
|
||||
build_nix_modules(args.sf, args.hf)
|
||||
}
|
||||
|
||||
13
src/main.rs
13
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<CrosstermBackend<Stdout>> = 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,
|
||||
@@ -29,11 +33,6 @@ fn main() -> Result<()> {
|
||||
handle_input_events(tx_to_input_events);
|
||||
});
|
||||
|
||||
// let tx_to_file_edit_events: Sender<Event> = 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
|
||||
|
||||
160
src/nix.rs
160
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<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 category: String,
|
||||
pub path: String,
|
||||
@@ -8,51 +18,53 @@ 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<Root> {
|
||||
let ast: Parse<Root> = 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<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(),
|
||||
"",
|
||||
"".to_string(),
|
||||
ConfigSource::System,
|
||||
);
|
||||
let home_modules: Vec<ConfigOption> =
|
||||
collect_nix_options(&home_ast.syntax(), "", "".to_string(), ConfigSource::Home);
|
||||
|
||||
NixModules {
|
||||
system_path,
|
||||
system_ast,
|
||||
system_modules,
|
||||
home_path,
|
||||
home_ast,
|
||||
home_modules,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<bool> {
|
||||
let options: Vec<ConfigOption> = collect_nix_options(node, "", ConfigSource::System);
|
||||
let options: Vec<ConfigOption> =
|
||||
collect_nix_options(node, "", "".to_string(), ConfigSource::System);
|
||||
|
||||
let map: HashMap<&str, bool> = options
|
||||
.iter()
|
||||
@@ -62,36 +74,6 @@ pub fn get_nix_value_by_path(node: &SyntaxNode, query_path: &str) -> Option<bool
|
||||
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();
|
||||
|
||||
@@ -175,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<ConfigOption>` – 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<ConfigOption> {
|
||||
let mut result: Vec<ConfigOption> = Vec::new();
|
||||
let mut category: String = String::new();
|
||||
|
||||
let children: Vec<NodeOrToken<SyntaxNode, SyntaxToken>> = node.children_with_tokens().collect();
|
||||
|
||||
@@ -231,7 +177,7 @@ pub fn collect_nix_options(
|
||||
.trim_end_matches("*/")
|
||||
.trim()
|
||||
.to_string();
|
||||
category = content;
|
||||
current_category = content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +208,7 @@ pub fn collect_nix_options(
|
||||
result.extend(collect_nix_options(
|
||||
&grand_node,
|
||||
&new_path,
|
||||
current_category.clone(),
|
||||
current_source.clone(),
|
||||
));
|
||||
}
|
||||
@@ -278,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(),
|
||||
@@ -290,6 +237,7 @@ pub fn collect_nix_options(
|
||||
result.extend(collect_nix_options(
|
||||
child_node,
|
||||
current_path,
|
||||
current_category.clone(),
|
||||
current_source.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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<CrosstermBackend<io::Stdout>> = 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::<Event>();
|
||||
|
||||
// let tx_to_input_events: Sender<Event> = event_tx.clone();
|
||||
// thread::spawn(move || {
|
||||
// handle_input_events(tx_to_input_events);
|
||||
// });
|
||||
|
||||
// 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
|
||||
// }
|
||||
|
||||
// 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> = 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<ConfigOption> = 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<ConfigOption> = 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<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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub 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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl App {
|
||||
// pub 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,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
76
src/test.nix
76
src/test.nix
@@ -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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user