Fortran · 12589 bytes Raw Blame History
1 ! Fortress Navigator Integration for fac
2 ! Main API for opening file/directory navigator (Ctrl-O)
3
4 module fortress_navigator_module
5 use iso_fortran_env, only: output_unit, input_unit
6 use fortress_fs_module
7 use fortress_display_module
8 use terminal_io_module, only: terminal_read_char, terminal_write, terminal_move_cursor
9 use favorites_module, only: favorites_add
10 implicit none
11 private
12
13 public :: open_fortress_navigator
14
15 ! Navigation state
16 character(len=MAX_PATH), dimension(MAX_FILES) :: current_files, parent_files
17 logical, dimension(MAX_FILES) :: current_is_dir, parent_is_dir
18 logical, dimension(MAX_FILES) :: current_is_exec, parent_is_exec
19 integer :: current_count, parent_count
20 integer :: selected, parent_selected
21 integer :: scroll_offset, parent_scroll_offset
22
23 contains
24
25 !> Main entry point: open fortress navigator and return selection
26 !! @param selected_path - Output: selected file/directory path (empty if cancelled)
27 !! @param is_directory - Output: true if selected item is directory
28 !! @param cancelled - Output: true if user pressed ESC/q
29 !! @param initial_path - Input (optional): starting directory
30 subroutine open_fortress_navigator(selected_path, is_directory, cancelled, initial_path)
31 character(len=:), allocatable, intent(out) :: selected_path
32 logical, intent(out) :: is_directory, cancelled
33 character(len=*), intent(in), optional :: initial_path
34 character(len=MAX_PATH) :: current_dir, parent_dir, temp_dir, last_dir, last_parent
35 character(len=1) :: key
36 integer :: rows, cols, ios, last_selected, last_scroll
37 logical :: running, dir_changed, first_draw, need_redraw
38
39 ! Initialize state
40 selected = 1
41 parent_selected = -1
42 scroll_offset = 0
43 parent_scroll_offset = 0
44 running = .true.
45 cancelled = .false.
46 last_dir = ""
47 last_parent = ""
48 first_draw = .true.
49 need_redraw = .true.
50 last_selected = -1
51 last_scroll = -1
52
53 ! Set initial directory
54 if (present(initial_path)) then
55 current_dir = initial_path
56 else
57 current_dir = get_pwd()
58 end if
59
60 ! Get terminal size once (assumes terminal doesn't resize during navigation)
61 call get_term_size(rows, cols)
62
63 ! Main navigation loop
64 do while (running)
65 ! Only refresh directory listings if directory changed
66 dir_changed = (current_dir /= last_dir)
67 if (dir_changed) then
68 parent_dir = get_parent_path(current_dir)
69
70 ! Only refresh parent if it changed too
71 if (parent_dir /= last_parent) then
72 call get_file_list(parent_dir, parent_files, parent_is_dir, parent_is_exec, parent_count)
73 last_parent = parent_dir
74 end if
75
76 call get_file_list(current_dir, current_files, current_is_dir, current_is_exec, current_count)
77 last_dir = current_dir
78 else
79 ! Just update parent_dir for consistency
80 parent_dir = get_parent_path(current_dir)
81 end if
82
83 ! Find current directory in parent listing
84 parent_selected = find_in_parent(current_dir, parent_files, parent_count)
85
86 ! Bounds check
87 if (selected < 1) selected = 1
88 if (selected > current_count) selected = current_count
89 if (current_count == 0) selected = 1
90
91 ! Adjust scroll offsets
92 call adjust_scroll(selected, scroll_offset, rows - 4)
93 call adjust_scroll(parent_selected, parent_scroll_offset, rows - 4)
94
95 ! Check if we need to redraw (directory changed, selection changed, or scroll changed)
96 need_redraw = dir_changed .or. first_draw .or. &
97 selected /= last_selected .or. scroll_offset /= last_scroll
98
99 ! Render interface only if something changed
100 if (need_redraw) then
101 call draw_fortress_interface(rows, cols, current_dir, &
102 current_files, current_is_dir, current_is_exec, current_count, &
103 parent_files, parent_is_dir, parent_count, &
104 selected, parent_selected, scroll_offset, parent_scroll_offset, first_draw)
105
106 ! Update tracking variables
107 last_selected = selected
108 last_scroll = scroll_offset
109 if (first_draw) first_draw = .false.
110 end if
111
112 ! Read key using raw terminal input
113 ! Note: terminal_read_char is non-blocking, returns -1 if no input
114 ios = terminal_read_char()
115 if (ios < 0) then
116 ! No input available - With conditional redraw optimization above,
117 ! we won't redraw unnecessarily, so this tight loop is acceptable
118 cycle
119 end if
120 key = achar(ios)
121
122 ! Handle input
123 select case (key)
124 case (char(27)) ! ESC
125 ! Check if arrow key or standalone ESC
126 if (check_arrow_key(key)) then
127 call handle_arrow_key(key, selected, current_dir, temp_dir, current_files, &
128 current_is_dir, current_count)
129 else
130 ! Standalone ESC - quit
131 cancelled = .true.
132 running = .false.
133 end if
134
135 case ('q', 'Q') ! Quit
136 cancelled = .true.
137 running = .false.
138
139 case (char(10), char(13)) ! Enter - select current item (file or directory)
140 if (current_count > 0) then
141 if (current_is_dir(selected)) then
142 ! Select directory and exit
143 selected_path = join_path(current_dir, trim(current_files(selected)))
144 is_directory = .true.
145 running = .false.
146 else
147 ! Select file and exit
148 selected_path = join_path(current_dir, trim(current_files(selected)))
149 is_directory = .false.
150 running = .false.
151 end if
152 end if
153
154 case ('~') ! Jump to home
155 call get_environment_variable("HOME", current_dir)
156 selected = 1
157 scroll_offset = 0
158
159 case ('/') ! Jump to root
160 current_dir = "/"
161 selected = 1
162 scroll_offset = 0
163
164 case ('f', 'F') ! Add current directory to favorites
165 call add_to_favorites(current_dir, rows)
166
167 end select
168 end do
169
170 ! Set outputs based on result
171 if (.not. cancelled) then
172 ! Check if selected_path was already set (file selection in loop)
173 if (.not. allocated(selected_path)) then
174 ! Directory selection or other exit - set to current directory
175 selected_path = trim(current_dir)
176 end if
177 ! Determine if the selected path is a directory
178 if (allocated(selected_path)) then
179 if (selected_path == trim(current_dir)) then
180 is_directory = .true.
181 else
182 is_directory = .false. ! Already set in loop for files
183 end if
184 end if
185 else
186 ! Cancelled - set empty path
187 selected_path = ""
188 is_directory = .false.
189 end if
190
191 end subroutine open_fortress_navigator
192
193 !> Adjust scroll offset to keep selection visible with margin
194 subroutine adjust_scroll(sel, offset, visible_height)
195 integer, intent(in) :: sel, visible_height
196 integer, intent(inout) :: offset
197 integer :: margin
198
199 ! Add a margin to avoid selection being at the very edge
200 margin = 3
201 if (margin > visible_height / 4) margin = visible_height / 4
202
203 ! If selection is above the visible window (with margin)
204 if (sel < offset + 1 + margin) then
205 offset = sel - margin - 1
206 if (offset < 0) offset = 0
207 ! If selection is below the visible window (with margin)
208 else if (sel > offset + visible_height - margin) then
209 offset = sel - visible_height + margin
210 end if
211
212 ! Ensure offset is not negative
213 if (offset < 0) offset = 0
214 end subroutine adjust_scroll
215
216 !> Check if ESC is start of arrow key sequence
217 function check_arrow_key(key) result(is_arrow)
218 character(len=1), intent(inout) :: key
219 logical :: is_arrow
220 integer :: char_code
221
222 is_arrow = .false.
223
224 if (key == char(27)) then
225 ! Try to read next character
226 char_code = terminal_read_char()
227 if (char_code >= 0) then
228 if (achar(char_code) == '[') then
229 ! It's an arrow key sequence - read the direction
230 char_code = terminal_read_char()
231 if (char_code >= 0) then
232 key = achar(char_code)
233 is_arrow = .true.
234 end if
235 end if
236 end if
237 end if
238 end function check_arrow_key
239
240 !> Handle arrow key navigation
241 subroutine handle_arrow_key(key, sel, curr_dir, temp_dir, files, is_dir, file_count)
242 character(len=1), intent(in) :: key
243 integer, intent(inout) :: sel
244 character(len=MAX_PATH), intent(inout) :: curr_dir, temp_dir
245 character(len=*), dimension(*), intent(in) :: files
246 logical, dimension(*), intent(in) :: is_dir
247 integer, intent(in) :: file_count
248
249 select case (key)
250 case ('A') ! Up arrow
251 if (sel > 1) sel = sel - 1
252
253 case ('B') ! Down arrow
254 if (sel < file_count) sel = sel + 1
255
256 case ('C') ! Right arrow - enter directory
257 if (file_count > 0 .and. is_dir(sel)) then
258 temp_dir = curr_dir
259 curr_dir = join_path(curr_dir, trim(files(sel)))
260 sel = 1
261 end if
262
263 case ('D') ! Left arrow - go to parent
264 temp_dir = curr_dir
265 curr_dir = get_parent_path(curr_dir)
266 sel = find_in_parent(temp_dir, files, file_count)
267 end select
268 end subroutine handle_arrow_key
269
270 !> Get terminal size using fac's terminal module
271 subroutine get_term_size(rows, cols)
272 use terminal_io_module, only: terminal_get_size
273 integer, intent(out) :: rows, cols
274
275 call terminal_get_size(rows, cols)
276
277 ! Sanity check
278 if (rows <= 0) rows = 24
279 if (cols <= 0) cols = 80
280 end subroutine get_term_size
281
282 !> Add current directory to favorites
283 subroutine add_to_favorites(dir_path, rows)
284 character(len=*), intent(in) :: dir_path
285 integer, intent(in) :: rows
286 character(len=256) :: label
287 logical :: success
288 integer :: i
289
290 ! Extract basename for label
291 label = dir_path
292 do i = len_trim(dir_path), 1, -1
293 if (dir_path(i:i) == '/') then
294 label = dir_path(i+1:)
295 exit
296 end if
297 end do
298
299 ! Add to favorites
300 call favorites_add(dir_path, trim(label), success)
301
302 ! Show feedback message
303 call terminal_move_cursor(rows, 1)
304 if (success) then
305 call terminal_write('Added to favorites: ' // trim(label))
306 else
307 call terminal_write('Already in favorites or error')
308 end if
309
310 ! Pause briefly so user can see message
311 call sleep_ms(800)
312 end subroutine add_to_favorites
313
314 !> Sleep for specified milliseconds
315 subroutine sleep_ms(milliseconds)
316 integer, intent(in) :: milliseconds
317 integer :: i, j, dummy
318
319 ! Simple busy-wait (not ideal but portable)
320 dummy = 0
321 do i = 1, milliseconds * 1000
322 do j = 1, 100
323 dummy = dummy + 1
324 end do
325 end do
326 end subroutine sleep_ms
327
328 end module fortress_navigator_module
329