Skip to main content

flake_edit/app/
handler.rs

1use std::path::PathBuf;
2
3use crate::cli::{CliArgs, Command};
4use crate::edit::FlakeEdit;
5use crate::tui;
6
7use super::commands::follow;
8use super::commands::{self};
9use super::editor::Editor;
10use super::error::{Error, Result};
11use super::state::AppState;
12
13mod root;
14
15/// Application entry point.
16///
17/// Parses CLI arguments, initializes state, and dispatches to command handlers.
18pub fn run(args: CliArgs) -> Result<()> {
19    if let Command::Follow {
20        paths,
21        transitive,
22        depth,
23    } = args.subcommand()
24        && !paths.is_empty()
25    {
26        if args.flake().is_some() || args.lock_file().is_some() {
27            return Err(Error::IncompatibleFollowOptions);
28        }
29        return follow::auto::run_batch(paths, *transitive, *depth, &args);
30    }
31
32    let (editor, mut flake_edit, mut state) = setup(&args)?;
33    let no_cache = args.no_cache();
34
35    match args.subcommand() {
36        Command::Add { .. } => dispatch_add(&args, &editor, &mut flake_edit, &state)?,
37        Command::Remove { .. } => dispatch_remove(&args, &editor, &mut flake_edit, &state)?,
38        Command::Change { .. } => dispatch_change(&args, &editor, &mut flake_edit, &state)?,
39        Command::List { .. } => dispatch_list(&args, &mut flake_edit)?,
40        Command::Update { .. } => dispatch_update(&args, &editor, &mut flake_edit, &state)?,
41        Command::Pin { .. } => dispatch_pin(&args, &editor, &mut flake_edit, &state)?,
42        Command::Unpin { .. } => dispatch_unpin(&args, &editor, &mut flake_edit, &state)?,
43        Command::Follow { .. } => dispatch_follow(&args, &editor, &mut flake_edit, &mut state)?,
44        Command::AddFollow { .. } => {
45            dispatch_add_follow(&args, &editor, &mut flake_edit, &mut state)?
46        }
47        Command::Completion { .. } => {
48            return dispatch_completion(&args, &mut flake_edit, no_cache);
49        }
50        Command::Config { .. } => return dispatch_config(&args),
51    }
52
53    crate::cache::populate_cache_from_input_map(flake_edit.curr_list(), no_cache);
54
55    Ok(())
56}
57
58fn setup(args: &CliArgs) -> Result<(Editor, FlakeEdit, AppState)> {
59    let flake_path = if let Some(flake) = args.flake() {
60        let path = PathBuf::from(flake);
61        if path.is_dir() {
62            let flake_nix = path.join("flake.nix");
63            if !flake_nix.exists() {
64                return Err(Error::FlakeDirEmpty { path });
65            }
66            flake_nix
67        } else {
68            path
69        }
70    } else {
71        let path = PathBuf::from("flake.nix");
72        let binding = root::Root::from_path(&path).map_err(|source| Error::FlakeNotFound {
73            path: path.clone(),
74            source,
75        })?;
76        binding.path().to_path_buf()
77    };
78
79    let editor = Editor::from_path(flake_path.clone()).map_err(|source| Error::FlakeNotFound {
80        path: flake_path.clone(),
81        source,
82    })?;
83    let flake_edit = editor.create_flake_edit()?;
84    let interactive = tui::is_interactive(args.non_interactive());
85
86    let state = AppState::new(flake_path, args.config().map(PathBuf::from))?
87        .with_diff(args.diff())
88        .with_no_lock(args.no_lock())
89        .with_interactive(interactive)
90        .with_lock_file(args.lock_file().map(PathBuf::from))
91        .with_no_cache(args.no_cache())
92        .with_cache_path(args.cache().map(PathBuf::from));
93
94    Ok((editor, flake_edit, state))
95}
96
97fn dispatch_add(
98    args: &CliArgs,
99    editor: &Editor,
100    flake_edit: &mut FlakeEdit,
101    state: &AppState,
102) -> Result<()> {
103    let Command::Add {
104        uri,
105        ref_or_rev,
106        id,
107        no_flake,
108        shallow,
109    } = args.subcommand()
110    else {
111        unreachable!("wrong Command variant");
112    };
113    commands::add(
114        editor,
115        flake_edit,
116        state,
117        id.clone(),
118        uri.clone(),
119        *no_flake,
120        commands::UriOptions {
121            ref_or_rev: ref_or_rev.as_deref(),
122            shallow: *shallow,
123        },
124    )
125}
126
127fn dispatch_remove(
128    args: &CliArgs,
129    editor: &Editor,
130    flake_edit: &mut FlakeEdit,
131    state: &AppState,
132) -> Result<()> {
133    let Command::Remove { id } = args.subcommand() else {
134        unreachable!("wrong Command variant");
135    };
136    commands::remove(editor, flake_edit, state, id.clone())
137}
138
139fn dispatch_change(
140    args: &CliArgs,
141    editor: &Editor,
142    flake_edit: &mut FlakeEdit,
143    state: &AppState,
144) -> Result<()> {
145    let Command::Change {
146        uri,
147        ref_or_rev,
148        id,
149        shallow,
150    } = args.subcommand()
151    else {
152        unreachable!("wrong Command variant");
153    };
154    commands::change(
155        editor,
156        flake_edit,
157        state,
158        id.clone(),
159        uri.clone(),
160        commands::UriOptions {
161            ref_or_rev: ref_or_rev.as_deref(),
162            shallow: *shallow,
163        },
164    )
165}
166
167fn dispatch_list(args: &CliArgs, flake_edit: &mut FlakeEdit) -> Result<()> {
168    let Command::List { format } = args.subcommand() else {
169        unreachable!("wrong Command variant");
170    };
171    commands::list(flake_edit, format)
172}
173
174fn dispatch_update(
175    args: &CliArgs,
176    editor: &Editor,
177    flake_edit: &mut FlakeEdit,
178    state: &AppState,
179) -> Result<()> {
180    let Command::Update { id, init } = args.subcommand() else {
181        unreachable!("wrong Command variant");
182    };
183    commands::update(editor, flake_edit, state, id.clone(), *init)
184}
185
186fn dispatch_pin(
187    args: &CliArgs,
188    editor: &Editor,
189    flake_edit: &mut FlakeEdit,
190    state: &AppState,
191) -> Result<()> {
192    let Command::Pin { id, rev } = args.subcommand() else {
193        unreachable!("wrong Command variant");
194    };
195    commands::pin(editor, flake_edit, state, id.clone(), rev.clone())
196}
197
198fn dispatch_unpin(
199    args: &CliArgs,
200    editor: &Editor,
201    flake_edit: &mut FlakeEdit,
202    state: &AppState,
203) -> Result<()> {
204    let Command::Unpin { id } = args.subcommand() else {
205        unreachable!("wrong Command variant");
206    };
207    commands::unpin(editor, flake_edit, state, id.clone())
208}
209
210fn dispatch_follow(
211    args: &CliArgs,
212    editor: &Editor,
213    flake_edit: &mut FlakeEdit,
214    state: &mut AppState,
215) -> Result<()> {
216    let Command::Follow {
217        paths: _,
218        transitive,
219        depth,
220    } = args.subcommand()
221    else {
222        unreachable!("wrong Command variant");
223    };
224    if let Some(min) = transitive {
225        state.config.follow.transitive_min = *min;
226    }
227    if let Some(max) = depth {
228        state.config.follow.max_depth = Some(*max);
229    }
230    state.lock_offline = true;
231    follow::auto::run(editor, flake_edit, state)
232}
233
234fn dispatch_add_follow(
235    args: &CliArgs,
236    editor: &Editor,
237    flake_edit: &mut FlakeEdit,
238    state: &mut AppState,
239) -> Result<()> {
240    let Command::AddFollow { input, target } = args.subcommand() else {
241        unreachable!("wrong Command variant");
242    };
243    state.lock_offline = true;
244    follow::add_follow(editor, flake_edit, state, input.clone(), target.clone())
245}
246
247fn dispatch_completion(args: &CliArgs, flake_edit: &mut FlakeEdit, no_cache: bool) -> Result<()> {
248    use crate::cache::{Cache, DEFAULT_URI_TYPES};
249    use crate::cli::CompletionMode;
250
251    let Command::Completion { inputs: _, mode } = args.subcommand() else {
252        unreachable!("wrong Command variant");
253    };
254    match mode {
255        CompletionMode::Add => {
256            for uri_type in DEFAULT_URI_TYPES {
257                println!("{}", uri_type);
258            }
259            let cache = Cache::load();
260            for uri in cache.list_uris() {
261                println!("{}", uri);
262            }
263        }
264        CompletionMode::Change => {
265            let inputs = flake_edit.list();
266            crate::cache::populate_cache_from_input_map(inputs, no_cache);
267            for id in inputs.keys() {
268                println!("{}", id);
269            }
270        }
271        CompletionMode::Follow => {
272            if let Ok(lock) = crate::lock::FlakeLock::from_default_path() {
273                for nested in lock.nested_inputs() {
274                    println!("{}", nested.path);
275                }
276            }
277        }
278    }
279    Ok(())
280}
281
282fn dispatch_config(args: &CliArgs) -> Result<()> {
283    let Command::Config {
284        print_default,
285        path,
286    } = args.subcommand()
287    else {
288        unreachable!("wrong Command variant");
289    };
290    commands::config(*print_default, *path)
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use clap::Parser;
297
298    const MINIMAL_FLAKE: &str = "{\n  inputs = {};\n  outputs = { self }: { };\n}\n";
299
300    fn write_minimal_flake(dir: &std::path::Path) -> std::path::PathBuf {
301        let path = dir.join("flake.nix");
302        std::fs::write(&path, MINIMAL_FLAKE).expect("write flake.nix");
303        path
304    }
305
306    fn parse(args: &[&str]) -> CliArgs {
307        CliArgs::try_parse_from(args).expect("parse CLI args")
308    }
309
310    #[test]
311    fn batch_follow_with_flake_flag_is_rejected() {
312        let args = parse(&[
313            "flake-edit",
314            "--flake",
315            "/does/not/exist/flake.nix",
316            "follow",
317            "/some/path/flake.nix",
318        ]);
319        let err = run(args).expect_err("batch follow + --flake must be rejected");
320        assert!(matches!(err, Error::IncompatibleFollowOptions));
321    }
322
323    #[test]
324    fn batch_follow_with_lock_file_flag_is_rejected() {
325        let args = parse(&[
326            "flake-edit",
327            "--lock-file",
328            "/does/not/exist/flake.lock",
329            "follow",
330            "/some/path/flake.nix",
331        ]);
332        let err = run(args).expect_err("batch follow + --lock-file must be rejected");
333        assert!(matches!(err, Error::IncompatibleFollowOptions));
334    }
335
336    #[test]
337    fn flake_dir_without_flake_nix_returns_flake_dir_empty() {
338        let tmp = tempfile::tempdir().expect("tempdir");
339        let args = parse(&[
340            "flake-edit",
341            "--flake",
342            tmp.path().to_str().unwrap(),
343            "list",
344        ]);
345        let err = run(args).expect_err("empty dir must yield FlakeDirEmpty");
346        assert!(matches!(err, Error::FlakeDirEmpty { .. }));
347    }
348
349    #[test]
350    fn missing_flake_file_returns_flake_not_found() {
351        let tmp = tempfile::tempdir().expect("tempdir");
352        let missing = tmp.path().join("missing.nix");
353        let args = parse(&["flake-edit", "--flake", missing.to_str().unwrap(), "list"]);
354        let err = run(args).expect_err("missing file must yield FlakeNotFound");
355        assert!(matches!(err, Error::FlakeNotFound { .. }));
356    }
357
358    #[test]
359    fn config_print_default_does_not_touch_flake_nix() {
360        let tmp = tempfile::tempdir().expect("tempdir");
361        let flake = write_minimal_flake(tmp.path());
362        let args = parse(&[
363            "flake-edit",
364            "--flake",
365            tmp.path().to_str().unwrap(),
366            "--non-interactive",
367            "--no-cache",
368            "config",
369            "--print-default",
370        ]);
371        run(args).expect("config --print-default must succeed");
372        assert_eq!(
373            std::fs::read_to_string(&flake).expect("read flake.nix"),
374            MINIMAL_FLAKE,
375            "config --print-default must not rewrite flake.nix",
376        );
377    }
378
379    #[test]
380    fn completion_change_does_not_touch_flake_nix() {
381        let tmp = tempfile::tempdir().expect("tempdir");
382        let flake = write_minimal_flake(tmp.path());
383        let args = parse(&[
384            "flake-edit",
385            "--flake",
386            tmp.path().to_str().unwrap(),
387            "--non-interactive",
388            "--no-cache",
389            "completion",
390            "change",
391        ]);
392        run(args).expect("completion change must succeed");
393        assert_eq!(
394            std::fs::read_to_string(&flake).expect("read flake.nix"),
395            MINIMAL_FLAKE,
396            "completion change must not rewrite flake.nix",
397        );
398    }
399}