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