@@ -0,0 +1,459 @@ |
| 1 | +# Sniffly Tabs Implementation Plan |
| 2 | + |
| 3 | +## Overview |
| 4 | +Add browser-style tabs to Sniffly for multi-directory viewing without window clutter. |
| 5 | + |
| 6 | +## Design Decisions |
| 7 | + |
| 8 | +### Architecture |
| 9 | +- **Tab limit**: 5 tabs maximum (configurable constant) |
| 10 | +- **Scan strategy**: Serialize scans (one active scan at a time, queue others) |
| 11 | +- **Widget strategy**: Single drawing area, swap data on tab switch |
| 12 | +- **Memory strategy**: Keep all tab data in memory (lazy approach for MVP) |
| 13 | +- **Layout**: Tab bar on right side of toolbar, grows leftward |
| 14 | + |
| 15 | +### State Management |
| 16 | +- Encapsulate ALL per-tab state in `tab_state` type |
| 17 | +- No global state except `active_tab_index` and `tabs` array |
| 18 | +- Each tab fully independent with own history, scan state, view |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## Phase 1: Core Infrastructure (Must Have) |
| 23 | + |
| 24 | +### 1.1 State Encapsulation |
| 25 | +**Goal**: Move all global state to per-tab structures |
| 26 | + |
| 27 | +**Tasks**: |
| 28 | +- [ ] Create `tab_state` type in new `src/gui/tab_manager.f90` |
| 29 | + - [ ] Path (current scan directory) |
| 30 | + - [ ] Navigation history array + position + count |
| 31 | + - [ ] Root node pointer (scan tree) |
| 32 | + - [ ] Current view node pointer |
| 33 | + - [ ] Selected index |
| 34 | + - [ ] Show hidden files flag |
| 35 | + - [ ] Show extensions flag |
| 36 | + - [ ] Render mode (flat/cushioned) |
| 37 | + - [ ] Scan state (embedded `scan_state_type`) |
| 38 | + - [ ] Tab label/title (last path segment) |
| 39 | + |
| 40 | +- [ ] Create `tab_manager` module with: |
| 41 | + - [ ] `tabs(MAX_TABS)` array |
| 42 | + - [ ] `active_tab_index` integer |
| 43 | + - [ ] `num_tabs` counter |
| 44 | + - [ ] Functions: |
| 45 | + - [ ] `create_tab(path)` - Initialize new tab |
| 46 | + - [ ] `close_tab(index)` - Cleanup and remove tab |
| 47 | + - [ ] `switch_to_tab(index)` - Make tab active |
| 48 | + - [ ] `get_active_tab()` - Return pointer to current tab |
| 49 | + - [ ] `get_tab(index)` - Return pointer to specific tab |
| 50 | + |
| 51 | +- [ ] Audit `gtk_app.f90` for global state: |
| 52 | + - [ ] `global_scan_path` → per-tab |
| 53 | + - [ ] `nav_history`, `nav_history_pos`, `nav_history_count` → per-tab |
| 54 | + - [ ] `navigating_history` flag → per-tab |
| 55 | + |
| 56 | +- [ ] Audit `treemap_renderer.f90` for global state: |
| 57 | + - [ ] `current_view` → per-tab |
| 58 | + - [ ] `root_node` → per-tab |
| 59 | + - [ ] `selected_index` → per-tab |
| 60 | + - [ ] `show_hidden_files` → per-tab |
| 61 | + - [ ] `show_extensions` → per-tab |
| 62 | + - [ ] `render_mode` → per-tab |
| 63 | + |
| 64 | +- [ ] Audit `progressive_scanner.f90`: |
| 65 | + - [ ] `scan_state` → per-tab (one scan context per tab) |
| 66 | + - [ ] Modify scan functions to accept `tab_index` parameter |
| 67 | + - [ ] Track which tab owns active scan |
| 68 | + |
| 69 | +### 1.2 Scan Serialization |
| 70 | +**Goal**: Prevent concurrent scans and I/O contention |
| 71 | + |
| 72 | +**Tasks**: |
| 73 | +- [ ] Add scan queue to `tab_manager.f90`: |
| 74 | + - [ ] `scan_queue(MAX_TABS)` - tab indices waiting to scan |
| 75 | + - [ ] `scan_queue_head`, `scan_queue_tail`, `scan_queue_size` |
| 76 | + - [ ] `active_scan_tab_index` - which tab is scanning (0 = none) |
| 77 | + |
| 78 | +- [ ] Create scan queueing functions: |
| 79 | + - [ ] `request_scan(tab_index, path)` - Add to queue or start immediately |
| 80 | + - [ ] `process_scan_queue()` - Start next scan when current finishes |
| 81 | + - [ ] `cancel_scan_for_tab(tab_index)` - Remove from queue or stop if active |
| 82 | + |
| 83 | +- [ ] Modify `progressive_scanner.f90`: |
| 84 | + - [ ] Add `active_tab_for_scan` parameter |
| 85 | + - [ ] Scan completion callback triggers `process_scan_queue()` |
| 86 | + - [ ] Only update UI if scanning tab is active |
| 87 | + |
| 88 | +- [ ] Update scan buttons: |
| 89 | + - [ ] Show "Waiting..." status if tab queued but not scanning |
| 90 | + - [ ] Cancel button removes from queue or stops active scan |
| 91 | + |
| 92 | +### 1.3 Tab UI Components |
| 93 | +**Goal**: Create visual tab bar and interaction |
| 94 | + |
| 95 | +**Tasks**: |
| 96 | +- [ ] Create `src/gui/tab_widget.f90`: |
| 97 | + - [ ] `create_tab_bar()` - Returns GTK box containing tabs |
| 98 | + - [ ] `create_tab_button(tab_index, label)` - Individual tab with close X |
| 99 | + - [ ] `create_add_tab_button()` - Plus button for new tabs |
| 100 | + - [ ] `update_tab_highlights()` - Yellow border on active tab |
| 101 | + - [ ] `refresh_tab_bar()` - Rebuild entire tab bar (on add/close) |
| 102 | + |
| 103 | +- [ ] Tab button design: |
| 104 | + - [ ] Width: 120px (enough for label + close button) |
| 105 | + - [ ] Height: 28px (compact) |
| 106 | + - [ ] Active: 2px yellow border (#FFD700) |
| 107 | + - [ ] Label: Last segment of path (truncate if needed) |
| 108 | + - [ ] Close button: Small X on right (8x8px) |
| 109 | + |
| 110 | +- [ ] Layout integration in `gtk_app.f90`: |
| 111 | + - [ ] Add horizontal box below toolbar |
| 112 | + - [ ] Tab bar aligned to right side |
| 113 | + - [ ] Grows leftward as tabs added |
| 114 | + - [ ] Plus button always on left edge of tab bar |
| 115 | + |
| 116 | +- [ ] Tab interactions: |
| 117 | + - [ ] Click tab → `switch_to_tab(index)` |
| 118 | + - [ ] Click close X → `close_tab(index)` (with confirmation if scanning) |
| 119 | + - [ ] Click plus → `create_tab(current_path_parent)` or show directory picker |
| 120 | + |
| 121 | +### 1.4 Tab Switching Logic |
| 122 | +**Goal**: Seamlessly switch between tab contexts |
| 123 | + |
| 124 | +**Tasks**: |
| 125 | +- [ ] Implement `switch_to_tab(index)` in `tab_manager.f90`: |
| 126 | + - [ ] Save current tab state (selected index, scroll position if any) |
| 127 | + - [ ] Set `active_tab_index = index` |
| 128 | + - [ ] Load new tab state into rendering context |
| 129 | + - [ ] Update breadcrumb to new tab's path |
| 130 | + - [ ] Update status bar with new tab's stats |
| 131 | + - [ ] Update treemap widget data pointers |
| 132 | + - [ ] Trigger full redraw |
| 133 | + |
| 134 | +- [ ] Update all navigation functions: |
| 135 | + - [ ] Get active tab first: `tab = get_active_tab()` |
| 136 | + - [ ] Operate on tab's state, not globals |
| 137 | + - [ ] Update tab's history, not global history |
| 138 | + |
| 139 | +- [ ] Update all button handlers: |
| 140 | + - [ ] Back/Forward use active tab's history |
| 141 | + - [ ] Up navigates active tab's tree |
| 142 | + - [ ] Rescan targets active tab |
| 143 | + |
| 144 | +### 1.5 Resource Cleanup |
| 145 | +**Goal**: Prevent memory leaks on tab close |
| 146 | + |
| 147 | +**Tasks**: |
| 148 | +- [ ] Implement `close_tab(index)` in `tab_manager.f90`: |
| 149 | + - [ ] Check if last tab → prevent close (must have at least 1) |
| 150 | + - [ ] Check if scan active → confirm with user dialog |
| 151 | + - [ ] Cancel scan if active: `cancel_scan_for_tab(index)` |
| 152 | + - [ ] Deallocate file tree: `recursive_deallocate_node(tab.root_node)` |
| 153 | + - [ ] Clear history array |
| 154 | + - [ ] Shift remaining tabs down (compact array) |
| 155 | + - [ ] Decrement `num_tabs` |
| 156 | + - [ ] If closed active tab → activate tab to the right (or left if last) |
| 157 | + - [ ] Refresh tab bar UI |
| 158 | + |
| 159 | +- [ ] Create recursive deallocation: |
| 160 | + - [ ] `recursive_deallocate_node(node)` in types or tab_manager |
| 161 | + - [ ] Free children array recursively |
| 162 | + - [ ] Deallocate path strings |
| 163 | + - [ ] Nullify pointers |
| 164 | + |
| 165 | +- [ ] Add confirmation dialog: |
| 166 | + - [ ] `show_close_tab_confirmation(tab_index)` if scan active |
| 167 | + - [ ] "Tab is scanning. Close anyway?" with Cancel/Close buttons |
| 168 | + |
| 169 | +### 1.6 Keyboard Shortcuts |
| 170 | +**Goal**: Cmd-1 through Cmd-5 switch tabs, Cmd-T new tab |
| 171 | + |
| 172 | +**Tasks**: |
| 173 | +- [ ] Add to key handler in `treemap_widget.f90` or `gtk_app.f90`: |
| 174 | + - [ ] Cmd-1 → `switch_to_tab(1)` |
| 175 | + - [ ] Cmd-2 → `switch_to_tab(2)` |
| 176 | + - [ ] ... |
| 177 | + - [ ] Cmd-5 → `switch_to_tab(5)` |
| 178 | + - [ ] Cmd-T → `create_tab()` with directory picker |
| 179 | + - [ ] Cmd-W → `close_tab(active_tab_index)` with confirmation |
| 180 | + |
| 181 | +- [ ] Handle out of bounds: |
| 182 | + - [ ] If `num_tabs < requested_index` → ignore or beep |
| 183 | + - [ ] Show feedback if tab doesn't exist |
| 184 | + |
| 185 | +--- |
| 186 | + |
| 187 | +## Phase 2: Should Have Features |
| 188 | + |
| 189 | +### 2.1 Tab Close Safety |
| 190 | +**Goal**: Prevent accidental data loss |
| 191 | + |
| 192 | +**Tasks**: |
| 193 | +- [ ] Prevent closing last tab: |
| 194 | + - [ ] Disable close button if `num_tabs == 1` |
| 195 | + - [ ] Grey out close X |
| 196 | + - [ ] Cmd-W does nothing if last tab |
| 197 | + |
| 198 | +- [ ] Confirm close if scan active: |
| 199 | + - [ ] Native dialog: "Scanning in progress. Close tab anyway?" |
| 200 | + - [ ] Only show if scan > 10% complete (skip if just started) |
| 201 | + |
| 202 | +- [ ] Confirm close if large tree: |
| 203 | + - [ ] If tree has > 10,000 nodes → confirm |
| 204 | + - [ ] "This tab contains a large scan. Close anyway?" |
| 205 | + |
| 206 | +### 2.2 Tab Tooltips |
| 207 | +**Goal**: Show full path on hover |
| 208 | + |
| 209 | +**Tasks**: |
| 210 | +- [ ] Add tooltip to each tab button: |
| 211 | + - [ ] Full path of tab's current directory |
| 212 | + - [ ] Scan status: "Scanning (45%)", "Ready", "Queued" |
| 213 | + - [ ] Item count: "1,234 items" |
| 214 | + |
| 215 | +- [ ] Use GTK tooltip API: |
| 216 | + - [ ] `gtk_widget_set_tooltip_text(tab_button, full_info)` |
| 217 | + - [ ] Update tooltip on scan progress, navigation |
| 218 | + |
| 219 | +### 2.3 Graceful Memory Degradation |
| 220 | +**Goal**: Handle low memory conditions |
| 221 | + |
| 222 | +**Tasks**: |
| 223 | +- [ ] Warn on tab creation if memory high: |
| 224 | + - [ ] Check system memory (platform-specific) |
| 225 | + - [ ] If > 80% used → warn "System memory low. New tab may be slow." |
| 226 | + |
| 227 | +- [ ] Limit tab count dynamically: |
| 228 | + - [ ] If memory pressure detected → disable plus button |
| 229 | + - [ ] Show message: "Maximum tabs reached (memory limit)" |
| 230 | + |
| 231 | +### 2.4 Tab Reordering |
| 232 | +**Goal**: Let users organize tabs |
| 233 | + |
| 234 | +**Tasks**: |
| 235 | +- [ ] Add drag-and-drop to tab buttons: |
| 236 | + - [ ] GTK drag source on each tab |
| 237 | + - [ ] GTK drop target on tab bar |
| 238 | + - [ ] On drop: reorder `tabs` array |
| 239 | + - [ ] Update `active_tab_index` if it changed |
| 240 | + - [ ] Refresh tab bar |
| 241 | + |
| 242 | +- [ ] Alternative: Shift-Left/Right arrow to move active tab: |
| 243 | + - [ ] Cmd-Shift-[ → move tab left |
| 244 | + - [ ] Cmd-Shift-] → move tab right |
| 245 | + |
| 246 | +--- |
| 247 | + |
| 248 | +## Phase 3: Nice to Have Features |
| 249 | + |
| 250 | +### 3.1 Tab Color Coding |
| 251 | +**Goal**: Visual distinction between tabs |
| 252 | + |
| 253 | +**Tasks**: |
| 254 | +- [ ] Assign color to each tab: |
| 255 | + - [ ] Predefined palette: Blue, Green, Orange, Purple, Teal |
| 256 | + - [ ] Cycle through colors as tabs created |
| 257 | + - [ ] 3px colored bar on top of tab button |
| 258 | + |
| 259 | +- [ ] Color persistence: |
| 260 | + - [ ] Store color in `tab_state` |
| 261 | + - [ ] Let user click to cycle colors (shift-click tab?) |
| 262 | + |
| 263 | +### 3.2 Recently Closed Tabs |
| 264 | +**Goal**: Undo accidental tab close |
| 265 | + |
| 266 | +**Tasks**: |
| 267 | +- [ ] Track last 5 closed tabs: |
| 268 | + - [ ] `recently_closed_tabs` array with path + timestamp |
| 269 | + - [ ] Don't store full tree (too much memory) |
| 270 | + |
| 271 | +- [ ] Restore closed tab: |
| 272 | + - [ ] Cmd-Shift-T → reopen last closed tab |
| 273 | + - [ ] Rescan directory (don't restore old tree) |
| 274 | + - [ ] Right-click on plus button → "Recently Closed" menu |
| 275 | + |
| 276 | +### 3.3 Tab Persistence Across Sessions |
| 277 | +**Goal**: Remember open tabs on quit/relaunch |
| 278 | + |
| 279 | +**Tasks**: |
| 280 | +- [ ] Save tab state on quit: |
| 281 | + - [ ] Write `~/.config/sniffly/tabs.conf` |
| 282 | + - [ ] Format: one path per line |
| 283 | + - [ ] Include active tab marker |
| 284 | + |
| 285 | +- [ ] Restore tabs on launch: |
| 286 | + - [ ] Read config file |
| 287 | + - [ ] Create tab for each path |
| 288 | + - [ ] Don't scan immediately (lazy) |
| 289 | + - [ ] Scan active tab only, others on-demand |
| 290 | + |
| 291 | +### 3.4 Duplicate Tab |
| 292 | +**Goal**: Explore same directory in parallel |
| 293 | + |
| 294 | +**Tasks**: |
| 295 | +- [ ] Add "Duplicate Tab" action: |
| 296 | + - [ ] Right-click tab → "Duplicate" |
| 297 | + - [ ] Or Cmd-D keyboard shortcut |
| 298 | + - [ ] Create new tab with same path |
| 299 | + - [ ] Copy history if desired |
| 300 | + |
| 301 | +### 3.5 Tab Renaming |
| 302 | +**Goal**: Custom labels for tabs |
| 303 | + |
| 304 | +**Tasks**: |
| 305 | +- [ ] Double-click tab label to edit: |
| 306 | + - [ ] Show inline text entry |
| 307 | + - [ ] Save custom name in `tab_state.custom_label` |
| 308 | + - [ ] Use custom label instead of path segment |
| 309 | + |
| 310 | +- [ ] Clear custom name: |
| 311 | + - [ ] Right-click → "Reset Label" |
| 312 | + - [ ] Reverts to automatic path-based label |
| 313 | + |
| 314 | +### 3.6 Split View Within Tab |
| 315 | +**Goal**: Advanced layout (future consideration) |
| 316 | + |
| 317 | +**Tasks**: |
| 318 | +- [ ] Allow horizontal split within tab: |
| 319 | + - [ ] Two treemaps side-by-side |
| 320 | + - [ ] Shared tab context, independent views |
| 321 | + - [ ] This is VERY advanced - only if highly requested |
| 322 | + |
| 323 | +--- |
| 324 | + |
| 325 | +## Implementation Order |
| 326 | + |
| 327 | +### Sprint 1: Foundation (1-2 weeks) |
| 328 | +1. State encapsulation (1.1) - CRITICAL |
| 329 | +2. Scan serialization (1.2) - CRITICAL |
| 330 | +3. Basic tab manager module |
| 331 | + |
| 332 | +### Sprint 2: UI & Switching (1 week) |
| 333 | +4. Tab UI components (1.3) |
| 334 | +5. Tab switching logic (1.4) |
| 335 | +6. Keyboard shortcuts (1.6) |
| 336 | + |
| 337 | +### Sprint 3: Safety & Polish (1 week) |
| 338 | +7. Resource cleanup (1.5) |
| 339 | +8. Tab close safety (2.1) |
| 340 | +9. Tab tooltips (2.2) |
| 341 | + |
| 342 | +### Sprint 4: Enhancements (1 week) |
| 343 | +10. Tab reordering (2.4) |
| 344 | +11. Memory degradation handling (2.3) |
| 345 | +12. Testing and bug fixes |
| 346 | + |
| 347 | +### Sprint 5: Nice-to-Haves (Optional) |
| 348 | +13. Color coding (3.1) |
| 349 | +14. Recently closed (3.2) |
| 350 | +15. Tab persistence (3.3) |
| 351 | +16. Other nice-to-haves as desired |
| 352 | + |
| 353 | +--- |
| 354 | + |
| 355 | +## Testing Strategy |
| 356 | + |
| 357 | +### Unit Tests |
| 358 | +- [ ] Tab creation and destruction |
| 359 | +- [ ] Scan queue operations |
| 360 | +- [ ] History isolation between tabs |
| 361 | +- [ ] Memory deallocation (valgrind) |
| 362 | + |
| 363 | +### Integration Tests |
| 364 | +- [ ] Switch tabs during scan |
| 365 | +- [ ] Close tab during scan |
| 366 | +- [ ] Multiple tabs with different settings |
| 367 | +- [ ] Keyboard shortcuts work correctly |
| 368 | + |
| 369 | +### Stress Tests |
| 370 | +- [ ] 5 tabs with large directories (1M+ files each) |
| 371 | +- [ ] Rapid tab switching |
| 372 | +- [ ] Create/close tabs rapidly |
| 373 | +- [ ] Memory leak detection over time |
| 374 | + |
| 375 | +### Edge Cases |
| 376 | +- [ ] Close last tab (should prevent) |
| 377 | +- [ ] Switch to nonexistent tab index |
| 378 | +- [ ] Scan queue with 5 queued scans |
| 379 | +- [ ] Tab close during scan completion callback |
| 380 | + |
| 381 | +--- |
| 382 | + |
| 383 | +## Risks & Mitigations |
| 384 | + |
| 385 | +| Risk | Severity | Mitigation | |
| 386 | +|------|----------|------------| |
| 387 | +| Missed global state → cross-tab bugs | HIGH | Systematic audit, grep for `save ::` | |
| 388 | +| Memory leaks on tab close | HIGH | Valgrind testing, careful deallocation | |
| 389 | +| UI sluggishness with 5 active scans | MEDIUM | Serialize scans, only 1 active | |
| 390 | +| Complex state management bugs | MEDIUM | Defensive programming, assertions | |
| 391 | +| Keyboard shortcuts conflict | LOW | Document shortcuts, test thoroughly | |
| 392 | + |
| 393 | +--- |
| 394 | + |
| 395 | +## Success Criteria |
| 396 | + |
| 397 | +**MVP (Phase 1)**: |
| 398 | +- [ ] Can create up to 5 tabs |
| 399 | +- [ ] Each tab has independent scan tree |
| 400 | +- [ ] Switching tabs updates UI correctly |
| 401 | +- [ ] Closing tabs doesn't leak memory |
| 402 | +- [ ] Scans are serialized (no concurrent scans) |
| 403 | +- [ ] Keyboard shortcuts work (Cmd-1 through Cmd-5) |
| 404 | + |
| 405 | +**Full Feature (Phase 2)**: |
| 406 | +- [ ] Tab tooltips show full path and status |
| 407 | +- [ ] Can't close last tab |
| 408 | +- [ ] Confirm dialog on close if scanning |
| 409 | +- [ ] Tab reordering works |
| 410 | + |
| 411 | +**Polished (Phase 3)**: |
| 412 | +- [ ] Color coded tabs |
| 413 | +- [ ] Recently closed tabs |
| 414 | +- [ ] Tab state persists across sessions |
| 415 | + |
| 416 | +--- |
| 417 | + |
| 418 | +## Open Questions |
| 419 | + |
| 420 | +1. **Tab limit**: Stick with 5 or make configurable? |
| 421 | + - Recommendation: Hardcode 5 for MVP, make constant easy to change |
| 422 | + |
| 423 | +2. **Scan queue size**: What if user queues 5 scans rapidly? |
| 424 | + - Recommendation: Queue size = MAX_TABS, show queue position in tooltip |
| 425 | + |
| 426 | +3. **Tab close during other tab's scan**: Allow or prevent? |
| 427 | + - Recommendation: Allow (only affects closed tab, not active scan) |
| 428 | + |
| 429 | +4. **Initial tab count**: Start with 1 tab or allow opening with multiple paths? |
| 430 | + - Recommendation: Always start with 1 tab, user creates more |
| 431 | + |
| 432 | +5. **Tab bar overflow**: What if tabs don't fit horizontally? |
| 433 | + - Recommendation: With 5 max tabs @ 120px = 600px, should fit on most screens |
| 434 | + - If needed: Scrollable tab bar or shrink tab width |
| 435 | + |
| 436 | +--- |
| 437 | + |
| 438 | +## File Structure |
| 439 | + |
| 440 | +New files to create: |
| 441 | +- `src/gui/tab_manager.f90` - Core tab state and management |
| 442 | +- `src/gui/tab_widget.f90` - GTK tab bar UI components |
| 443 | + |
| 444 | +Modified files: |
| 445 | +- `src/gui/gtk_app.f90` - Remove globals, use active tab |
| 446 | +- `src/gui/treemap_renderer.f90` - Accept tab context parameter |
| 447 | +- `src/gui/treemap_widget.f90` - Key handler for tab shortcuts |
| 448 | +- `src/core/progressive_scanner.f90` - Per-tab scan state |
| 449 | +- `meson.build` - Add new source files |
| 450 | + |
| 451 | +--- |
| 452 | + |
| 453 | +## Next Steps |
| 454 | + |
| 455 | +1. Review this plan with user |
| 456 | +2. Create feature branch: `git checkout -b tabs-feature` |
| 457 | +3. Start with Phase 1.1 (State Encapsulation) |
| 458 | +4. Commit frequently with descriptive messages |
| 459 | +5. Test thoroughly after each sprint |