gitkraft 1.2.4

GitKraft — Git IDE desktop application (Iced GUI)
Documentation
//! Internal macros shared across all GUI feature modules.

/// Convenience extension for mapping any `Display` error to `String`.
pub(crate) trait StringErr<T> {
    fn str_err(self) -> Result<T, String>;
}

impl<T, E: std::fmt::Display> StringErr<T> for Result<T, E> {
    fn str_err(self) -> Result<T, String> {
        self.map_err(|e| e.to_string())
    }
}

/// Build an `iced::Task<Message>` that runs blocking git work on a background
/// thread via a `futures` oneshot channel.
///
/// # Arguments
///
/// * `$msg` – the `Message` constructor passed to `Task::perform` as the
///   mapping function (e.g. `Message::BranchCheckedOut`).
/// * `$body` – an expression evaluated inside the spawned thread that must
///   return `Result<T, String>`.  Use an IIFE `(|| { … })()` when the
///   `?`-operator is needed, or a plain function call when a single fallible
///   call suffices.
///
/// # Example
///
/// ```ignore
/// git_task!(Message::BranchCheckedOut, (|| {
///     let repo = open_repo(&path)?;
///     checkout_branch(&repo, &name).str_err()
/// })())
/// ```
macro_rules! git_task {
    ($msg:expr, $body:expr) => {
        Task::perform(
            async move {
                let (tx, rx) = futures::channel::oneshot::channel();
                std::thread::spawn(move || {
                    let _ = tx.send($body);
                });
                rx.await.map_err(|_| "Task cancelled".to_string())?
            },
            $msg,
        )
    };
}

/// Guard an update-handler arm behind the active tab's `repo_path`.
///
/// Reads `$state.active_tab().repo_path`, returns `Task::none()` when it is
/// `None`, and evaluates `$cmd` with `repo_path: PathBuf` in scope when it is
/// `Some`.
///
/// Two forms:
///
/// * **Plain** – sets only `status_message`, then evaluates `$cmd`:
///   ```ignore
///   with_repo!(state, "Staging…".into(), |repo_path|
///       commands::stage_file(repo_path, f))
///   ```
///
/// * **Loading** – additionally sets `is_loading = true` before `$cmd`:
///   ```ignore
///   with_repo!(state, loading, "Checking out…".into(), |repo_path|
///       commands::checkout_branch(repo_path, name))
///   ```
///
/// The `|$ident|` token before `$cmd` names the `PathBuf` variable that is
/// bound from `repo_path` and made available inside `$cmd`.  Passing the name
/// as a macro parameter avoids Rust's `macro_rules!` hygiene restriction that
/// would otherwise hide a macro-internal binding from call-site expressions.
///
/// Extra tab mutations can be performed inside a block `$cmd`; the
/// `is_loading` / `status_message` mutable borrow has already been dropped.
macro_rules! with_repo {
    // ── dismiss + plain variant ────────────────────────────────────────
    ($state:expr, dismiss, $status:expr, |$rp:ident| $cmd:expr) => {{
        $state.active_tab_mut().context_menu = None;
        with_repo!($state, $status, |$rp| $cmd)
    }};

    // ── dismiss + loading variant ─────────────────────────────────────
    ($state:expr, dismiss, loading, $status:expr, |$rp:ident| $cmd:expr) => {{
        $state.active_tab_mut().context_menu = None;
        with_repo!($state, loading, $status, |$rp| $cmd)
    }};

    // ── plain variant (status only) ────────────────────────────────────────
    ($state:expr, $status:expr, |$rp:ident| $cmd:expr) => {{
        let $rp = $state.active_tab().repo_path.clone();
        if let Some($rp) = $rp {
            $state.active_tab_mut().status_message = Some($status);
            $cmd
        } else {
            Task::none()
        }
    }};

    // ── loading variant (is_loading + status) ──────────────────────────────
    ($state:expr, loading, $status:expr, |$rp:ident| $cmd:expr) => {{
        let $rp = $state.active_tab().repo_path.clone();
        if let Some($rp) = $rp {
            {
                let tab = $state.active_tab_mut();
                tab.is_loading = true;
                tab.status_message = Some($status);
            }
            $cmd
        } else {
            Task::none()
        }
    }};
}

/// Create a Bootstrap icon text widget.
///
/// Two forms: 3-arg `icon!(CHAR, SIZE, COLOR)` or 2-arg `icon!(CHAR, SIZE)`.
macro_rules! icon {
    ($char:expr, $size:expr, $color:expr) => {
        text($char)
            .font(iced::Font::with_name("bootstrap-icons"))
            .size($size)
            .color($color)
    };
    ($char:expr, $size:expr) => {
        text($char)
            .font(iced::Font::with_name("bootstrap-icons"))
            .size($size)
    };
}

/// Set a field on the active tab and return `Task::none()`.
///
/// ```ignore
/// set_field!(state, commit_message, msg)
/// // Expands to:
/// // state.active_tab_mut().commit_message = msg;
/// // Task::none()
/// ```
macro_rules! set_field {
    ($state:expr, $field:ident, $value:expr) => {{
        $state.active_tab_mut().$field = $value;
        Task::none()
    }};
}

/// Clear the file preview editor state.
macro_rules! clear_preview {
    ($state:expr) => {
        $state.preview_editor = None;
        $state.preview_path = None;
    };
}

/// Open a git repository at the given path, mapping the error to `String`.
///
/// Shorthand for `gitkraft_core::features::repo::open_repo($path).str_err()?`.
/// Used inside `git_task!` closures where the `?` operator is available.
macro_rules! open_repo {
    ($path:expr) => {
        gitkraft_core::features::repo::open_repo($path).str_err()?
    };
}

/// Run a git mutation that requires a `Repository` handle, then reload
/// the full repo snapshot.  Returns `Task<Message::GitOperationResult>`.
macro_rules! git_repo_then_reload {
    ($path:expr, |$repo:ident| $op:expr) => {
        git_task!(
            Message::GitOperationResult,
            (|| {
                let $repo = open_repo!(&$path);
                { $op }.str_err()?;
                crate::features::repo::commands::load_repo_blocking(&$path)
            })()
        )
    };
}

/// Same as `git_repo_then_reload!` but extracts the workdir first.
macro_rules! git_wd_then_reload {
    ($path:expr, |$wd:ident| $op:expr) => {
        git_task!(
            Message::GitOperationResult,
            (|| {
                let $wd = crate::features::repo::commands::workdir(&$path)?;
                { $op }.str_err()?;
                crate::features::repo::commands::load_repo_blocking(&$path)
            })()
        )
    };
}

/// Run a staging operation then refresh the staging state.
macro_rules! staging_op {
    ($path:expr, |$repo:ident| $op:expr) => {
        git_task!(
            Message::StagingUpdated,
            (|| {
                let $repo = open_repo!(&$path);
                { $op }.str_err()?;
                crate::features::staging::commands::refresh_staging_state(&$path)
            })()
        )
    };
}

/// Selects a row's `container` style function from a prioritized list of
/// `condition => style_fn` pairs, evaluated in order (first match wins),
/// falling back to `crate::theme::surface_style` when none apply.
///
/// This exists purely to avoid repeating the
/// `... as fn(&iced::Theme) -> iced::widget::container::Style` cast and the
/// `if / else if / else` chain at every call site that picks between
/// `theme::selected_row_style`, `theme::highlight_row_style`,
/// `theme::hover_row_style` and `theme::surface_style` (commit rows, file
/// rows, search results, ...).
///
/// ```ignore
/// let style_fn = row_style!(
///     is_selected => theme::selected_row_style,
///     is_in_range => theme::highlight_row_style,
///     is_hovered => theme::hover_row_style,
/// );
/// container(content).style(style_fn);
/// ```
macro_rules! row_style {
    ($($cond:expr => $style:expr),+ $(,)?) => {{
        type RowStyleFn = fn(&iced::Theme) -> iced::widget::container::Style;
        if false {
            unreachable!()
        }
        $(else if $cond { $style as RowStyleFn })+
        else { crate::theme::surface_style as RowStyleFn }
    }};
}