gardesk/garfield / c7ca947

Browse files

ui: add tab completion for address bar paths

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c7ca9476a72d00786e7693c103efd0e786211ec1
Parents
cb20d1a
Tree
0054e2d

1 changed file

StatusFile+-
M garfield/src/ui/address_bar.rs 102 0
garfield/src/ui/address_bar.rsmodified
@@ -2,6 +2,7 @@
22
 
33
 use gartk_core::{Key, Rect};
44
 use gartk_render::{Renderer, TextStyle};
5
+use std::fs;
56
 use std::path::PathBuf;
67
 
78
 /// Address bar for editing the current path.
@@ -18,6 +19,12 @@ pub struct AddressBar {
1819
     padding: u32,
1920
     /// Blink counter for cursor animation.
2021
     blink_counter: u32,
22
+    /// Tab completion candidates.
23
+    completions: Vec<String>,
24
+    /// Current completion index.
25
+    completion_index: usize,
26
+    /// Text that triggered completion (to detect changes).
27
+    completion_base: String,
2128
 }
2229
 
2330
 /// Frames per blink cycle (on + off).
@@ -33,6 +40,9 @@ impl AddressBar {
3340
             active: false,
3441
             padding: 8,
3542
             blink_counter: 0,
43
+            completions: Vec::new(),
44
+            completion_index: 0,
45
+            completion_base: String::new(),
3646
         }
3747
     }
3848
 
@@ -117,12 +127,14 @@ impl AddressBar {
117127
                 if self.cursor > 0 {
118128
                     self.cursor -= 1;
119129
                     self.text.remove(self.cursor);
130
+                    self.completions.clear();
120131
                 }
121132
                 true
122133
             }
123134
             Key::Delete => {
124135
                 if self.cursor < self.text.len() {
125136
                     self.text.remove(self.cursor);
137
+                    self.completions.clear();
126138
                 }
127139
                 true
128140
             }
@@ -146,15 +158,105 @@ impl AddressBar {
146158
                 self.cursor = self.text.len();
147159
                 true
148160
             }
161
+            Key::Tab => {
162
+                self.complete();
163
+                true
164
+            }
149165
             Key::Char(c) => {
150166
                 self.text.insert(self.cursor, *c);
151167
                 self.cursor += 1;
168
+                // Reset completions when text changes
169
+                self.completions.clear();
152170
                 true
153171
             }
154172
             _ => false,
155173
         }
156174
     }
157175
 
176
+    /// Perform tab completion on the current text.
177
+    fn complete(&mut self) {
178
+        // If we already have completions and base matches, cycle to next
179
+        if !self.completions.is_empty() && self.text == self.completion_base {
180
+            self.completion_index = (self.completion_index + 1) % self.completions.len();
181
+            self.text = self.completions[self.completion_index].clone();
182
+            self.cursor = self.text.len();
183
+            self.completion_base = self.text.clone();
184
+            return;
185
+        }
186
+
187
+        // Expand ~ to home directory for completion
188
+        let expanded = if self.text.starts_with('~') {
189
+            if let Some(home) = dirs::home_dir() {
190
+                let rest = self.text.strip_prefix('~').unwrap_or("");
191
+                home.to_string_lossy().to_string() + rest
192
+            } else {
193
+                self.text.clone()
194
+            }
195
+        } else {
196
+            self.text.clone()
197
+        };
198
+
199
+        let path = PathBuf::from(&expanded);
200
+
201
+        // Determine the directory to search and the prefix to match
202
+        let (search_dir, prefix) = if path.is_dir() && expanded.ends_with('/') {
203
+            // Path is a directory ending with /, list its contents
204
+            (path.clone(), String::new())
205
+        } else if let Some(parent) = path.parent() {
206
+            // Get the filename prefix to match
207
+            let prefix = path.file_name()
208
+                .map(|s| s.to_string_lossy().to_string())
209
+                .unwrap_or_default();
210
+            (parent.to_path_buf(), prefix)
211
+        } else {
212
+            return;
213
+        };
214
+
215
+        // Find matching entries
216
+        let mut matches = Vec::new();
217
+        if let Ok(entries) = fs::read_dir(&search_dir) {
218
+            for entry in entries.filter_map(|e| e.ok()) {
219
+                let name = entry.file_name().to_string_lossy().to_string();
220
+                if prefix.is_empty() || name.starts_with(&prefix) {
221
+                    let full_path = search_dir.join(&name);
222
+                    // Add trailing slash for directories
223
+                    let display = if full_path.is_dir() {
224
+                        full_path.to_string_lossy().to_string() + "/"
225
+                    } else {
226
+                        full_path.to_string_lossy().to_string()
227
+                    };
228
+
229
+                    // Convert back to ~ notation if applicable
230
+                    let display = if let Some(home) = dirs::home_dir() {
231
+                        if let Ok(stripped) = PathBuf::from(&display).strip_prefix(&home) {
232
+                            format!("~/{}", stripped.to_string_lossy())
233
+                        } else {
234
+                            display
235
+                        }
236
+                    } else {
237
+                        display
238
+                    };
239
+
240
+                    matches.push(display);
241
+                }
242
+            }
243
+        }
244
+
245
+        // Sort matches
246
+        matches.sort();
247
+
248
+        if matches.is_empty() {
249
+            return;
250
+        }
251
+
252
+        // Store completions and apply first one
253
+        self.completions = matches;
254
+        self.completion_index = 0;
255
+        self.text = self.completions[0].clone();
256
+        self.cursor = self.text.len();
257
+        self.completion_base = self.text.clone();
258
+    }
259
+
158260
     /// Render the address bar.
159261
     pub fn render(&mut self, renderer: &Renderer) -> anyhow::Result<()> {
160262
         if !self.active {