Sniffly Tabs Implementation Plan
Overview
Add browser-style tabs to Sniffly for multi-directory viewing without window clutter.
Design Decisions
Architecture
- Tab limit: 5 tabs maximum (configurable constant)
- Scan strategy: Serialize scans (one active scan at a time, queue others)
- Widget strategy: Single drawing area, swap data on tab switch
- Memory strategy: Keep all tab data in memory (lazy approach for MVP)
- Layout: Tab bar on right side of toolbar, grows leftward
State Management
- Encapsulate ALL per-tab state in
tab_statetype - No global state except
active_tab_indexandtabsarray - Each tab fully independent with own history, scan state, view
Phase 1: Core Infrastructure (Must Have)
1.1 State Encapsulation
Goal: Move all global state to per-tab structures
Tasks:
-
Create
tab_statetype in newsrc/gui/tab_manager.f90- Path (current scan directory)
- Navigation history array + position + count
- Root node pointer (scan tree)
- Current view node pointer
- Selected index
- Show hidden files flag
- Show extensions flag
- Render mode (flat/cushioned)
- Scan state (embedded
scan_state_type) - Tab label/title (last path segment)
-
Create
tab_managermodule with:-
tabs(MAX_TABS)array -
active_tab_indexinteger -
num_tabscounter - Functions:
-
create_tab(path)- Initialize new tab -
close_tab(index)- Cleanup and remove tab -
switch_to_tab(index)- Make tab active -
get_active_tab()- Return pointer to current tab -
get_tab(index)- Return pointer to specific tab
-
-
-
Audit
gtk_app.f90for global state:-
global_scan_path→ per-tab -
nav_history,nav_history_pos,nav_history_count→ per-tab -
navigating_historyflag → per-tab
-
-
Audit
treemap_renderer.f90for global state:-
current_view→ per-tab -
root_node→ per-tab -
selected_index→ per-tab -
show_hidden_files→ per-tab -
show_extensions→ per-tab -
render_mode→ per-tab
-
-
Audit
progressive_scanner.f90:-
scan_state→ per-tab (one scan context per tab) - Modify scan functions to accept
tab_indexparameter - Track which tab owns active scan
-
1.2 Scan Serialization
Goal: Prevent concurrent scans and I/O contention
Tasks:
-
Add scan queue to
tab_manager.f90:-
scan_queue(MAX_TABS)- tab indices waiting to scan -
scan_queue_head,scan_queue_tail,scan_queue_size -
active_scan_tab_index- which tab is scanning (0 = none)
-
-
Create scan queueing functions:
-
request_scan(tab_index, path)- Add to queue or start immediately -
process_scan_queue()- Start next scan when current finishes -
cancel_scan_for_tab(tab_index)- Remove from queue or stop if active
-
-
Modify
progressive_scanner.f90:- Add
active_tab_for_scanparameter - Scan completion callback triggers
process_scan_queue() - Only update UI if scanning tab is active
- Add
-
Update scan buttons:
- Show "Waiting..." status if tab queued but not scanning
- Cancel button removes from queue or stops active scan
1.3 Tab UI Components
Goal: Create visual tab bar and interaction
Tasks:
-
Create
src/gui/tab_widget.f90:-
create_tab_bar()- Returns GTK box containing tabs -
create_tab_button(tab_index, label)- Individual tab with close X -
create_add_tab_button()- Plus button for new tabs -
update_tab_highlights()- Yellow border on active tab -
refresh_tab_bar()- Rebuild entire tab bar (on add/close)
-
-
Tab button design:
- Width: 120px (enough for label + close button)
- Height: 28px (compact)
- Active: 2px yellow border (#FFD700)
- Label: Last segment of path (truncate if needed)
- Close button: Small X on right (8x8px)
-
Layout integration in
gtk_app.f90:- Add horizontal box below toolbar
- Tab bar aligned to right side
- Grows leftward as tabs added
- Plus button always on left edge of tab bar
-
Tab interactions:
- Click tab →
switch_to_tab(index) - Click close X →
close_tab(index)(with confirmation if scanning) - Click plus →
create_tab(current_path_parent)or show directory picker
- Click tab →
1.4 Tab Switching Logic
Goal: Seamlessly switch between tab contexts
Tasks:
-
Implement
switch_to_tab(index)intab_manager.f90:- Save current tab state (selected index, scroll position if any)
- Set
active_tab_index = index - Load new tab state into rendering context
- Update breadcrumb to new tab's path
- Update status bar with new tab's stats
- Update treemap widget data pointers
- Trigger full redraw
-
Update all navigation functions:
- Get active tab first:
tab = get_active_tab() - Operate on tab's state, not globals
- Update tab's history, not global history
- Get active tab first:
-
Update all button handlers:
- Back/Forward use active tab's history
- Up navigates active tab's tree
- Rescan targets active tab
1.5 Resource Cleanup
Goal: Prevent memory leaks on tab close
Tasks:
-
Implement
close_tab(index)intab_manager.f90:- Check if last tab → prevent close (must have at least 1)
- Check if scan active → confirm with user dialog
- Cancel scan if active:
cancel_scan_for_tab(index) - Deallocate file tree:
recursive_deallocate_node(tab.root_node) - Clear history array
- Shift remaining tabs down (compact array)
- Decrement
num_tabs - If closed active tab → activate tab to the right (or left if last)
- Refresh tab bar UI
-
Create recursive deallocation:
-
recursive_deallocate_node(node)in types or tab_manager - Free children array recursively
- Deallocate path strings
- Nullify pointers
-
-
Add confirmation dialog:
-
show_close_tab_confirmation(tab_index)if scan active - "Tab is scanning. Close anyway?" with Cancel/Close buttons
-
1.6 Keyboard Shortcuts
Goal: Cmd-1 through Cmd-5 switch tabs, Cmd-T new tab
Tasks:
-
Add to key handler in
treemap_widget.f90orgtk_app.f90:- Cmd-1 →
switch_to_tab(1) - Cmd-2 →
switch_to_tab(2) - ...
- Cmd-5 →
switch_to_tab(5) - Cmd-T →
create_tab()with directory picker - Cmd-W →
close_tab(active_tab_index)with confirmation
- Cmd-1 →
-
Handle out of bounds:
- If
num_tabs < requested_index→ ignore or beep - Show feedback if tab doesn't exist
- If
Phase 2: Should Have Features
2.1 Tab Close Safety
Goal: Prevent accidental data loss
Tasks:
-
Prevent closing last tab:
- Disable close button if
num_tabs == 1 - Grey out close X
- Cmd-W does nothing if last tab
- Disable close button if
-
Confirm close if scan active:
- Native dialog: "Scanning in progress. Close tab anyway?"
- Only show if scan > 10% complete (skip if just started)
-
Confirm close if large tree:
- If tree has > 10,000 nodes → confirm
- "This tab contains a large scan. Close anyway?"
2.2 Tab Tooltips
Goal: Show full path on hover
Tasks:
-
Add tooltip to each tab button:
- Full path of tab's current directory
- Scan status: "Scanning (45%)", "Ready", "Queued"
- Item count: "1,234 items"
-
Use GTK tooltip API:
-
gtk_widget_set_tooltip_text(tab_button, full_info) - Update tooltip on scan progress, navigation
-
2.3 Graceful Memory Degradation
Goal: Handle low memory conditions
Tasks:
-
Warn on tab creation if memory high:
- Check system memory (platform-specific)
- If > 80% used → warn "System memory low. New tab may be slow."
-
Limit tab count dynamically:
- If memory pressure detected → disable plus button
- Show message: "Maximum tabs reached (memory limit)"
2.4 Tab Reordering
Goal: Let users organize tabs
Tasks:
-
Add drag-and-drop to tab buttons:
- GTK drag source on each tab
- GTK drop target on tab bar
- On drop: reorder
tabsarray - Update
active_tab_indexif it changed - Refresh tab bar
-
Alternative: Shift-Left/Right arrow to move active tab:
- Cmd-Shift-[ → move tab left
- Cmd-Shift-] → move tab right
Phase 3: Nice to Have Features
3.1 Tab Color Coding
Goal: Visual distinction between tabs
Tasks:
-
Assign color to each tab:
- Predefined palette: Blue, Green, Orange, Purple, Teal
- Cycle through colors as tabs created
- 3px colored bar on top of tab button
-
Color persistence:
- Store color in
tab_state - Let user click to cycle colors (shift-click tab?)
- Store color in
3.2 Recently Closed Tabs
Goal: Undo accidental tab close
Tasks:
-
Track last 5 closed tabs:
-
recently_closed_tabsarray with path + timestamp - Don't store full tree (too much memory)
-
-
Restore closed tab:
- Cmd-Shift-T → reopen last closed tab
- Rescan directory (don't restore old tree)
- Right-click on plus button → "Recently Closed" menu
3.3 Tab Persistence Across Sessions
Goal: Remember open tabs on quit/relaunch
Tasks:
-
Save tab state on quit:
- Write
~/.config/sniffly/tabs.conf - Format: one path per line
- Include active tab marker
- Write
-
Restore tabs on launch:
- Read config file
- Create tab for each path
- Don't scan immediately (lazy)
- Scan active tab only, others on-demand
3.4 Duplicate Tab
Goal: Explore same directory in parallel
Tasks:
- Add "Duplicate Tab" action:
- Right-click tab → "Duplicate"
- Or Cmd-D keyboard shortcut
- Create new tab with same path
- Copy history if desired
3.5 Tab Renaming
Goal: Custom labels for tabs
Tasks:
-
Double-click tab label to edit:
- Show inline text entry
- Save custom name in
tab_state.custom_label - Use custom label instead of path segment
-
Clear custom name:
- Right-click → "Reset Label"
- Reverts to automatic path-based label
3.6 Split View Within Tab
Goal: Advanced layout (future consideration)
Tasks:
- Allow horizontal split within tab:
- Two treemaps side-by-side
- Shared tab context, independent views
- This is VERY advanced - only if highly requested
Implementation Order
Sprint 1: Foundation (1-2 weeks)
- State encapsulation (1.1) - CRITICAL
- Scan serialization (1.2) - CRITICAL
- Basic tab manager module
Sprint 2: UI & Switching (1 week)
- Tab UI components (1.3)
- Tab switching logic (1.4)
- Keyboard shortcuts (1.6)
Sprint 3: Safety & Polish (1 week)
- Resource cleanup (1.5)
- Tab close safety (2.1)
- Tab tooltips (2.2)
Sprint 4: Enhancements (1 week)
- Tab reordering (2.4)
- Memory degradation handling (2.3)
- Testing and bug fixes
Sprint 5: Nice-to-Haves (Optional)
- Color coding (3.1)
- Recently closed (3.2)
- Tab persistence (3.3)
- Other nice-to-haves as desired
Testing Strategy
Unit Tests
- Tab creation and destruction
- Scan queue operations
- History isolation between tabs
- Memory deallocation (valgrind)
Integration Tests
- Switch tabs during scan
- Close tab during scan
- Multiple tabs with different settings
- Keyboard shortcuts work correctly
Stress Tests
- 5 tabs with large directories (1M+ files each)
- Rapid tab switching
- Create/close tabs rapidly
- Memory leak detection over time
Edge Cases
- Close last tab (should prevent)
- Switch to nonexistent tab index
- Scan queue with 5 queued scans
- Tab close during scan completion callback
Risks & Mitigations
| Risk | Severity | Mitigation |
|---|---|---|
| Missed global state → cross-tab bugs | HIGH | Systematic audit, grep for save :: |
| Memory leaks on tab close | HIGH | Valgrind testing, careful deallocation |
| UI sluggishness with 5 active scans | MEDIUM | Serialize scans, only 1 active |
| Complex state management bugs | MEDIUM | Defensive programming, assertions |
| Keyboard shortcuts conflict | LOW | Document shortcuts, test thoroughly |
Success Criteria
MVP (Phase 1):
- Can create up to 5 tabs
- Each tab has independent scan tree
- Switching tabs updates UI correctly
- Closing tabs doesn't leak memory
- Scans are serialized (no concurrent scans)
- Keyboard shortcuts work (Cmd-1 through Cmd-5)
Full Feature (Phase 2):
- Tab tooltips show full path and status
- Can't close last tab
- Confirm dialog on close if scanning
- Tab reordering works
Polished (Phase 3):
- Color coded tabs
- Recently closed tabs
- Tab state persists across sessions
Open Questions
-
Tab limit: Stick with 5 or make configurable?
- Recommendation: Hardcode 5 for MVP, make constant easy to change
-
Scan queue size: What if user queues 5 scans rapidly?
- Recommendation: Queue size = MAX_TABS, show queue position in tooltip
-
Tab close during other tab's scan: Allow or prevent?
- Recommendation: Allow (only affects closed tab, not active scan)
-
Initial tab count: Start with 1 tab or allow opening with multiple paths?
- Recommendation: Always start with 1 tab, user creates more
-
Tab bar overflow: What if tabs don't fit horizontally?
- Recommendation: With 5 max tabs @ 120px = 600px, should fit on most screens
- If needed: Scrollable tab bar or shrink tab width
File Structure
New files to create:
src/gui/tab_manager.f90- Core tab state and managementsrc/gui/tab_widget.f90- GTK tab bar UI components
Modified files:
src/gui/gtk_app.f90- Remove globals, use active tabsrc/gui/treemap_renderer.f90- Accept tab context parametersrc/gui/treemap_widget.f90- Key handler for tab shortcutssrc/core/progressive_scanner.f90- Per-tab scan statemeson.build- Add new source files
Next Steps
- Review this plan with user
- Create feature branch:
git checkout -b tabs-feature - Start with Phase 1.1 (State Encapsulation)
- Commit frequently with descriptive messages
- Test thoroughly after each sprint
View source
| 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 |