mod actions;
mod handlers;
mod undo;
use std::io;
use std::path::PathBuf;
use crate::cursor::Cursor;
use crate::document::{EntryValue, TomlDocument};
use crate::providers::{
BackendProvider, EmptyBackendProvider, EmptyToolProvider, EmptyVersionProvider, ToolProvider,
VersionProvider,
};
use crate::render::{Mode, Renderer};
use undo::UndoAction;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigResult {
Saved(String),
Cancelled,
}
pub struct InteractiveConfig {
path: PathBuf,
pub(crate) doc: TomlDocument,
pub(crate) cursor: Cursor,
pub(crate) mode: Mode,
dry_run: bool,
title: String,
pub(crate) renderer: Renderer,
pub(crate) tool_provider: Box<dyn ToolProvider>,
pub(crate) version_provider: Box<dyn VersionProvider>,
pub(crate) backend_provider: Box<dyn BackendProvider>,
pub(crate) preferred_specificity: usize,
pub(crate) undo_stack: Vec<UndoAction>,
pub(crate) path_display: String,
}
impl InteractiveConfig {
pub fn new(path: PathBuf) -> Self {
let path_display = Self::compute_path_display(&path);
Self {
path,
doc: TomlDocument::new(),
cursor: Cursor::new(),
mode: Mode::Navigate,
dry_run: false,
title: "mise config editor".to_string(),
renderer: Renderer::new(),
tool_provider: Box::new(EmptyToolProvider),
version_provider: Box::new(EmptyVersionProvider),
backend_provider: Box::new(EmptyBackendProvider),
preferred_specificity: 0, undo_stack: Vec::new(),
path_display,
}
}
pub fn open(path: PathBuf) -> io::Result<Self> {
let doc = if path.exists() {
TomlDocument::load(&path)?
} else {
TomlDocument::new()
};
let preferred_specificity = Self::infer_specificity_from_doc(&doc);
let path_display = Self::compute_path_display(&path);
Ok(Self {
path,
doc,
cursor: Cursor::new(),
mode: Mode::Navigate,
dry_run: false,
title: "mise config editor".to_string(),
renderer: Renderer::new(),
tool_provider: Box::new(EmptyToolProvider),
version_provider: Box::new(EmptyVersionProvider),
backend_provider: Box::new(EmptyBackendProvider),
preferred_specificity,
undo_stack: Vec::new(),
path_display,
})
}
fn infer_specificity_from_doc(doc: &TomlDocument) -> usize {
let tools_section = doc.sections.iter().find(|s| s.name == "tools");
if let Some(section) = tools_section {
if let Some(entry) = section.entries.last()
&& let EntryValue::Simple(version) = &entry.value
{
return Self::version_to_specificity(version);
}
}
0 }
fn version_to_specificity(version: &str) -> usize {
if version == "latest" {
0
} else {
let dots = version.chars().filter(|c| *c == '.').count();
dots + 1
}
}
fn compute_path_display(path: &std::path::Path) -> String {
let abs_path = if path.is_relative() {
std::env::current_dir()
.map(|cwd| cwd.join(path))
.unwrap_or_else(|_| path.to_path_buf())
} else {
path.to_path_buf()
};
xx::file::display_path(&abs_path)
}
pub fn dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
pub fn title(mut self, title: &str) -> Self {
self.title = title.to_string();
self
}
pub fn with_tool_provider(mut self, provider: Box<dyn ToolProvider>) -> Self {
self.tool_provider = provider;
self
}
pub fn with_version_provider(mut self, provider: Box<dyn VersionProvider>) -> Self {
self.version_provider = provider;
self
}
pub fn with_backend_provider(mut self, provider: Box<dyn BackendProvider>) -> Self {
self.backend_provider = provider;
self
}
pub fn add_tool(&mut self, name: &str, version: &str) {
if let Some(tools_idx) = self.doc.sections.iter().position(|s| s.name == "tools") {
if !self.doc.sections[tools_idx]
.entries
.iter()
.any(|e| e.key == name)
{
self.doc
.add_entry(tools_idx, name.to_string(), version.to_string());
}
}
}
pub fn add_deps(&mut self, provider: &str) {
let deps_idx = if let Some(idx) = self.doc.sections.iter().position(|s| s.name == "deps") {
idx
} else {
let settings_idx = self.doc.sections.iter().position(|s| s.name == "settings");
let insert_idx = settings_idx.unwrap_or(self.doc.sections.len());
self.doc.sections.insert(
insert_idx,
crate::document::Section {
name: "deps".to_string(),
entries: Vec::new(),
expanded: false,
comments: Vec::new(),
},
);
insert_idx
};
if !self.doc.sections[deps_idx]
.entries
.iter()
.any(|e| e.key == provider)
{
self.doc.sections[deps_idx]
.entries
.push(crate::document::Entry {
key: provider.to_string(),
value: EntryValue::InlineTable(vec![("auto".to_string(), "true".to_string())]),
expanded: false,
comments: Vec::new(),
});
self.doc.modified = true;
}
}
pub async fn run(mut self) -> io::Result<ConfigResult> {
self.render_current_mode()?;
loop {
let term = self.renderer.term().clone();
let key = tokio::task::spawn_blocking(move || term.read_key())
.await
.map_err(io::Error::other)??;
let should_exit = self.handle_key(key).await?;
if let Some(result) = should_exit {
self.renderer.clear()?;
return Ok(result);
}
self.render_current_mode()?;
}
}
pub(crate) fn render_current_mode(&mut self) -> io::Result<()> {
match &self.mode {
Mode::Picker(kind, picker) => {
self.renderer
.render_picker(picker, kind, &self.path_display)?;
}
Mode::Loading(message) => {
self.renderer
.render_loading(message, &self.title, &self.path_display)?;
}
_ => {
self.renderer.render(
&self.doc,
&self.cursor,
&self.mode,
&self.title,
&self.path_display,
self.dry_run,
!self.undo_stack.is_empty(),
)?;
}
}
Ok(())
}
}