Nix flake configuration manager
at main 243 lines 8.1 kB view raw
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}