Fortran · 16124 bytes Raw Blame History
1 module display_module
2 use types_module
3 use tree_module
4 use git_module
5 use terminal_module
6 implicit none
7
8 contains
9
10 subroutine display_tree(files, n_files)
11 type(file_entry), intent(in) :: files(:)
12 integer, intent(in) :: n_files
13 type(tree_node), pointer :: root
14 integer :: i
15
16 ! Create root
17 allocate(root)
18 root%name = '.'
19 root%is_file = .false.
20 root%is_staged = .false.
21 root%is_unstaged = .false.
22 root%is_untracked = .false.
23 root%has_incoming = .false.
24 root%is_expanded = .true. ! Root is always expanded
25 root%first_child => null()
26 root%next_sibling => null()
27
28 ! Build tree
29 do i = 1, n_files
30 call add_to_tree(root, files(i)%path, files(i)%is_staged, files(i)%is_unstaged, files(i)%is_untracked, files(i)%has_incoming, files(i)%is_gitignored)
31 end do
32
33 ! Sort tree
34 call sort_tree(root)
35
36 ! Print tree
37 call print_tree_node(root, '', .true., .true.)
38
39 ! Cleanup
40 call free_tree(root)
41 end subroutine display_tree
42
43 recursive subroutine print_tree_node(node, prefix, is_last, is_root)
44 type(tree_node), pointer, intent(in) :: node
45 character(len=*), intent(in) :: prefix
46 logical, intent(in) :: is_last, is_root
47
48 character(len=1024) :: line
49 character(len=:), allocatable :: new_prefix
50 type(tree_node), pointer :: child
51 integer :: n_children, i
52
53 ! UTF-8 box-drawing characters
54 character(len=*), parameter :: branch_last = '└──'
55 character(len=*), parameter :: branch_mid = '├──'
56 character(len=*), parameter :: vertical = '│'
57 character(len=1), parameter :: ESC = achar(27)
58
59 ! Build colored marks
60 character(len=50) :: mark_unstaged
61 character(len=50) :: mark_untracked
62 character(len=50) :: mark_staged
63 character(len=50) :: mark_incoming
64
65 write(mark_unstaged, '(A,A,A,A,A)') ESC, '[31m', ' ✗', ESC, '[0m'
66 write(mark_untracked, '(A,A,A,A,A)') ESC, '[90m', ' ✗', ESC, '[0m'
67 write(mark_staged, '(A,A,A,A,A)') ESC, '[32m', ' ↑', ESC, '[0m'
68 write(mark_incoming, '(A,A,A,A,A)') ESC, '[34m', ' ↓', ESC, '[0m'
69
70 ! Count children first
71 n_children = 0
72 child => node%first_child
73 do while (associated(child))
74 n_children = n_children + 1
75 child => child%next_sibling
76 end do
77
78 ! Don't print root node
79 if (.not. is_root) then
80 ! Build line
81 if (is_last) then
82 line = prefix // branch_last // ' '
83 else
84 line = prefix // branch_mid // ' '
85 end if
86
87 ! Add name with grey color if gitignored
88 if (node%is_gitignored) then
89 line = trim(line) // ESC // '[90m' // trim(node%name) // ESC // '[0m'
90 else
91 line = trim(line) // trim(node%name)
92 end if
93
94 ! Show all applicable indicators
95 if (node%is_staged) then
96 line = trim(line) // trim(mark_staged)
97 end if
98 if (node%is_unstaged) then
99 line = trim(line) // trim(mark_unstaged)
100 end if
101 if (node%is_untracked) then
102 line = trim(line) // trim(mark_untracked)
103 end if
104 if (node%has_incoming) then
105 line = trim(line) // trim(mark_incoming)
106 end if
107 print '(A)', trim(line)
108 end if
109
110 ! Print children
111 i = 0
112 child => node%first_child
113 do while (associated(child))
114 i = i + 1
115
116 if (is_root) then
117 new_prefix = ''
118 else
119 if (is_last) then
120 new_prefix = prefix // ' '
121 else
122 new_prefix = prefix // vertical // ' '
123 end if
124 end if
125
126 call print_tree_node(child, new_prefix, i == n_children, .false.)
127 child => child%next_sibling
128 end do
129 end subroutine print_tree_node
130
131 subroutine draw_interactive_tree(tree_root, items, n_items, selected, &
132 repo_name, branch_name, viewport_offset, visible_items, top_padding, mode, &
133 in_rename_mode, rename_buffer, rename_cursor_pos)
134 type(tree_node), pointer, intent(in) :: tree_root
135 integer, intent(in) :: n_items, selected
136 type(selectable_item), intent(in) :: items(:)
137 character(len=*), intent(in) :: repo_name, branch_name, mode, rename_buffer
138 integer, intent(in) :: viewport_offset, visible_items, top_padding, rename_cursor_pos
139 logical, intent(in) :: in_rename_mode
140 integer :: item_idx, viewport_end, i
141 character(len=512) :: status_line
142
143 ! Add blank lines at top as padding for terminals that need it (fixes WezTerm/Ghostty)
144 do i = 1, top_padding
145 print '(A)', ''
146 end do
147
148 ! Display repo:branch info at top with mode indicator
149 if (len_trim(repo_name) > 0 .and. len_trim(branch_name) > 0) then
150 if (mode == 'git') then
151 ! Git mode: show in yellow/orange
152 write(status_line, '(A,A,A,A,A,A,A,A,A,A,A)') &
153 achar(27) // '[1;36m', trim(repo_name), achar(27) // '[0m', &
154 ':', &
155 achar(27) // '[1;33m', trim(branch_name), achar(27) // '[0m', &
156 ' ', &
157 achar(27) // '[1;33m[ GIT MODE ]', achar(27) // '[0m'
158 else
159 ! Normal mode
160 write(status_line, '(A,A,A,A,A,A,A)') &
161 achar(27) // '[1;36m', trim(repo_name), achar(27) // '[0m', &
162 ':', &
163 achar(27) // '[1;33m', trim(branch_name), achar(27) // '[0m'
164 end if
165 print '(A)', trim(status_line)
166 print '(A)', ''
167 end if
168
169 ! Calculate viewport range
170 viewport_end = viewport_offset + visible_items - 1
171 if (viewport_end > n_items) viewport_end = n_items
172
173 ! Print tree with selection highlighting
174 item_idx = 0
175 print '(A)', '.'
176 call print_interactive_node(tree_root, '', .true., .true., items, selected, &
177 item_idx, viewport_offset, viewport_end, in_rename_mode, rename_buffer, rename_cursor_pos)
178
179 ! Print help (mode and rename state dependent)
180 print '(A)', ''
181 if (in_rename_mode) then
182 ! Rename mode help - show in cyan
183 print '(A)', achar(27) // '[36mRENAME MODE: [a-zA-Z0-9._- ] | ←/→:cursor | Backspace:del | Tab:save | ESC:cancel' // achar(27) // '[0m'
184 else if (mode == 'git') then
185 ! Git mode help - show in yellow tint
186 print '(A)', achar(27) // '[33mLegend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
187 achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
188 achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
189 achar(27) // '[34m↓' // achar(27) // '[0m=incoming' // achar(27) // '[0m'
190 print '(A)', achar(27) // '[33mKeys: j/k/↑/↓:nav | ←/→:nav tree | space:toggle | .:hide-dots | alt-n:rename | a:stage | u:unstage | S:stage-all | U:unstage-all | x:discard | z:stash | Z:unstash | b:switch | n:new-br | R:del-br | G:merge | O:reset | I:rebase | f:fetch | d:diff | c/alt-v:view | w:blame | h:history | L:reflog | y:cherry-pick | v:revert | r:delete | l:pull | m:commit | M:amend | p:push | t:tag | s/alt-s:status | q:exit-mode | ESC:exit-mode | ctrl-c:quit' // achar(27) // '[0m'
191 else
192 ! Normal mode help
193 print '(A)', 'Legend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
194 achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
195 achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
196 achar(27) // '[34m↓' // achar(27) // '[0m=incoming'
197 print '(A)', 'Keys: j/k/↑/↓:nav | ←/→:nav tree | space:toggle | .:hide-dots | alt-n:rename | alt-v:view | alt-s:status | alt-g:git-mode | ctrl-c:quit'
198 end if
199
200 ! Don't free tree - it's owned by interactive_mode
201 end subroutine draw_interactive_tree
202
203 recursive subroutine print_interactive_node(node, prefix, is_last, is_root, items, selected, &
204 item_idx, viewport_offset, viewport_end, in_rename_mode, rename_buffer, rename_cursor_pos)
205 type(tree_node), pointer, intent(in) :: node
206 character(len=*), intent(in) :: prefix, rename_buffer
207 logical, intent(in) :: is_last, is_root, in_rename_mode
208 type(selectable_item), intent(in) :: items(:)
209 integer, intent(in) :: selected, viewport_offset, viewport_end, rename_cursor_pos
210 integer, intent(inout) :: item_idx
211
212 character(len=1024) :: line
213 character(len=:), allocatable :: new_prefix
214 type(tree_node), pointer :: child
215 integer :: n_children, i
216 logical :: is_selected
217
218 character(len=*), parameter :: branch_last = '└──'
219 character(len=*), parameter :: branch_mid = '├──'
220 character(len=*), parameter :: vertical = '│'
221 character(len=*), parameter :: highlight_on = achar(27) // '[7m'
222 character(len=*), parameter :: highlight_off = achar(27) // '[0m'
223 character(len=1), parameter :: ESC = achar(27)
224
225 ! Build colored marks
226 character(len=50) :: mark_unstaged
227 character(len=50) :: mark_untracked
228 character(len=50) :: mark_staged
229 character(len=50) :: mark_incoming
230
231 write(mark_unstaged, '(A,A,A,A,A)') ESC, '[31m', ' ✗', ESC, '[0m'
232 write(mark_untracked, '(A,A,A,A,A)') ESC, '[90m', ' ✗', ESC, '[0m'
233 write(mark_staged, '(A,A,A,A,A)') ESC, '[32m', ' ↑', ESC, '[0m'
234 write(mark_incoming, '(A,A,A,A,A)') ESC, '[34m', ' ↓', ESC, '[0m'
235
236 ! Count children first
237 n_children = 0
238 child => node%first_child
239 do while (associated(child))
240 n_children = n_children + 1
241 child => child%next_sibling
242 end do
243
244 ! Don't print root node
245 if (.not. is_root) then
246 ! Increment item index for all nodes
247 item_idx = item_idx + 1
248 is_selected = (item_idx == selected)
249
250 ! Only print if within viewport range
251 if (item_idx >= viewport_offset .and. item_idx <= viewport_end) then
252 ! Build line with appropriate branch character
253 if (is_last) then
254 line = prefix // branch_last // ' '
255 else
256 line = prefix // branch_mid // ' '
257 end if
258
259 ! Add expand/collapse indicator for directories
260 if (.not. node%is_file) then
261 if (node%is_expanded) then
262 line = trim(line) // '▼ '
263 else
264 line = trim(line) // '▶ '
265 end if
266 end if
267
268 ! Add name with highlighting if selected
269 if (is_selected) then
270 ! Special handling for rename mode
271 if (in_rename_mode) then
272 ! Show editable name with cursor at correct position
273 if (rename_cursor_pos == len_trim(rename_buffer)) then
274 ! Cursor at end
275 line = trim(line) // highlight_on // trim(rename_buffer) // '█' // highlight_off
276 else if (rename_cursor_pos == 0) then
277 ! Cursor at beginning
278 line = trim(line) // highlight_on // '█' // trim(rename_buffer) // highlight_off
279 else
280 ! Cursor in middle
281 line = trim(line) // highlight_on // rename_buffer(1:rename_cursor_pos) // '█' // &
282 rename_buffer(rename_cursor_pos+1:len_trim(rename_buffer)) // highlight_off
283 end if
284 else
285 ! Normal selection highlighting
286 if (node%is_gitignored) then
287 line = trim(line) // highlight_on // ESC // '[90m' // trim(node%name) // ESC // '[0m'
288 else
289 line = trim(line) // highlight_on // trim(node%name)
290 end if
291 if (node%is_staged) then
292 line = trim(line) // trim(mark_staged)
293 end if
294 if (node%is_unstaged) then
295 line = trim(line) // trim(mark_unstaged)
296 end if
297 if (node%is_untracked) then
298 line = trim(line) // trim(mark_untracked)
299 end if
300 if (node%has_incoming) then
301 line = trim(line) // trim(mark_incoming)
302 end if
303 line = trim(line) // highlight_off
304 end if
305 else
306 if (node%is_gitignored) then
307 line = trim(line) // ESC // '[90m' // trim(node%name) // ESC // '[0m'
308 else
309 line = trim(line) // trim(node%name)
310 end if
311 if (node%is_staged) then
312 line = trim(line) // trim(mark_staged)
313 end if
314 if (node%is_unstaged) then
315 line = trim(line) // trim(mark_unstaged)
316 end if
317 if (node%is_untracked) then
318 line = trim(line) // trim(mark_untracked)
319 end if
320 if (node%has_incoming) then
321 line = trim(line) // trim(mark_incoming)
322 end if
323 end if
324
325 print '(A)', trim(line)
326 end if
327 end if
328
329 ! Print children only if expanded (or if this is root)
330 if (node%is_expanded) then
331 i = 0
332 child => node%first_child
333 do while (associated(child))
334 i = i + 1
335
336 if (is_root) then
337 new_prefix = ''
338 else
339 if (is_last) then
340 new_prefix = prefix // ' '
341 else
342 new_prefix = prefix // vertical // ' '
343 end if
344 end if
345
346 call print_interactive_node(child, new_prefix, i == n_children, .false., items, selected, &
347 item_idx, viewport_offset, viewport_end, in_rename_mode, rename_buffer, rename_cursor_pos)
348 child => child%next_sibling
349 end do
350 end if
351 end subroutine print_interactive_node
352
353 subroutine draw_status_view(status_lines, n_lines)
354 character(len=*), intent(in) :: status_lines(:)
355 integer, intent(in) :: n_lines
356 integer :: i
357
358 ! Clear screen
359 call clear_screen()
360
361 ! Display header
362 print '(A)', achar(27) // '[1mGit Status' // achar(27) // '[0m'
363 print '(A)', '=========================================='
364 print '(A)', ''
365
366 ! Display status output
367 do i = 1, n_lines
368 print '(A)', trim(status_lines(i))
369 end do
370
371 ! Display footer
372 print '(A)', ''
373 print '(A)', '=========================================='
374 print '(A)', 'Press any key to return to tree view...'
375 end subroutine draw_status_view
376
377 end module display_module
378