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