mise_interactive_config/editor/
mod.rs1mod actions;
7mod handlers;
8mod undo;
9
10use std::io;
11use std::path::PathBuf;
12
13use crate::cursor::Cursor;
14use crate::document::{EntryValue, TomlDocument};
15use crate::providers::{
16 BackendProvider, EmptyBackendProvider, EmptyToolProvider, EmptyVersionProvider, ToolProvider,
17 VersionProvider,
18};
19use crate::render::{Mode, Renderer};
20
21use undo::UndoAction;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ConfigResult {
26 Saved(String),
28 Cancelled,
30}
31
32pub struct InteractiveConfig {
34 path: PathBuf,
35 pub(crate) doc: TomlDocument,
36 pub(crate) cursor: Cursor,
37 pub(crate) mode: Mode,
38 dry_run: bool,
39 title: String,
40 pub(crate) renderer: Renderer,
41 pub(crate) tool_provider: Box<dyn ToolProvider>,
42 pub(crate) version_provider: Box<dyn VersionProvider>,
43 pub(crate) backend_provider: Box<dyn BackendProvider>,
44 pub(crate) preferred_specificity: usize,
46 pub(crate) undo_stack: Vec<UndoAction>,
48 pub(crate) path_display: String,
50}
51
52impl InteractiveConfig {
53 pub fn new(path: PathBuf) -> Self {
55 let path_display = Self::compute_path_display(&path);
56 Self {
57 path,
58 doc: TomlDocument::new(),
59 cursor: Cursor::new(),
60 mode: Mode::Navigate,
61 dry_run: false,
62 title: "mise config editor".to_string(),
63 renderer: Renderer::new(),
64 tool_provider: Box::new(EmptyToolProvider),
65 version_provider: Box::new(EmptyVersionProvider),
66 backend_provider: Box::new(EmptyBackendProvider),
67 preferred_specificity: 0, undo_stack: Vec::new(),
69 path_display,
70 }
71 }
72
73 pub fn open(path: PathBuf) -> io::Result<Self> {
75 let doc = if path.exists() {
76 TomlDocument::load(&path)?
77 } else {
78 TomlDocument::new()
79 };
80
81 let preferred_specificity = Self::infer_specificity_from_doc(&doc);
83 let path_display = Self::compute_path_display(&path);
84
85 Ok(Self {
86 path,
87 doc,
88 cursor: Cursor::new(),
89 mode: Mode::Navigate,
90 dry_run: false,
91 title: "mise config editor".to_string(),
92 renderer: Renderer::new(),
93 tool_provider: Box::new(EmptyToolProvider),
94 version_provider: Box::new(EmptyVersionProvider),
95 backend_provider: Box::new(EmptyBackendProvider),
96 preferred_specificity,
97 undo_stack: Vec::new(),
98 path_display,
99 })
100 }
101
102 fn infer_specificity_from_doc(doc: &TomlDocument) -> usize {
104 let tools_section = doc.sections.iter().find(|s| s.name == "tools");
106 if let Some(section) = tools_section {
107 if let Some(entry) = section.entries.last()
109 && let EntryValue::Simple(version) = &entry.value
110 {
111 return Self::version_to_specificity(version);
112 }
113 }
114 0 }
116
117 fn version_to_specificity(version: &str) -> usize {
119 if version == "latest" {
120 0
121 } else {
122 let dots = version.chars().filter(|c| *c == '.').count();
127 dots + 1
128 }
129 }
130
131 fn compute_path_display(path: &std::path::Path) -> String {
133 let abs_path = if path.is_relative() {
134 std::env::current_dir()
135 .map(|cwd| cwd.join(path))
136 .unwrap_or_else(|_| path.to_path_buf())
137 } else {
138 path.to_path_buf()
139 };
140 xx::file::display_path(&abs_path)
141 }
142
143 pub fn dry_run(mut self, dry_run: bool) -> Self {
145 self.dry_run = dry_run;
146 self
147 }
148
149 pub fn title(mut self, title: &str) -> Self {
151 self.title = title.to_string();
152 self
153 }
154
155 pub fn with_tool_provider(mut self, provider: Box<dyn ToolProvider>) -> Self {
157 self.tool_provider = provider;
158 self
159 }
160
161 pub fn with_version_provider(mut self, provider: Box<dyn VersionProvider>) -> Self {
163 self.version_provider = provider;
164 self
165 }
166
167 pub fn with_backend_provider(mut self, provider: Box<dyn BackendProvider>) -> Self {
169 self.backend_provider = provider;
170 self
171 }
172
173 pub fn add_tool(&mut self, name: &str, version: &str) {
175 if let Some(tools_idx) = self.doc.sections.iter().position(|s| s.name == "tools") {
176 if !self.doc.sections[tools_idx]
178 .entries
179 .iter()
180 .any(|e| e.key == name)
181 {
182 self.doc
183 .add_entry(tools_idx, name.to_string(), version.to_string());
184 }
185 }
186 }
187
188 pub fn add_prepare(&mut self, provider: &str) {
190 let prepare_idx =
192 if let Some(idx) = self.doc.sections.iter().position(|s| s.name == "prepare") {
193 idx
194 } else {
195 let settings_idx = self.doc.sections.iter().position(|s| s.name == "settings");
197 let insert_idx = settings_idx.unwrap_or(self.doc.sections.len());
198 self.doc.sections.insert(
199 insert_idx,
200 crate::document::Section {
201 name: "prepare".to_string(),
202 entries: Vec::new(),
203 expanded: false,
204 comments: Vec::new(),
205 },
206 );
207 insert_idx
208 };
209
210 if !self.doc.sections[prepare_idx]
212 .entries
213 .iter()
214 .any(|e| e.key == provider)
215 {
216 self.doc.sections[prepare_idx]
218 .entries
219 .push(crate::document::Entry {
220 key: provider.to_string(),
221 value: EntryValue::InlineTable(vec![("auto".to_string(), "true".to_string())]),
222 expanded: false,
223 comments: Vec::new(),
224 });
225 self.doc.modified = true;
226 }
227 }
228
229 pub async fn run(mut self) -> io::Result<ConfigResult> {
231 self.render_current_mode()?;
233
234 loop {
235 let term = self.renderer.term().clone();
237 let key = tokio::task::spawn_blocking(move || term.read_key())
238 .await
239 .map_err(io::Error::other)??;
240
241 let should_exit = self.handle_key(key).await?;
242 if let Some(result) = should_exit {
243 self.renderer.clear()?;
245 return Ok(result);
246 }
247
248 self.render_current_mode()?;
250 }
251 }
252
253 pub(crate) fn render_current_mode(&mut self) -> io::Result<()> {
255 match &self.mode {
256 Mode::Picker(kind, picker) => {
257 self.renderer
258 .render_picker(picker, kind, &self.path_display)?;
259 }
260 Mode::Loading(message) => {
261 self.renderer
262 .render_loading(message, &self.title, &self.path_display)?;
263 }
264 _ => {
265 self.renderer.render(
266 &self.doc,
267 &self.cursor,
268 &self.mode,
269 &self.title,
270 &self.path_display,
271 self.dry_run,
272 !self.undo_stack.is_empty(),
273 )?;
274 }
275 }
276 Ok(())
277 }
278}