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