use std::io::Write;
use std::path::PathBuf;
use crossterm::event::{self, Event, KeyCode, KeyEvent};
use crossterm::terminal;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrustLevel {
Full,
ReadOnly,
Untrusted,
}
impl TrustLevel {
pub fn label(self) -> &'static str {
match self {
TrustLevel::Full => "full",
TrustLevel::ReadOnly => "read-only",
TrustLevel::Untrusted => "untrusted",
}
}
}
impl std::fmt::Display for TrustLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
fn trust_file(working_dir: &str) -> PathBuf {
crate::config::project_data_dir(working_dir).join("trust.json")
}
pub fn load_trust(working_dir: &str) -> Option<TrustLevel> {
let path = trust_file(working_dir);
let content = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
pub fn save_trust(working_dir: &str, level: TrustLevel) {
let path = trust_file(working_dir);
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(json) = serde_json::to_string(&level) {
let _ = std::fs::write(path, json);
}
}
const OPTIONS: &[(TrustLevel, &str, &str)] = &[
(TrustLevel::Full, "Trust folder", "full tool access"),
(
TrustLevel::ReadOnly,
"Read-only access",
"write tools disabled",
),
(TrustLevel::Untrusted, "Don't trust", "all tools disabled"),
];
pub fn prompt_trust(working_dir: &str) -> TrustLevel {
let folder_name = std::path::Path::new(working_dir)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(working_dir)
.to_string();
let mut stderr = std::io::stderr();
let _ = writeln!(stderr);
let _ = writeln!(
stderr,
" \x1b[1mDo you trust the files in this folder?\x1b[0m"
);
let _ = writeln!(stderr);
let _ = writeln!(
stderr,
" Trusting a folder allows collet to load its local configurations,"
);
let _ = writeln!(
stderr,
" execute shell commands, and modify files on your behalf."
);
let _ = writeln!(stderr);
let selected = run_selector(&folder_name);
let level = OPTIONS[selected].0;
save_trust(working_dir, level);
let label = match level {
TrustLevel::Full => "\x1b[32m✓ Trusted\x1b[0m — full tool access enabled",
TrustLevel::ReadOnly => "\x1b[33m◐ Read-only\x1b[0m — write tools disabled",
TrustLevel::Untrusted => "\x1b[31m✗ Untrusted\x1b[0m — all tools disabled",
};
let _ = writeln!(stderr, " {label}");
let _ = writeln!(stderr);
level
}
fn run_selector(folder_name: &str) -> usize {
let mut selected: usize = 0;
let mut stderr = std::io::stderr();
let _ = terminal::enable_raw_mode();
let _ = crossterm::execute!(stderr, crossterm::cursor::Hide);
render_options(&mut stderr, selected, folder_name);
loop {
if let Ok(Event::Key(KeyEvent { code, .. })) = event::read() {
match code {
KeyCode::Up | KeyCode::Char('k') => {
selected = selected.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
if selected + 1 < OPTIONS.len() {
selected += 1;
}
}
KeyCode::Enter | KeyCode::Char(' ') => {
break;
}
KeyCode::Char('q') | KeyCode::Esc => {
selected = OPTIONS.len() - 1; break;
}
_ => {}
}
clear_options(&mut stderr);
render_options(&mut stderr, selected, folder_name);
}
}
let _ = crossterm::execute!(stderr, crossterm::cursor::Show);
let _ = terminal::disable_raw_mode();
clear_options(&mut stderr);
selected
}
fn render_options(w: &mut impl Write, selected: usize, folder_name: &str) {
let colors = ["\x1b[32m", "\x1b[33m", "\x1b[31m"];
for (i, (_, label, hint)) in OPTIONS.iter().enumerate() {
let suffix = if i == 0 {
format!(" ({folder_name})")
} else {
String::new()
};
if i == selected {
let _ = write!(
w,
"\r {}● {}\x1b[0m\x1b[1m {label}{suffix}\x1b[0m \x1b[90m{hint}\x1b[0m\r\n",
colors[i], colors[i],
);
} else {
let _ = write!(w, "\r \x1b[90m{label}{suffix}\x1b[0m\r\n",);
}
}
let _ = w.flush();
}
fn clear_options(w: &mut impl Write) {
for _ in 0..OPTIONS.len() {
let _ = write!(w, "\x1b[A\x1b[2K");
}
let _ = w.flush();
}