feat: root takes left two panes in miller browsers and sidebar toggles with ctrl-b
2 files changed, +50 -26
+18-9src/app.rs
| @@ -58,6 +58,7 @@ pub struct JvApp { | ||
| 58 | 58 | new_session_name: String, |
| 59 | 59 | show_new_session_dialog: bool, |
| 60 | 60 | sidebar_width: f32, |
| 61 | + sidebar_visible: bool, | |
| 61 | 62 | theme_applied: bool, |
| 62 | 63 | |
| 63 | 64 | // File drop handling |
| @@ -250,6 +251,7 @@ impl JvApp { | ||
| 250 | 251 | new_session_name: String::new(), |
| 251 | 252 | show_new_session_dialog: false, |
| 252 | 253 | sidebar_width: 240.0, |
| 254 | + sidebar_visible: true, | |
| 253 | 255 | theme_applied: false, |
| 254 | 256 | dropped_files: Vec::new(), |
| 255 | 257 | frame_times: std::collections::VecDeque::with_capacity(60), |
| @@ -1189,16 +1191,23 @@ impl eframe::App for JvApp { | ||
| 1189 | 1191 | }); |
| 1190 | 1192 | } |
| 1191 | 1193 | |
| 1194 | + // Ctrl-B: toggle sidebar | |
| 1195 | + if ctx.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::B)) { | |
| 1196 | + self.sidebar_visible = !self.sidebar_visible; | |
| 1197 | + } | |
| 1198 | + | |
| 1192 | 1199 | // Sidebar |
| 1193 | - egui::SidePanel::left("sidebar") | |
| 1194 | - .default_width(self.sidebar_width) | |
| 1195 | - .min_width(180.0) | |
| 1196 | - .max_width(400.0) | |
| 1197 | - .resizable(true) | |
| 1198 | - .frame(egui::Frame::new().fill(CatppuccinMocha::MANTLE).inner_margin(12.0)) | |
| 1199 | - .show(ctx, |ui| { | |
| 1200 | - self.show_sidebar(ui); | |
| 1201 | - }); | |
| 1200 | + if self.sidebar_visible { | |
| 1201 | + egui::SidePanel::left("sidebar") | |
| 1202 | + .default_width(self.sidebar_width) | |
| 1203 | + .min_width(180.0) | |
| 1204 | + .max_width(400.0) | |
| 1205 | + .resizable(true) | |
| 1206 | + .frame(egui::Frame::new().fill(CatppuccinMocha::MANTLE).inner_margin(12.0)) | |
| 1207 | + .show(ctx, |ui| { | |
| 1208 | + self.show_sidebar(ui); | |
| 1209 | + }); | |
| 1210 | + } | |
| 1202 | 1211 | |
| 1203 | 1212 | // Main content |
| 1204 | 1213 | egui::CentralPanel::default() |
+32-17src/views/browser.rs
| @@ -192,10 +192,20 @@ impl BrowserView { | ||
| 192 | 192 | } |
| 193 | 193 | } |
| 194 | 194 | |
| 195 | + // Estimate max label chars from available width | |
| 196 | + // ~7px per monospace char at 12pt, use 60% of the current column for the label | |
| 197 | + let avail_w = ui.available_width() - 12.0; | |
| 198 | + let current_col_w = if self.path.is_empty() { | |
| 199 | + avail_w * 0.60 // root: left+center merged | |
| 200 | + } else { | |
| 201 | + avail_w * 0.38 // normal center column | |
| 202 | + }; | |
| 203 | + let max_label_chars = ((current_col_w * 0.65) / 7.0).max(20.0) as usize; | |
| 204 | + | |
| 195 | 205 | let (current_entries, parent_entries) = if in_focus_root { |
| 196 | 206 | // Focus mode root: show pinned items as the root list |
| 197 | 207 | let entries: Vec<Entry> = self.focused.iter().map(|fp| { |
| 198 | - let label = focus_path_label(fp); | |
| 208 | + let label = focus_path_label(fp, max_label_chars); | |
| 199 | 209 | let value = resolve_from_files(files, fp); |
| 200 | 210 | let (icon, color) = value.map(type_icon_color).unwrap_or(("{}", CatppuccinMocha::OVERLAY0)); |
| 201 | 211 | let preview = value.map(value_preview).unwrap_or_default(); |
| @@ -442,22 +452,30 @@ impl BrowserView { | ||
| 442 | 452 | }; |
| 443 | 453 | |
| 444 | 454 | let col_height = avail.height(); |
| 455 | + let at_root = self.path.is_empty(); | |
| 445 | 456 | ui.horizontal(|ui| { |
| 446 | 457 | ui.set_height(col_height); |
| 447 | 458 | |
| 448 | - // Left: parent | |
| 449 | - ui.vertical(|ui| { | |
| 450 | - ui.set_width(col_widths[0]); | |
| 451 | - ui.set_height(col_height); | |
| 452 | - crate::widgets::miller::pane_title(ui, &left_title); | |
| 453 | - self.render_parent_column(ui, &parent_entries, col_height); | |
| 454 | - }); | |
| 459 | + if !at_root { | |
| 460 | + // Left: parent | |
| 461 | + ui.vertical(|ui| { | |
| 462 | + ui.set_width(col_widths[0]); | |
| 463 | + ui.set_height(col_height); | |
| 464 | + crate::widgets::miller::pane_title(ui, &left_title); | |
| 465 | + self.render_parent_column(ui, &parent_entries, col_height); | |
| 466 | + }); | |
| 455 | 467 | |
| 456 | - Self::draw_separator(ui, col_height); | |
| 468 | + Self::draw_separator(ui, col_height); | |
| 469 | + } | |
| 457 | 470 | |
| 458 | - // Middle: current (with optional filter) | |
| 471 | + // Middle: current (with optional filter) — takes left+center width at root | |
| 472 | + let current_width = if at_root { | |
| 473 | + col_widths[0] + col_widths[1] + 5.0 | |
| 474 | + } else { | |
| 475 | + col_widths[1] | |
| 476 | + }; | |
| 459 | 477 | ui.vertical(|ui| { |
| 460 | - ui.set_width(col_widths[1]); | |
| 478 | + ui.set_width(current_width); | |
| 461 | 479 | ui.set_height(col_height); |
| 462 | 480 | crate::widgets::miller::pane_title(ui, &mid_title); |
| 463 | 481 | let filter_resp = self.filter.show(ui, "? to filter"); |
| @@ -1236,12 +1254,10 @@ fn focus_path_full(fp: &[PathSegment]) -> String { | ||
| 1236 | 1254 | }).collect::<Vec<_>>().join(".") |
| 1237 | 1255 | } |
| 1238 | 1256 | |
| 1239 | -const FOCUS_LABEL_MAX: usize = 40; | |
| 1240 | - | |
| 1241 | 1257 | /// Short label for a focused path: "first_word...last_chars" if over max, else full. |
| 1242 | -fn focus_path_label(fp: &[PathSegment]) -> String { | |
| 1258 | +fn focus_path_label(fp: &[PathSegment], max_chars: usize) -> String { | |
| 1243 | 1259 | let full = focus_path_full(fp); |
| 1244 | - if full.len() <= FOCUS_LABEL_MAX { | |
| 1260 | + if full.len() <= max_chars { | |
| 1245 | 1261 | return full; |
| 1246 | 1262 | } |
| 1247 | 1263 | |
| @@ -1250,9 +1266,8 @@ fn focus_path_label(fp: &[PathSegment]) -> String { | ||
| 1250 | 1266 | PathSegment::Index(i) => format!("[{}]", i), |
| 1251 | 1267 | }; |
| 1252 | 1268 | |
| 1253 | - // "first...tail" where tail fills remaining chars | |
| 1254 | 1269 | let ellipsis = "..."; |
| 1255 | - let budget = FOCUS_LABEL_MAX.saturating_sub(first.len() + ellipsis.len()); | |
| 1270 | + let budget = max_chars.saturating_sub(first.len() + ellipsis.len()); | |
| 1256 | 1271 | if budget == 0 { |
| 1257 | 1272 | return format!("{}{}", first, ellipsis); |
| 1258 | 1273 | } |