use crate::AppModel;
use crate::cli_outbound::Cli;
use crate::config_edit::RealFileIo;
use crate::config_edit::branch::config_branches;
use crate::config_edit::branch::edit::{
DraftFile, EditOrigin, EditPlan, drive, next_nonce, stage_files,
};
use crate::config_edit::brazen::{
Applied, BUILT_IN_ROWS_HINT, BrazenEditor, BrazenPaths, RealBzRunner,
};
use crate::config_edit::lernie_global::{Editor, LernieGlobal, Saved, WorkflowNameError};
use crate::shell::config_marks::{self, MarksPane};
use crate::theme;
use crate::xdg::{Env, current_os};
use std::path::PathBuf;
pub struct ConfigState {
pub active: bool,
io: RealFileIo,
bz: RealBzRunner,
yog_binary: PathBuf,
stage_root: PathBuf,
brazen: BrazenEditor,
brazen_status: String,
brazen_effective: String,
lernie: LernieGlobal,
lernie_editor: Editor,
lernie_status: String,
new_workflow: String,
cb_name: String,
cb_origin: EditOrigin,
cb_path: String,
cb_body: String,
cb_status: String,
marks: MarksPane,
}
impl ConfigState {
pub fn new(env: &Env) -> std::io::Result<Self> {
let io = RealFileIo;
let brazen = BrazenEditor::load(BrazenPaths::resolve(env, current_os()), &io)?;
let lernie = LernieGlobal::resolve(env);
let lernie_editor = Editor::load(lernie.models(), &io)?;
Ok(Self {
active: false,
io,
bz: RealBzRunner::resolve(env),
yog_binary: std::env::current_exe().unwrap_or_default(),
stage_root: env.yog_stage_root(),
brazen,
brazen_status: String::new(),
brazen_effective: String::new(),
lernie,
lernie_editor,
lernie_status: String::new(),
new_workflow: String::new(),
cb_name: String::new(),
cb_origin: EditOrigin::Advance,
cb_path: String::new(),
cb_body: String::new(),
cb_status: String::new(),
marks: MarksPane::resolve(env),
})
}
}
pub fn center(ui: &mut egui::Ui, model: &mut AppModel, config: &mut ConfigState, lernie: &Cli) {
ui.heading("Config editors");
ui.weak("§9 — stage → validate → hash-guard → atomic rename");
egui::ScrollArea::vertical().show(ui, |ui| {
brazen_editor(ui, config);
ui.separator();
lernie_global(ui, config);
ui.separator();
config_marks::render(ui, model, &mut config.marks);
ui.separator();
config_branch(ui, model, config, lernie);
});
}
fn brazen_editor(ui: &mut egui::Ui, config: &mut ConfigState) {
ui.heading(egui::RichText::new("brazen config.toml").color(theme::integration_hue("bz")));
ui.add(
egui::TextEdit::multiline(config.brazen.draft_mut())
.desired_rows(6)
.code_editor(),
);
ui.horizontal(|ui| {
if ui.button("Apply").clicked() {
config.brazen_status = describe_applied(config.brazen.apply(&config.bz, &config.io));
}
if ui.button("Reload").clicked() {
config.brazen_status = reload_status(config.brazen.reload(&config.io));
}
if ui.button("Effective (bz --dump-config)").clicked() {
config.brazen_effective = config.brazen.effective(&config.bz).stdout;
}
});
status_line(ui, &config.brazen_status);
if !config.brazen_effective.is_empty() {
ui.monospace(&config.brazen_effective);
}
ui.weak(BUILT_IN_ROWS_HINT);
}
fn lernie_global(ui: &mut egui::Ui, config: &mut ConfigState) {
ui.heading(egui::RichText::new("lernie global config").color(theme::integration_hue("lernie")));
ui.monospace(config.lernie_editor.path().display().to_string());
if config.lernie_editor.is_new() {
ui.weak("(new file)");
}
ui.add(
egui::TextEdit::multiline(config.lernie_editor.draft_mut())
.desired_rows(6)
.code_editor(),
);
ui.horizontal(|ui| {
if ui.button("Apply").clicked() {
config.lernie_status = describe_saved(config.lernie_editor.apply(&config.io));
}
if ui.button("Reload").clicked() {
config.lernie_status = reload_status(config.lernie_editor.reload(&config.io));
}
if ui.button("models.yaml").clicked() {
open_lernie(config, config.lernie.models());
}
});
workflow_list(ui, config);
ui.horizontal(|ui| {
ui.label("new workflow:");
ui.text_edit_singleline(&mut config.new_workflow);
if ui.button("Create").clicked() {
new_workflow(config);
}
});
status_line(ui, &config.lernie_status);
}
fn workflow_list(ui: &mut egui::Ui, config: &mut ConfigState) {
let Ok(files) = config.lernie.workflows(&config.io) else {
return;
};
ui.horizontal_wrapped(|ui| {
for file in files {
let label = file.file_name().map(|n| n.to_string_lossy().into_owned());
if ui.button(label.unwrap_or_default()).clicked() {
open_lernie(config, file);
}
}
});
}
fn new_workflow(config: &mut ConfigState) {
match config.lernie.new_workflow(&config.new_workflow) {
Ok(path) => {
config.lernie_editor = Editor::seeded(path, b"");
config.lernie_status = "new workflow ready".to_string();
}
Err(e) => config.lernie_status = describe_name_err(e),
}
}
fn open_lernie(config: &mut ConfigState, path: PathBuf) {
match Editor::load(path.clone(), &config.io) {
Ok(editor) => config.lernie_editor = editor,
Err(_) => config.lernie_editor = Editor::seeded(path, b""),
}
}
fn config_branch(ui: &mut egui::Ui, model: &mut AppModel, config: &mut ConfigState, lernie: &Cli) {
ui.heading("workspace config branches");
let Some(ws) = model.focused_workspace().map(PathBuf::from) else {
ui.weak("focus a workspace to edit its config branches");
return;
};
match config_branches(&ws) {
Ok(branches) => {
for b in &branches {
ui.monospace(format!("config/{} @ {}", b.name, b.tip_short_oid));
}
}
Err(e) => {
ui.weak(format!("branches: {e}"));
}
}
ui.weak("staged edit → lernie config");
ui.horizontal(|ui| {
ui.label("name:");
ui.text_edit_singleline(&mut config.cb_name);
ui.selectable_value(&mut config.cb_origin, EditOrigin::Advance, "advance");
ui.selectable_value(&mut config.cb_origin, EditOrigin::Orphan, "orphan");
});
ui.horizontal(|ui| {
ui.label("file:");
ui.text_edit_singleline(&mut config.cb_path);
});
ui.add(
egui::TextEdit::multiline(&mut config.cb_body)
.desired_rows(4)
.code_editor(),
);
if ui.button("Send (stage + lernie config)").clicked() {
config.cb_status = send_edit(config, &ws, lernie, model.state_root());
}
status_line(ui, &config.cb_status);
}
fn send_edit(
config: &ConfigState,
ws: &std::path::Path,
lernie: &Cli,
state_root: &std::path::Path,
) -> String {
let files = vec![DraftFile {
rel_path: config.cb_path.clone(),
bytes: config.cb_body.clone().into_bytes(),
}];
match stage_files(&config.stage_root, &next_nonce(), &files) {
Ok(dir) => {
let plan = EditPlan::compose(
&config.yog_binary,
ws,
&config.cb_name,
&config.cb_origin,
&dir,
);
let entry = drive(lernie, ws, &plan, &super::now_ts(), state_root);
format!("lernie config exit {}", entry.exit)
}
Err(e) => e.to_string(),
}
}
fn status_line(ui: &mut egui::Ui, status: &str) {
if !status.is_empty() {
ui.weak(status);
}
}
fn reload_status(result: std::io::Result<()>) -> String {
match result {
Ok(()) => "reloaded".to_string(),
Err(e) => e.to_string(),
}
}
fn describe_applied(applied: Applied) -> String {
match applied {
Applied::Ok => "applied".to_string(),
Applied::Rejected { stderr } => format!("rejected: {stderr}"),
Applied::Conflict => "conflict — reload to re-diff".to_string(),
Applied::Io { error } => format!("io error: {error}"),
}
}
fn describe_saved(saved: Saved) -> String {
match saved {
Saved::Ok => "saved".to_string(),
Saved::Conflict => "conflict — reload to re-diff".to_string(),
Saved::Io { error } => format!("io error: {error}"),
}
}
fn describe_name_err(err: WorkflowNameError) -> String {
match err {
WorkflowNameError::Empty => "name required".to_string(),
WorkflowNameError::Separator => "name must be a single file, no path".to_string(),
WorkflowNameError::DotLeading => "name must not start with a dot".to_string(),
}
}