1use ropey::Rope;
2
3use crate::change::Change;
4use crate::edit::{FlakeEdit, InputMap};
5use crate::error::Error as FlakeError;
6use crate::forge::update::Updater;
7use crate::lock::FlakeLock;
8use crate::tui;
9use crate::validate;
10
11use super::editor::Editor;
12use super::error::{Error, Result};
13use super::state::AppState;
14
15mod add;
16mod change;
17mod config;
18pub mod follow;
19pub mod list;
20mod pin;
21mod remove;
22mod toggle;
23mod update;
24mod uri;
25
26pub use add::add;
27pub use change::change;
28pub use config::config;
29pub use list::list;
30pub use pin::{pin, unpin};
31pub use remove::remove;
32pub use toggle::toggle;
33pub use update::update;
34pub use uri::UriOptions;
35
36pub(super) fn updater(editor: &Editor, inputs: InputMap) -> Updater {
37 Updater::new(Rope::from_str(&editor.text()), inputs)
38}
39
40pub(super) fn load_flake_lock(state: &AppState) -> std::result::Result<FlakeLock, FlakeError> {
42 if let Some(lock_path) = &state.lock_file {
43 FlakeLock::from_file(lock_path)
44 } else {
45 FlakeLock::from_default_path()
46 }
47}
48
49enum ConfirmResult {
51 Applied,
53 Cancelled,
55 Back,
57}
58
59pub(super) fn pick_one(
61 state: &AppState,
62 title: &str,
63 prompt: &str,
64 items: Vec<String>,
65) -> Result<Option<(String, bool)>> {
66 let select_app = tui::App::select_one(title, prompt, items, state.diff);
67 let Some(tui::AppResult::SingleSelect(result)) = tui::run(select_app)? else {
68 return Ok(None);
69 };
70 Ok(Some((result.item, result.show_diff)))
71}
72
73pub(super) fn interactive_single_select<F, OnApplied, ExtraData>(
81 editor: &Editor,
82 state: &AppState,
83 title: &str,
84 prompt: &str,
85 items: Vec<String>,
86 make_change: F,
87 on_applied: OnApplied,
88) -> Result<()>
89where
90 F: Fn(&str) -> Result<(String, ExtraData)>,
91 OnApplied: Fn(&str, ExtraData),
92{
93 loop {
94 let Some((id, show_diff)) = pick_one(state, title, prompt, items.clone())? else {
95 return Ok(());
96 };
97 let (change, extra_data) = make_change(&id)?;
98
99 match confirm_or_apply(editor, state, title, &change, show_diff)? {
100 ConfirmResult::Applied => {
101 on_applied(&id, extra_data);
102 break;
103 }
104 ConfirmResult::Back => continue,
105 ConfirmResult::Cancelled => return Ok(()),
106 }
107 }
108 Ok(())
109}
110
111pub(super) fn interactive_multi_select<F>(
113 editor: &Editor,
114 state: &AppState,
115 title: &str,
116 prompt: &str,
117 items: Vec<String>,
118 make_change: F,
119) -> Result<()>
120where
121 F: Fn(&[String]) -> String,
122{
123 loop {
124 let select_app = tui::App::select_many(title, prompt, items.clone(), state.diff);
125 let Some(tui::AppResult::MultiSelect(result)) = tui::run(select_app)? else {
126 return Ok(());
127 };
128 let tui::MultiSelectResultData {
129 items: selected,
130 show_diff,
131 } = result;
132 let change = make_change(&selected);
133
134 match confirm_or_apply(editor, state, title, &change, show_diff)? {
135 ConfirmResult::Applied => break,
136 ConfirmResult::Back => continue,
137 ConfirmResult::Cancelled => return Ok(()),
138 }
139 }
140 Ok(())
141}
142
143fn confirm_or_apply(
148 editor: &Editor,
149 state: &AppState,
150 context: &str,
151 change: &str,
152 show_diff: bool,
153) -> Result<ConfirmResult> {
154 if show_diff || state.diff {
155 let diff = crate::diff::Diff::new(&editor.text(), change).to_string_plain();
156 let confirm_app = tui::App::confirm(context, &diff);
157 let Some(tui::AppResult::Confirm(action)) = tui::run(confirm_app)? else {
158 return Ok(ConfirmResult::Cancelled);
159 };
160 match action {
161 tui::ConfirmResultAction::Apply => {
162 let mut apply_state = state.clone();
163 apply_state.diff = false;
164 editor.apply_or_diff(change, &apply_state)?;
165 Ok(ConfirmResult::Applied)
166 }
167 tui::ConfirmResultAction::Back => Ok(ConfirmResult::Back),
168 tui::ConfirmResultAction::Exit => Ok(ConfirmResult::Cancelled),
169 }
170 } else {
171 editor.apply_or_diff(change, state)?;
172 Ok(ConfirmResult::Applied)
173 }
174}
175
176pub(super) fn apply_change(
177 editor: &Editor,
178 flake_edit: &mut FlakeEdit,
179 state: &AppState,
180 change: Change,
181) -> Result<()> {
182 let original_content = flake_edit.source_text();
183 let outcome = flake_edit.apply_change(change.clone())?;
184 let resulting_change = match outcome.text {
185 Some(t) => t,
186 None => {
187 if change.is_remove() {
188 let id = change
189 .id()
190 .expect("bug: Change::Remove always carries an id");
191 return Err(Error::CouldNotRemove { id });
192 }
193 if change.is_follows() {
194 let id = change.id().map(|id| id.to_string()).unwrap_or_default();
195 return Err(Error::FollowsCreateFailed { id });
196 }
197 println!("Nothing changed.");
198 return Ok(());
199 }
200 };
201
202 if change.is_follows() && resulting_change == original_content {
203 if let Some(id) = change.id() {
204 let follows_str = id
205 .follows()
206 .map(|s| s.render())
207 .unwrap_or_else(|| "?".to_string());
208 let target_str = change
209 .follows_target()
210 .map(|t| t.to_string())
211 .unwrap_or_else(|| "?".to_string());
212 println!(
213 "Already follows: {}.inputs.{}.follows = \"{}\"",
214 id.input().render(),
215 follows_str,
216 target_str,
217 );
218 }
219 return Ok(());
220 }
221
222 let validation = validate::validate(&resulting_change);
223 if validation.has_errors() {
224 for e in &validation.errors {
225 tracing::error!("validation error: {e}");
226 }
227 return Err(Error::ValidationAfterEdit(validation.errors));
228 }
229
230 editor.apply_or_diff(&resulting_change, state)?;
231
232 if !state.diff {
233 if let Change::Add {
235 id: Some(id),
236 uri: Some(uri),
237 ..
238 } = &change
239 {
240 let mut cache = crate::cache::Cache::load();
241 cache.add_entry(id.to_string(), uri.clone());
242 if let Err(e) = cache.commit() {
243 tracing::debug!("Could not write to cache: {}", e);
244 }
245 }
246
247 for msg in change.success_messages() {
248 println!("{}", msg);
249 }
250 }
251
252 Ok(())
253}
254
255#[cfg(test)]
256mod tests {
257 use std::collections::HashSet;
258
259 use super::*;
260 use crate::follows::AttrPath;
261
262 #[test]
263 fn existing_follows_via_graph_handles_quoted_attrs() {
264 use crate::follows::{FollowsGraph, Segment};
265 use crate::input::{Follows, Input};
266
267 let mut inputs = InputMap::new();
271 let hm_seg = Segment::from_unquoted("home-manager").unwrap();
272 let mut hm_input = Input::new(hm_seg.clone());
273 hm_input.follows.push(Follows::Indirect {
274 path: AttrPath::new(Segment::from_unquoted("nixpkgs").unwrap()),
275 target: Some(AttrPath::parse("nixpkgs").unwrap()),
276 });
277 inputs.insert("home-manager".to_string(), hm_input);
278
279 let graph = FollowsGraph::from_declared(&inputs);
280 let sources: HashSet<AttrPath> = graph.edges().map(|e| e.source.clone()).collect();
281 assert!(
282 sources.contains(&AttrPath::parse("home-manager.nixpkgs").unwrap()),
283 "expected typed-AttrPath edge sourced at `home-manager.nixpkgs`, got {sources:?}",
284 );
285 }
286}