Fortran · 20366 bytes Raw Blame History
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, CYAN, REVERSE
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, top_padding, 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, mode, &
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 search_buffer, search_length, &
24 in_rename_mode, rename_buffer, rename_cursor_pos)
25 integer, intent(in) :: r, c, top_padding, current_count, parent_count, selected, parent_selected
26 integer, intent(in) :: scroll_offset, parent_scroll_offset
27 character(len=*), intent(in) :: current_dir, repo_name, branch_name, mode
28 character(len=*), dimension(*), intent(in) :: current_files, parent_files
29 logical, dimension(*), intent(in) :: current_is_dir, parent_is_dir
30 logical, dimension(*), intent(in) :: current_is_exec, parent_is_exec
31 logical, dimension(*), intent(in) :: current_is_staged, current_is_unstaged, current_is_untracked
32 logical, dimension(*), intent(in) :: current_has_incoming
33 logical, intent(in) :: in_git_repo, move_mode
34 character(len=*), intent(in) :: move_source_name
35 integer, intent(in) :: move_dest_selected
36 logical, intent(in) :: has_clipboard, clipboard_is_cut
37 character(len=*), intent(in) :: clipboard_source_name
38 integer, intent(in) :: clipboard_count
39 logical, dimension(*), intent(in) :: is_selected
40 integer, intent(in) :: selection_count
41 logical, dimension(*), intent(in) :: current_is_favorite, parent_is_favorite
42 character(len=*), intent(in) :: search_buffer
43 integer, intent(in) :: search_length
44 logical, intent(in) :: in_rename_mode
45 character(len=*), intent(in) :: rename_buffer
46 integer, intent(in) :: rename_cursor_pos
47 integer :: left_w, i, parent_idx, current_idx, vis_h, display_len
48 character(len=256) :: fname
49 character(len=20) :: color_code
50 character(len=600) :: footer_text
51 integer :: footer_len
52
53 left_w = c * 3 / 10
54 vis_h = r - top_padding - 4 ! Visible height with buffer to prevent scrolling
55
56 ! Add blank lines at top as padding for terminals that need it
57 do i = 1, top_padding
58 write(output_unit, '()') ! Write blank line with explicit format
59 end do
60
61 ! Header - Line 1: Always show path
62 write(output_unit, '(a)') BOLD // "FORTRESS" // RESET // " - " // trim(current_dir)
63
64 ! Header - Line 2: Status info (always present to prevent shifting)
65 if (in_rename_mode) then
66 write(output_unit, '(a)') YELLOW // "RENAME MODE" // RESET
67 else if (move_mode) then
68 write(output_unit, '(a)') RED // "MOVE: " // trim(move_source_name) // RESET
69 else if (selection_count > 0) then
70 ! Show selection count
71 write(output_unit, '(a)') BLUE // trim(adjustl(itoa(selection_count))) // " selected" // RESET
72 else if (has_clipboard) then
73 if (clipboard_count > 1) then
74 ! Multiple items in clipboard
75 if (clipboard_is_cut) then
76 write(output_unit, '(a)') YELLOW // "CUT: " // trim(adjustl(itoa(clipboard_count))) // " items" // RESET
77 else
78 write(output_unit, '(a)') GREEN // "COPY: " // trim(adjustl(itoa(clipboard_count))) // " items" // RESET
79 end if
80 else
81 ! Single item in clipboard
82 if (clipboard_is_cut) then
83 write(output_unit, '(a)') YELLOW // "CUT: " // trim(clipboard_source_name) // RESET
84 else
85 write(output_unit, '(a)') GREEN // "COPY: " // trim(clipboard_source_name) // RESET
86 end if
87 end if
88 else if (in_git_repo .and. trim(mode) == 'git') then
89 ! Show git mode indicator
90 write(output_unit, '(a)') CYAN // trim(repo_name) // ":" // YELLOW // trim(branch_name) // " " // &
91 YELLOW // BOLD // "[ GIT MODE ]" // RESET
92 else if (in_git_repo) then
93 ! Show git repo info when no other status
94 write(output_unit, '(a)') DIM // trim(repo_name) // ":" // trim(branch_name) // RESET
95 else
96 ! Empty status line to maintain consistent 2-line header
97 write(output_unit, '()')
98 end if
99
100 ! Files (render based on scroll offsets)
101 do i = 1, vis_h
102 parent_idx = i + parent_scroll_offset
103 current_idx = i + scroll_offset
104
105 ! Parent pane
106 if (parent_idx >= 1 .and. parent_idx <= parent_count) then
107 fname = parent_files(parent_idx)
108
109 ! Track if this item has a star (for visual width adjustment)
110 display_len = 0
111 if (parent_is_favorite(parent_idx)) then
112 fname = "★ " // trim(fname)
113 display_len = -2 ! "★ " is 4 bytes but 2 visual cols, so subtract 2
114 end if
115
116 if (parent_is_dir(parent_idx) .and. parent_files(parent_idx) /= "." .and. parent_files(parent_idx) /= "..") then
117 fname = trim(fname) // "/"
118 end if
119
120 ! Get color for parent file
121 color_code = get_file_color(parent_files(parent_idx), parent_is_dir(parent_idx), parent_is_exec(parent_idx))
122
123 ! Calculate visual width: string length + extra for wide char
124 display_len = min(len_trim(fname) + display_len, left_w)
125
126 if (parent_idx == parent_selected) then
127 write(output_unit, '(a)', advance='no') DIM // BOLD // trim(color_code) // &
128 fname(1:min(len_trim(fname), left_w)) // RESET
129 else
130 write(output_unit, '(a)', advance='no') DIM // trim(color_code) // &
131 fname(1:min(len_trim(fname), left_w)) // RESET
132 end if
133 write(output_unit, '(a)', advance='no') repeat(" ", max(0, left_w - display_len))
134 else
135 write(output_unit, '(a)', advance='no') repeat(" ", left_w)
136 end if
137
138 ! RESET before separator to clear any state from parent pane
139 write(output_unit, '(a)', advance='no') RESET
140
141 ! Separator
142 write(output_unit, '(a)', advance='no') " │ "
143
144 ! Current pane
145 if (current_idx >= 1 .and. current_idx <= current_count) then
146
147 fname = current_files(current_idx)
148
149 ! Track if this item has a star (for visual width - star takes 2 columns)
150 display_len = 0
151 if (current_is_favorite(current_idx)) then
152 fname = "★ " // trim(fname)
153 display_len = -2 ! "★ " is 4 bytes but 2 visual cols, so subtract 2
154 end if
155
156 if (current_is_dir(current_idx) .and. current_files(current_idx) /= "." .and. current_files(current_idx) /= "..") then
157 fname = trim(fname) // "/"
158 end if
159
160 ! Store the visual display length for this line (used by git indicators)
161 display_len = len_trim(fname) + display_len
162
163 ! Get color for current file
164 color_code = get_file_color(current_files(current_idx), current_is_dir(current_idx), current_is_exec(current_idx))
165
166 ! Check if this file is cut to clipboard (show in dark red)
167 ! Only highlight for single-item cuts (multi-cuts shown in header)
168 if (has_clipboard .and. clipboard_is_cut .and. clipboard_count == 1 .and. &
169 trim(current_files(current_idx)) == trim(clipboard_source_name)) then
170 ! File is cut - show in dark red (dimmed red)
171 write(output_unit, '(a)', advance='no') DIM // RED // trim(fname)
172 if (in_git_repo) then
173 call write_git_indicators(current_is_staged(current_idx), &
174 current_is_unstaged(current_idx), &
175 current_is_untracked(current_idx), &
176 current_has_incoming(current_idx), .false.)
177 end if
178 write(output_unit, '(a)') RESET
179 ! Move mode: show source in red, destination in white
180 else if (move_mode .and. trim(current_files(current_idx)) == trim(move_source_name)) then
181 ! Source file - show in RED
182 write(output_unit, '(a)', advance='no') RED // BOLD // trim(fname) // RESET
183 write(output_unit, '(a)') ""
184 else if (move_mode .and. current_idx == move_dest_selected) then
185 ! Destination cursor - show with bold+underline
186 write(output_unit, '(a)', advance='no') BOLD // UNDERLINE // WHITE // trim(fname)
187 if (in_git_repo) then
188 call write_git_indicators(current_is_staged(current_idx), &
189 current_is_unstaged(current_idx), &
190 current_is_untracked(current_idx), &
191 current_has_incoming(current_idx), .true.)
192 end if
193 write(output_unit, '(a)') RESET
194 else if (current_idx == selected .and. .not. move_mode .and. is_selected(current_idx)) then
195 ! Cursor on a selected item - use bold+underline with original color
196 write(output_unit, '(a)', advance='no') BOLD // UNDERLINE // trim(color_code) // trim(fname)
197 ! Add git indicators if in repo
198 if (in_git_repo) then
199 call write_git_indicators(current_is_staged(current_idx), &
200 current_is_unstaged(current_idx), &
201 current_is_untracked(current_idx), &
202 current_has_incoming(current_idx), .true.)
203 end if
204 write(output_unit, '(a)') RESET
205 else if (current_idx == selected .and. .not. move_mode) then
206 ! Normal selection cursor (not in move mode)
207 ! Check if in rename mode - show editable buffer with cursor
208 if (in_rename_mode) then
209 ! Show rename buffer with block cursor (█) at cursor position
210 if (current_is_favorite(current_idx)) then
211 write(output_unit, '(a)', advance='no') REVERSE // trim(color_code) // "★ "
212 else
213 write(output_unit, '(a)', advance='no') REVERSE // trim(color_code)
214 end if
215
216 if (rename_cursor_pos == len_trim(rename_buffer)) then
217 ! Cursor at end
218 write(output_unit, '(a)', advance='no') trim(rename_buffer) // '█'
219 else if (rename_cursor_pos == 0) then
220 ! Cursor at beginning
221 write(output_unit, '(a)', advance='no') '█' // trim(rename_buffer)
222 else
223 ! Cursor in middle
224 write(output_unit, '(a)', advance='no') rename_buffer(1:rename_cursor_pos) // '█' // &
225 rename_buffer(rename_cursor_pos+1:len_trim(rename_buffer))
226 end if
227 if (current_is_dir(current_idx) .and. current_files(current_idx) /= "." .and. &
228 current_files(current_idx) /= "..") then
229 write(output_unit, '(a)', advance='no') "/"
230 end if
231 write(output_unit, '(a)') RESET
232 ! Not in rename mode - check if cut file selected
233 else if (has_clipboard .and. clipboard_is_cut .and. clipboard_count == 1 .and. &
234 trim(current_files(current_idx)) == trim(clipboard_source_name)) then
235 ! If file is cut, show selected with red reverse
236 write(output_unit, '(a)', advance='no') REVERSE // RED // trim(fname)
237 ! Add git indicators if in repo
238 if (in_git_repo) then
239 call write_git_indicators(current_is_staged(current_idx), &
240 current_is_unstaged(current_idx), &
241 current_is_untracked(current_idx), &
242 current_has_incoming(current_idx), .true.)
243 end if
244 write(output_unit, '(a)') RESET
245 else
246 ! Normal selection cursor - use bold+underline with original color
247 write(output_unit, '(a)', advance='no') BOLD // UNDERLINE // trim(color_code) // trim(fname)
248 ! Add git indicators if in repo
249 if (in_git_repo) then
250 call write_git_indicators(current_is_staged(current_idx), &
251 current_is_unstaged(current_idx), &
252 current_is_untracked(current_idx), &
253 current_has_incoming(current_idx), .true.)
254 end if
255 write(output_unit, '(a)') RESET
256 end if
257 else if (is_selected(current_idx)) then
258 ! Multi-selected item (not the cursor) - show with underline
259 write(output_unit, '(a)', advance='no') UNDERLINE // trim(color_code) // trim(fname)
260 ! Add git indicators if in repo
261 if (in_git_repo) then
262 call write_git_indicators(current_is_staged(current_idx), &
263 current_is_unstaged(current_idx), &
264 current_is_untracked(current_idx), &
265 current_has_incoming(current_idx), .true.)
266 end if
267 write(output_unit, '(a)') RESET
268 else
269 ! Normal rendering
270 write(output_unit, '(a)', advance='no') trim(color_code) // trim(fname)
271 ! Add git indicators if in repo
272 if (in_git_repo) then
273 call write_git_indicators(current_is_staged(current_idx), &
274 current_is_unstaged(current_idx), &
275 current_is_untracked(current_idx), &
276 current_has_incoming(current_idx), .false.)
277 end if
278 write(output_unit, '(a)') RESET
279 end if
280 else
281 write(output_unit, *)
282 end if
283 end do
284
285 ! Footer - help text only (status moved to header)
286 ! Build footer text and truncate to prevent wrapping which causes screen scroll
287 if (in_rename_mode) then
288 footer_text = YELLOW // "RENAME MODE: " // RESET // &
289 DIM // "Type to edit | Enter:confirm ESC:cancel" // RESET
290 else if (move_mode) then
291 footer_text = RED // "MOVE MODE: " // RESET // &
292 DIM // "↑↓:next/prev dir →:enter dir ←:parent ~:home /:root alt-m:move here q:cancel" // RESET
293 else if (selection_count > 0) then
294 ! Selection mode footer - show multi-select help
295 footer_text = BLUE // "MULTI-SELECT: " // RESET // &
296 DIM // "Space:toggle Shift+↑↓:block e:exit | alt-y:copy alt-x:cut alt-p:paste alt-r:delete | " // &
297 "→:enter ←:back ~:home /:root alt-c:cd ctrl-q:quit" // RESET
298 else if (in_git_repo .and. trim(mode) == 'git') then
299 ! Git mode footer - show git operations
300 footer_text = YELLOW // trim(repo_name) // ":" // trim(branch_name) // " [ GIT MODE ]" // RESET // " | " // &
301 YELLOW // "a:add u:unstage m:commit h:push l:pull f:fetch d:diff t:tag" // RESET // " | " // &
302 DIM // "Space:select ↑↓:nav →:enter ←:back ~:home /:root 8:fav *:star " // &
303 "alt-n:rename alt-v:view alt-m:move alt-y:copy alt-x:cut alt-p:paste alt-r:delete .:hidden " // &
304 "alt-s:search alt-g:exit-mode alt-c:cd ctrl-q:quit" // RESET
305 else if (in_git_repo) then
306 ! Normal mode in git repo - show alt-g to enter git mode
307 footer_text = DIM // trim(repo_name) // ":" // trim(branch_name) // " | " // &
308 "Space:select Shift+↑↓:block | ↑↓:nav →:enter ←:back ~:home /:root " // &
309 "8:fav *:star alt-n:rename alt-v:view alt-m:move alt-y:copy alt-x:cut alt-p:paste alt-r:delete " // &
310 ".:hidden alt-s:search alt-g:git-mode alt-c:cd ctrl-q:quit" // RESET
311 else
312 ! Non-git repo footer
313 footer_text = DIM // "Space:select Shift+↑↓:block | ↑↓:nav →:enter ←:back ~:home /:root " // &
314 "8:fav *:star alt-n:rename alt-v:view alt-m:move alt-y:copy alt-x:cut alt-p:paste alt-r:delete " // &
315 ".:hidden alt-s:search alt-c:cd ctrl-q:quit" // RESET
316 end if
317
318 ! Truncate footer to terminal width - CRITICAL to prevent wrapping which causes screen scroll
319 ! Conservative truncation: assume ANSI codes are ~30% of string, so truncate to ~130% of terminal width
320 footer_len = len_trim(footer_text)
321 if (footer_len > (c * 13 / 10)) then
322 footer_text = footer_text(1:c * 13 / 10)
323 end if
324
325 ! Write footer WITHOUT newline (newline on last row causes wrap which scrolls screen up)
326 write(output_unit, '(a)', advance='no') trim(footer_text)
327
328 contains
329 function itoa(n) result(str)
330 integer, intent(in) :: n
331 character(len=10) :: str
332 write(str, '(i0)') n
333 end function itoa
334 end subroutine draw_interface
335
336 function get_file_color(filename, is_dir, is_exec) result(color)
337 character(len=*), intent(in) :: filename
338 logical, intent(in) :: is_dir, is_exec
339 character(len=20) :: color
340
341 ! Directories: Blue and bold
342 if (is_dir) then
343 color = BOLD // BLUE
344 ! Dotfiles: Grey
345 else if (filename(1:1) == '.') then
346 color = GREY
347 ! Executable files: Green
348 else if (is_exec) then
349 color = GREEN
350 ! All other files: White
351 else
352 color = WHITE
353 end if
354 end function get_file_color
355
356 end module ui_display
357