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