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 } = 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, &args).map_err(HandlerError::Command);
51 }
52
53 let flake_path = if let Some(flake) = args.flake() {
55 PathBuf::from(flake)
56 } else {
57 let path = PathBuf::from("flake.nix");
58 let binding = root::Root::from_path(path).map_err(|_| HandlerError::FlakeNotFound)?;
59 binding.path().to_path_buf()
60 };
61
62 let editor = Editor::from_path(flake_path.clone())?;
64 let mut flake_edit = editor.create_flake_edit()?;
65 let interactive = tui::is_interactive(args.non_interactive());
66
67 let no_cache = args.no_cache();
68 let state = AppState::new(editor.text(), flake_path, args.config().map(PathBuf::from))?
69 .with_diff(args.diff())
70 .with_no_lock(args.no_lock())
71 .with_interactive(interactive)
72 .with_lock_file(args.lock_file().map(PathBuf::from))
73 .with_no_cache(no_cache)
74 .with_cache_path(args.cache().map(PathBuf::from));
75
76 match args.subcommand() {
78 Command::Add {
79 uri,
80 ref_or_rev,
81 id,
82 no_flake,
83 shallow,
84 } => {
85 commands::add(
86 &editor,
87 &mut flake_edit,
88 &state,
89 id.clone(),
90 uri.clone(),
91 commands::UriOptions {
92 ref_or_rev: ref_or_rev.as_deref(),
93 shallow: *shallow,
94 no_flake: *no_flake,
95 },
96 )?;
97 }
98
99 Command::Remove { id } => {
100 commands::remove(&editor, &mut flake_edit, &state, id.clone())?;
101 }
102
103 Command::Change {
104 uri,
105 ref_or_rev,
106 id,
107 shallow,
108 } => {
109 commands::change(
110 &editor,
111 &mut flake_edit,
112 &state,
113 id.clone(),
114 uri.clone(),
115 ref_or_rev.as_deref(),
116 *shallow,
117 )?;
118 }
119
120 Command::List { format } => {
121 commands::list(&mut flake_edit, format)?;
122 }
123
124 Command::Update { id, init } => {
125 commands::update(&editor, &mut flake_edit, &state, id.clone(), *init)?;
126 }
127
128 Command::Pin { id, rev } => {
129 commands::pin(&editor, &mut flake_edit, &state, id.clone(), rev.clone())?;
130 }
131
132 Command::Unpin { id } => {
133 commands::unpin(&editor, &mut flake_edit, &state, id.clone())?;
134 }
135
136 Command::Follow { paths: _ } => {
137 commands::follow_auto(&editor, &mut flake_edit, &state)?;
139 }
140
141 Command::AddFollow { input, target } => {
142 commands::add_follow(
143 &editor,
144 &mut flake_edit,
145 &state,
146 input.clone(),
147 target.clone(),
148 )?;
149 }
150
151 Command::Completion { inputs: _, mode } => {
152 use crate::cache::{Cache, DEFAULT_URI_TYPES};
153 use crate::cli::CompletionMode;
154 match mode {
155 CompletionMode::Add => {
156 for uri_type in DEFAULT_URI_TYPES {
157 println!("{}", uri_type);
158 }
159 let cache = Cache::load();
160 for uri in cache.list_uris() {
161 println!("{}", uri);
162 }
163 std::process::exit(0);
164 }
165 CompletionMode::Change => {
166 let inputs = flake_edit.list();
167 crate::cache::populate_cache_from_input_map(inputs, no_cache);
169 for id in inputs.keys() {
170 println!("{}", id);
171 }
172 std::process::exit(0);
173 }
174 CompletionMode::Follow => {
175 if let Ok(lock) = crate::lock::FlakeLock::from_default_path() {
177 for path in lock.nested_input_paths() {
178 println!("{}", path);
179 }
180 }
181 std::process::exit(0);
182 }
183 CompletionMode::None => {}
184 }
185 }
186
187 Command::Config {
188 print_default,
189 path,
190 } => {
191 commands::config(*print_default, *path)?;
192 return Ok(());
193 }
194 }
195
196 crate::cache::populate_cache_from_input_map(flake_edit.curr_list(), no_cache);
200
201 Ok(())
202}
203
204pub fn list_inputs(inputs: &InputMap, format: &ListFormat) {
206 match format {
207 ListFormat::Simple => list_simple(inputs),
208 ListFormat::Json => list_json(inputs),
209 ListFormat::Detailed => list_detailed(inputs),
210 ListFormat::Raw => list_raw(inputs),
211 ListFormat::Toplevel => list_toplevel(inputs),
212 ListFormat::None => unreachable!("Should not be possible"),
213 }
214}
215
216fn list_simple(inputs: &InputMap) {
217 let mut buf = String::new();
218 for key in sorted_input_ids(inputs) {
219 let input = &inputs[key];
220 if !buf.is_empty() {
221 buf.push('\n');
222 }
223 buf.push_str(input.id());
224 for follows in input.follows() {
225 if let Follows::Indirect(id, _) = follows {
226 let id = format!("{}.{}", input.id(), id);
227 if !buf.is_empty() {
228 buf.push('\n');
229 }
230 buf.push_str(&id);
231 }
232 }
233 }
234 println!("{buf}");
235}
236
237fn list_json(inputs: &InputMap) {
238 let sorted: BTreeMap<_, _> = inputs.iter().collect();
239 let json = serde_json::to_string(&sorted).unwrap();
240 println!("{json}");
241}
242
243fn list_toplevel(inputs: &InputMap) {
244 let mut buf = String::new();
245 for key in sorted_input_ids(inputs) {
246 if !buf.is_empty() {
247 buf.push('\n');
248 }
249 buf.push_str(&key.to_string());
250 }
251 println!("{buf}");
252}
253
254fn list_raw(inputs: &InputMap) {
255 let sorted: BTreeMap<_, _> = inputs.iter().collect();
256 println!("{:#?}", sorted);
257}
258
259fn is_toplevel_follows(url: &str) -> bool {
262 let url_trimmed = url.trim_matches('"');
263 !url_trimmed.is_empty()
264 && !url_trimmed.contains(':')
265 && url_trimmed.contains('/')
266 && !url_trimmed.starts_with('/')
267}
268
269fn list_detailed(inputs: &InputMap) {
270 let mut buf = String::new();
271 for key in sorted_input_ids(inputs) {
272 let input = &inputs[key];
273 if !buf.is_empty() {
274 buf.push('\n');
275 }
276 let line = if is_toplevel_follows(input.url()) {
277 format!("· {} <= {}", input.id(), input.url())
278 } else {
279 format!("· {} - {}", input.id(), input.url())
280 };
281 buf.push_str(&line);
282 for follows in input.follows() {
283 if let Follows::Indirect(id, follow_id) = follows {
284 let id = format!("{}{} => {}", " ".repeat(5), id, follow_id);
285 if !buf.is_empty() {
286 buf.push('\n');
287 }
288 buf.push_str(&id);
289 }
290 }
291 }
292 println!("{buf}");
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_is_toplevel_follows() {
301 assert!(is_toplevel_follows("\"harmonia/treefmt-nix\""));
303 assert!(is_toplevel_follows("\"clan-core/treefmt-nix\""));
304 assert!(is_toplevel_follows("clan-core/systems"));
305
306 assert!(!is_toplevel_follows("\"github:NixOS/nixpkgs\""));
308 assert!(!is_toplevel_follows(
309 "\"git+https://git.clan.lol/clan/clan-core\""
310 ));
311 assert!(!is_toplevel_follows("\"path:/some/local/path\""));
312 assert!(!is_toplevel_follows("\"https://github.com/pinpox.keys\""));
313
314 assert!(!is_toplevel_follows("\"/nix/store/abc\""));
316
317 assert!(!is_toplevel_follows("\"nixpkgs\""));
319
320 assert!(!is_toplevel_follows(""));
322 assert!(!is_toplevel_follows("\"\""));
323 }
324}