| 1 | ! Welcome menu module |
| 2 | ! Displays favorites and recents lists for quick workspace access |
| 3 | |
| 4 | module welcome_menu_module |
| 5 | use iso_fortran_env, only: int32 |
| 6 | use terminal_io_module, only: terminal_write, terminal_move_cursor, terminal_clear_screen |
| 7 | use input_handler_module, only: get_key_input |
| 8 | use favorites_module, only: favorite_t, favorites_load, favorites_remove |
| 9 | use recents_module, only: recent_t, recents_load, recents_remove |
| 10 | implicit none |
| 11 | private |
| 12 | |
| 13 | public :: show_welcome_menu |
| 14 | |
| 15 | integer, parameter :: MAX_DISPLAY = 20 |
| 16 | |
| 17 | contains |
| 18 | |
| 19 | !> Show welcome menu and return selected path |
| 20 | subroutine show_welcome_menu(selected_path, cancelled) |
| 21 | character(len=:), allocatable, intent(out) :: selected_path |
| 22 | logical, intent(out) :: cancelled |
| 23 | type(favorite_t), allocatable :: favorites(:) |
| 24 | type(recent_t), allocatable :: recents(:) |
| 25 | integer :: fav_count, rec_count, max_recents |
| 26 | logical :: success, showing_favorites |
| 27 | integer :: selected_index, scroll_offset |
| 28 | character(len=32) :: key_input |
| 29 | integer :: status, rows, cols |
| 30 | character(len=512) :: cwd |
| 31 | |
| 32 | cancelled = .false. |
| 33 | showing_favorites = .true. ! Start with favorites view |
| 34 | selected_index = 0 ! Start at CURRENT DIRECTORY option |
| 35 | scroll_offset = 0 |
| 36 | |
| 37 | ! Get current working directory for display |
| 38 | call get_cwd(cwd) |
| 39 | |
| 40 | ! Load favorites and recents |
| 41 | call favorites_load(favorites, fav_count, success) |
| 42 | if (.not. success) fav_count = 0 |
| 43 | |
| 44 | call recents_load(recents, rec_count, max_recents, success) |
| 45 | if (.not. success) rec_count = 0 |
| 46 | |
| 47 | ! Get terminal size |
| 48 | call terminal_get_size(rows, cols) |
| 49 | |
| 50 | ! Main loop |
| 51 | do |
| 52 | ! Render menu |
| 53 | call render_welcome_menu(favorites, fav_count, recents, rec_count, & |
| 54 | showing_favorites, selected_index, scroll_offset, rows, cwd) |
| 55 | |
| 56 | ! Get input |
| 57 | call get_key_input(key_input, status) |
| 58 | if (status /= 0) cycle |
| 59 | |
| 60 | ! Handle input |
| 61 | select case (key_input) |
| 62 | case ('up', 'k') |
| 63 | if (selected_index > 0) then |
| 64 | selected_index = selected_index - 1 |
| 65 | call adjust_scroll(scroll_offset) |
| 66 | end if |
| 67 | |
| 68 | case ('down', 'j') |
| 69 | if (showing_favorites) then |
| 70 | ! Allow selecting up to fav_count (0 = CWD, 1..fav_count = favorites) |
| 71 | if (selected_index < fav_count) then |
| 72 | selected_index = selected_index + 1 |
| 73 | call adjust_scroll(scroll_offset) |
| 74 | end if |
| 75 | else |
| 76 | ! Allow selecting up to rec_count (0 = CWD, 1..rec_count = recents) |
| 77 | if (selected_index < rec_count) then |
| 78 | selected_index = selected_index + 1 |
| 79 | call adjust_scroll(scroll_offset) |
| 80 | end if |
| 81 | end if |
| 82 | |
| 83 | case ('enter', 'right') ! Enter or right arrow |
| 84 | ! Handle selection |
| 85 | if (selected_index == 0) then |
| 86 | ! CURRENT DIRECTORY selected |
| 87 | selected_path = "CWD" |
| 88 | cancelled = .false. |
| 89 | exit |
| 90 | else if (showing_favorites .and. fav_count > 0) then |
| 91 | ! Favorites list - adjust index to account for CWD at index 0 |
| 92 | if (selected_index >= 1 .and. selected_index <= fav_count) then |
| 93 | ! Check if directory exists |
| 94 | if (directory_exists(trim(favorites(selected_index)%path))) then |
| 95 | selected_path = trim(favorites(selected_index)%path) |
| 96 | cancelled = .false. |
| 97 | exit |
| 98 | else |
| 99 | ! Directory doesn't exist - show warning and remove |
| 100 | call handle_deleted_workspace(trim(favorites(selected_index)%path), & |
| 101 | showing_favorites, selected_index) |
| 102 | ! Reload favorites and recents |
| 103 | call favorites_load(favorites, fav_count, success) |
| 104 | if (.not. success) fav_count = 0 |
| 105 | ! Adjust selection if needed |
| 106 | if (selected_index > fav_count) selected_index = fav_count |
| 107 | if (selected_index < 0) selected_index = 0 |
| 108 | end if |
| 109 | end if |
| 110 | else if (.not. showing_favorites .and. rec_count > 0) then |
| 111 | ! Recents list - adjust index to account for CWD at index 0 |
| 112 | if (selected_index >= 1 .and. selected_index <= rec_count) then |
| 113 | ! Check if directory exists |
| 114 | if (directory_exists(trim(recents(selected_index)%path))) then |
| 115 | selected_path = trim(recents(selected_index)%path) |
| 116 | cancelled = .false. |
| 117 | exit |
| 118 | else |
| 119 | ! Directory doesn't exist - show warning and remove |
| 120 | call handle_deleted_workspace(trim(recents(selected_index)%path), & |
| 121 | showing_favorites, selected_index) |
| 122 | ! Reload recents |
| 123 | call recents_load(recents, rec_count, max_recents, success) |
| 124 | if (.not. success) rec_count = 0 |
| 125 | ! Adjust selection if needed |
| 126 | if (selected_index > rec_count) selected_index = rec_count |
| 127 | if (selected_index < 0) selected_index = 0 |
| 128 | end if |
| 129 | end if |
| 130 | end if |
| 131 | |
| 132 | case ('8') |
| 133 | ! Toggle between favorites and recents |
| 134 | showing_favorites = .not. showing_favorites |
| 135 | selected_index = 0 ! Reset to CURRENT DIRECTORY |
| 136 | scroll_offset = 0 |
| 137 | |
| 138 | case ('esc', 'q') |
| 139 | cancelled = .true. |
| 140 | exit |
| 141 | |
| 142 | case ('b') |
| 143 | ! Browse filesystem - launch fortress navigator |
| 144 | cancelled = .true. ! Signal to launch navigator instead |
| 145 | selected_path = "BROWSE" |
| 146 | exit |
| 147 | end select |
| 148 | end do |
| 149 | |
| 150 | call terminal_clear_screen() |
| 151 | end subroutine show_welcome_menu |
| 152 | |
| 153 | !> Render the welcome menu |
| 154 | subroutine render_welcome_menu(favorites, fav_count, recents, rec_count, & |
| 155 | showing_favorites, selected_index, scroll_offset, rows, cwd) |
| 156 | type(favorite_t), intent(in) :: favorites(:) |
| 157 | type(recent_t), intent(in) :: recents(:) |
| 158 | integer, intent(in) :: fav_count, rec_count |
| 159 | logical, intent(in) :: showing_favorites |
| 160 | integer, intent(in) :: selected_index, scroll_offset, rows |
| 161 | character(len=*), intent(in) :: cwd |
| 162 | character(len=512) :: line |
| 163 | integer :: i, display_row, visible_height, item_count, actual_index |
| 164 | character(len=64) :: title |
| 165 | |
| 166 | call terminal_clear_screen() |
| 167 | call terminal_move_cursor(1, 1) |
| 168 | |
| 169 | ! Header |
| 170 | line = '╔═══════════════════════' // & |
| 171 | '═══════════════════════' // & |
| 172 | '══════════════════════╗' |
| 173 | call terminal_write(trim(line)) |
| 174 | call terminal_move_cursor(2, 1) |
| 175 | write(line, '(A)') '║ FAC - Welcome Menu ║' |
| 176 | call terminal_write(trim(line)) |
| 177 | call terminal_move_cursor(3, 1) |
| 178 | line = '╚═══════════════════════' // & |
| 179 | '═══════════════════════' // & |
| 180 | '══════════════════════╝' |
| 181 | call terminal_write(trim(line)) |
| 182 | |
| 183 | ! View title |
| 184 | call terminal_move_cursor(5, 1) |
| 185 | if (showing_favorites) then |
| 186 | write(title, '(A,I0,A)') 'FAVORITES (', fav_count, ' total)' |
| 187 | item_count = fav_count |
| 188 | else |
| 189 | write(title, '(A,I0,A)') 'RECENT WORKSPACES (', rec_count, ' total)' |
| 190 | item_count = rec_count |
| 191 | end if |
| 192 | call terminal_write(trim(title)) |
| 193 | |
| 194 | ! Separator |
| 195 | call terminal_move_cursor(6, 1) |
| 196 | line = '────────────────────────' // & |
| 197 | '────────────────────────' // & |
| 198 | '──────────────────────' |
| 199 | call terminal_write(trim(line)) |
| 200 | |
| 201 | ! List items |
| 202 | visible_height = rows - 8 ! Reserve space for header and footer |
| 203 | display_row = 7 |
| 204 | |
| 205 | ! Always show at least the CURRENT DIRECTORY option |
| 206 | ! Render items from scroll_offset to scroll_offset + visible_height |
| 207 | ! Index 0 = CURRENT DIRECTORY, indices 1..item_count = actual items |
| 208 | do i = 0, min(visible_height - 1, item_count) |
| 209 | actual_index = i + scroll_offset |
| 210 | if (actual_index > item_count) exit |
| 211 | |
| 212 | call terminal_move_cursor(display_row, 1) |
| 213 | |
| 214 | ! Check if this item is selected |
| 215 | if (actual_index == selected_index) then |
| 216 | ! Highlight selected |
| 217 | write(line, '(A)') char(27) // '[7m' ! Reverse video |
| 218 | else |
| 219 | write(line, '(A)') '' |
| 220 | end if |
| 221 | |
| 222 | if (actual_index == 0) then |
| 223 | ! CURRENT DIRECTORY option |
| 224 | write(line, '(A,A,I0,A,A)') trim(line), ' [', actual_index, '] ', & |
| 225 | 'CURRENT DIRECTORY → ' // trim(cwd) |
| 226 | else |
| 227 | ! Regular items (favorites or recents) |
| 228 | if (showing_favorites) then |
| 229 | write(line, '(A,A,I0,A,A,A,A)') trim(line), ' [', actual_index, '] ', & |
| 230 | trim(favorites(actual_index)%label), ' → ', & |
| 231 | trim(favorites(actual_index)%path) |
| 232 | else |
| 233 | write(line, '(A,A,I0,A,A,A,A)') trim(line), ' [', actual_index, '] ', & |
| 234 | trim(recents(actual_index)%label), ' → ', & |
| 235 | trim(recents(actual_index)%path) |
| 236 | end if |
| 237 | end if |
| 238 | |
| 239 | if (actual_index == selected_index) then |
| 240 | write(line, '(A,A)') trim(line), char(27) // '[0m' ! Reset |
| 241 | end if |
| 242 | |
| 243 | call terminal_write(trim(line)) |
| 244 | display_row = display_row + 1 |
| 245 | end do |
| 246 | |
| 247 | ! Show message if no items (but CWD is always there) |
| 248 | if (item_count == 0 .and. selected_index /= 0) then |
| 249 | call terminal_move_cursor(display_row, 1) |
| 250 | if (showing_favorites) then |
| 251 | call terminal_write(' (No favorites yet - press ''f'' in Fortress to add)') |
| 252 | else |
| 253 | call terminal_write(' (No recent workspaces)') |
| 254 | end if |
| 255 | end if |
| 256 | |
| 257 | ! Footer with keybindings |
| 258 | call terminal_move_cursor(rows - 2, 1) |
| 259 | line = '────────────────────────' // & |
| 260 | '────────────────────────' // & |
| 261 | '──────────────────────' |
| 262 | call terminal_write(trim(line)) |
| 263 | |
| 264 | call terminal_move_cursor(rows - 1, 1) |
| 265 | write(line, '(A)') '↑/↓:navigate Enter:select 8:toggle fav/recent b:browse ESC/q:quit' |
| 266 | call terminal_write(trim(line)) |
| 267 | end subroutine render_welcome_menu |
| 268 | |
| 269 | !> Adjust scroll offset to keep CURRENT DIRECTORY visible |
| 270 | subroutine adjust_scroll(offset) |
| 271 | integer, intent(inout) :: offset |
| 272 | |
| 273 | ! IMPORTANT: offset must always be 0 to keep CWD (index 0) visible |
| 274 | ! Since CWD is always at the top, we never scroll past it |
| 275 | offset = 0 |
| 276 | |
| 277 | ! Note: This means with many items, CWD is always visible but |
| 278 | ! you scroll through the rest of the list below it |
| 279 | end subroutine adjust_scroll |
| 280 | |
| 281 | !> Get terminal size (wrapper) |
| 282 | subroutine terminal_get_size(rows, cols) |
| 283 | integer, intent(out) :: rows, cols |
| 284 | ! Default size |
| 285 | rows = 24 |
| 286 | cols = 80 |
| 287 | ! TODO: Query actual terminal size if available |
| 288 | end subroutine terminal_get_size |
| 289 | |
| 290 | !> Check if a directory exists (Phase 7: deleted workspace detection) |
| 291 | function directory_exists(path) result(exists) |
| 292 | character(len=*), intent(in) :: path |
| 293 | logical :: exists |
| 294 | integer :: ios |
| 295 | |
| 296 | ! Try to open directory (will fail if doesn't exist) |
| 297 | call execute_command_line('test -d "' // trim(path) // '"', wait=.true., exitstat=ios) |
| 298 | exists = (ios == 0) |
| 299 | end function directory_exists |
| 300 | |
| 301 | !> Handle deleted workspace (Phase 7: show warning and remove from list) |
| 302 | subroutine handle_deleted_workspace(path, is_favorite, index) |
| 303 | character(len=*), intent(in) :: path |
| 304 | logical, intent(in) :: is_favorite |
| 305 | integer, intent(in) :: index |
| 306 | character(len=512) :: warning_msg |
| 307 | logical :: remove_success |
| 308 | |
| 309 | ! Show warning message |
| 310 | call terminal_move_cursor(1, 1) |
| 311 | warning_msg = "Warning: Workspace no longer exists: " // trim(path) |
| 312 | call terminal_write(trim(warning_msg)) |
| 313 | call terminal_move_cursor(2, 1) |
| 314 | call terminal_write("Removing from list...") |
| 315 | |
| 316 | ! Remove from appropriate list |
| 317 | if (is_favorite) then |
| 318 | call favorites_remove(index, remove_success) |
| 319 | else |
| 320 | call recents_remove(index, remove_success) |
| 321 | end if |
| 322 | |
| 323 | ! Brief pause so user can see the message |
| 324 | call execute_command_line("sleep 1.0", wait=.true.) |
| 325 | end subroutine handle_deleted_workspace |
| 326 | |
| 327 | !> Get current working directory |
| 328 | subroutine get_cwd(cwd) |
| 329 | character(len=*), intent(out) :: cwd |
| 330 | integer :: unit, ios |
| 331 | |
| 332 | ! Use pwd command to get current directory |
| 333 | call execute_command_line('pwd > /tmp/.fac_cwd 2>/dev/null', wait=.true.) |
| 334 | |
| 335 | open(newunit=unit, file='/tmp/.fac_cwd', status='old', iostat=ios) |
| 336 | if (ios == 0) then |
| 337 | read(unit, '(A)', iostat=ios) cwd |
| 338 | close(unit) |
| 339 | call execute_command_line('rm -f /tmp/.fac_cwd', wait=.true.) |
| 340 | else |
| 341 | cwd = '.' |
| 342 | end if |
| 343 | end subroutine get_cwd |
| 344 | |
| 345 | end module welcome_menu_module |
| 346 |