| 1 | ! Tab Manager Module for Sniffly |
| 2 | ! Manages multiple tabs with independent scan states and navigation history |
| 3 | module tab_manager |
| 4 | use, intrinsic :: iso_c_binding |
| 5 | use iso_fortran_env, only: int64 |
| 6 | use types, only: file_node |
| 7 | implicit none |
| 8 | private |
| 9 | |
| 10 | public :: tab_state, tabs, active_tab_index, num_tabs, MAX_TABS, MAX_HISTORY |
| 11 | public :: init_tab_manager, create_tab, close_tab, switch_to_tab |
| 12 | public :: get_active_tab, get_tab, get_path_basename |
| 13 | |
| 14 | ! Constants |
| 15 | integer, parameter :: MAX_TABS = 5 |
| 16 | integer, parameter :: MAX_HISTORY = 50 |
| 17 | integer, parameter :: MAX_PATH_DEPTH = 100 |
| 18 | |
| 19 | ! Scan state (embedded in each tab) |
| 20 | type :: queue_entry |
| 21 | character(len=512) :: path |
| 22 | integer :: node_index |
| 23 | integer :: parent_id |
| 24 | integer :: depth |
| 25 | end type queue_entry |
| 26 | |
| 27 | type :: scan_state_type |
| 28 | logical :: active |
| 29 | integer :: total_dirs_found |
| 30 | integer :: dirs_scanned |
| 31 | real :: progress |
| 32 | type(queue_entry), dimension(10000) :: queue |
| 33 | integer :: queue_head |
| 34 | integer :: queue_tail |
| 35 | integer :: queue_size |
| 36 | type(file_node), pointer :: root => null() |
| 37 | character(len=512) :: root_path |
| 38 | end type scan_state_type |
| 39 | |
| 40 | ! Per-tab state encapsulation |
| 41 | type :: tab_state |
| 42 | ! Identity |
| 43 | integer :: tab_id |
| 44 | character(len=256) :: label ! Tab display name |
| 45 | |
| 46 | ! Path and navigation |
| 47 | character(len=512) :: scan_path |
| 48 | character(len=512), dimension(MAX_HISTORY) :: nav_history |
| 49 | integer :: nav_history_count |
| 50 | integer :: nav_history_pos |
| 51 | logical :: navigating_history |
| 52 | |
| 53 | ! Synthetic navigation (for future use) |
| 54 | logical :: pending_synthetic_nav |
| 55 | character(len=512) :: pending_synthetic_child_name |
| 56 | integer :: synthetic_nav_attempts |
| 57 | |
| 58 | ! Tree view state |
| 59 | type(file_node), pointer :: root_node => null() |
| 60 | type(file_node), pointer :: current_view_node => null() |
| 61 | logical :: has_data |
| 62 | integer :: selected_index |
| 63 | |
| 64 | ! Layout state |
| 65 | logical :: layout_calculated |
| 66 | integer :: last_width |
| 67 | integer :: last_height |
| 68 | |
| 69 | ! View settings |
| 70 | logical :: show_file_extensions |
| 71 | logical :: use_age_based_coloring |
| 72 | logical :: show_allocated_size |
| 73 | logical :: show_hidden_files |
| 74 | logical :: use_cushion_shading |
| 75 | |
| 76 | ! Path navigation breadcrumb |
| 77 | character(len=256), dimension(MAX_PATH_DEPTH) :: path_names |
| 78 | integer :: path_depth |
| 79 | |
| 80 | ! Scan state |
| 81 | type(scan_state_type) :: scan_state |
| 82 | integer(c_int) :: scan_idle_id |
| 83 | end type tab_state |
| 84 | |
| 85 | ! Global tab management |
| 86 | type(tab_state), dimension(MAX_TABS), save, target :: tabs |
| 87 | integer, save :: active_tab_index = 1 |
| 88 | integer, save :: num_tabs = 0 |
| 89 | integer, save :: next_tab_id = 1 |
| 90 | |
| 91 | contains |
| 92 | |
| 93 | ! Initialize tab manager (call once at startup) |
| 94 | subroutine init_tab_manager() |
| 95 | active_tab_index = 1 |
| 96 | num_tabs = 0 |
| 97 | next_tab_id = 1 |
| 98 | print *, "Tab manager initialized" |
| 99 | end subroutine init_tab_manager |
| 100 | |
| 101 | ! Create a new tab with given path |
| 102 | function create_tab(path) result(tab_index) |
| 103 | character(len=*), intent(in) :: path |
| 104 | integer :: tab_index |
| 105 | |
| 106 | ! Check if we've reached the limit |
| 107 | if (num_tabs >= MAX_TABS) then |
| 108 | print *, "ERROR: Maximum tabs reached (", MAX_TABS, ")" |
| 109 | tab_index = -1 |
| 110 | return |
| 111 | end if |
| 112 | |
| 113 | ! Increment tab count |
| 114 | num_tabs = num_tabs + 1 |
| 115 | tab_index = num_tabs |
| 116 | |
| 117 | ! Initialize tab state |
| 118 | tabs(tab_index)%tab_id = next_tab_id |
| 119 | next_tab_id = next_tab_id + 1 |
| 120 | |
| 121 | tabs(tab_index)%scan_path = trim(path) |
| 122 | tabs(tab_index)%label = get_path_basename(path) |
| 123 | |
| 124 | ! Initialize navigation history |
| 125 | tabs(tab_index)%nav_history_count = 0 |
| 126 | tabs(tab_index)%nav_history_pos = 0 |
| 127 | tabs(tab_index)%navigating_history = .false. |
| 128 | |
| 129 | ! Initialize synthetic nav |
| 130 | tabs(tab_index)%pending_synthetic_nav = .false. |
| 131 | tabs(tab_index)%pending_synthetic_child_name = "" |
| 132 | tabs(tab_index)%synthetic_nav_attempts = 0 |
| 133 | |
| 134 | ! Initialize tree pointers |
| 135 | tabs(tab_index)%root_node => null() |
| 136 | tabs(tab_index)%current_view_node => null() |
| 137 | tabs(tab_index)%has_data = .false. |
| 138 | tabs(tab_index)%selected_index = 0 |
| 139 | |
| 140 | ! Initialize layout |
| 141 | tabs(tab_index)%layout_calculated = .false. |
| 142 | tabs(tab_index)%last_width = 0 |
| 143 | tabs(tab_index)%last_height = 0 |
| 144 | |
| 145 | ! Initialize view settings (defaults) |
| 146 | tabs(tab_index)%show_file_extensions = .true. |
| 147 | tabs(tab_index)%use_age_based_coloring = .false. |
| 148 | tabs(tab_index)%show_allocated_size = .false. |
| 149 | tabs(tab_index)%show_hidden_files = .true. |
| 150 | tabs(tab_index)%use_cushion_shading = .true. |
| 151 | |
| 152 | ! Initialize path navigation |
| 153 | tabs(tab_index)%path_depth = 0 |
| 154 | |
| 155 | ! Initialize scan state |
| 156 | tabs(tab_index)%scan_state%active = .false. |
| 157 | tabs(tab_index)%scan_state%total_dirs_found = 0 |
| 158 | tabs(tab_index)%scan_state%dirs_scanned = 0 |
| 159 | tabs(tab_index)%scan_state%progress = 0.0 |
| 160 | tabs(tab_index)%scan_state%queue_head = 1 |
| 161 | tabs(tab_index)%scan_state%queue_tail = 1 |
| 162 | tabs(tab_index)%scan_state%queue_size = 0 |
| 163 | tabs(tab_index)%scan_state%root => null() |
| 164 | tabs(tab_index)%scan_state%root_path = "" |
| 165 | tabs(tab_index)%scan_idle_id = 0_c_int |
| 166 | |
| 167 | print *, "Created tab ", tab_index, " for path: ", trim(path) |
| 168 | end function create_tab |
| 169 | |
| 170 | ! Close a tab (with cleanup) |
| 171 | subroutine close_tab(tab_index) |
| 172 | integer, intent(in) :: tab_index |
| 173 | integer :: i |
| 174 | |
| 175 | if (tab_index < 1 .or. tab_index > num_tabs) then |
| 176 | print *, "ERROR: Invalid tab index: ", tab_index |
| 177 | return |
| 178 | end if |
| 179 | |
| 180 | ! Prevent closing last tab |
| 181 | if (num_tabs == 1) then |
| 182 | print *, "ERROR: Cannot close last tab" |
| 183 | return |
| 184 | end if |
| 185 | |
| 186 | print *, "Closing tab ", tab_index |
| 187 | |
| 188 | ! TODO: Cancel any active scan for this tab |
| 189 | ! TODO: Deallocate file tree recursively |
| 190 | |
| 191 | ! Shift tabs down to fill gap |
| 192 | do i = tab_index, num_tabs - 1 |
| 193 | tabs(i) = tabs(i + 1) |
| 194 | end do |
| 195 | |
| 196 | ! Decrement count |
| 197 | num_tabs = num_tabs - 1 |
| 198 | |
| 199 | ! Adjust active tab index if needed |
| 200 | if (active_tab_index == tab_index) then |
| 201 | ! Closed active tab - activate next tab (or previous if was last) |
| 202 | if (active_tab_index > num_tabs) then |
| 203 | active_tab_index = num_tabs |
| 204 | end if |
| 205 | print *, "Switched to tab ", active_tab_index |
| 206 | else if (active_tab_index > tab_index) then |
| 207 | ! Closed tab before active - adjust index |
| 208 | active_tab_index = active_tab_index - 1 |
| 209 | end if |
| 210 | end subroutine close_tab |
| 211 | |
| 212 | ! Switch to a different tab |
| 213 | subroutine switch_to_tab(tab_index) |
| 214 | integer, intent(in) :: tab_index |
| 215 | |
| 216 | if (tab_index < 1 .or. tab_index > num_tabs) then |
| 217 | print *, "ERROR: Invalid tab index: ", tab_index |
| 218 | return |
| 219 | end if |
| 220 | |
| 221 | if (tab_index == active_tab_index) then |
| 222 | ! Already on this tab |
| 223 | return |
| 224 | end if |
| 225 | |
| 226 | print *, "Switching from tab ", active_tab_index, " to tab ", tab_index |
| 227 | |
| 228 | ! Just update the index - actual UI update happens in caller |
| 229 | active_tab_index = tab_index |
| 230 | end subroutine switch_to_tab |
| 231 | |
| 232 | ! Get pointer to active tab |
| 233 | function get_active_tab() result(tab) |
| 234 | type(tab_state), pointer :: tab |
| 235 | if (num_tabs == 0) then |
| 236 | tab => null() |
| 237 | else |
| 238 | tab => tabs(active_tab_index) |
| 239 | end if |
| 240 | end function get_active_tab |
| 241 | |
| 242 | ! Get pointer to specific tab |
| 243 | function get_tab(tab_index) result(tab) |
| 244 | integer, intent(in) :: tab_index |
| 245 | type(tab_state), pointer :: tab |
| 246 | if (tab_index < 1 .or. tab_index > num_tabs) then |
| 247 | tab => null() |
| 248 | else |
| 249 | tab => tabs(tab_index) |
| 250 | end if |
| 251 | end function get_tab |
| 252 | |
| 253 | ! Helper: Extract basename from path |
| 254 | function get_path_basename(path) result(basename) |
| 255 | character(len=*), intent(in) :: path |
| 256 | character(len=256) :: basename |
| 257 | integer :: last_slash |
| 258 | |
| 259 | ! Special case: empty path (new empty tabs) |
| 260 | if (len_trim(path) == 0) then |
| 261 | basename = "Empty" |
| 262 | return |
| 263 | end if |
| 264 | |
| 265 | ! Find last slash |
| 266 | last_slash = index(trim(path), "/", back=.true.) |
| 267 | |
| 268 | if (last_slash == 0) then |
| 269 | ! No slash - use whole path |
| 270 | basename = trim(path) |
| 271 | else if (last_slash == len_trim(path)) then |
| 272 | ! Trailing slash - find previous slash |
| 273 | last_slash = index(trim(path(1:len_trim(path)-1)), "/", back=.true.) |
| 274 | if (last_slash == 0) then |
| 275 | basename = trim(path(1:len_trim(path)-1)) |
| 276 | else |
| 277 | basename = trim(path(last_slash+1:len_trim(path)-1)) |
| 278 | end if |
| 279 | else |
| 280 | ! Normal case |
| 281 | basename = trim(path(last_slash+1:)) |
| 282 | end if |
| 283 | |
| 284 | ! Special case: root |
| 285 | if (len_trim(basename) == 0) then |
| 286 | basename = "/" |
| 287 | end if |
| 288 | end function get_path_basename |
| 289 | |
| 290 | end module tab_manager |
| 291 |