Skip to main content

flake_edit/app/commands/
change.rs

1//! `flake-edit change`: replace an input's URI in place.
2//!
3//! Four branches: full interactive (pick + URI), URI-only
4//! interactive (with the ID known), scripted (id + uri), and
5//! infer-id (uri only). All route the resulting URI through
6//! [`super::uri::transform_uri`].
7
8use nix_uri::FlakeRef;
9
10use crate::change::{Change, ChangeId};
11use crate::edit::{FlakeEdit, InputMap, sorted_input_ids};
12use crate::tui;
13
14use super::super::editor::Editor;
15use super::super::state::AppState;
16use super::uri::{BuildKind, UriOptions, apply_uri_options, build_uri_change, transform_uri};
17use super::{Error, Result, apply_change};
18
19pub fn change(
20    editor: &Editor,
21    flake_edit: &mut FlakeEdit,
22    state: &AppState,
23    id: Option<String>,
24    uri: Option<String>,
25    opts: UriOptions<'_>,
26) -> Result<()> {
27    let inputs = flake_edit.list();
28
29    let change = match (id, uri, state.interactive) {
30        // Full interactive: select input, then enter URI. Also covers the
31        // case where only URI was provided interactively (need to select input).
32        (None, None, true) | (None, Some(_), true) => {
33            change_full_interactive(editor, state, inputs, &opts)?
34        }
35        // ID provided, no URI, interactive: show URI input for that ID.
36        (Some(id), None, true) => change_uri_interactive(editor, state, inputs, &id, &opts)?,
37        // Both ID and URI provided: non-interactive.
38        (Some(id_val), Some(uri_str), _) => {
39            build_uri_change(BuildKind::Change, id_val, uri_str, &opts)?
40        }
41        // Only one positional arg: infer ID from URI.
42        (Some(uri), None, false) | (None, Some(uri), false) => change_infer_id(uri, &opts)?,
43        (None, None, false) => {
44            return Err(Error::NoId);
45        }
46    };
47
48    apply_change(editor, flake_edit, state, change)
49}
50
51/// Runs the full interactive flow: pick an input from the list, then
52/// enter the new URI.
53fn change_full_interactive(
54    editor: &Editor,
55    state: &AppState,
56    inputs: &InputMap,
57    opts: &UriOptions<'_>,
58) -> Result<Change> {
59    let input_pairs: Vec<(String, String)> = sorted_input_ids(inputs)
60        .into_iter()
61        .map(|id| (id.clone(), inputs[id].url().to_string()))
62        .collect();
63
64    if input_pairs.is_empty() {
65        return Err(Error::NoInputs);
66    }
67
68    let tui_app = tui::App::change("Change", editor.text(), input_pairs, state.cache_config());
69    let Some(tui::AppResult::Change(tui_change)) = tui::run(tui_app)? else {
70        return Ok(Change::None);
71    };
72
73    // CLI options override the TUI result.
74    if let Change::Change { id, uri, .. } = tui_change {
75        let final_uri = uri
76            .map(|u| transform_uri(u, opts.ref_or_rev, opts.shallow))
77            .transpose()?;
78        Ok(Change::Change { id, uri: final_uri })
79    } else {
80        Ok(tui_change)
81    }
82}
83
84/// Runs the interactive flow with the ID already known, showing only
85/// the URI input widget.
86fn change_uri_interactive(
87    editor: &Editor,
88    state: &AppState,
89    inputs: &InputMap,
90    id: &str,
91    opts: &UriOptions<'_>,
92) -> Result<Change> {
93    let current_uri = inputs.get(id).map(|i| i.url());
94    let tui_app = tui::App::change_uri(
95        "Change",
96        editor.text(),
97        id,
98        current_uri,
99        state.diff,
100        state.cache_config(),
101    );
102
103    let Some(tui::AppResult::Change(tui_change)) = tui::run(tui_app)? else {
104        return Ok(Change::None);
105    };
106
107    // CLI options override the TUI result.
108    if let Change::Change {
109        uri: Some(new_uri), ..
110    } = tui_change
111    {
112        let final_uri = transform_uri(new_uri, opts.ref_or_rev, opts.shallow)?;
113        let id = ChangeId::parse(id).map_err(|source| Error::InvalidInputId {
114            id: id.to_string(),
115            source,
116        })?;
117        Ok(Change::Change {
118            id: Some(id),
119            uri: Some(final_uri),
120        })
121    } else {
122        Err(Error::NoUri)
123    }
124}
125
126/// Builds a `Change::Change` when only the URI is supplied, inferring
127/// the ID from the parsed flake reference.
128fn change_infer_id(uri: String, opts: &UriOptions<'_>) -> Result<Change> {
129    let flake_ref: FlakeRef = uri.parse().map_err(|source| Error::InvalidUri {
130        uri: uri.clone(),
131        source,
132    })?;
133    let flake_ref = apply_uri_options(flake_ref, opts.ref_or_rev, opts.shallow);
134
135    let id = flake_ref
136        .id()
137        .map(str::to_owned)
138        .ok_or_else(|| Error::CouldNotInferId { uri: uri.clone() })?;
139    let id = ChangeId::parse(&id).map_err(|source| Error::InvalidInputId { id, source })?;
140    let final_uri = flake_ref.into_uri();
141
142    Ok(Change::Change {
143        id: Some(id),
144        uri: Some(final_uri),
145    })
146}