gostui-setup #1

Merged
GarandPLG merged 9 commits from gostui-setup into main 2025-12-06 22:09:37 +00:00
7 changed files with 140 additions and 441 deletions
Showing only changes of commit ba24e36c7a - Show all commits

View File

@@ -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=";
}

View File

@@ -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"

View File

@@ -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> = Root::parse(&new_node.to_string().as_str());
let new_modules: Vec<ConfigOption> =
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<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".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."),

View File

@@ -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<CrosstermBackend<Stdout>> = ratatui::init();
let mut app: App = App {
exit: false,

View File

@@ -43,10 +43,14 @@ pub fn build_nix_modules(system_path: PathBuf, home_path: PathBuf) -> NixModules
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 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(), "", 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<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()
@@ -97,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();
@@ -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<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();
@@ -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(),
));
}

View File

@@ -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,
// );
// }
// }

View File

@@ -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: 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
};
}