flake_edit/tui/
workflow.rs

1//! Workflow state and logic for TUI interactions.
2//!
3//! This module contains the pure workflow logic separated from screen handling:
4//! - Workflow data types (Add, Change, Remove, etc.)
5//! - Result types returned by workflows
6//! - Helper functions for URI parsing and diff computation
7
8use std::collections::HashMap;
9
10use nix_uri::FlakeRef;
11use nix_uri::urls::UrlWrapper;
12
13use crate::change::Change;
14use crate::diff::Diff;
15use crate::edit::FlakeEdit;
16use crate::lock::NestedInput;
17
18/// Result from single-select including selected item and whether diff preview is enabled
19#[derive(Debug, Clone)]
20pub struct SingleSelectResult {
21    pub item: String,
22    pub show_diff: bool,
23}
24
25/// Result from multi-select including selected items and whether diff preview is enabled
26#[derive(Debug, Clone)]
27pub struct MultiSelectResultData {
28    pub items: Vec<String>,
29    pub show_diff: bool,
30}
31
32/// Result from confirmation screen
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ConfirmResultAction {
35    Apply,
36    Back,
37    Exit,
38}
39
40/// Phase within the Add workflow
41#[derive(Debug, Clone, PartialEq)]
42pub enum AddPhase {
43    Uri,
44    Id,
45}
46
47/// Phase within the Follow workflow
48#[derive(Debug, Clone, PartialEq)]
49pub enum FollowPhase {
50    /// Select the nested input path (e.g., "crane.nixpkgs")
51    SelectInput,
52    /// Select the target to follow (e.g., "nixpkgs")
53    SelectTarget,
54}
55
56/// Result of calling update()
57#[derive(Debug, Clone)]
58pub enum UpdateResult {
59    /// Keep processing events
60    Continue,
61    /// Workflow complete
62    Done,
63    /// Workflow cancelled
64    Cancelled,
65}
66
67/// Result returned by run() - depends on the workflow type
68#[derive(Debug, Clone)]
69pub enum AppResult {
70    /// Result from Add/Change/Remove workflows
71    Change(Change),
72    /// Result from SelectOne workflow
73    SingleSelect(SingleSelectResult),
74    /// Result from SelectMany workflow
75    MultiSelect(MultiSelectResultData),
76    /// Result from ConfirmOnly workflow
77    Confirm(ConfirmResultAction),
78}
79
80/// Workflow-specific data tracking the state of the current operation
81#[derive(Debug, Clone)]
82pub enum WorkflowData {
83    Add {
84        phase: AddPhase,
85        uri: Option<String>,
86        id: Option<String>,
87    },
88    Change {
89        selected_input: Option<String>,
90        uri: Option<String>,
91        input_uris: HashMap<String, String>,
92        all_inputs: Vec<String>,
93    },
94    Remove {
95        selected_inputs: Vec<String>,
96        all_inputs: Vec<String>,
97    },
98    SelectOne {
99        selected_input: Option<String>,
100    },
101    SelectMany {
102        selected_inputs: Vec<String>,
103    },
104    ConfirmOnly {
105        action: Option<ConfirmResultAction>,
106    },
107    Follow {
108        phase: FollowPhase,
109        /// The selected nested input path (e.g., "crane.nixpkgs")
110        selected_input: Option<String>,
111        /// The selected target to follow (e.g., "nixpkgs")
112        selected_target: Option<String>,
113        /// All available nested inputs with their follows info
114        nested_inputs: Vec<NestedInput>,
115        /// All available top-level inputs (possible targets)
116        top_level_inputs: Vec<String>,
117    },
118}
119
120impl WorkflowData {
121    /// Build a Change based on the current workflow state.
122    pub fn build_change(&self) -> Change {
123        match self {
124            WorkflowData::Add { id, uri, .. } => Change::Add {
125                id: id.clone(),
126                uri: uri.clone(),
127                flake: true,
128            },
129            WorkflowData::Change {
130                selected_input,
131                uri,
132                ..
133            } => Change::Change {
134                id: selected_input.clone(),
135                uri: uri.clone(),
136                ref_or_rev: None,
137            },
138            WorkflowData::Remove {
139                selected_inputs, ..
140            } => {
141                if selected_inputs.is_empty() {
142                    Change::None
143                } else {
144                    Change::Remove {
145                        ids: selected_inputs.iter().map(|s| s.clone().into()).collect(),
146                    }
147                }
148            }
149            // Standalone workflows don't produce Changes
150            WorkflowData::SelectOne { .. }
151            | WorkflowData::SelectMany { .. }
152            | WorkflowData::ConfirmOnly { .. } => Change::None,
153            WorkflowData::Follow {
154                selected_input,
155                selected_target,
156                ..
157            } => {
158                if let (Some(input), Some(target)) = (selected_input, selected_target) {
159                    Change::Follows {
160                        input: input.clone().into(),
161                        target: target.clone(),
162                    }
163                } else {
164                    Change::None
165                }
166            }
167        }
168    }
169}
170
171/// Parse a URI and try to infer the ID from it.
172///
173/// Returns (inferred_id, normalized_uri) where normalized_uri is the parsed
174/// string representation if valid, or the original URI if parsing failed.
175pub fn parse_uri_and_infer_id(uri: &str) -> (Option<String>, String) {
176    let flake_ref: Result<FlakeRef, _> = UrlWrapper::convert_or_parse(uri);
177    if let Ok(flake_ref) = flake_ref {
178        let parsed_uri = flake_ref.to_string();
179        let final_uri = if parsed_uri.is_empty() || parsed_uri == "none" {
180            uri.to_string()
181        } else {
182            parsed_uri
183        };
184        (flake_ref.id(), final_uri)
185    } else {
186        (None, uri.to_string())
187    }
188}
189
190/// Compute a unified diff between the original flake text and the result of applying a change.
191pub fn compute_diff(flake_text: &str, change: &Change) -> String {
192    // Return empty string for None change (no preview possible)
193    if matches!(change, Change::None) {
194        return String::new();
195    }
196
197    let Ok(mut edit) = FlakeEdit::from_text(flake_text) else {
198        return "Error parsing flake".to_string();
199    };
200
201    let new_text = match edit.apply_change(change.clone()) {
202        Ok(Some(text)) => text,
203        Ok(None) => flake_text.to_string(),
204        Err(e) => return format!("Error: {e}"),
205    };
206
207    Diff::new(flake_text, &new_text).to_string_plain()
208}