Fortran · 15987 bytes Raw Blame History
1 program fuss
2 use iso_fortran_env, only: error_unit
3 use types_module
4 use git_module
5 use tree_module
6 use display_module
7 use terminal_module
8 implicit none
9
10 ! Main program variables
11 logical :: show_all, interactive
12 character(len=:), allocatable :: root_path
13
14 ! Parse command line arguments
15 call parse_arguments(show_all, interactive)
16
17 ! Get current directory
18 call get_current_dir(root_path)
19
20 ! Build and display tree
21 if (interactive) then
22 call interactive_mode(show_all)
23 else
24 call build_and_display_tree(root_path, show_all)
25 end if
26
27 contains
28
29 subroutine parse_arguments(show_all, interactive)
30 logical, intent(out) :: show_all, interactive
31 integer :: i, nargs
32 character(len=256) :: arg
33
34 show_all = .false.
35 interactive = .false.
36 nargs = command_argument_count()
37
38 do i = 1, nargs
39 call get_command_argument(i, arg)
40 if (trim(arg) == '--all' .or. trim(arg) == '-a') then
41 show_all = .true.
42 else if (trim(arg) == '-i' .or. trim(arg) == '--interactive') then
43 interactive = .true.
44 end if
45 end do
46 end subroutine parse_arguments
47
48 subroutine get_current_dir(path)
49 character(len=:), allocatable, intent(out) :: path
50 character(len=1024) :: buffer
51 integer :: status
52
53 call execute_command_line('pwd > /tmp/fuss_pwd.txt', exitstat=status)
54
55 open(unit=99, file='/tmp/fuss_pwd.txt', status='old', action='read')
56 read(99, '(A)') buffer
57 close(99, status='delete')
58
59 path = trim(buffer)
60 end subroutine get_current_dir
61
62 subroutine build_and_display_tree(root_path, show_all)
63 character(len=*), intent(in) :: root_path
64 logical, intent(in) :: show_all
65 type(file_entry), allocatable :: files(:)
66 integer :: n_files
67
68 ! Get files from git or filesystem
69 if (show_all) then
70 call get_all_files(files, n_files)
71 else
72 call get_dirty_files(files, n_files)
73 end if
74
75 ! Mark files with incoming changes
76 call mark_incoming_changes(files, n_files)
77
78 ! Display the tree
79 if (n_files > 0) then
80 print '(A)', '.'
81 call display_tree(files, n_files)
82 else
83 print '(A)', 'No files to display'
84 end if
85 end subroutine build_and_display_tree
86
87 subroutine interactive_mode(show_all)
88 logical, intent(in) :: show_all
89 type(file_entry), allocatable :: files(:)
90 type(selectable_item), allocatable :: items(:)
91 integer :: n_files, n_items, selected, i, status
92 character(len=1) :: key
93 logical :: running
94 character(len=256) :: repo_name, branch_name
95 integer :: term_height, viewport_offset, visible_items
96
97 ! Get repo and branch info
98 call get_repo_info(repo_name, branch_name)
99
100 ! Get terminal height
101 call get_terminal_height(term_height)
102
103 ! DEBUG: Show terminal height
104 ! print '(A,I0)', 'DEBUG: Terminal height detected: ', term_height
105
106 ! Get files
107 if (show_all) then
108 call get_all_files(files, n_files)
109 else
110 call get_dirty_files(files, n_files)
111 end if
112
113 ! Mark files with incoming changes
114 call mark_incoming_changes(files, n_files)
115
116 if (n_files == 0) then
117 print '(A)', 'No files to display'
118 return
119 end if
120
121 ! Build flat list of items for navigation
122 call build_item_list(files, n_files, items, n_items)
123
124 ! Calculate visible items accurately
125 ! Fixed UI elements that take screen space:
126 ! Line 1: repo:branch (e.g., "fuss:trunk")
127 ! Line 2: blank line after repo
128 ! Line 3: "." root
129 ! Lines 4 to N-3: tree items (VIEWPORT)
130 ! Line N-2: blank line before help
131 ! Line N-1: help legend (↑=staged ✗=modified ✗=untracked)
132 ! Line N: help controls (j/k/↓/↑: navigate | ...)
133 ! Total fixed: 6 lines (2 + 1 + 3)
134 visible_items = term_height - 6
135 if (visible_items < 3) visible_items = 3 ! Absolute minimum
136 if (visible_items > n_items) visible_items = n_items ! Don't exceed total items
137
138 ! Initialize selection and viewport at TOP of tree
139 selected = 1
140 viewport_offset = 1
141 running = .true.
142
143 ! Enable raw terminal mode
144 call enable_raw_mode()
145
146 ! Main interactive loop
147 do while (running)
148 ! Center viewport on selection - keeps highlighted item in middle of screen
149 viewport_offset = selected - visible_items / 2
150
151 ! Clamp viewport to valid range
152 if (viewport_offset < 1) viewport_offset = 1
153 if (viewport_offset > n_items - visible_items + 1 .and. n_items > visible_items) then
154 viewport_offset = n_items - visible_items + 1
155 end if
156
157 ! Clear screen and redraw
158 call clear_screen()
159 call draw_interactive_tree(files, n_files, items, n_items, selected, &
160 repo_name, branch_name, viewport_offset, visible_items)
161
162 ! Read key
163 call read_key(key)
164
165 ! Handle input
166 select case (key)
167 case ('j', 'B') ! j or down arrow
168 if (selected < n_items) selected = selected + 1
169 case ('k', 'A') ! k or up arrow
170 if (selected > 1) selected = selected - 1
171 case ('a') ! Stage file (lowercase to avoid conflict with arrow A)
172 if (items(selected)%is_file .and. (items(selected)%is_unstaged .or. items(selected)%is_untracked)) then
173 call git_add_file(items(selected)%path)
174 ! Refresh files after git add
175 if (show_all) then
176 call get_all_files(files, n_files)
177 else
178 call get_dirty_files(files, n_files)
179 end if
180 call mark_incoming_changes(files, n_files)
181 call build_item_list(files, n_files, items, n_items)
182 if (selected > n_items .and. n_items > 0) selected = n_items
183 if (n_items == 0) running = .false.
184 end if
185 case ('u') ! Unstage file (lowercase)
186 if (items(selected)%is_file .and. items(selected)%is_staged) then
187 call git_unstage_file(items(selected)%path)
188 ! Refresh files after git unstage
189 if (show_all) then
190 call get_all_files(files, n_files)
191 else
192 call get_dirty_files(files, n_files)
193 end if
194 call mark_incoming_changes(files, n_files)
195 call build_item_list(files, n_files, items, n_items)
196 if (selected > n_items .and. n_items > 0) selected = n_items
197 end if
198 case ('m') ! Commit (lowercase)
199 call commit_prompt()
200 ! Refresh files after commit
201 if (show_all) then
202 call get_all_files(files, n_files)
203 else
204 call get_dirty_files(files, n_files)
205 end if
206 call mark_incoming_changes(files, n_files)
207 call build_item_list(files, n_files, items, n_items)
208 if (selected > n_items .and. n_items > 0) selected = n_items
209 case ('s') ! Show git status (lowercase)
210 call show_status_view()
211 case ('p') ! Push (lowercase)
212 call push_prompt()
213 ! Refresh files after push
214 if (show_all) then
215 call get_all_files(files, n_files)
216 else
217 call get_dirty_files(files, n_files)
218 end if
219 call mark_incoming_changes(files, n_files)
220 call build_item_list(files, n_files, items, n_items)
221 if (selected > n_items .and. n_items > 0) selected = n_items
222 case ('t') ! Tag (lowercase)
223 call tag_prompt()
224 case ('f') ! Git fetch
225 call git_fetch()
226 ! Refresh files after fetch and include files with incoming changes
227 if (show_all) then
228 call get_all_files(files, n_files)
229 call mark_incoming_changes(files, n_files)
230 else
231 ! In non-all mode, add files that only have incoming changes
232 call get_dirty_files(files, n_files)
233 call add_incoming_files(files, n_files)
234 end if
235 call build_item_list(files, n_files, items, n_items)
236 if (selected > n_items .and. n_items > 0) selected = n_items
237 case ('d') ! Git diff with less
238 if (items(selected)%is_file) then
239 call git_diff_file(items(selected)%path, items(selected)%has_incoming)
240 end if
241 case ('l') ! Git pull
242 call git_pull()
243 ! Refresh files after pull (incoming indicators will automatically clear)
244 if (show_all) then
245 call get_all_files(files, n_files)
246 call mark_incoming_changes(files, n_files)
247 else
248 call get_dirty_files(files, n_files)
249 call add_incoming_files(files, n_files)
250 end if
251 call build_item_list(files, n_files, items, n_items)
252 if (selected > n_items .and. n_items > 0) selected = n_items
253 ! Note: After successful pull, git diff will show no upstream differences
254 ! so has_incoming will be .false. for all files automatically
255 case ('q', 'Q') ! Quit
256 running = .false.
257 end select
258 end do
259
260 ! Restore terminal
261 call disable_raw_mode()
262
263 ! Final display
264 call clear_screen()
265 call build_and_display_tree('', show_all)
266 end subroutine interactive_mode
267
268 subroutine build_item_list(files, n_files, items, n_items)
269 type(file_entry), intent(in) :: files(:)
270 integer, intent(in) :: n_files
271 type(selectable_item), allocatable, intent(out) :: items(:)
272 integer, intent(out) :: n_items
273 type(tree_node), pointer :: root
274 type(selectable_item), allocatable :: temp_items(:)
275 integer :: i, max_items
276
277 ! Build the tree first
278 allocate(root)
279 root%name = '.'
280 root%is_file = .false.
281 root%is_staged = .false.
282 root%is_unstaged = .false.
283 root%is_untracked = .false.
284 root%has_incoming = .false.
285 root%first_child => null()
286 root%next_sibling => null()
287
288 do i = 1, n_files
289 call add_to_tree(root, files(i)%path, files(i)%is_staged, files(i)%is_unstaged, files(i)%is_untracked, files(i)%has_incoming)
290 end do
291
292 call sort_tree(root)
293
294 ! Collect items from tree in traversal order
295 max_items = 1000
296 allocate(temp_items(max_items))
297 n_items = 0
298
299 ! Traverse tree and collect all items
300 call collect_items_from_tree(root, '', temp_items, n_items, max_items)
301
302 ! Copy to output
303 allocate(items(n_items))
304 if (n_items > 0) items(1:n_items) = temp_items(1:n_items)
305 deallocate(temp_items)
306
307 call free_tree(root)
308 end subroutine build_item_list
309
310 recursive subroutine collect_items_from_tree(node, parent_path, items, n_items, max_items)
311 type(tree_node), pointer, intent(in) :: node
312 character(len=*), intent(in) :: parent_path
313 type(selectable_item), allocatable, intent(inout) :: items(:)
314 integer, intent(inout) :: n_items, max_items
315 type(tree_node), pointer :: child
316 character(len=512) :: full_path
317
318 ! Skip root node
319 if (len_trim(parent_path) > 0 .or. trim(node%name) /= '.') then
320 ! Build full path
321 if (len_trim(parent_path) == 0) then
322 full_path = trim(node%name)
323 else
324 full_path = trim(parent_path) // '/' // trim(node%name)
325 end if
326
327 ! Add this item
328 n_items = n_items + 1
329 if (n_items > max_items) then
330 call resize_item_array(items, max_items)
331 end if
332
333 items(n_items)%path = trim(full_path)
334 items(n_items)%is_file = node%is_file
335 items(n_items)%is_staged = node%is_staged
336 items(n_items)%is_unstaged = node%is_unstaged
337 items(n_items)%is_untracked = node%is_untracked
338 items(n_items)%has_incoming = node%has_incoming
339 else
340 full_path = ''
341 end if
342
343 ! Recursively add children
344 child => node%first_child
345 do while (associated(child))
346 call collect_items_from_tree(child, full_path, items, n_items, max_items)
347 child => child%next_sibling
348 end do
349 end subroutine collect_items_from_tree
350
351 subroutine resize_item_array(items, max_items)
352 type(selectable_item), allocatable, intent(inout) :: items(:)
353 integer, intent(inout) :: max_items
354 type(selectable_item), allocatable :: temp_items(:)
355 integer :: old_size
356
357 old_size = max_items
358 allocate(temp_items(old_size))
359 temp_items = items(1:old_size)
360 deallocate(items)
361 max_items = max_items * 2
362 allocate(items(max_items))
363 items(1:old_size) = temp_items
364 deallocate(temp_items)
365 end subroutine resize_item_array
366
367 subroutine commit_prompt()
368 character(len=512) :: commit_msg
369 logical :: success
370 character(len=1) :: key
371
372 ! Clear screen for commit prompt
373 call clear_screen()
374 print '(A)', achar(27) // '[1mGit Commit' // achar(27) // '[0m'
375 print '(A)', ''
376
377 ! Read commit message
378 call read_line('Commit message: ', commit_msg)
379
380 ! Execute commit if message is not empty
381 if (len_trim(commit_msg) > 0) then
382 call git_commit_with_message(commit_msg, success)
383
384 ! Wait for keypress to continue
385 call read_key(key)
386 end if
387 end subroutine commit_prompt
388
389 subroutine show_status_view()
390 ! Use less for scrollable, searchable git status view
391 call show_git_status_paged()
392 end subroutine show_status_view
393
394 subroutine push_prompt()
395 logical :: success
396 character(len=1) :: key
397
398 ! Clear screen for push prompt
399 call clear_screen()
400 print '(A)', achar(27) // '[1mGit Push' // achar(27) // '[0m'
401 print '(A)', ''
402 print '(A)', 'Pushing to remote...'
403 print '(A)', ''
404
405 ! Execute push
406 call git_push(success)
407
408 ! Wait for keypress to continue
409 call read_key(key)
410 end subroutine push_prompt
411
412 subroutine tag_prompt()
413 character(len=512) :: tag_name, tag_message
414 logical :: success
415 character(len=1) :: key
416
417 ! Clear screen for tag prompt
418 call clear_screen()
419 print '(A)', achar(27) // '[1mGit Tag' // achar(27) // '[0m'
420 print '(A)', ''
421
422 ! Read tag name
423 call read_line('Tag name: ', tag_name)
424
425 ! Execute tag if name is not empty
426 if (len_trim(tag_name) > 0) then
427 ! Read tag message (optional)
428 call read_line('Tag message (enter for none): ', tag_message)
429
430 call git_tag(tag_name, tag_message, success)
431
432 ! Wait for keypress to continue
433 call read_key(key)
434 end if
435 end subroutine tag_prompt
436
437 end program fuss
438