tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
#[allow(clippy::wildcard_imports)]
use super::*;

impl App {
    /// Launch the editor on a temp copy of `filename` and settle the result
    /// (tix #1510).
    ///
    /// The issue file's raw bytes are snapshotted **before** the child runs, and
    /// the edited temp copy is compared against that snapshot **after** it exits
    /// by [`App::after_editor`] — the one place the conflict decision is made.
    /// Splitting the round trip this way is what lets the live
    /// `Action::OpenEditor` shell-out and the programmatic entry point be the same
    /// code path: the only difference between them is who suspends the terminal
    /// around the child.
    pub fn open_editor_round_trip(&mut self, filename: &str) -> Result<()> {
        let name = editor::resolve_editor(self.store.config.editor.as_deref())?;
        let original = self
            .store
            .get(IssueSelector::Filename(filename.to_string()))?;
        let disk_path = self.store.issues_dir.join(&original.filename);
        let snapshot = std::fs::read(&disk_path).map_err(TixError::Io)?;
        let temp = std::env::temp_dir().join(format!(
            "tix-edit-{}-{}.md",
            std::process::id(),
            original.number
        ));
        std::fs::write(&temp, original.to_file_contents()).map_err(TixError::Io)?;
        self.pending_edit = Some(PendingEdit {
            filename: original.filename.clone(),
            temp: temp.clone(),
            snapshot,
        });
        if let Err(e) = editor::run_editor(&name, &temp, &self.store.config) {
            // The child failed to launch or exited non-zero, so nothing was
            // written — and the copy is **kept**. Deleting it here (as the old
            // flow did) is the automatic discard tix #1506 exists to remove; the
            // temp copy is the only copy of anything the editor might have
            // managed to save before it failed.
            self.pending_edit = None;
            self.status_msg = Some(format!(
                "editor: {e}; your copy is kept at {}",
                temp.display()
            ));
            return Ok(());
        }
        self.after_editor(&original.filename);
        Ok(())
    }

    /// Finish an editor round trip: commit the edit, or refuse to because the file
    /// changed underneath it (tix #1510).
    ///
    /// Both entry points arrive here — [`App::open_editor_round_trip`] for the live
    /// shell-out, and direct callers that write the file themselves before calling
    /// this. A round trip that recorded a pending edit is compared against its
    /// pre-edit snapshot and may enter [`Mode::EditConflict`] **writing nothing**;
    /// one with no pending edit has no pre-edit bytes to compare against, so it
    /// keeps the pre-#1510 behaviour of persisting the file it is asked to read.
    pub fn after_editor(&mut self, filename: &str) {
        // The lint below reports the project's current rules, and the user may
        // have declared a custom attribute in `tix.toml` since this session
        // started (tix #1506): adopt the file before judging the edit.
        self.reload_config_if_changed();
        if let Some(pending) = self
            .pending_edit
            .take_if(|pending| pending.filename == filename)
        {
            self.settle_pending_edit(pending);
            return;
        }
        match self
            .store
            .get(IssueSelector::Filename(filename.to_string()))
        {
            // No pending edit means this caller wrote the file itself, so there
            // is no retained temp copy: the issue file is the working copy.
            Ok(issue) => self.commit_edited_issue(filename, issue, "Edited", None),
            Err(e) => {
                self.status_msg = Some(format!("editor: {e}"));
            }
        }
    }
}