Fortran · 14225 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, 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