1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use crate::cli::{CliArgs, Command, ListFormat};
5use crate::config::ConfigError;
6use crate::edit::{InputMap, sorted_input_ids};
7use crate::input::Follows;
8use crate::tui;
9
10use super::commands::{self, CommandError};
11use super::editor::Editor;
12use super::state::AppState;
13
14mod root;
15
16pub type Result<T> = std::result::Result<T, HandlerError>;
17
18#[derive(Debug, thiserror::Error)]
19pub enum HandlerError {
20 #[error(transparent)]
21 Command(#[from] CommandError),
22
23 #[error(transparent)]
24 Io(#[from] std::io::Error),
25
26 #[error(transparent)]
27 FlakeEdit(#[from] crate::error::FlakeEditError),
28
29 #[error(transparent)]
30 Config(#[from] ConfigError),
31
32 #[error("Flake not found")]
33 FlakeNotFound,
34
35 #[error("--flake and --lock cannot be used with 'follow [PATHS]'")]
36 IncompatibleFollowOptions,
37}
38
39pub fn run(args: CliArgs) -> Result<()> {
43 if let Command::Follow { paths, transitive } = args.subcommand()
45 && !paths.is_empty()
46 {
47 if args.flake().is_some() || args.lock_file().is_some() {
48 return Err(HandlerError::IncompatibleFollowOptions);
49 }
50 return commands::follow_auto_batch(paths, *transitive, &args)
51 .map_err(HandlerError::Command);
52 }
53
54 let flake_path = if let Some(flake) = args.flake() {
56 PathBuf::from(flake)
57 } else {
58 let path = PathBuf::from("flake.nix");
59 let binding = root::Root::from_path(path).map_err(|_| HandlerError::FlakeNotFound)?;
60 binding.path().to_path_buf()
61 };
62
63 let editor = Editor::from_path(flake_path.clone())?;
65 let mut flake_edit = editor.create_flake_edit()?;
66 let interactive = tui::is_interactive(args.non_interactive());
67
68 let no_cache = args.no_cache();
69 let mut state = AppState::new(editor.text(), flake_path, args.config().map(PathBuf::from))?
70 .with_diff(args.diff())
71 .with_no_lock(args.no_lock())
72 .with_interactive(interactive)
73 .with_lock_file(args.lock_file().map(PathBuf::from))
74 .with_no_cache(no_cache)
75 .with_cache_path(args.cache().map(PathBuf::from));
76
77 match args.subcommand() {
79 Command::Add {
80 uri,
81 ref_or_rev,
82 id,
83 no_flake,
84 shallow,
85 } => {
86 commands::add(
87 &editor,
88 &mut flake_edit,
89 &state,
90 id.clone(),
91 uri.clone(),
92 commands::UriOptions {
93 ref_or_rev: ref_or_rev.as_deref(),
94 shallow: *shallow,
95 no_flake: *no_flake,
96 },
97 )?;
98 }
99
100 Command::Remove { id } => {
101 commands::remove(&editor, &mut flake_edit, &state, id.clone())?;
102 }
103
104 Command::Change {
105 uri,
106 ref_or_rev,
107 id,
108 shallow,
109 } => {
110 commands::change(
111 &editor,
112 &mut flake_edit,
113 &state,
114 id.clone(),
115 uri.clone(),
116 ref_or_rev.as_deref(),
117 *shallow,
118 )?;
119 }
120
121 Command::List { format } => {
122 commands::list(&mut flake_edit, format)?;
123 }
124
125 Command::Update { id, init } => {
126 commands::update(&editor, &mut flake_edit, &state, id.clone(), *init)?;
127 }
128
129 Command::Pin { id, rev } => {
130 commands::pin(&editor, &mut flake_edit, &state, id.clone(), rev.clone())?;
131 }
132
133 Command::Unpin { id } => {
134 commands::unpin(&editor, &mut flake_edit, &state, id.clone())?;
135 }
136
137 Command::Follow {
138 paths: _,
139 transitive,
140 } => {
141 if let Some(min) = transitive {
143 state.config.follow.transitive_min = *min;
144 }
145 commands::follow_auto(&editor, &mut flake_edit, &state)?;
146 }
147
148 Command::AddFollow { input, target } => {
149 commands::add_follow(
150 &editor,
151 &mut flake_edit,
152 &state,
153 input.clone(),
154 target.clone(),
155 )?;
156 }
157
158 Command::Completion { inputs: _, mode } => {
159 use crate::cache::{Cache, DEFAULT_URI_TYPES};
160 use crate::cli::CompletionMode;
161 match mode {
162 CompletionMode::Add => {
163 for uri_type in DEFAULT_URI_TYPES {
164 println!("{}", uri_type);
165 }
166 let cache = Cache::load();
167 for uri in cache.list_uris() {
168 println!("{}", uri);
169 }
170 std::process::exit(0);
171 }
172 CompletionMode::Change => {
173 let inputs = flake_edit.list();
174 crate::cache::populate_cache_from_input_map(inputs, no_cache);
176 for id in inputs.keys() {
177 println!("{}", id);
178 }
179 std::process::exit(0);
180 }
181 CompletionMode::Follow => {
182 if let Ok(lock) = crate::lock::FlakeLock::from_default_path() {
184 for path in lock.nested_input_paths() {
185 println!("{}", path);
186 }
187 }
188 std::process::exit(0);
189 }
190 CompletionMode::None => {}
191 }
192 }
193
194 Command::Config {
195 print_default,
196 path,
197 } => {
198 commands::config(*print_default, *path)?;
199 return Ok(());
200 }
201 }
202
203 crate::cache::populate_cache_from_input_map(flake_edit.curr_list(), no_cache);
207
208 Ok(())
209}
210
211pub fn list_inputs(inputs: &InputMap, format: &ListFormat) {
213 match format {
214 ListFormat::Simple => list_simple(inputs),
215 ListFormat::Json => list_json(inputs),
216 ListFormat::Detailed => list_detailed(inputs),
217 ListFormat::Raw => list_raw(inputs),
218 ListFormat::Toplevel => list_toplevel(inputs),
219 ListFormat::None => unreachable!("Should not be possible"),
220 }
221}
222
223fn list_simple(inputs: &InputMap) {
224 let mut buf = String::new();
225 for key in sorted_input_ids(inputs) {
226 let input = &inputs[key];
227 if !buf.is_empty() {
228 buf.push('\n');
229 }
230 buf.push_str(input.bare_id());
231 for follows in input.follows() {
232 if let Follows::Indirect(id, _) = follows {
233 let id = format!("{}.{}", input.bare_id(), id);
234 if !buf.is_empty() {
235 buf.push('\n');
236 }
237 buf.push_str(&id);
238 }
239 }
240 }
241 println!("{buf}");
242}
243
244fn list_json(inputs: &InputMap) {
245 let sorted: BTreeMap<_, _> = inputs.iter().collect();
246 let json = serde_json::to_string(&sorted).unwrap();
247 println!("{json}");
248}
249
250fn list_toplevel(inputs: &InputMap) {
251 let mut buf = String::new();
252 for key in sorted_input_ids(inputs) {
253 if !buf.is_empty() {
254 buf.push('\n');
255 }
256 buf.push_str(&key.to_string());
257 }
258 println!("{buf}");
259}
260
261fn list_raw(inputs: &InputMap) {
262 let sorted: BTreeMap<_, _> = inputs.iter().collect();
263 println!("{:#?}", sorted);
264}
265
266fn is_toplevel_follows(url: &str) -> bool {
269 let url_trimmed = url.trim_matches('"');
270 !url_trimmed.is_empty()
271 && !url_trimmed.contains(':')
272 && url_trimmed.contains('/')
273 && !url_trimmed.starts_with('/')
274}
275
276fn list_detailed(inputs: &InputMap) {
277 let mut buf = String::new();
278 for key in sorted_input_ids(inputs) {
279 let input = &inputs[key];
280 if !buf.is_empty() {
281 buf.push('\n');
282 }
283 let line = if is_toplevel_follows(input.url()) {
284 format!("· {} <= {}", input.id(), input.url())
285 } else {
286 format!("· {} - {}", input.id(), input.url())
287 };
288 buf.push_str(&line);
289 for follows in input.follows() {
290 if let Follows::Indirect(id, follow_id) = follows {
291 let id = format!("{}{} => {}", " ".repeat(5), id, follow_id);
292 if !buf.is_empty() {
293 buf.push('\n');
294 }
295 buf.push_str(&id);
296 }
297 }
298 }
299 println!("{buf}");
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_is_toplevel_follows() {
308 assert!(is_toplevel_follows("\"harmonia/treefmt-nix\""));
310 assert!(is_toplevel_follows("\"clan-core/treefmt-nix\""));
311 assert!(is_toplevel_follows("clan-core/systems"));
312
313 assert!(!is_toplevel_follows("\"github:NixOS/nixpkgs\""));
315 assert!(!is_toplevel_follows(
316 "\"git+https://git.clan.lol/clan/clan-core\""
317 ));
318 assert!(!is_toplevel_follows("\"path:/some/local/path\""));
319 assert!(!is_toplevel_follows("\"https://github.com/pinpox.keys\""));
320
321 assert!(!is_toplevel_follows("\"/nix/store/abc\""));
323
324 assert!(!is_toplevel_follows("\"nixpkgs\""));
326
327 assert!(!is_toplevel_follows(""));
329 assert!(!is_toplevel_follows("\"\""));
330 }
331}