flake-edit 0.3.6

Edit your flake inputs with ease.
Documentation
//! Workflow state and logic for TUI interactions.
//!
//! This module contains the pure workflow logic separated from screen handling:
//! - Workflow data types (Add, Change, Remove, etc.)
//! - Result types returned by workflows
//! - Helper functions for URI parsing and diff computation

use std::collections::HashMap;

use nix_uri::FlakeRef;

use crate::change::Change;
use crate::diff::Diff;
use crate::edit::FlakeEdit;
use crate::lock::NestedInput;

/// Result from single-select including selected item and whether diff preview is enabled
#[derive(Debug, Clone)]
pub struct SingleSelectResult {
    pub item: String,
    pub show_diff: bool,
}

/// Result from multi-select including selected items and whether diff preview is enabled
#[derive(Debug, Clone)]
pub struct MultiSelectResultData {
    pub items: Vec<String>,
    pub show_diff: bool,
}

/// Result from confirmation screen
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfirmResultAction {
    Apply,
    Back,
    Exit,
}

/// Step within the Add workflow.
#[derive(Debug, Clone, PartialEq)]
pub enum AddStep {
    Uri,
    Id,
}

/// Step within the Follow workflow.
#[derive(Debug, Clone, PartialEq)]
pub enum FollowStep {
    /// Select the nested input path (e.g., "crane.nixpkgs")
    SelectInput,
    /// Select the target to follow (e.g., "nixpkgs")
    SelectTarget,
}

/// Result of calling update()
#[derive(Debug, Clone)]
pub enum UpdateResult {
    /// Keep processing events
    Continue,
    /// Workflow complete
    Done,
    /// Workflow cancelled
    Cancelled,
}

/// Result returned by run() - depends on the workflow type
#[derive(Debug, Clone)]
pub enum AppResult {
    /// Result from Add/Change/Remove workflows
    Change(Change),
    /// Result from SelectOne workflow
    SingleSelect(SingleSelectResult),
    /// Result from SelectMany workflow
    MultiSelect(MultiSelectResultData),
    /// Result from ConfirmOnly workflow
    Confirm(ConfirmResultAction),
}

/// Workflow-specific data tracking the state of the current operation
#[derive(Debug, Clone)]
pub enum WorkflowData {
    Add {
        step: AddStep,
        uri: Option<String>,
        id: Option<String>,
    },
    Change {
        selected_input: Option<String>,
        uri: Option<String>,
        input_uris: HashMap<String, String>,
        all_inputs: Vec<String>,
    },
    Remove {
        selected_inputs: Vec<String>,
        all_inputs: Vec<String>,
    },
    SelectOne {
        selected_input: Option<String>,
    },
    SelectMany {
        selected_inputs: Vec<String>,
    },
    ConfirmOnly {
        action: Option<ConfirmResultAction>,
    },
    Follow {
        step: FollowStep,
        /// The selected nested input path (e.g., "crane.nixpkgs")
        selected_input: Option<String>,
        /// The selected target to follow (e.g., "nixpkgs")
        selected_target: Option<String>,
        /// All available nested inputs with their follows info
        nested_inputs: Vec<NestedInput>,
        /// All available top-level inputs (possible targets)
        top_level_inputs: Vec<String>,
    },
}

impl WorkflowData {
    /// Build a Change based on the current workflow state.
    pub fn build_change(&self) -> Change {
        match self {
            WorkflowData::Add { id, uri, .. } => Change::Add {
                id: id
                    .as_deref()
                    .and_then(|s| crate::change::ChangeId::parse(s).ok()),
                uri: uri.clone(),
                flake: true,
            },
            WorkflowData::Change {
                selected_input,
                uri,
                ..
            } => Change::Change {
                id: selected_input
                    .as_deref()
                    .and_then(|s| crate::change::ChangeId::parse(s).ok()),
                uri: uri.clone(),
            },
            WorkflowData::Remove {
                selected_inputs, ..
            } => {
                if selected_inputs.is_empty() {
                    Change::None
                } else {
                    Change::Remove {
                        ids: selected_inputs
                            .iter()
                            .filter_map(|s| crate::change::ChangeId::parse(s).ok())
                            .collect(),
                    }
                }
            }
            // Standalone workflows don't produce Changes
            WorkflowData::SelectOne { .. }
            | WorkflowData::SelectMany { .. }
            | WorkflowData::ConfirmOnly { .. } => Change::None,
            WorkflowData::Follow {
                selected_input,
                selected_target,
                ..
            } => {
                if let (Some(input), Some(target)) = (selected_input, selected_target) {
                    let parsed = crate::change::ChangeId::parse(input)
                        .ok()
                        .zip(crate::follows::AttrPath::parse(target).ok());
                    if let Some((change_id, target_path)) = parsed {
                        Change::Follows {
                            input: change_id,
                            target: target_path,
                        }
                    } else {
                        Change::None
                    }
                } else {
                    Change::None
                }
            }
        }
    }
}

/// Parse a URI and try to infer the ID from it.
///
/// Returns (inferred_id, normalized_uri) where normalized_uri is the parsed
/// string representation if valid, or the original URI if parsing failed.
pub fn parse_uri_and_infer_id(uri: &str) -> (Option<String>, String) {
    match uri.parse::<FlakeRef>() {
        Ok(flake_ref) => {
            let id = flake_ref.id().map(str::to_owned);
            (id, flake_ref.into_uri())
        }
        Err(_) => (None, uri.to_string()),
    }
}

/// Compute a unified diff between the original flake text and the result of applying a change.
pub fn compute_diff(flake_text: &str, change: &Change) -> String {
    // Return empty string for None change (no preview possible)
    if matches!(change, Change::None) {
        return String::new();
    }

    let Ok(mut edit) = FlakeEdit::from_text(flake_text) else {
        return "Error parsing flake".to_string();
    };

    let new_text = match edit.apply_change(change.clone()) {
        Ok(outcome) => outcome.text.unwrap_or_else(|| flake_text.to_string()),
        Err(e) => return format!("Error: {e}"),
    };

    Diff::new(flake_text, &new_text).to_string_plain()
}