irongit

fix: focus view stars and data

huncholanehuncholaneauthored
parent ec592bdcommit 626935c459675814479ea3d0b398fb6f7b4c2a7eBrowse files

2 files changed, +96 -37

+6-1src/app.rs
@@ -715,8 +715,13 @@ impl JvApp {
715715 // Mode switcher
716716 ui.horizontal(|ui| {
717717 ui.spacing_mut().item_spacing.x = 4.0;
718+ let jv_icon = if self.browser_view.focus_mode {
719+ regular::STAR
720+ } else {
721+ regular::BROWSERS
722+ };
718723 for (mode, label, icon) in [
719- (AppMode::Jv, "jv", regular::BROWSERS),
724+ (AppMode::Jv, "jv", jv_icon),
720725 (AppMode::Groups, "Groups", regular::TREE_STRUCTURE),
721726 (AppMode::Schema, "Schema", regular::GRAPH),
722727 (AppMode::Code, "Code", regular::CODE),
+90-36src/views/browser.rs
@@ -53,7 +53,7 @@ pub struct BrowserView {
5353 cache_key: u64,
5454 // Focus list: full paths to pinned entries (e.g. ["activity_Aug.responseData.status"])
5555 focused: Vec<Vec<PathSegment>>,
56- focus_mode: bool,
56+ pub focus_mode: bool,
5757 focus_dirty: bool,
5858 }
5959
@@ -177,10 +177,7 @@ impl BrowserView {
177177 let (current_entries, parent_entries) = if in_focus_root {
178178 // Focus mode root: show pinned items as the root list
179179 let entries: Vec<Entry> = self.focused.iter().map(|fp| {
180- let label = fp.iter().map(|seg| match seg {
181- PathSegment::Key(k) => k.clone(),
182- PathSegment::Index(i) => format!("[{}]", i),
183- }).collect::<Vec<_>>().join(".");
180+ let label = focus_path_label(fp);
184181 let value = resolve_from_files(files, fp);
185182 let (icon, color) = value.map(type_icon_color).unwrap_or(("{}", CatppuccinMocha::OVERLAY0));
186183 let preview = value.map(value_preview).unwrap_or_default();
@@ -238,19 +235,9 @@ impl BrowserView {
238235 }
239236
240237 let selected_child = if in_focus_root {
241- // Focus root — find the focused path matching the selected entry's label
242- current_entries.get(self.selection)
243- .and_then(|entry| {
244- self.focused.iter()
245- .find(|fp| {
246- let label = fp.iter().map(|seg| match seg {
247- PathSegment::Key(k) => k.clone(),
248- PathSegment::Index(i) => format!("[{}]", i),
249- }).collect::<Vec<_>>().join(".");
250- label == entry.label
251- })
252- .and_then(|fp| resolve_from_files(files, fp))
253- })
238+ // Focus root — entry index maps directly to self.focused index
239+ self.focused.get(self.selection)
240+ .and_then(|fp| resolve_from_files(files, fp))
254241 } else if let Some(cv) = current_value {
255242 // Inside a file — look up child by key/index
256243 current_entries.get(self.selection)
@@ -317,15 +304,14 @@ impl BrowserView {
317304 if entry.is_container {
318305 self.filter.query.clear();
319306 if in_focus_root {
320- // Enter a focused item: set path to its full path
321- if let Some(fp) = self.focused.get(self.selection) {
322- self.path = fp.clone();
307+ if let Some(fp) = self.focused.get(self.selection).cloned() {
308+ self.path = fp;
323309 self.selection = 0;
324310 self.jq_synced = true;
325311 self.sync_jq_from_path();
326312 }
327313 } else if self.path.is_empty() {
328- self.path.push(PathSegment::Key(entry.label.clone()));
314+ self.path.push(label_to_segment(&entry.label));
329315 self.selection = 0;
330316 self.jq_synced = true;
331317 self.sync_jq_from_path();
@@ -358,11 +344,22 @@ impl BrowserView {
358344 // 'f' toggles focus on selected entry (stores full path)
359345 if ui.input(|i| i.key_pressed(egui::Key::F) && !i.modifiers.shift) {
360346 if let Some(entry) = current_entries.get(self.selection) {
361- let mut full_path = self.path.clone();
362- full_path.push(PathSegment::Key(entry.label.clone()));
363- if let Some(pos) = self.focused.iter().position(|p| *p == full_path) {
364- self.focused.remove(pos);
347+ // Find by label match (works at focus root) or by path (works at any depth)
348+ let pos = if in_focus_root {
349+ // At focus root, selection index = focused index
350+ Some(self.selection)
365351 } else {
352+ // At depth, match by full path
353+ let mut check = self.path.clone();
354+ check.push(label_to_segment(&entry.label));
355+ self.focused.iter().position(|fp| *fp == check)
356+ };
357+ if let Some(pos) = pos {
358+ self.focused.remove(pos);
359+ } else if !in_focus_root {
360+ // Only add when NOT at focus root (focus root entries are already focused)
361+ let mut full_path = self.path.clone();
362+ full_path.push(label_to_segment(&entry.label));
366363 self.focused.push(full_path);
367364 }
368365 self.focus_dirty = true;
@@ -466,14 +463,14 @@ impl BrowserView {
466463 if entry.is_container {
467464 self.filter.query.clear();
468465 if in_focus_root {
469- if let Some(fp) = self.focused.get(self.selection) {
470- self.path = fp.clone();
466+ if let Some(fp) = self.focused.get(self.selection).cloned() {
467+ self.path = fp;
471468 self.selection = 0;
472469 self.jq_synced = true;
473470 self.sync_jq_from_path();
474471 }
475472 } else if self.path.is_empty() {
476- self.path.push(PathSegment::Key(entry.label.clone()));
473+ self.path.push(label_to_segment(&entry.label));
477474 self.selection = 0;
478475 self.jq_synced = true;
479476 self.sync_jq_from_path();
@@ -517,14 +514,14 @@ impl BrowserView {
517514 if let Some(entry) = current_entries.get(idx) {
518515 if entry.is_container {
519516 if in_focus_root {
520- if let Some(fp) = self.focused.get(idx) {
521- self.path = fp.clone();
517+ if let Some(fp) = self.focused.get(idx).cloned() {
518+ self.path = fp;
522519 self.selection = 0;
523520 self.jq_synced = true;
524521 self.sync_jq_from_path();
525522 }
526523 } else if self.path.is_empty() {
527- self.path.push(PathSegment::Key(entry.label.clone()));
524+ self.path.push(label_to_segment(&entry.label));
528525 self.selection = 0;
529526 self.jq_synced = true;
530527 self.sync_jq_from_path();
@@ -718,6 +715,7 @@ impl BrowserView {
718715 let mut dbl_clicked: Option<usize> = None;
719716 let current_path = self.path.clone();
720717 let focused = self.focused.clone();
718+ let is_focus_root = self.focus_mode && self.path.is_empty();
721719
722720 // Map original selection index → filtered list position
723721 let filtered_pos = entries.iter().position(|(orig, _)| *orig == self.selection)
@@ -789,9 +787,14 @@ impl BrowserView {
789787 ui.ctx().data_mut(|d| d.insert_temp(copy_btn_id, icon_rect));
790788
791789 // Focus indicator
792- let mut entry_path = current_path.clone();
793- entry_path.push(PathSegment::Key(entry.label.clone()));
794- if focused.iter().any(|p| *p == entry_path) {
790+ let is_focused = if is_focus_root {
791+ true // all entries at focus root are focused
792+ } else {
793+ let mut entry_path = current_path.clone();
794+ entry_path.push(label_to_segment(&entry.label));
795+ focused.iter().any(|p| *p == entry_path)
796+ };
797+ if is_focused {
795798 ui.label(
796799 RichText::new(egui_phosphor::regular::STAR)
797800 .color(CatppuccinMocha::YELLOW)
@@ -1152,7 +1155,7 @@ impl BrowserView {
11521155 }
11531156 match current {
11541157 serde_json::Value::Object(_) => {
1155- self.path.push(PathSegment::Key(entry.label.clone()));
1158+ self.path.push(label_to_segment(&entry.label));
11561159 }
11571160 serde_json::Value::Array(_) => {
11581161 self.path.push(PathSegment::Index(self.selection));
@@ -1200,6 +1203,57 @@ impl BrowserView {
12001203
12011204 /// Resolve a path starting from the file list. path[0] is a file display name,
12021205 /// remaining segments navigate into that file's JSON value. No cloning.
1206+/// Full dot-joined label for a focused path.
1207+fn focus_path_full(fp: &[PathSegment]) -> String {
1208+ fp.iter().map(|seg| match seg {
1209+ PathSegment::Key(k) => k.clone(),
1210+ PathSegment::Index(i) => format!("[{}]", i),
1211+ }).collect::<Vec<_>>().join(".")
1212+}
1213+
1214+/// Short label for a focused path: "filename...lastKey[idx]"
1215+fn focus_path_label(fp: &[PathSegment]) -> String {
1216+ if fp.is_empty() {
1217+ return String::new();
1218+ }
1219+ if fp.len() <= 2 {
1220+ return focus_path_full(fp);
1221+ }
1222+
1223+ let first = match &fp[0] {
1224+ PathSegment::Key(k) => k.clone(),
1225+ PathSegment::Index(i) => format!("[{}]", i),
1226+ };
1227+
1228+ // Build the tail: last key + any trailing indices
1229+ let mut tail_parts = Vec::new();
1230+ for seg in fp.iter().rev() {
1231+ match seg {
1232+ PathSegment::Index(i) => tail_parts.push(format!("[{}]", i)),
1233+ PathSegment::Key(k) => {
1234+ tail_parts.push(k.clone());
1235+ break;
1236+ }
1237+ }
1238+ }
1239+ tail_parts.reverse();
1240+ let tail = tail_parts.join(".");
1241+
1242+ format!("{}...{}", first, tail)
1243+}
1244+
1245+/// Convert an entry label to the correct PathSegment.
1246+/// Array items have labels like "[0]", "[1]" → Index(n).
1247+/// Everything else → Key(label).
1248+fn label_to_segment(label: &str) -> PathSegment {
1249+ if let Some(inner) = label.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
1250+ if let Ok(i) = inner.parse::<usize>() {
1251+ return PathSegment::Index(i);
1252+ }
1253+ }
1254+ PathSegment::Key(label.to_string())
1255+}
1256+
12031257 fn resolve_from_files<'a>(
12041258 files: &'a [(String, serde_json::Value)],
12051259 path: &[PathSegment],