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