Skip to main content

flake_edit/app/
editor.rs

1use std::fs::File;
2use std::io;
3use std::path::PathBuf;
4use std::process::Command;
5
6use ropey::Rope;
7
8use crate::diff::Diff;
9use crate::edit::FlakeEdit;
10use crate::error::Error;
11use crate::validate;
12
13use super::state::AppState;
14
15/// Buffer for a flake file with its content and path.
16#[derive(Debug, Default)]
17pub struct FlakeBuf {
18    text: Rope,
19    path: PathBuf,
20}
21
22impl FlakeBuf {
23    pub fn from_path(path: PathBuf) -> io::Result<Self> {
24        let text = Rope::from_reader(&mut io::BufReader::new(File::open(&path)?))?;
25        Ok(Self { text, path })
26    }
27
28    pub fn text(&self) -> &Rope {
29        &self.text
30    }
31
32    pub fn path(&self) -> &PathBuf {
33        &self.path
34    }
35
36    pub fn write(&self, content: &str) -> io::Result<()> {
37        std::fs::write(&self.path, content)
38    }
39}
40
41/// Editor that drives changes to flake.nix files.
42///
43/// Handles file I/O, applying changes, and running nix flake lock.
44#[derive(Debug)]
45pub struct Editor {
46    flake: FlakeBuf,
47}
48
49impl Editor {
50    pub fn new(flake: FlakeBuf) -> Self {
51        Self { flake }
52    }
53
54    pub fn from_path(path: PathBuf) -> io::Result<Self> {
55        let flake = FlakeBuf::from_path(path)?;
56        Ok(Self { flake })
57    }
58
59    pub fn text(&self) -> String {
60        self.flake.text().to_string()
61    }
62
63    pub fn path(&self) -> &PathBuf {
64        self.flake.path()
65    }
66
67    pub fn create_flake_edit(&self) -> Result<FlakeEdit, Error> {
68        FlakeEdit::from_text(&self.text())
69    }
70
71    fn run_nix_flake_lock(&self, offline: bool) -> io::Result<()> {
72        let flake_dir = match self.flake.path.parent() {
73            Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
74            _ => PathBuf::from("."),
75        };
76
77        let mut cmd = Command::new("nix");
78        if offline {
79            cmd.arg("--offline");
80        }
81        cmd.args(["flake", "lock"]);
82        let output = cmd.current_dir(&flake_dir).output()?;
83
84        if !output.status.success() {
85            let stderr = String::from_utf8_lossy(&output.stderr);
86            return Err(io::Error::other(format!(
87                "nix flake lock failed: {}",
88                stderr
89            )));
90        }
91
92        println!("Updated flake.lock");
93        Ok(())
94    }
95
96    /// Apply changes to the flake file, or show diff if in diff mode.
97    ///
98    /// Validates the new content for duplicate attributes before writing.
99    pub fn apply_or_diff(&self, new_content: &str, state: &AppState) -> Result<(), Error> {
100        let validation = validate::validate(new_content);
101        if validation.has_errors() {
102            return Err(Error::Validation(validation.errors));
103        }
104
105        if state.diff {
106            let old = self.text();
107            let diff = Diff::new(&old, new_content);
108            diff.compare();
109        } else {
110            self.flake
111                .write(new_content)
112                .map_err(|source| Error::Write {
113                    path: self.flake.path().clone(),
114                    source,
115                })?;
116
117            if !state.no_lock
118                && let Err(e) = self.run_nix_flake_lock(state.lock_offline)
119            {
120                tracing::warn!("failed to update lockfile: {e}");
121            }
122        }
123        Ok(())
124    }
125}