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
171 crate::cache::populate_cache_from_input_map(flake_edit.curr_list(), no_cache);
175
176 Ok(())
177}
178
179pub fn list_inputs(inputs: &InputMap, format: &ListFormat) {
181 match format {
182 ListFormat::Simple => list_simple(inputs),
183 ListFormat::Json => list_json(inputs),
184 ListFormat::Detailed => list_detailed(inputs),
185 ListFormat::Raw => list_raw(inputs),
186 ListFormat::Toplevel => list_toplevel(inputs),
187 ListFormat::None => unreachable!("Should not be possible"),
188 }
189}
190
191fn list_simple(inputs: &InputMap) {
192 let mut buf = String::new();
193 for key in sorted_input_ids(inputs) {
194 let input = &inputs[key];
195 if !buf.is_empty() {
196 buf.push('\n');
197 }
198 buf.push_str(input.id());
199 for follows in input.follows() {
200 if let Follows::Indirect(id, _) = follows {
201 let id = format!("{}.{}", input.id(), id);
202 if !buf.is_empty() {
203 buf.push('\n');
204 }
205 buf.push_str(&id);
206 }
207 }
208 }
209 println!("{buf}");
210}
211
212fn list_json(inputs: &InputMap) {
213 let sorted: BTreeMap<_, _> = inputs.iter().collect();
214 let json = serde_json::to_string(&sorted).unwrap();
215 println!("{json}");
216}
217
218fn list_toplevel(inputs: &InputMap) {
219 let mut buf = String::new();
220 for key in sorted_input_ids(inputs) {
221 if !buf.is_empty() {
222 buf.push('\n');
223 }
224 buf.push_str(&key.to_string());
225 }
226 println!("{buf}");
227}
228
229fn list_raw(inputs: &InputMap) {
230 let sorted: BTreeMap<_, _> = inputs.iter().collect();
231 println!("{:#?}", sorted);
232}
233
234fn is_toplevel_follows(url: &str) -> bool {
237 let url_trimmed = url.trim_matches('"');
238 !url_trimmed.is_empty()
239 && !url_trimmed.contains(':')
240 && url_trimmed.contains('/')
241 && !url_trimmed.starts_with('/')
242}
243
244fn list_detailed(inputs: &InputMap) {
245 let mut buf = String::new();
246 for key in sorted_input_ids(inputs) {
247 let input = &inputs[key];
248 if !buf.is_empty() {
249 buf.push('\n');
250 }
251 let line = if is_toplevel_follows(input.url()) {
252 format!("· {} <= {}", input.id(), input.url())
253 } else {
254 format!("· {} - {}", input.id(), input.url())
255 };
256 buf.push_str(&line);
257 for follows in input.follows() {
258 if let Follows::Indirect(id, follow_id) = follows {
259 let id = format!("{}{} => {}", " ".repeat(5), id, follow_id);
260 if !buf.is_empty() {
261 buf.push('\n');
262 }
263 buf.push_str(&id);
264 }
265 }
266 }
267 println!("{buf}");
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_is_toplevel_follows() {
276 assert!(is_toplevel_follows("\"harmonia/treefmt-nix\""));
278 assert!(is_toplevel_follows("\"clan-core/treefmt-nix\""));
279 assert!(is_toplevel_follows("clan-core/systems"));
280
281 assert!(!is_toplevel_follows("\"github:NixOS/nixpkgs\""));
283 assert!(!is_toplevel_follows(
284 "\"git+https://git.clan.lol/clan/clan-core\""
285 ));
286 assert!(!is_toplevel_follows("\"path:/some/local/path\""));
287 assert!(!is_toplevel_follows("\"https://github.com/pinpox.keys\""));
288
289 assert!(!is_toplevel_follows("\"/nix/store/abc\""));
291
292 assert!(!is_toplevel_follows("\"nixpkgs\""));
294
295 assert!(!is_toplevel_follows(""));
297 assert!(!is_toplevel_follows("\"\""));
298 }
299}