fortrangoingonforty/sniffly / 2931df0

Browse files

fix empty tab UI state (label, breadcrumb, path clearing)

Authored by espadonne
SHA
2931df0a19ab663ac318012fd499519e0ab815ea
Parents
f0f1463
Tree
95de62a

6 changed files

StatusFile+-
M SNIFFLY-TABS.md 2 0
A STATE-AUDIT.md 130 0
A TABS-IMPLEMENTATION-PLAN.md 459 0
M src/gui/gtk_app.f90 10 0
M src/gui/tab_widget.f90 6 2
M src/gui/treemap_widget.f90 8 8
SNIFFLY-TABS.mdmodified
@@ -20,3 +20,5 @@ We'll have to be careful about scans in quicl succession or concurrent scans sin
2020
 
2121
 what other pitfalls do you see? Let's discuss and plan this feature'
2222
 
23
+The tab bar should fit cleanly into the existing interface. we shouldn't ahve to condense or stretch a whole lot, we should just slot it underneath te toolbar on the right side, and again have it grow leftwards, we should cap at 5 tabs or whatever your performance analysis comes back with. My arbitrary 5 came from just wondering who would need that realistically. anyway. let's plan this!''
24
+
STATE-AUDIT.mdadded
@@ -0,0 +1,130 @@
1
+# Global State Audit for Tabs Feature
2
+
3
+## Overview
4
+This document identifies all global state that needs to be encapsulated into per-tab state.
5
+
6
+---
7
+
8
+## gtk_app.f90
9
+
10
+### Per-Tab State (MUST MOVE)
11
+| Variable | Line | Purpose |
12
+|----------|------|---------|
13
+| `global_scan_path` | 58 | Current directory being viewed |
14
+| `nav_history(MAX_HISTORY)` | 65 | Navigation history array |
15
+| `nav_history_count` | 66 | Number of entries in history |
16
+| `nav_history_pos` | 67 | Current position in history |
17
+| `navigating_history` | 68 | Flag for back/forward navigation |
18
+| `pending_synthetic_nav` | 82 | Synthetic navigation pending flag |
19
+| `pending_synthetic_child_name` | 83 | Child name for synthetic nav |
20
+| `synthetic_nav_attempts` | 84 | Attempt counter |
21
+
22
+### Global State (STAYS)
23
+| Variable | Line | Purpose | Notes |
24
+|----------|------|---------|-------|
25
+| `app_ptr` | 48 | GTK application | App-level |
26
+| `main_window_ptr` | 49 | Main window | App-level |
27
+| `status_label_ptr` | 50 | Status bar label | Shared, updated per active tab |
28
+| `progress_bar_ptr` | 51 | Progress bar widget | Shared, updated per active tab |
29
+| `path_entry_ptr` | 52 | Path display entry | Shared, updated per active tab |
30
+| `app_is_shutting_down` | 55 | Shutdown flag | Global state |
31
+| `pending_scan_path` | 61 | Initial scan path | Only for startup |
32
+| `back_btn_ptr` | 71 | Back button widget | Shared UI |
33
+| `forward_btn_ptr` | 72 | Forward button widget | Shared UI |
34
+| `cancel_scan_btn_ptr` | 73 | Cancel button widget | Shared UI |
35
+| `info_btn_ptr` | 76 | Info button widget | Shared UI |
36
+| `copy_path_btn_ptr` | 77 | Copy path button widget | Shared UI |
37
+| `open_finder_btn_ptr` | 78 | Open finder button widget | Shared UI |
38
+| `delete_btn_ptr` | 79 | Delete button widget | Shared UI |
39
+
40
+---
41
+
42
+## treemap_renderer.f90
43
+
44
+### Per-Tab State (MUST MOVE)
45
+| Variable | Line | Purpose |
46
+|----------|------|---------|
47
+| `current_view_node` | 57 | Pointer to current view in tree |
48
+| `has_data` | 58 | Whether scan data exists |
49
+| `scanned_path` | 59 | Path that was scanned |
50
+| `layout_calculated` | 67 | Layout calculation flag |
51
+| `last_width` | 68 | Last rendered width |
52
+| `last_height` | 68 | Last rendered height |
53
+| `show_file_extensions` | 71 | Toggle for file extensions |
54
+| `use_age_based_coloring` | 72 | Toggle for age-based colors |
55
+| `show_allocated_size` | 73 | Toggle allocated vs actual size |
56
+| `show_hidden_files` | 74 | Toggle dotfiles visibility |
57
+| `use_cushion_shading` | 75 | Toggle 3D vs flat rendering |
58
+| `path_names(MAX_PATH_DEPTH)` | 80 | Path components for navigation |
59
+| `path_depth` | 81 | Depth of current path |
60
+
61
+### Global State (STAYS)
62
+| Variable | Line | Purpose | Notes |
63
+|----------|------|---------|-------|
64
+| `dir_cache(MAX_CACHE_SIZE)` | 63 | Directory scan cache | Shared cache across tabs |
65
+| `cache_count` | 64 | Cache entry count | Shared |
66
+| `show_progress_cb` | 84 | Progress callback | Shared callbacks |
67
+| `hide_progress_cb` | 85 | Hide progress callback | Shared callbacks |
68
+| `update_progress_cb` | 86 | Update progress callback | Shared callbacks |
69
+| `scan_completion_cb` | 87 | Completion callback | Shared callbacks |
70
+| `widget_for_redraw` | 90 | Drawing widget pointer | Shared widget |
71
+
72
+---
73
+
74
+## progressive_scanner.f90
75
+
76
+### Per-Tab State (MUST MOVE)
77
+| Variable | Line | Purpose |
78
+|----------|------|---------|
79
+| `show_hidden_files` | 15 | Toggle hidden files in scan |
80
+| `scan_state` | 43 | Complete scan state (queue, progress, root node) |
81
+| `scan_idle_id` | 46 | GTK idle callback ID for cancellation |
82
+
83
+### Global State (STAYS)
84
+| Variable | Line | Purpose | Notes |
85
+|----------|------|---------|-------|
86
+| `update_callback` | 66 | Scan update callback | Shared callback |
87
+| `complete_callback` | 67 | Scan completion callback | Shared callback |
88
+| `initial_level_cb` | 68 | Initial level callback | Shared callback |
89
+
90
+---
91
+
92
+## Summary
93
+
94
+### Total Variables to Move: 29
95
+
96
+**gtk_app.f90**: 8 variables
97
+**treemap_renderer.f90**: 13 variables
98
+**progressive_scanner.f90**: 3 variables (including entire scan_state struct)
99
+
100
+### Critical Observations
101
+
102
+1. **Scan State is Complex**: The `scan_state` in progressive_scanner.f90 is a large struct containing:
103
+   - Queue for breadth-first scanning
104
+   - Progress tracking
105
+   - Root node pointer
106
+   - All scan context
107
+
108
+   This entire structure needs to be per-tab.
109
+
110
+2. **View Settings Are Per-Tab**: Each tab should remember its own:
111
+   - Show/hide extensions
112
+   - Flat vs cushioned rendering
113
+   - Hidden files visibility
114
+   - Color scheme
115
+
116
+3. **Navigation History is Per-Tab**: Each tab maintains its own back/forward history.
117
+
118
+4. **Cache Can Stay Global**: The directory scan cache can be shared across tabs for efficiency.
119
+
120
+5. **Widget Pointers Stay Global**: All GTK widget pointers stay global, but their content is updated when switching tabs.
121
+
122
+---
123
+
124
+## Next Steps
125
+
126
+1. Create `tab_state` type in `tab_manager.f90` containing all 29 variables
127
+2. Create `tabs(MAX_TABS)` array to hold tab states
128
+3. Add `active_tab_index` to track current tab
129
+4. Refactor all functions to operate on tab state instead of globals
130
+5. Create tab switching logic to swap active state
TABS-IMPLEMENTATION-PLAN.mdadded
@@ -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
src/gui/gtk_app.f90modified
@@ -937,6 +937,10 @@ contains
937937
 
938938
     print *, "  Active tab index: ", active_tab_index
939939
     print *, "  Tab has_data: ", tab%has_data
940
+    print *, "  Tab scan_path: ", trim(tab%scan_path)
941
+
942
+    ! Sync treemap widget's scan path with active tab's scan path
943
+    call set_scan_path(trim(tab%scan_path))
940944
 
941945
     ! Sync renderer state with active tab (CRITICAL for correct rendering)
942946
     call set_renderer_state_from_tab(tab%root_node, tab%current_view_node, tab%has_data)
@@ -950,6 +954,12 @@ contains
950954
         call gtk_widget_add_css_class(open_dir_btn_ptr, "suggested-action"//c_null_char)
951955
       end if
952956
 
957
+      ! Clear breadcrumb display
958
+      call update_breadcrumb_cache("")
959
+
960
+      ! Clear path entry
961
+      call update_path_entry("")
962
+
953963
       ! Update status bar to guide user
954964
       call sniffly_update_status("No directory selected - click the folder icon to choose a directory")
955965
 
src/gui/tab_widget.f90modified
@@ -118,8 +118,12 @@ contains
118118
       ! Create horizontal container for this tab (label + close button)
119119
       tab_container = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2_c_int)
120120
 
121
-      ! Format label as ".../basename"
122
-      label_text = ".../" // trim(tab%label)
121
+      ! Format label - show just "Empty" for empty tabs, ".../basename" for normal tabs
122
+      if (trim(tab%label) == "Empty") then
123
+        label_text = "Empty"
124
+      else
125
+        label_text = ".../" // trim(tab%label)
126
+      end if
123127
 
124128
       ! Create tab label button
125129
       tab_btn = gtk_button_new_with_label(trim(label_text)//c_null_char)
src/gui/treemap_widget.f90modified
@@ -429,16 +429,16 @@ contains
429429
       return
430430
     end if
431431
 
432
+    ! Skip rendering if no scan path set (empty tab - show blank canvas)
433
+    if (len_trim(scan_path) == 0) then
434
+      print *, "Skipping render - no scan path set (empty tab)"
435
+      return
436
+    end if
437
+
432438
     ! Render the actual treemap with hover and selection
433439
     ! mouse_x and mouse_y are updated by both mouse motion and arrow keys
434
-    if (len_trim(scan_path) > 0) then
435
-      call scan_and_render_with_interaction(cr, width, height, mouse_x, mouse_y, &
436
-                                             selected_index, trim(scan_path))
437
-    else
438
-      ! Fallback to default if no path set
439
-      call scan_and_render_with_interaction(cr, width, height, mouse_x, mouse_y, &
440
-                                             selected_index)
441
-    end if
440
+    call scan_and_render_with_interaction(cr, width, height, mouse_x, mouse_y, &
441
+                                           selected_index, trim(scan_path))
442442
 
443443
     ! Update breadcrumbs after first render (when scan is complete)
444444
     if (first_render) then