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