fix: jq bar is usable
2 files changed, +96 -53
+40-12src/views/browser.rs
| @@ -204,6 +204,11 @@ impl BrowserView { | ||
| 204 | 204 | self.go_up(); |
| 205 | 205 | } |
| 206 | 206 | |
| 207 | + // '/' focuses the jq bar | |
| 208 | + if ui.input(|i| i.key_pressed(egui::Key::Slash)) { | |
| 209 | + self.jq_bar.focus(); | |
| 210 | + } | |
| 211 | + | |
| 207 | 212 | // Copy selected value: c or Ctrl+C |
| 208 | 213 | let copy = ui.input(|i| i.key_pressed(egui::Key::C)) |
| 209 | 214 | || ui.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::C)); |
| @@ -289,21 +294,46 @@ impl BrowserView { | ||
| 289 | 294 | self.jq_error = None; |
| 290 | 295 | } |
| 291 | 296 | |
| 292 | - if resp.completion_applied { | |
| 293 | - if let Some(path) = jq_path_to_segments(&self.jq_bar.query) { | |
| 294 | - self.path = path; | |
| 297 | + // Build a full path from jq segments by prepending the current file segment | |
| 298 | + let file_seg = self.path.first().cloned(); | |
| 299 | + let make_full_path = |jq_segs: Vec<PathSegment>| -> Vec<PathSegment> { | |
| 300 | + let mut full = Vec::with_capacity(jq_segs.len() + 1); | |
| 301 | + if let Some(ref seg) = file_seg { | |
| 302 | + full.push(seg.clone()); | |
| 303 | + } | |
| 304 | + full.extend(jq_segs); | |
| 305 | + full | |
| 306 | + }; | |
| 307 | + | |
| 308 | + // Cycling through completions — preview the path without committing | |
| 309 | + if resp.previewing { | |
| 310 | + if let Some(jq_segs) = jq_path_to_segments(&self.jq_bar.query) { | |
| 311 | + if resolve_path(root, &jq_segs).is_some() { | |
| 312 | + self.path = make_full_path(jq_segs); | |
| 313 | + self.selection = 0; | |
| 314 | + self.jq_synced = true; | |
| 315 | + self.jq_result = None; | |
| 316 | + self.jq_error = None; | |
| 317 | + self.scroll_to_selection = true; | |
| 318 | + } | |
| 319 | + } | |
| 320 | + } | |
| 321 | + | |
| 322 | + // Final acceptance — Enter/Tab/click on a completion | |
| 323 | + if resp.accepted { | |
| 324 | + if let Some(jq_segs) = jq_path_to_segments(&self.jq_bar.query) { | |
| 325 | + self.path = make_full_path(jq_segs); | |
| 295 | 326 | self.selection = 0; |
| 296 | 327 | self.jq_synced = true; |
| 297 | 328 | self.jq_result = None; |
| 298 | 329 | self.jq_error = None; |
| 330 | + self.scroll_to_selection = true; | |
| 299 | 331 | } |
| 300 | - } | |
| 301 | - | |
| 302 | - if resp.run { | |
| 303 | - // First try as navigation path | |
| 304 | - if let Some(path) = jq_path_to_segments(&self.jq_bar.query) { | |
| 305 | - if resolve_path(root, &path).is_some() { | |
| 306 | - self.path = path; | |
| 332 | + } else if resp.run { | |
| 333 | + // Manual Enter (no completion) — try as path, then as jq query | |
| 334 | + if let Some(jq_segs) = jq_path_to_segments(&self.jq_bar.query) { | |
| 335 | + if resolve_path(root, &jq_segs).is_some() { | |
| 336 | + self.path = make_full_path(jq_segs); | |
| 307 | 337 | self.selection = 0; |
| 308 | 338 | self.jq_synced = true; |
| 309 | 339 | self.jq_result = None; |
| @@ -312,7 +342,6 @@ impl BrowserView { | ||
| 312 | 342 | return; |
| 313 | 343 | } |
| 314 | 344 | } |
| 315 | - // Otherwise run as jq query | |
| 316 | 345 | let result = JqEngine::execute(&self.jq_bar.query, root); |
| 317 | 346 | if let Some(err) = &result.error { |
| 318 | 347 | self.jq_error = Some(err.clone()); |
| @@ -323,7 +352,6 @@ impl BrowserView { | ||
| 323 | 352 | } |
| 324 | 353 | } |
| 325 | 354 | |
| 326 | - // Show jq error inline | |
| 327 | 355 | if let Some(err) = &self.jq_error { |
| 328 | 356 | ui.label( |
| 329 | 357 | RichText::new(err) |
+56-41src/widgets/jq_bar.rs
| @@ -4,14 +4,16 @@ use crate::theme::CatppuccinMocha; | ||
| 4 | 4 | |
| 5 | 5 | /// Response from the jq bar — tells the caller what happened this frame. |
| 6 | 6 | pub struct JqBarResponse { |
| 7 | - /// Enter was pressed (caller should execute or navigate) | |
| 7 | + /// Enter/Tab/click accepted a completion — navigate to the path | |
| 8 | + pub accepted: bool, | |
| 9 | + /// Enter with no completions — execute as jq query | |
| 8 | 10 | pub run: bool, |
| 9 | - /// Escape was pressed (caller should cancel/reset) | |
| 11 | + /// Escape was pressed — cancel and reset | |
| 10 | 12 | pub escaped: bool, |
| 11 | - /// Query text was edited by the user | |
| 13 | + /// Query text was edited by the user (typing) | |
| 12 | 14 | pub changed: bool, |
| 13 | - /// A completion was accepted (Tab or click) | |
| 14 | - pub completion_applied: bool, | |
| 15 | + /// Cycling through completions — preview the path but don't commit | |
| 16 | + pub previewing: bool, | |
| 15 | 17 | } |
| 16 | 18 | |
| 17 | 19 | /// Reusable jq filter bar with fuzzy autocompletion. |
| @@ -35,26 +37,34 @@ impl JqBar { | ||
| 35 | 37 | } |
| 36 | 38 | |
| 37 | 39 | /// The egui Id used for the text input (for focus checks). |
| 38 | - pub fn input_id(ui: &Ui) -> egui::Id { | |
| 39 | - ui.id().with("jq_bar_input") | |
| 40 | + /// Uses a stable global id so callers can check focus from any ui context. | |
| 41 | + pub fn input_id() -> egui::Id { | |
| 42 | + egui::Id::new("jq_bar_input_global") | |
| 40 | 43 | } |
| 41 | 44 | |
| 42 | 45 | /// Returns true if the jq bar input currently has focus. |
| 43 | 46 | pub fn has_focus(ui: &Ui) -> bool { |
| 44 | - let id = Self::input_id(ui); | |
| 47 | + let id = Self::input_id(); | |
| 45 | 48 | ui.ctx().memory(|m| m.focused().map_or(false, |f| f == id)) |
| 46 | 49 | } |
| 47 | 50 | |
| 51 | + /// Request focus on the jq bar input next frame. | |
| 52 | + pub fn focus(&mut self) { | |
| 53 | + self.refocus = true; | |
| 54 | + } | |
| 55 | + | |
| 48 | 56 | /// Render the jq bar. Caller provides `root` for autocompletion. |
| 49 | 57 | pub fn show(&mut self, ui: &mut Ui, root: &serde_json::Value) -> JqBarResponse { |
| 50 | 58 | let mut response = JqBarResponse { |
| 59 | + accepted: false, | |
| 51 | 60 | run: false, |
| 52 | 61 | escaped: false, |
| 53 | 62 | changed: false, |
| 54 | - completion_applied: false, | |
| 63 | + previewing: false, | |
| 55 | 64 | }; |
| 56 | 65 | |
| 57 | 66 | let mut accepted_completion: Option<String> = None; |
| 67 | + let suppress_completions = self.refocus; // completion was just applied | |
| 58 | 68 | |
| 59 | 69 | ui.horizontal(|ui| { |
| 60 | 70 | ui.label( |
| @@ -63,7 +73,7 @@ impl JqBar { | ||
| 63 | 73 | .size(14.0), |
| 64 | 74 | ); |
| 65 | 75 | |
| 66 | - let input_id = Self::input_id(ui); | |
| 76 | + let input_id = Self::input_id(); | |
| 67 | 77 | let text_response = ui.add( |
| 68 | 78 | egui::TextEdit::singleline(&mut self.query) |
| 69 | 79 | .id(input_id) |
| @@ -84,22 +94,50 @@ impl JqBar { | ||
| 84 | 94 | self.refocus = false; |
| 85 | 95 | } |
| 86 | 96 | |
| 97 | + // Check focus — Enter causes lost_focus, so check both | |
| 87 | 98 | let has_focus = text_response.has_focus(); |
| 99 | + let just_lost_focus = text_response.lost_focus(); | |
| 88 | 100 | |
| 89 | - if has_focus { | |
| 90 | - if text_response.changed() { | |
| 101 | + if has_focus || just_lost_focus { | |
| 102 | + if has_focus && text_response.changed() && !suppress_completions { | |
| 91 | 103 | response.changed = true; |
| 92 | 104 | self.rebuild_completions(root); |
| 93 | 105 | } |
| 94 | 106 | |
| 95 | 107 | let tab = ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Tab)); |
| 108 | + let enter = ui.input(|i| i.key_pressed(egui::Key::Enter)); | |
| 96 | 109 | let ctrl_space = |
| 97 | 110 | ui.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::Space)); |
| 98 | 111 | |
| 99 | - if tab && self.show_completions && !self.completions.is_empty() { | |
| 100 | - accepted_completion = Some(self.completions[self.completion_index].clone()); | |
| 112 | + if self.show_completions && !self.completions.is_empty() { | |
| 113 | + // Arrow keys / Ctrl-N/P cycle and live-preview the selection | |
| 114 | + let down = ui.input(|i| i.key_pressed(egui::Key::ArrowDown)) | |
| 115 | + || ui.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::N)); | |
| 116 | + let up = ui.input(|i| i.key_pressed(egui::Key::ArrowUp)) | |
| 117 | + || ui.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::P)); | |
| 118 | + if down { | |
| 119 | + self.completion_index = | |
| 120 | + (self.completion_index + 1).min(self.completions.len() - 1); | |
| 121 | + self.apply_completion(&self.completions[self.completion_index].clone()); | |
| 122 | + self.refocus = true; | |
| 123 | + response.previewing = true; | |
| 124 | + } | |
| 125 | + if up { | |
| 126 | + self.completion_index = self.completion_index.saturating_sub(1); | |
| 127 | + self.apply_completion(&self.completions[self.completion_index].clone()); | |
| 128 | + self.refocus = true; | |
| 129 | + response.previewing = true; | |
| 130 | + } | |
| 131 | + | |
| 132 | + // Enter/Tab: accept current selection and close | |
| 133 | + if tab || enter { | |
| 134 | + accepted_completion = Some(self.completions[self.completion_index].clone()); | |
| 135 | + } | |
| 136 | + } else if enter { | |
| 137 | + response.run = true; | |
| 101 | 138 | } |
| 102 | - if tab { | |
| 139 | + | |
| 140 | + if tab && !self.show_completions { | |
| 103 | 141 | text_response.request_focus(); |
| 104 | 142 | } |
| 105 | 143 | |
| @@ -116,38 +154,15 @@ impl JqBar { | ||
| 116 | 154 | self.rebuild_completions(root); |
| 117 | 155 | } |
| 118 | 156 | } |
| 119 | - | |
| 120 | - // Enter: signal caller to execute/navigate | |
| 121 | - if text_response.lost_focus() | |
| 122 | - && ui.input(|i| i.key_pressed(egui::Key::Enter)) | |
| 123 | - { | |
| 124 | - response.run = true; | |
| 125 | - } | |
| 126 | - | |
| 127 | - // Navigate completions | |
| 128 | - if self.show_completions && !self.completions.is_empty() { | |
| 129 | - let down = ui.input(|i| i.key_pressed(egui::Key::ArrowDown)); | |
| 130 | - let up = ui.input(|i| i.key_pressed(egui::Key::ArrowUp)); | |
| 131 | - if down { | |
| 132 | - self.completion_index = | |
| 133 | - (self.completion_index + 1).min(self.completions.len() - 1); | |
| 134 | - } | |
| 135 | - if up { | |
| 136 | - self.completion_index = self.completion_index.saturating_sub(1); | |
| 137 | - } | |
| 138 | - if ui.input(|i| i.key_pressed(egui::Key::Enter)) { | |
| 139 | - accepted_completion = Some(self.completions[self.completion_index].clone()); | |
| 140 | - } | |
| 141 | - } | |
| 142 | 157 | } |
| 143 | 158 | }); |
| 144 | 159 | |
| 145 | - // Apply completion | |
| 160 | + // Apply completion — sets the query, closes suggestions, signals caller to navigate | |
| 146 | 161 | if let Some(comp) = accepted_completion { |
| 147 | 162 | self.apply_completion(&comp); |
| 148 | 163 | self.show_completions = false; |
| 149 | 164 | self.refocus = true; |
| 150 | - response.completion_applied = true; | |
| 165 | + response.accepted = true; | |
| 151 | 166 | } |
| 152 | 167 | |
| 153 | 168 | // Show completion popup |
| @@ -197,7 +212,7 @@ impl JqBar { | ||
| 197 | 212 | self.apply_completion(&c); |
| 198 | 213 | self.show_completions = false; |
| 199 | 214 | self.refocus = true; |
| 200 | - response.completion_applied = true; | |
| 215 | + response.accepted = true; | |
| 201 | 216 | } |
| 202 | 217 | } |
| 203 | 218 |