Skip to main content

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