use super::{ConfigState, form_ui, status};
use crate::AppModel;
use crate::boundary::Action;
use crate::boundary::config::ConfigFile;
use crate::cli_outbound::Cli;
use crate::config_edit::branch::edit::EditOrigin;
use crate::config_edit::branch::{config_branches, config_file, config_tree};
use crate::config_edit::form::{self, Schema};
use status::status_line;
use std::path::{Path, PathBuf};
const NEW_LINEAGE: &str = "new lineage…";
const LINEAGE_HINT: &str = "Which config branch to write. `default` is the one new conversations in \
this workspace read; any other name is a lineage of its own. Typed, the lineage is \
`/config branch|fork|orphan <name> <text…>`.";
const FILE_HINT: &str = "Which file inside the config branch to edit — `providers.yaml`, for \
instance. It is created if the branch has no such file. Typed, the file rides the \
same `/config` line.";
pub(super) fn render(
ui: &mut egui::Ui,
model: &mut AppModel,
config: &mut ConfigState,
provider_rows: &[String],
lernie: &Cli,
bl: &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;
};
for b in &config.branches {
ui.monospace(format!("config/{} @ {}", b.name, b.tip_short_oid));
}
lineage(ui, config, &ws);
file_row(ui, config, &ws);
match schema_of(&config.cb_path) {
Some(schema) => settings(ui, config, &schema, provider_rows),
None => {
ui.weak(form_ui::NO_READER);
}
}
egui::CollapsingHeader::new("raw file body")
.show(ui, |ui| {
form_ui::raw_editor(ui, &mut config.cb_body);
})
.header_response
.on_hover_text(
"Open the file's whole contents, exactly as they will be committed — \
the raw text behind the settings above. No key of its own: Tab reaches \
it, Space presses it.",
);
if ui
.button("Send (stage + lernie config)")
.on_hover_text(
"Stage this file and run `lernie config` — the only thing allowed to \
write a config branch. New conversations in this workspace read the \
result; running ones stay on the commit they started from. Typed, it is \
`/config branch <name> <text…>`.",
)
.clicked()
{
config.cb_status = send_edit(model, config, &ws, lernie, bl);
reread(config, Some(&ws));
}
status_line(ui, &config.cb_status);
}
pub(super) fn reread(config: &mut ConfigState, workspace: Option<&Path>) {
let Some(ws) = workspace else {
config.branches.clear();
config.cb_files.clear();
return;
};
config.branches = config_branches(ws).unwrap_or_default();
config.cb_files = tree(config, ws);
}
fn tree(config: &ConfigState, ws: &Path) -> Vec<String> {
if !config.branches.iter().any(|b| b.name == config.cb_name) {
return Vec::new();
}
config_tree(ws, &format!("config/{}", config.cb_name)).unwrap_or_default()
}
fn lineage(ui: &mut egui::Ui, config: &mut ConfigState, ws: &Path) {
let before = config.cb_name.clone();
let known = config.branches.iter().any(|b| b.name == config.cb_name);
ui.horizontal(|ui| {
ui.label("lineage:");
let shown = if known {
config.cb_name.clone()
} else {
NEW_LINEAGE.to_string()
};
egui::ComboBox::from_id_salt("config-branch")
.selected_text(shown)
.show_ui(ui, |ui| {
for b in &config.branches {
ui.selectable_value(
&mut config.cb_name,
b.name.clone(),
format!("config/{} @ {}", b.name, b.tip_short_oid),
)
.on_hover_text(LINEAGE_HINT);
}
ui.selectable_value(&mut config.cb_name, String::new(), NEW_LINEAGE)
.on_hover_text(
"Name a config branch this workspace does not have yet, in the \
field this choice reveals — the name `/config orphan <name>` \
would carry.",
);
})
.response
.on_hover_text(LINEAGE_HINT);
if !known {
ui.text_edit_singleline(&mut config.cb_name)
.on_hover_text(LINEAGE_HINT);
}
ui.selectable_value(&mut config.cb_origin, EditOrigin::Advance, "advance")
.on_hover_text(
"Commit on top of the branch as it stands, keeping everything already \
on it. Typed, it is `/config branch <name> <text…>`.",
);
ui.selectable_value(&mut config.cb_origin, EditOrigin::Orphan, "orphan")
.on_hover_text(
"Start the branch over from nothing — only the file below survives, \
and its previous history is left behind. Typed, it is \
`/config orphan <name> <text…>`.",
);
});
if config.cb_name != before {
config.cb_files = tree(config, ws);
}
}
fn file_row(ui: &mut egui::Ui, config: &mut ConfigState, ws: &Path) {
ui.horizontal(|ui| {
ui.label("file:").on_hover_text(FILE_HINT);
egui::ComboBox::from_id_salt("config-branch-file")
.selected_text(&config.cb_path)
.show_ui(ui, |ui| {
for path in &config.cb_files.clone() {
ui.selectable_value(&mut config.cb_path, path.clone(), path)
.on_hover_text(FILE_HINT);
}
})
.response
.on_hover_text(FILE_HINT);
if config.cb_files.is_empty() {
ui.text_edit_singleline(&mut config.cb_path)
.on_hover_text(FILE_HINT);
}
if ui
.button("Load")
.on_hover_text(
"Read this file out of the selected lineage's tip into the editor \
below, so the edit starts from what is actually there. \
`/config branch <name>` with no text reads the same bytes.",
)
.clicked()
{
config.cb_status = load(config, ws);
}
});
}
fn load(config: &mut ConfigState, ws: &Path) -> String {
let refspec = format!("config/{}", config.cb_name);
match config_file(ws, &refspec, &config.cb_path) {
Ok(bytes) => {
config.cb_body = String::from_utf8_lossy(&bytes).into_owned();
format!("loaded {refspec}:{}", config.cb_path)
}
Err(e) => format!("load: {e}"),
}
}
fn schema_of(path: &str) -> Option<Schema> {
form::schema_for(path.rsplit('/').next()?)
}
fn settings(
ui: &mut egui::Ui,
config: &mut ConfigState,
schema: &Schema,
provider_rows: &[String],
) {
let groups = form::read(schema, &config.cb_body, provider_rows);
if let Some((row, value)) = form_ui::render(ui, "config-branch", &groups, provider_rows) {
match form::write(schema, &config.cb_body, &row, &value) {
Ok(text) => {
config.cb_body = text;
config.cb_status = format!("{}.{} drafted — Send to commit", row.entry, row.field);
}
Err(e) => config.cb_status = e.to_string(),
}
}
}
fn send_edit(
model: &mut AppModel,
config: &ConfigState,
ws: &Path,
lernie: &Cli,
bl: &Cli,
) -> String {
let deps = model.boundary_deps(lernie, bl);
let action = Action::ApplyConfig {
file: ConfigFile::Branch {
workspace: ws.to_path_buf(),
lineage: config.cb_name.clone(),
origin: config.cb_origin.clone(),
path: config.cb_path.clone(),
},
text: config.cb_body.clone(),
};
match model.dispatch(&deps, &crate::shell::now_ts(), &action) {
Ok(crate::boundary::reply::Reply::Outcome(outcome)) => format!(
"lernie config: {}",
crate::opslog::exit::ExitKind::of(outcome.exit, "lernie").label()
),
Ok(other) => format!("unexpected reply: {other:?}"),
Err(e) => e,
}
}