magi-code 0.80.0

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

impl MissionControlApp {
    pub(super) fn open_theme_picker(&mut self, ui_state: &mut state::MissionControlState) {
        if ui_state.theme_picker_visible() {
            return;
        }
        let opening = self.appearance.clone();
        let cached = self.theme_catalog.clone();
        self.next_theme_request_id = self.next_theme_request_id.saturating_add(1);
        // Reopening subscribes to the one in-flight read rather than spawning more.
        let catalog_worker = self
            .theme_workers
            .iter()
            .find(|worker| worker.kind == ThemeWorkerKind::Catalog);
        let request_id =
            catalog_worker.map_or(self.next_theme_request_id, |worker| worker.request_id);
        let catalog_running = catalog_worker.is_some();
        self.theme_picker_runtime = Some(ThemePickerRuntime {
            opening_appearance: opening.clone(),
            opening_revision: self.theme_revision,
            catalog: cached.clone(),
            preview_appearance: opening.clone(),
            preview_revision: self.theme_revision,
        });
        let (entries, notice, loading) =
            cached.as_ref().map_or((Vec::new(), None, true), |catalog| {
                (
                    catalog.entries(),
                    (!catalog.diagnostics().is_empty()).then(|| catalog.diagnostics().join("; ")),
                    false,
                )
            });
        ui_state.open_theme_picker(
            opening.theme.id.clone(),
            self.theme_revision,
            loading,
            Some(request_id),
            entries,
            notice,
        );
        if catalog_running {
            return;
        }
        let sender = self.events.clone();
        let home = self.config.paths.root.clone();
        let handle = thread::spawn(move || {
            let result = Ok(crate::appearance::ThemeCatalog::load(&home));
            if sender
                .try_send(TuiEvent::ThemeCatalogLoaded {
                    request_id,
                    result: result.clone(),
                })
                .is_ok()
            {
                ThemeWorkerResult::CatalogDelivered
            } else {
                ThemeWorkerResult::Catalog(result)
            }
        });
        self.theme_workers.push(ThemeWorker {
            request_id,
            kind: ThemeWorkerKind::Catalog,
            handle,
        });
    }

    fn install_theme(
        &mut self,
        appearance: crate::appearance::RuntimeAppearance,
        ui_state: &mut state::MissionControlState,
    ) {
        self.theme_revision = self.theme_revision.saturating_add(1);
        self.appearance = appearance.clone();
        let theme = crate::tui::theme::MissionControlTheme::from_runtime_appearance(
            &appearance,
            self.theme_revision,
        );
        ui_state.color_enabled = appearance.policy.color_enabled;
        ui_state.set_theme(theme, appearance.reduced_motion);
        if let Some(runtime) = self.theme_picker_runtime.as_mut() {
            runtime.preview_appearance = appearance;
            runtime.preview_revision = self.theme_revision;
        }
    }

    pub(super) fn preview_theme(&mut self, id: &str, ui_state: &mut state::MissionControlState) {
        let Some(runtime) = self.theme_picker_runtime.as_ref() else {
            return;
        };
        let Some(catalog) = runtime.catalog.as_ref() else {
            return;
        };
        let Some(appearance) = catalog.resolve_runtime_appearance(id, &self.appearance) else {
            return;
        };
        if ui_state.theme_picker_selected_id().as_deref() != Some(id) {
            return;
        }
        self.install_theme(appearance, ui_state);
        if let Some(picker) = ui_state.modals.theme_picker.as_mut() {
            picker.preview_theme_id = id.to_string();
        }
    }

    pub(super) fn cancel_theme(&mut self, ui_state: &mut state::MissionControlState) {
        if self.pending_theme_save.is_some() {
            return;
        }
        if let Some(runtime) = self.theme_picker_runtime.take() {
            self.theme_revision = self.theme_revision.max(runtime.opening_revision);
            self.install_theme(runtime.opening_appearance, ui_state);
        }
        ui_state.close_theme_picker();
    }

    pub(super) fn save_theme(&mut self, ui_state: &mut state::MissionControlState) {
        let Some(id) = ui_state.theme_picker_selected_id() else {
            return;
        };
        if ui_state.theme_picker_preview_id() != Some(id.as_str())
            || self.pending_theme_save.is_some()
            || ui_state.theme_picker_save_pending()
            || self
                .theme_workers
                .iter()
                .any(|worker| worker.kind == ThemeWorkerKind::Save)
        {
            return;
        }
        let revision = self.theme_revision;
        self.next_theme_request_id = self.next_theme_request_id.saturating_add(1);
        let request_id = self.next_theme_request_id;
        self.pending_theme_save = Some(PendingThemeSave {
            request_id,
            theme_id: id.clone(),
            revision,
        });
        ui_state.set_theme_picker_save_pending();
        let paths = self.config.paths.clone();
        let sender = self.events.clone();
        let worker_id = request_id;
        let handle = thread::spawn(move || {
            let result =
                crate::config::set_appearance_theme(&paths, &id).map_err(|e| e.to_string());
            let _ = sender.try_send(TuiEvent::ThemePersisted {
                request_id,
                theme_id: id.clone(),
                revision,
                result: result.clone(),
            });
            ThemeWorkerResult::Saved {
                theme_id: id,
                revision,
                result,
            }
        });
        self.theme_workers.push(ThemeWorker {
            request_id: worker_id,
            kind: ThemeWorkerKind::Save,
            handle,
        });
    }

    pub(super) fn handle_theme_catalog(
        &mut self,
        ui_state: &mut state::MissionControlState,
        request_id: u64,
        result: Result<crate::appearance::ThemeCatalog, String>,
    ) -> bool {
        let current = ui_state.theme_picker_catalog_request_id() == Some(request_id);
        match result {
            Ok(catalog) => {
                if request_id >= self.theme_catalog_request_id {
                    self.theme_catalog_request_id = request_id;
                    self.theme_catalog = Some(catalog.clone());
                }
                if current {
                    if let Some(runtime) = self.theme_picker_runtime.as_mut() {
                        runtime.catalog = Some(catalog.clone());
                    }
                    let notice = (!catalog.diagnostics().is_empty())
                        .then(|| catalog.diagnostics().join("; "));
                    return ui_state.set_theme_picker_catalog(catalog.entries(), notice, 10);
                }
                false
            }
            Err(error) if current => ui_state.set_theme_picker_error(error),
            Err(_) => false,
        }
    }

    pub(super) fn handle_theme_persisted(
        &mut self,
        ui_state: &mut state::MissionControlState,
        request_id: u64,
        id: String,
        revision: u64,
        result: Result<(), String>,
    ) -> bool {
        let Some(pending) = self.pending_theme_save.as_ref() else {
            return false;
        };
        if pending.request_id != request_id
            || pending.theme_id != id
            || pending.revision != revision
        {
            return false;
        }
        self.pending_theme_save = None;
        match result {
            Ok(()) => {
                if self.theme_revision == revision {
                    self.theme_cli_override = false;
                }
                ui_state.close_theme_picker();
                self.theme_picker_runtime = None;
                true
            }
            Err(error) => ui_state.set_theme_picker_save_failed(error),
        }
    }

    pub(super) fn reap_theme_workers(&mut self, ui_state: &mut state::MissionControlState) -> bool {
        let mut changed = false;
        let mut active = Vec::with_capacity(self.theme_workers.len());
        for worker in std::mem::take(&mut self.theme_workers) {
            if !worker.handle.is_finished() {
                active.push(worker);
                continue;
            }
            let kind = worker.kind;
            let request_id = worker.request_id;
            match worker.handle.join() {
                Ok(ThemeWorkerResult::CatalogDelivered) => {}
                Ok(ThemeWorkerResult::Catalog(result)) => {
                    changed |= self.handle_theme_catalog(ui_state, request_id, result);
                }
                Ok(ThemeWorkerResult::Saved {
                    theme_id,
                    revision,
                    result,
                }) => {
                    changed |= self
                        .handle_theme_persisted(ui_state, request_id, theme_id, revision, result);
                }
                Err(_) => {
                    changed = true;
                    if kind == ThemeWorkerKind::Save {
                        self.pending_theme_save = None;
                        ui_state.set_theme_picker_save_failed(format!(
                            "theme save worker {request_id} panicked"
                        ));
                    } else if ui_state.theme_picker_catalog_request_id() == Some(request_id) {
                        ui_state.set_theme_picker_error("theme catalog worker panicked".into());
                    }
                }
            }
        }
        self.theme_workers = active;
        changed
    }
    pub(super) fn join_theme_workers_for_cleanup(&mut self) -> Vec<String> {
        let mut errors = Vec::new();
        for worker in std::mem::take(&mut self.theme_workers) {
            // Catalogs only read files. Bound shutdown even if a filesystem read
            // stalls; persistence writes must finish before their owner exits.
            if worker.kind == ThemeWorkerKind::Catalog {
                let deadline = Instant::now() + Duration::from_millis(100);
                while !worker.handle.is_finished() && Instant::now() < deadline {
                    thread::sleep(Duration::from_millis(1));
                }
                if !worker.handle.is_finished() {
                    continue;
                }
            }
            match worker.handle.join() {
                Ok(ThemeWorkerResult::Saved {
                    result: Err(error), ..
                }) => errors.push(error),
                Ok(_) => {}
                Err(_) => errors.push(format!(
                    "theme {:?} worker {} panicked",
                    worker.kind, worker.request_id
                )),
            }
        }
        self.pending_theme_save = None;
        errors
    }
}