use ropey::Rope;
use crate::change::Change;
use crate::edit::{FlakeEdit, InputMap};
use crate::error::Error as FlakeError;
use crate::forge::update::Updater;
use crate::lock::FlakeLock;
use crate::tui;
use crate::validate;
use super::editor::Editor;
use super::error::{Error, Result};
use super::state::AppState;
mod add;
mod change;
mod config;
pub mod follow;
pub mod list;
mod pin;
mod remove;
mod update;
mod uri;
pub use add::add;
pub use change::change;
pub use config::config;
pub use list::list;
pub use pin::{pin, unpin};
pub use remove::remove;
pub use update::update;
pub use uri::UriOptions;
pub(super) fn updater(editor: &Editor, inputs: InputMap) -> Updater {
Updater::new(Rope::from_str(&editor.text()), inputs)
}
pub(super) fn load_flake_lock(state: &AppState) -> std::result::Result<FlakeLock, FlakeError> {
if let Some(lock_path) = &state.lock_file {
FlakeLock::from_file(lock_path)
} else {
FlakeLock::from_default_path()
}
}
enum ConfirmResult {
Applied,
Cancelled,
Back,
}
pub(super) fn interactive_single_select<F, OnApplied, ExtraData>(
editor: &Editor,
state: &AppState,
title: &str,
prompt: &str,
items: Vec<String>,
make_change: F,
on_applied: OnApplied,
) -> Result<()>
where
F: Fn(&str) -> Result<(String, ExtraData)>,
OnApplied: Fn(&str, ExtraData),
{
loop {
let select_app = tui::App::select_one(title, prompt, items.clone(), state.diff);
let Some(tui::AppResult::SingleSelect(result)) = tui::run(select_app)? else {
return Ok(());
};
let tui::SingleSelectResult {
item: id,
show_diff,
} = result;
let (change, extra_data) = make_change(&id)?;
match confirm_or_apply(editor, state, title, &change, show_diff)? {
ConfirmResult::Applied => {
on_applied(&id, extra_data);
break;
}
ConfirmResult::Back => continue,
ConfirmResult::Cancelled => return Ok(()),
}
}
Ok(())
}
pub(super) fn interactive_multi_select<F>(
editor: &Editor,
state: &AppState,
title: &str,
prompt: &str,
items: Vec<String>,
make_change: F,
) -> Result<()>
where
F: Fn(&[String]) -> String,
{
loop {
let select_app = tui::App::select_many(title, prompt, items.clone(), state.diff);
let Some(tui::AppResult::MultiSelect(result)) = tui::run(select_app)? else {
return Ok(());
};
let tui::MultiSelectResultData {
items: selected,
show_diff,
} = result;
let change = make_change(&selected);
match confirm_or_apply(editor, state, title, &change, show_diff)? {
ConfirmResult::Applied => break,
ConfirmResult::Back => continue,
ConfirmResult::Cancelled => return Ok(()),
}
}
Ok(())
}
fn confirm_or_apply(
editor: &Editor,
state: &AppState,
context: &str,
change: &str,
show_diff: bool,
) -> Result<ConfirmResult> {
if show_diff || state.diff {
let diff = crate::diff::Diff::new(&editor.text(), change).to_string_plain();
let confirm_app = tui::App::confirm(context, &diff);
let Some(tui::AppResult::Confirm(action)) = tui::run(confirm_app)? else {
return Ok(ConfirmResult::Cancelled);
};
match action {
tui::ConfirmResultAction::Apply => {
let mut apply_state = state.clone();
apply_state.diff = false;
editor.apply_or_diff(change, &apply_state)?;
Ok(ConfirmResult::Applied)
}
tui::ConfirmResultAction::Back => Ok(ConfirmResult::Back),
tui::ConfirmResultAction::Exit => Ok(ConfirmResult::Cancelled),
}
} else {
editor.apply_or_diff(change, state)?;
Ok(ConfirmResult::Applied)
}
}
pub(super) fn apply_change(
editor: &Editor,
flake_edit: &mut FlakeEdit,
state: &AppState,
change: Change,
) -> Result<()> {
let original_content = flake_edit.source_text();
let outcome = flake_edit.apply_change(change.clone())?;
let resulting_change = match outcome.text {
Some(t) => t,
None => {
if change.is_remove() {
let id = change
.id()
.expect("bug: Change::Remove always carries an id");
return Err(Error::CouldNotRemove { id });
}
if change.is_follows() {
let id = change.id().map(|id| id.to_string()).unwrap_or_default();
return Err(Error::FollowsCreateFailed { id });
}
println!("Nothing changed.");
return Ok(());
}
};
if change.is_follows() && resulting_change == original_content {
if let Some(id) = change.id() {
let follows_str = id
.follows()
.map(|s| s.render())
.unwrap_or_else(|| "?".to_string());
let target_str = change
.follows_target()
.map(|t| t.to_string())
.unwrap_or_else(|| "?".to_string());
println!(
"Already follows: {}.inputs.{}.follows = \"{}\"",
id.input().render(),
follows_str,
target_str,
);
}
return Ok(());
}
let validation = validate::validate(&resulting_change);
if validation.has_errors() {
for e in &validation.errors {
tracing::error!("validation error: {e}");
}
return Err(Error::ValidationAfterEdit(validation.errors));
}
editor.apply_or_diff(&resulting_change, state)?;
if !state.diff {
if let Change::Add {
id: Some(id),
uri: Some(uri),
..
} = &change
{
let mut cache = crate::cache::Cache::load();
cache.add_entry(id.to_string(), uri.clone());
if let Err(e) = cache.commit() {
tracing::debug!("Could not write to cache: {}", e);
}
}
for msg in change.success_messages() {
println!("{}", msg);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
use crate::follows::AttrPath;
#[test]
fn existing_follows_via_graph_handles_quoted_attrs() {
use crate::follows::{FollowsGraph, Segment};
use crate::input::{Follows, Input};
let mut inputs = InputMap::new();
let hm_seg = Segment::from_unquoted("home-manager").unwrap();
let mut hm_input = Input::new(hm_seg.clone());
hm_input.follows.push(Follows::Indirect {
path: AttrPath::new(Segment::from_unquoted("nixpkgs").unwrap()),
target: Some(AttrPath::parse("nixpkgs").unwrap()),
});
inputs.insert("home-manager".to_string(), hm_input);
let graph = FollowsGraph::from_declared(&inputs);
let sources: HashSet<AttrPath> = graph.edges().map(|e| e.source.clone()).collect();
assert!(
sources.contains(&AttrPath::parse("home-manager.nixpkgs").unwrap()),
"expected typed-AttrPath edge sourced at `home-manager.nixpkgs`, got {sources:?}",
);
}
}