Add TUI application and Nix option helpers
Introduce a terminal UI with a progress bar and key controls, and add utilities for collecting and querying boolean Nix options. The toggle function also allows flipping a boolean value in the source.
This commit is contained in:
140
src/app.rs
Normal file
140
src/app.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
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 enum Event {
|
||||||
|
Input(KeyEvent),
|
||||||
|
Progress(f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
pub exit: bool,
|
||||||
|
pub progress_bar_color: Color,
|
||||||
|
pub background_progress: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod nix;
|
||||||
93
src/main.rs
93
src/main.rs
@@ -1,24 +1,81 @@
|
|||||||
use rnix::{Parse, Root};
|
use garandos_tui::{
|
||||||
use std::fs;
|
app::{App, Event, handle_input_events, run_background_thread},
|
||||||
|
// nix::{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,
|
||||||
|
};
|
||||||
|
|
||||||
mod nix;
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
let (event_tx, event_rx) = mpsc::channel::<Event>();
|
||||||
let content: String =
|
|
||||||
fs::read_to_string("src/test.nix").expect("Nie można odczytać pliku src/test.nix");
|
|
||||||
|
|
||||||
let ast: Parse<Root> = Root::parse(&content);
|
let tx_to_input_events: Sender<Event> = event_tx.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
handle_input_events(tx_to_input_events);
|
||||||
|
});
|
||||||
|
|
||||||
if !ast.errors().is_empty() {
|
let tx_to_background_events: Sender<Event> = event_tx.clone();
|
||||||
eprintln!("Błędy parsowania:");
|
thread::spawn(move || {
|
||||||
for error in ast.errors() {
|
run_background_thread(tx_to_background_events);
|
||||||
eprintln!(" - {}", error);
|
});
|
||||||
}
|
|
||||||
eprintln!();
|
|
||||||
}
|
|
||||||
|
|
||||||
let options: Vec<(String, String, bool)> = nix::collect_nix_options(&ast.syntax());
|
let app_result: Result<(), io::Error> = app.run(&mut terminal, event_rx);
|
||||||
for (category, path, value) in options {
|
|
||||||
println!("{}: {} = {};", category, path, value);
|
ratatui::restore();
|
||||||
}
|
app_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fn main() {
|
||||||
|
// let content: String =
|
||||||
|
// fs::read_to_string("src/test.nix").expect("Nie można odczytać pliku src/test.nix");
|
||||||
|
|
||||||
|
// let ast: Parse<Root> = Root::parse(&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<(String, String, bool)> = collect_nix_options(&node);
|
||||||
|
|
||||||
|
// const OPTION: &str = "flatpak.enable";
|
||||||
|
|
||||||
|
// println!("Options:");
|
||||||
|
// for (_, path, value) in options {
|
||||||
|
// if path == OPTION {
|
||||||
|
// println!("{} = {};", path, value);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // if let Some(value) = get_nix_value_by_path(&node, OPTION) {
|
||||||
|
// // println!("Flatseal ma wartość: {}", value);
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// let new_node: SyntaxNode = toggle_bool_at_path(&node, OPTION);
|
||||||
|
// let new_options: Vec<(String, String, bool)> = collect_nix_options(&new_node);
|
||||||
|
|
||||||
|
// println!("\nNew Options:");
|
||||||
|
// for (_, path, value) in new_options {
|
||||||
|
// if path == OPTION {
|
||||||
|
// println!("{} = {};", path, value);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fs::write("src/test.nix", new_node.to_string()).expect("Nie można zapisać tego pliku");
|
||||||
|
// }
|
||||||
|
|||||||
223
src/nix.rs
223
src/nix.rs
@@ -1,9 +1,220 @@
|
|||||||
use rnix::{NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken};
|
use rnix::{NodeOrToken, Root, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Zbiera wszystkie opcje typu `bool` z drzewa składniowego Nix.
|
||||||
|
///
|
||||||
|
/// Jest to niewielka funkcja‑wrapper, która deleguje właściwe zebranie opcji do
|
||||||
|
/// `collect_nix_options_with_path`, przekazując jako początkową ścieżkę i kategorię
|
||||||
|
/// pusty ciąg znaków.
|
||||||
|
///
|
||||||
|
/// # Argumenty
|
||||||
|
///
|
||||||
|
/// * `node` – węzeł korzenia drzewa składniowego, od którego rozpoczynamy przeszukiwanie.
|
||||||
|
///
|
||||||
|
/// # Zwraca
|
||||||
|
///
|
||||||
|
/// `Vec<(String, String, bool)>`, gdzie:
|
||||||
|
/// - pierwszy element (`String`) to nazwa kategorii (pobrana z najbliższego
|
||||||
|
/// blokowego komentarza `/* … */`);
|
||||||
|
/// - drugi element (`String`) to pełna ścieżka do opcji (np. `services.ssh.enable`);
|
||||||
|
/// - trzeci element (`bool`) to wartość logiczna tej opcji.
|
||||||
|
///
|
||||||
|
/// # 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());
|
||||||
|
/// assert_eq!(options.len(), 2);
|
||||||
|
/// assert!(options.iter().any(|(_, path, val)| path == "services.ssh.enable" && *val));
|
||||||
|
/// assert!(options.iter().any(|(_, path, val)| path == "services.httpd.enable" && !*val));
|
||||||
|
/// ```
|
||||||
pub fn collect_nix_options(node: &SyntaxNode) -> Vec<(String, String, bool)> {
|
pub fn collect_nix_options(node: &SyntaxNode) -> Vec<(String, String, bool)> {
|
||||||
collect_nix_options_with_path(node, "", "")
|
collect_nix_options_with_path(node, "", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<(String, String, bool)> = collect_nix_options(node);
|
||||||
|
|
||||||
|
let map: HashMap<&str, bool> = options
|
||||||
|
.iter()
|
||||||
|
.map(|(_, path, val)| (path.as_str(), *val))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
fn walk(
|
||||||
|
syntax_node: &SyntaxNode,
|
||||||
|
cur_path: &str,
|
||||||
|
query_path: &str,
|
||||||
|
) -> Option<(rnix::TextRange, bool)> {
|
||||||
|
let children: Vec<NodeOrToken<SyntaxNode, SyntaxToken>> =
|
||||||
|
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<SyntaxNode> = 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 = <TextSize as Into<usize>>::into(range.start());
|
||||||
|
let end: usize = <TextSize as Into<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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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).
|
||||||
|
///
|
||||||
|
/// Funkcja jest wewnętrzną implementacją używaną przez `collect_nix_options`.
|
||||||
|
/// Nie jest częścią publicznego API, ale jej zachowanie jest opisane poniżej,
|
||||||
|
/// aby ułatwić utrzymanie kodu.
|
||||||
|
///
|
||||||
|
/// # 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<(String, String, bool)>` – wektor trójek `(kategoria, pełna_ścieżka,
|
||||||
|
/// wartość_bool)`.
|
||||||
fn collect_nix_options_with_path(
|
fn collect_nix_options_with_path(
|
||||||
node: &SyntaxNode,
|
node: &SyntaxNode,
|
||||||
current_path: &str,
|
current_path: &str,
|
||||||
@@ -52,7 +263,6 @@ fn collect_nix_options_with_path(
|
|||||||
} else {
|
} else {
|
||||||
format!("{}.{}", current_path, attr_path)
|
format!("{}.{}", current_path, attr_path)
|
||||||
};
|
};
|
||||||
|
|
||||||
result.extend(collect_nix_options_with_path(
|
result.extend(collect_nix_options_with_path(
|
||||||
&grand_node,
|
&grand_node,
|
||||||
&new_path,
|
&new_path,
|
||||||
@@ -70,14 +280,7 @@ fn collect_nix_options_with_path(
|
|||||||
} else {
|
} else {
|
||||||
format!("{}.{}", current_path, attr_path)
|
format!("{}.{}", current_path, attr_path)
|
||||||
};
|
};
|
||||||
|
let bool_value: bool = value_node.text() == "true";
|
||||||
let value_text: String = value_node.text().to_string();
|
|
||||||
let mut bool_value: bool = false;
|
|
||||||
|
|
||||||
if value_text == "true" {
|
|
||||||
bool_value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push((category.clone(), full_path, bool_value));
|
result.push((category.clone(), full_path, bool_value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ _: {
|
|||||||
flatpak = {
|
flatpak = {
|
||||||
enable = true; # Flatpak: universal packaging system for Linux
|
enable = true; # Flatpak: universal packaging system for Linux
|
||||||
packages = {
|
packages = {
|
||||||
sober = false; # Roblox client
|
sober.enable = false; # Roblox client
|
||||||
warehouse = true; # Flatpak manager
|
warehouse.enable = true; # Flatpak manager
|
||||||
flatseal = true; # Flatpak permissions manager
|
flatseal.enable = true; # Flatpak permissions manager
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user