Nix flake configuration manager
1mod config;
2mod flake;
3mod run;
4
5use std::sync::mpsc;
6use std::thread;
7use std::time::Duration;
8
9use sly::block::Block;
10use sly::color::Color;
11use sly::component::RunError;
12use sly::flow::Flow;
13use sly::input;
14use sly::style::{Span, Style};
15use sly::terminal;
16use sly::view::View;
17use sly::widgets::{
18 checklist::{CheckState, Checklist, ChecklistItem},
19 multi_selector::{MultiSelector, MultiSelectorItem},
20 selector::{Selector, SelectorItem},
21 spinner::Spinner,
22};
23
24fn main() {
25 let config = config::load_or_default();
26 let width = terminal::get_size()
27 .map(|(c, _)| c as usize)
28 .unwrap_or(80);
29
30 // ── Discovery ─────────────────────────────────────────────────────────
31 let configs = discover_with_spinner(&config.flake.path, width);
32
33 if configs.is_empty() {
34 eprintln!("No NixOS, Home Manager, or Colmena configurations found in the flake at '{}'.", config.flake.path);
35 eprintln!("Check your flake.nix and ensure `nix eval` works.");
36 std::process::exit(1);
37 }
38
39 // ── Selection ──────────────────────────────────────────────────────────
40 println!("Lumar — Nix Configuration Manager");
41 println!(" Space: toggle Enter: confirm Ctrl+A: all Ctrl+D: none Esc: cancel");
42 println!();
43
44 let default_parallel = config.flake.parallel;
45 let configs_for_flow = configs.clone();
46
47 let flow = build_flow(configs_for_flow, default_parallel);
48
49 let selection = match input::with_raw_mode(|mode| flow.run(mode)) {
50 Ok(Ok(sel)) => sel,
51 Ok(Err(RunError::Cancelled)) => {
52 println!("Cancelled.");
53 return;
54 }
55 Ok(Err(RunError::InputError(e))) => {
56 eprintln!("Input error: {e}");
57 std::process::exit(1);
58 }
59 Err(e) => {
60 eprintln!("Terminal error: {e}");
61 std::process::exit(1);
62 }
63 };
64
65 // ── Execution ──────────────────────────────────────────────────────────
66 println!();
67 execute_with_display(selection.entries, &config, selection.parallel, width);
68}
69
70// ── Flow ──────────────────────────────────────────────────────────────────
71
72struct Selection {
73 entries: Vec<flake::ConfigEntry>,
74 parallel: bool,
75}
76
77fn build_flow(configs: Vec<flake::ConfigEntry>, default_parallel: bool) -> Flow<Selection> {
78 let items: Vec<MultiSelectorItem<usize>> = configs
79 .iter()
80 .enumerate()
81 .map(|(i, entry)| {
82 MultiSelectorItem::label_line(
83 vec![
84 Span::styled(
85 format!("{:>12} ", entry.kind.label()),
86 Style::new().fg(kind_color(&entry.kind)),
87 ),
88 Span::plain(entry.name.clone()),
89 ],
90 i,
91 )
92 })
93 .collect();
94
95 let ms = MultiSelector::new(items).min_selections(1);
96
97 Flow::step(
98 sly::widgets::multi_selector::multi_selector(ms),
99 move |indices: Vec<usize>| {
100 let selected: Vec<flake::ConfigEntry> =
101 indices.iter().map(|&i| configs[i].clone()).collect();
102
103 if selected.len() <= 1 {
104 return Flow::done(Selection { entries: selected, parallel: false });
105 }
106
107 // Ask for execution mode
108 let mut mode_items = vec![
109 SelectorItem::new("Sequential — apply one after another", false),
110 SelectorItem::new("Parallel — apply all at once", true),
111 ];
112 if default_parallel {
113 mode_items.reverse();
114 }
115
116 Flow::step(
117 sly::widgets::selector::selector(Selector::new(mode_items)),
118 move |parallel: bool| Flow::done(Selection { entries: selected, parallel }),
119 )
120 },
121 )
122}
123
124fn kind_color(kind: &flake::ConfigKind) -> Color {
125 match kind {
126 flake::ConfigKind::NixOS => Color::Blue,
127 flake::ConfigKind::HomeManager => Color::Green,
128 flake::ConfigKind::Colmena => Color::Magenta,
129 }
130}
131
132// ── Discovery ──────────────────────────────────────────────────────────────
133
134fn discover_with_spinner(flake_path: &str, width: usize) -> Vec<flake::ConfigEntry> {
135 let spin = Spinner::dots().label("Discovering flake configurations…");
136 let mut view = View::create(&spin.frame(0), width);
137
138 let (tx, rx) = mpsc::sync_channel::<Vec<flake::ConfigEntry>>(1);
139 let path = flake_path.to_string();
140 thread::spawn(move || {
141 tx.send(flake::discover(&path)).ok();
142 });
143
144 let mut tick = 0usize;
145 loop {
146 thread::sleep(Duration::from_millis(80));
147 if let Ok(configs) = rx.try_recv() {
148 view.erase();
149 return configs;
150 }
151 tick += 1;
152 view.update(&spin.frame(tick));
153 }
154}
155
156// ── Execution ──────────────────────────────────────────────────────────────
157
158fn execute_with_display(
159 entries: Vec<flake::ConfigEntry>,
160 config: &config::Config,
161 parallel: bool,
162 width: usize,
163) {
164 let total = entries.len();
165 let mode_label = if parallel { "parallel" } else { "sequential" };
166 println!("Applying {} configuration(s) [{}]…\n", total, mode_label);
167
168 let mut checklist = Checklist::new(
169 entries
170 .iter()
171 .map(|e| ChecklistItem::new(format!("[{}] {}", e.kind.label(), e.name)))
172 .collect(),
173 );
174
175 let spin = Spinner::braille();
176 let mut view = View::create(
177 &render_exec_block(&spin, 0, &checklist, width),
178 width,
179 );
180
181 let (tx, rx) = mpsc::sync_channel::<run::RunMsg>(64);
182 run::start(entries, config.clone(), parallel, tx);
183
184 let mut tick = 0usize;
185 let mut done_count = 0usize;
186
187 loop {
188 thread::sleep(Duration::from_millis(80));
189
190 while let Ok(msg) = rx.try_recv() {
191 match msg {
192 run::RunMsg::Started(i) => checklist.set_state(i, CheckState::InProgress),
193 run::RunMsg::Done(i, ok) => {
194 checklist.set_state(i, if ok { CheckState::Done } else { CheckState::Failed });
195 done_count += 1;
196 }
197 }
198 }
199
200 if done_count >= total {
201 break;
202 }
203
204 tick += 1;
205 view.update(&render_exec_block(&spin, tick, &checklist, width));
206 }
207
208 // Final render without spinner
209 view.update(&checklist.to_block());
210 view.finish();
211
212 println!();
213 let failed = (0..total)
214 .filter(|&i| checklist.get(i).map(|e| e.state == CheckState::Failed).unwrap_or(false))
215 .count();
216
217 if failed == 0 {
218 println!(
219 "{}",
220 sly::ansi::styled("All configurations applied successfully.", &[sly::ansi::fg(&Color::Green)])
221 );
222 } else {
223 eprintln!(
224 "{}",
225 sly::ansi::styled(
226 &format!("{} configuration(s) failed.", failed),
227 &[sly::ansi::fg(&Color::Red)],
228 )
229 );
230 std::process::exit(1);
231 }
232}
233
234fn render_exec_block(spin: &Spinner, tick: usize, checklist: &Checklist, _width: usize) -> Block {
235 Block::vstack(
236 vec![
237 Block::hstack(vec![spin.frame(tick), Block::text_plain(" Applying…")], 0),
238 Block::empty(1),
239 checklist.to_block(),
240 ],
241 0,
242 )
243}