| 1 | module ui_display |
| 2 | use iso_fortran_env, only: output_unit |
| 3 | use terminal_control, only: DIM, BOLD, RESET, UNDERLINE, & |
| 4 | BLUE, GREEN, RED, GREY, WHITE, YELLOW |
| 5 | use git_ops, only: write_git_indicators |
| 6 | use filesystem_ops, only: MAX_PATH, MAX_FILES |
| 7 | implicit none |
| 8 | private |
| 9 | |
| 10 | public :: draw_interface, get_file_color |
| 11 | |
| 12 | contains |
| 13 | |
| 14 | subroutine draw_interface(r, c, current_dir, current_files, current_is_dir, current_is_exec, & |
| 15 | current_is_staged, current_is_unstaged, current_is_untracked, current_has_incoming, & |
| 16 | current_count, parent_files, parent_is_dir, parent_is_exec, parent_count, & |
| 17 | selected, parent_selected, scroll_offset, parent_scroll_offset, & |
| 18 | in_git_repo, repo_name, branch_name, & |
| 19 | move_mode, move_source_name, move_dest_selected, & |
| 20 | has_clipboard, clipboard_is_cut, clipboard_source_name, clipboard_count, & |
| 21 | is_selected, selection_count, & |
| 22 | current_is_favorite, parent_is_favorite) |
| 23 | integer, intent(in) :: r, c, current_count, parent_count, selected, parent_selected |
| 24 | integer, intent(in) :: scroll_offset, parent_scroll_offset |
| 25 | character(len=*), intent(in) :: current_dir, repo_name, branch_name |
| 26 | character(len=*), dimension(*), intent(in) :: current_files, parent_files |
| 27 | logical, dimension(*), intent(in) :: current_is_dir, parent_is_dir |
| 28 | logical, dimension(*), intent(in) :: current_is_exec, parent_is_exec |
| 29 | logical, dimension(*), intent(in) :: current_is_staged, current_is_unstaged, current_is_untracked |
| 30 | logical, dimension(*), intent(in) :: current_has_incoming |
| 31 | logical, intent(in) :: in_git_repo, move_mode |
| 32 | character(len=*), intent(in) :: move_source_name |
| 33 | integer, intent(in) :: move_dest_selected |
| 34 | logical, intent(in) :: has_clipboard, clipboard_is_cut |
| 35 | character(len=*), intent(in) :: clipboard_source_name |
| 36 | integer, intent(in) :: clipboard_count |
| 37 | logical, dimension(*), intent(in) :: is_selected |
| 38 | integer, intent(in) :: selection_count |
| 39 | logical, dimension(*), intent(in) :: current_is_favorite, parent_is_favorite |
| 40 | integer :: left_w, i, parent_idx, current_idx, vis_h, display_len |
| 41 | character(len=256) :: fname |
| 42 | character(len=20) :: color_code |
| 43 | |
| 44 | left_w = c * 3 / 10 |
| 45 | vis_h = r - 3 ! Visible height |
| 46 | |
| 47 | ! Header |
| 48 | if (move_mode) then |
| 49 | write(output_unit, '(a)') BOLD // "FORTRESS" // RESET // " - " // trim(current_dir) // & |
| 50 | " | " // RED // "MOVE: " // trim(move_source_name) // RESET |
| 51 | else if (selection_count > 0) then |
| 52 | ! Show selection count |
| 53 | write(output_unit, '(a)') BOLD // "FORTRESS" // RESET // " - " // trim(current_dir) // & |
| 54 | " | " // BLUE // trim(adjustl(itoa(selection_count))) // " selected" // RESET |
| 55 | else if (has_clipboard) then |
| 56 | if (clipboard_count > 1) then |
| 57 | ! Multiple items in clipboard |
| 58 | if (clipboard_is_cut) then |
| 59 | write(output_unit, '(a)') BOLD // "FORTRESS" // RESET // " - " // trim(current_dir) // & |
| 60 | " | " // YELLOW // "CUT: " // trim(adjustl(itoa(clipboard_count))) // " items" // RESET |
| 61 | else |
| 62 | write(output_unit, '(a)') BOLD // "FORTRESS" // RESET // " - " // trim(current_dir) // & |
| 63 | " | " // GREEN // "COPY: " // trim(adjustl(itoa(clipboard_count))) // " items" // RESET |
| 64 | end if |
| 65 | else |
| 66 | ! Single item in clipboard |
| 67 | if (clipboard_is_cut) then |
| 68 | write(output_unit, '(a)') BOLD // "FORTRESS" // RESET // " - " // trim(current_dir) // & |
| 69 | " | " // YELLOW // "CUT: " // trim(clipboard_source_name) // RESET |
| 70 | else |
| 71 | write(output_unit, '(a)') BOLD // "FORTRESS" // RESET // " - " // trim(current_dir) // & |
| 72 | " | " // GREEN // "COPY: " // trim(clipboard_source_name) // RESET |
| 73 | end if |
| 74 | end if |
| 75 | else |
| 76 | write(output_unit, '(a)') BOLD // "FORTRESS" // RESET // " - " // trim(current_dir) |
| 77 | end if |
| 78 | |
| 79 | ! Files (render based on scroll offsets) |
| 80 | do i = 1, vis_h |
| 81 | parent_idx = i + parent_scroll_offset |
| 82 | current_idx = i + scroll_offset |
| 83 | |
| 84 | ! Parent pane |
| 85 | if (parent_idx >= 1 .and. parent_idx <= parent_count) then |
| 86 | fname = parent_files(parent_idx) |
| 87 | |
| 88 | ! Track if this item has a star (for visual width adjustment) |
| 89 | display_len = 0 |
| 90 | if (parent_is_favorite(parent_idx)) then |
| 91 | fname = "★ " // trim(fname) |
| 92 | display_len = -2 ! "★ " is 4 bytes but 2 visual cols, so subtract 2 |
| 93 | end if |
| 94 | |
| 95 | if (parent_is_dir(parent_idx) .and. parent_files(parent_idx) /= "." .and. parent_files(parent_idx) /= "..") then |
| 96 | fname = trim(fname) // "/" |
| 97 | end if |
| 98 | |
| 99 | ! Get color for parent file |
| 100 | color_code = get_file_color(parent_files(parent_idx), parent_is_dir(parent_idx), parent_is_exec(parent_idx)) |
| 101 | |
| 102 | ! Calculate visual width: string length + extra for wide char |
| 103 | display_len = min(len_trim(fname) + display_len, left_w) |
| 104 | |
| 105 | if (parent_idx == parent_selected) then |
| 106 | write(output_unit, '(a)', advance='no') DIM // BOLD // trim(color_code) // & |
| 107 | fname(1:min(len_trim(fname), left_w)) // RESET |
| 108 | else |
| 109 | write(output_unit, '(a)', advance='no') DIM // trim(color_code) // & |
| 110 | fname(1:min(len_trim(fname), left_w)) // RESET |
| 111 | end if |
| 112 | write(output_unit, '(a)', advance='no') repeat(" ", max(0, left_w - display_len)) |
| 113 | else |
| 114 | write(output_unit, '(a)', advance='no') repeat(" ", left_w) |
| 115 | end if |
| 116 | |
| 117 | ! RESET before separator to clear any state from parent pane |
| 118 | write(output_unit, '(a)', advance='no') RESET |
| 119 | |
| 120 | ! Separator |
| 121 | write(output_unit, '(a)', advance='no') " │ " |
| 122 | |
| 123 | ! Current pane |
| 124 | if (current_idx >= 1 .and. current_idx <= current_count) then |
| 125 | |
| 126 | fname = current_files(current_idx) |
| 127 | |
| 128 | ! Track if this item has a star (for visual width - star takes 2 columns) |
| 129 | display_len = 0 |
| 130 | if (current_is_favorite(current_idx)) then |
| 131 | fname = "★ " // trim(fname) |
| 132 | display_len = -2 ! "★ " is 4 bytes but 2 visual cols, so subtract 2 |
| 133 | end if |
| 134 | |
| 135 | if (current_is_dir(current_idx) .and. current_files(current_idx) /= "." .and. current_files(current_idx) /= "..") then |
| 136 | fname = trim(fname) // "/" |
| 137 | end if |
| 138 | |
| 139 | ! Store the visual display length for this line (used by git indicators) |
| 140 | display_len = len_trim(fname) + display_len |
| 141 | |
| 142 | ! Get color for current file |
| 143 | color_code = get_file_color(current_files(current_idx), current_is_dir(current_idx), current_is_exec(current_idx)) |
| 144 | |
| 145 | ! Check if this file is cut to clipboard (show in dark red) |
| 146 | ! Only highlight for single-item cuts (multi-cuts shown in header) |
| 147 | if (has_clipboard .and. clipboard_is_cut .and. clipboard_count == 1 .and. & |
| 148 | trim(current_files(current_idx)) == trim(clipboard_source_name)) then |
| 149 | ! File is cut - show in dark red (dimmed red) |
| 150 | write(output_unit, '(a)', advance='no') DIM // RED // trim(fname) |
| 151 | if (in_git_repo) then |
| 152 | call write_git_indicators(current_is_staged(current_idx), & |
| 153 | current_is_unstaged(current_idx), & |
| 154 | current_is_untracked(current_idx), & |
| 155 | current_has_incoming(current_idx), .false.) |
| 156 | end if |
| 157 | write(output_unit, '(a)') RESET |
| 158 | ! Move mode: show source in red, destination in white |
| 159 | else if (move_mode .and. trim(current_files(current_idx)) == trim(move_source_name)) then |
| 160 | ! Source file - show in RED |
| 161 | write(output_unit, '(a)', advance='no') RED // BOLD // trim(fname) // RESET |
| 162 | write(output_unit, '(a)') "" |
| 163 | else if (move_mode .and. current_idx == move_dest_selected) then |
| 164 | ! Destination cursor - show with bold+underline |
| 165 | write(output_unit, '(a)', advance='no') BOLD // UNDERLINE // WHITE // trim(fname) |
| 166 | if (in_git_repo) then |
| 167 | call write_git_indicators(current_is_staged(current_idx), & |
| 168 | current_is_unstaged(current_idx), & |
| 169 | current_is_untracked(current_idx), & |
| 170 | current_has_incoming(current_idx), .true.) |
| 171 | end if |
| 172 | write(output_unit, '(a)') RESET |
| 173 | else if (current_idx == selected .and. .not. move_mode) then |
| 174 | ! Normal selection cursor (not in move mode) - use bold+underline with original color |
| 175 | write(output_unit, '(a)', advance='no') BOLD // UNDERLINE // trim(color_code) // trim(fname) |
| 176 | ! Add git indicators if in repo |
| 177 | if (in_git_repo) then |
| 178 | call write_git_indicators(current_is_staged(current_idx), & |
| 179 | current_is_unstaged(current_idx), & |
| 180 | current_is_untracked(current_idx), & |
| 181 | current_has_incoming(current_idx), .true.) |
| 182 | end if |
| 183 | write(output_unit, '(a)') RESET |
| 184 | else if (is_selected(current_idx)) then |
| 185 | ! Multi-selected item (not the cursor) - show with underline |
| 186 | write(output_unit, '(a)', advance='no') UNDERLINE // trim(color_code) // trim(fname) |
| 187 | ! Add git indicators if in repo |
| 188 | if (in_git_repo) then |
| 189 | call write_git_indicators(current_is_staged(current_idx), & |
| 190 | current_is_unstaged(current_idx), & |
| 191 | current_is_untracked(current_idx), & |
| 192 | current_has_incoming(current_idx), .true.) |
| 193 | end if |
| 194 | write(output_unit, '(a)') RESET |
| 195 | else |
| 196 | ! Normal rendering |
| 197 | write(output_unit, '(a)', advance='no') trim(color_code) // trim(fname) |
| 198 | ! Add git indicators if in repo |
| 199 | if (in_git_repo) then |
| 200 | call write_git_indicators(current_is_staged(current_idx), & |
| 201 | current_is_unstaged(current_idx), & |
| 202 | current_is_untracked(current_idx), & |
| 203 | current_has_incoming(current_idx), .false.) |
| 204 | end if |
| 205 | write(output_unit, '(a)') RESET |
| 206 | end if |
| 207 | else |
| 208 | write(output_unit, *) |
| 209 | end if |
| 210 | end do |
| 211 | |
| 212 | ! Footer |
| 213 | if (move_mode) then |
| 214 | write(output_unit, '(a)') RED // "MOVE MODE: " // RESET // & |
| 215 | DIM // "↑↓:next/prev dir →:enter dir ←:parent ~:home /:root v:move here q:cancel" // RESET |
| 216 | else if (selection_count > 0) then |
| 217 | ! Selection mode footer - show multi-select help |
| 218 | write(output_unit, '(a)') BLUE // "MULTI-SELECT: " // RESET // & |
| 219 | DIM // "Space:toggle Shift+↑↓:block select y:copy x:cut p:paste r:delete | " // RESET // & |
| 220 | DIM // "→:enter ←:back ~:home /:root c:cd q:quit" // RESET |
| 221 | else if (in_git_repo) then |
| 222 | write(output_unit, '(a)') DIM // trim(repo_name) // ":" // trim(branch_name) // " | " // RESET // & |
| 223 | DIM // "Space:select Shift+↑↓:block | ↑↓:nav →:enter ←:back ~:home /:root s:search 8:favorites *:star o:open n:rename r:remove v:move y:copy x:cut p:paste .:hidden a:add u:unstage m:commit d:diff f:fetch l:pull h:push c:cd q:quit" // RESET |
| 224 | else |
| 225 | write(output_unit, '(a)') DIM // "Space:select Shift+↑↓:block | ↑↓:nav →:enter ←:back ~:home /:root s:search 8:favorites *:star o:open n:rename r:remove v:move y:copy x:cut p:paste .:hidden c:cd q:quit" // RESET |
| 226 | end if |
| 227 | |
| 228 | contains |
| 229 | function itoa(n) result(str) |
| 230 | integer, intent(in) :: n |
| 231 | character(len=10) :: str |
| 232 | write(str, '(i0)') n |
| 233 | end function itoa |
| 234 | end subroutine draw_interface |
| 235 | |
| 236 | function get_file_color(filename, is_dir, is_exec) result(color) |
| 237 | character(len=*), intent(in) :: filename |
| 238 | logical, intent(in) :: is_dir, is_exec |
| 239 | character(len=20) :: color |
| 240 | |
| 241 | ! Directories: Blue and bold |
| 242 | if (is_dir) then |
| 243 | color = BOLD // BLUE |
| 244 | ! Dotfiles: Grey |
| 245 | else if (filename(1:1) == '.') then |
| 246 | color = GREY |
| 247 | ! Executable files: Green |
| 248 | else if (is_exec) then |
| 249 | color = GREEN |
| 250 | ! All other files: White |
| 251 | else |
| 252 | color = WHITE |
| 253 | end if |
| 254 | end function get_file_color |
| 255 | |
| 256 | end module ui_display |
| 257 |