reovim_tui_mod_explorer/
lib.rs1#![cfg_attr(coverage_nightly, allow(unused_features))]
2#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
3mod layout;
24mod render;
25
26use reovim_client_driver::{
27 ChromePosition, ClientModule, ClientModuleError, ModuleContext, PlatformCapabilities,
28 ProbeResult, Rect, RenderSurface, Version,
29};
30
31use crate::layout::SidebarBounds;
32
33#[derive(Debug, Default)]
35pub(crate) struct ExplorerData {
36 pub active: bool,
37 pub root_name: String,
38 pub cursor_index: usize,
39 pub scroll_offset: usize,
40 pub width: u16,
41 pub input_mode: String,
42 pub input_buffer: String,
43 pub input_label: String,
44 pub message: Option<String>,
45 pub show_hidden: bool,
46 pub nodes: Vec<NodeData>,
47}
48
49#[derive(Debug)]
51#[allow(clippy::struct_excessive_bools)]
52pub(crate) struct NodeData {
53 pub name: String,
54 pub depth: usize,
55 pub is_dir: bool,
56 pub is_expanded: bool,
57 pub is_hidden: bool,
58 pub is_last: bool,
59 pub vertical_lines: Vec<bool>,
60 pub is_symlink: bool,
61 #[allow(dead_code)] pub size: u64,
63}
64
65pub struct ExplorerModule {
69 data: ExplorerData,
70}
71
72impl ExplorerModule {
73 #[must_use]
75 pub fn new() -> Self {
76 Self {
77 data: ExplorerData::default(),
78 }
79 }
80}
81
82impl Default for ExplorerModule {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88#[cfg_attr(coverage_nightly, coverage(off))]
89impl ClientModule for ExplorerModule {
90 fn id(&self) -> &'static str {
91 "explorer"
92 }
93
94 fn kind(&self) -> &'static str {
95 "explorer"
96 }
97
98 fn name(&self) -> &'static str {
99 "Explorer"
100 }
101
102 fn version(&self) -> Version {
103 Version::new(0, 1, 0)
104 }
105
106 fn init(&mut self, _ctx: &ModuleContext) -> ProbeResult {
107 ProbeResult::Success
108 }
109
110 fn exit(&mut self) -> Result<(), ClientModuleError> {
111 Ok(())
112 }
113
114 fn has_chrome(&self) -> bool {
115 true
116 }
117
118 fn chrome_position(&self) -> ChromePosition {
119 ChromePosition::Left
120 }
121
122 fn chrome_requested_size(&self, _caps: &dyn PlatformCapabilities) -> u16 {
123 if self.data.active { self.data.width } else { 0 }
124 }
125
126 fn chrome_priority(&self) -> u16 {
127 60
128 }
129
130 #[allow(clippy::cast_possible_truncation)]
131 fn on_notification(&mut self, data: &str) {
132 let Ok(json) = serde_json::from_str::<serde_json::Value>(data) else {
133 return;
134 };
135
136 let active = json["active"].as_bool().unwrap_or(false);
137 if !active {
138 self.data.active = false;
139 return;
140 }
141
142 self.data.active = true;
143 json["rootName"]
144 .as_str()
145 .unwrap_or("")
146 .clone_into(&mut self.data.root_name);
147 self.data.cursor_index = json["cursorIndex"].as_u64().unwrap_or(0) as usize;
148 self.data.scroll_offset = json["scrollOffset"].as_u64().unwrap_or(0) as usize;
149 self.data.width = json["width"].as_u64().unwrap_or(30) as u16;
150 json["inputMode"]
151 .as_str()
152 .unwrap_or("none")
153 .clone_into(&mut self.data.input_mode);
154 json["inputBuffer"]
155 .as_str()
156 .unwrap_or("")
157 .clone_into(&mut self.data.input_buffer);
158 self.data.show_hidden = json["showHidden"].as_bool().unwrap_or(false);
159 self.data.message = json["message"].as_str().map(str::to_owned);
160
161 self.data.input_label = match self.data.input_mode.as_str() {
163 "createFile" => "New file: ".to_owned(),
164 "createDir" => "New dir: ".to_owned(),
165 "rename" => "Rename: ".to_owned(),
166 "confirmDelete" => "Delete? (y/n): ".to_owned(),
167 _ => String::new(),
168 };
169
170 if let Some(arr) = json["nodes"].as_array() {
173 self.data.nodes = arr
174 .iter()
175 .map(|item| NodeData {
176 name: item["name"].as_str().unwrap_or("").to_owned(),
177 depth: item["depth"].as_u64().unwrap_or(0) as usize,
178 is_dir: item["isDir"].as_bool().unwrap_or(false),
179 is_expanded: item["isExpanded"].as_bool().unwrap_or(false),
180 is_hidden: item["isHidden"].as_bool().unwrap_or(false),
181 is_last: item["isLast"].as_bool().unwrap_or(false),
182 vertical_lines: item["verticalLines"]
183 .as_array()
184 .map(|a| a.iter().filter_map(serde_json::Value::as_bool).collect())
185 .unwrap_or_default(),
186 is_symlink: item["isSymlink"].as_bool().unwrap_or(false),
187 size: item["size"].as_u64().unwrap_or(0),
188 })
189 .collect();
190 }
191 }
192
193 #[allow(clippy::cast_possible_truncation)]
194 fn cursor_position(&self, _w: u16, h: u16) -> Option<(u16, u16)> {
195 if !self.data.active || self.data.input_mode == "none" {
196 return None;
197 }
198
199 let bounds = SidebarBounds::calculate(0, 0, self.data.width, h, true);
202 let input_y = bounds.input_y?;
203
204 let cursor_x = 1 + self.data.input_label.len() as u16 + self.data.input_buffer.len() as u16;
206 Some((cursor_x.min(self.data.width.saturating_sub(2)), input_y))
207 }
208
209 fn chrome_render(
210 &self,
211 surface: &mut dyn RenderSurface,
212 bounds: Rect,
213 _caps: &dyn PlatformCapabilities,
214 ) {
215 if !self.data.active {
216 return;
217 }
218
219 let has_input = self.data.input_mode != "none";
220 let sidebar_bounds =
221 SidebarBounds::calculate(bounds.x, bounds.y, bounds.width, bounds.height, has_input);
222 render::render_explorer(surface, &self.data, &sidebar_bounds);
223 }
224}
225
226#[cfg(test)]
227#[path = "lib_tests.rs"]
228mod tests;