use super::{ConfigState, form_ui, status};
use crate::AppModel;
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;
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],
) {
ui.heading("workspace config branches");
let Some(ws) = model.focused_workspace() 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));
}
super::send::settle(model, config, &ws);
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()
{
super::send::edit(model, config, &ws);
}
status_line(ui, &config.cb_act.line());
}
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);
crate::shell::row::peers(ui, |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.add(egui::TextEdit::singleline(&mut config.cb_name).desired_width(f32::INFINITY))
.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) {
let files = config.cb_files.clone();
let load_asked = crate::shell::row::peers(ui, |ui| {
ui.label("file:").on_hover_text(FILE_HINT);
egui::ComboBox::from_id_salt("config-branch-file")
.selected_text(config.cb_path.as_str())
.show_ui(ui, |ui| {
for path in &files {
ui.selectable_value(&mut config.cb_path, path.clone(), path)
.on_hover_text(FILE_HINT);
}
})
.response
.on_hover_text(FILE_HINT);
if files.is_empty() {
ui.add(egui::TextEdit::singleline(&mut config.cb_path).desired_width(f32::INFINITY))
.on_hover_text(FILE_HINT);
}
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> <path>` with no text reads the same \
bytes, and `/lineages` lists what there is to read.",
)
.clicked()
});
if load_asked {
let said = load(config, ws);
config.cb_act.say(said);
}
}
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_act.say(format!(
"{}.{} drafted — Send to commit",
row.entry, row.field
));
}
Err(e) => config.cb_act.say(e.to_string()),
}
}
}