use std::io::ErrorKind;
use crate::config::Config;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetupGap {
Name,
Location,
Timezone,
Title,
SpeechToText,
TextToSpeech,
ImageGeneration,
}
impl SetupGap {
pub fn label(self) -> &'static str {
match self {
Self::Name => "name (USER.md)",
Self::Location => "location (USER.md)",
Self::Timezone => "timezone (USER.md)",
Self::Title => "role (USER.md)",
Self::SpeechToText => "speech to text",
Self::TextToSpeech => "text to speech",
Self::ImageGeneration => "image generation",
}
}
}
const PROFILE_FIELDS: [(&str, SetupGap); 4] = [
("Name", SetupGap::Name),
("Location", SetupGap::Location),
("Timezone", SetupGap::Timezone),
("Title", SetupGap::Title),
];
fn row_value<'a>(user_md: &'a str, key: &str) -> Option<&'a str> {
user_md.lines().find_map(|line| {
let mut cells = line.trim().trim_matches('|').split('|').map(str::trim);
let label = cells.next()?.trim_matches('*').trim();
if !label.eq_ignore_ascii_case(key) {
return None;
}
cells.next()
})
}
fn is_placeholder(value: &str) -> bool {
value.is_empty() || value.starts_with("*(")
}
pub fn profile_gaps(user_md: &str) -> Vec<SetupGap> {
PROFILE_FIELDS
.iter()
.filter(|(key, _)| row_value(user_md, key).is_none_or(is_placeholder))
.map(|(_, gap)| *gap)
.collect()
}
pub fn collect_gaps(user_md: &str, stt: bool, tts: bool, image: bool) -> Vec<SetupGap> {
let mut gaps = profile_gaps(user_md);
if !stt {
gaps.push(SetupGap::SpeechToText);
}
if !tts {
gaps.push(SetupGap::TextToSpeech);
}
if !image {
gaps.push(SetupGap::ImageGeneration);
}
gaps
}
pub fn render_gaps(gaps: &[SetupGap]) -> String {
if gaps.is_empty() {
return "nothing, everything is set".to_string();
}
gaps.iter()
.map(|gap| gap.label())
.collect::<Vec<_>>()
.join(", ")
}
pub fn read_setup_gaps() -> Vec<SetupGap> {
let path = crate::config::opencrabs_home().join("USER.md");
let user_md = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(e) => {
if e.kind() != ErrorKind::NotFound {
tracing::warn!(error = %e, "setup gaps: could not read USER.md");
}
String::new()
}
};
let (stt, tts, image) = match Config::load() {
Ok(config) => {
let voice = config.voice_config();
let image_ready = config.image.generation.enabled
&& config
.image
.generation
.api_key
.as_deref()
.is_some_and(|key| !key.is_empty());
(voice.stt_enabled, voice.tts_enabled, image_ready)
}
Err(e) => {
tracing::warn!(error = %e, "setup gaps: config did not load, treating voice and image as unset");
(false, false, false)
}
};
collect_gaps(&user_md, stt, tts, image)
}