Fortran · 14491 bytes Raw Blame History
1 module filesystem_ops
2 implicit none
3 private
4
5 public :: get_file_list, get_pwd, get_parent_path, join_path
6 public :: find_in_parent, find_file_in_list, fzf_search, write_exit_dir
7 public :: open_file_in_default_app
8 public :: rename_file_prompt
9 public :: MAX_PATH, MAX_FILES
10
11 integer, parameter :: MAX_PATH = 512
12 integer, parameter :: MAX_FILES = 500
13
14 contains
15
16 subroutine get_file_list(dir, files, is_dir, is_exec, count)
17 character(len=*), intent(in) :: dir
18 character(len=*), dimension(*), intent(out) :: files
19 logical, dimension(*), intent(out) :: is_dir, is_exec
20 integer, intent(out) :: count
21 integer :: unit, ios, i, stat_code
22 character(len=MAX_PATH) :: temp_file, stat_file
23 character(len=MAX_PATH) :: line, filename, file_type, fullpath
24
25 ! Get list of files
26 call get_environment_variable("HOME", temp_file)
27 temp_file = trim(temp_file) // "/.fortress_ls"
28 stat_file = trim(temp_file) // "_stat"
29
30 call execute_command_line("ls -1a '" // trim(dir) // "' > " // trim(temp_file) // " 2>/dev/null", wait=.true.)
31
32 open(newunit=unit, file=temp_file, status='old', iostat=ios)
33 if (ios /= 0) then
34 count = 0
35 call execute_command_line("rm -f " // trim(temp_file) // " 2>/dev/null")
36 return
37 end if
38
39 ! Read all filenames first
40 count = 0
41 do
42 count = count + 1
43 if (count > MAX_FILES) exit
44 read(unit, '(a)', iostat=ios) files(count)
45 if (ios /= 0) then
46 count = count - 1
47 exit
48 end if
49 end do
50 close(unit)
51
52 ! Now check file attributes - use simpler approach with stat via ls
53 ! Generate a script that checks each file and outputs "filename:type"
54 call execute_command_line("cd '" // trim(dir) // "' && " // &
55 "for f in $(ls -1a 2>/dev/null); do " // &
56 " if [ -d ""$f"" ]; then echo ""$f:d""; " // &
57 " elif [ -x ""$f"" ] && [ ! -d ""$f"" ]; then echo ""$f:x""; " // &
58 " else echo ""$f:f""; fi; " // &
59 "done > " // trim(stat_file) // " 2>/dev/null", wait=.true.)
60
61 ! Initialize all as regular non-executable files
62 do i = 1, count
63 is_dir(i) = .false.
64 is_exec(i) = .false.
65 end do
66
67 ! Read the stat results and update file types
68 open(newunit=unit, file=stat_file, status='old', iostat=ios)
69 if (ios == 0) then
70 do
71 read(unit, '(a)', iostat=ios) line
72 if (ios /= 0) exit
73
74 ! Parse "filename:type" format
75 stat_code = index(line, ':', back=.true.)
76 if (stat_code > 0) then
77 filename = line(1:stat_code-1)
78 file_type = line(stat_code+1:stat_code+1)
79
80 ! Find this file in our list and update its type
81 do i = 1, count
82 if (trim(files(i)) == trim(filename)) then
83 if (file_type == 'd') then
84 is_dir(i) = .true.
85 is_exec(i) = .false.
86 else if (file_type == 'x') then
87 is_dir(i) = .false.
88 is_exec(i) = .true.
89 else
90 is_dir(i) = .false.
91 is_exec(i) = .false.
92 end if
93 exit
94 end if
95 end do
96 end if
97 end do
98 close(unit)
99 end if
100
101 ! Cleanup temp files
102 call execute_command_line("rm -f " // trim(temp_file) // " " // trim(stat_file) // " 2>/dev/null")
103 end subroutine get_file_list
104
105 function get_pwd() result(path)
106 character(len=MAX_PATH) :: path
107 integer :: unit, ios
108
109 call execute_command_line("pwd > .fortress_pwd 2>/dev/null", wait=.true.)
110 open(newunit=unit, file=".fortress_pwd", status='old', iostat=ios)
111 if (ios == 0) then
112 read(unit, '(a)') path
113 close(unit)
114 else
115 path = "."
116 end if
117 call execute_command_line("rm -f .fortress_pwd 2>/dev/null")
118 end function get_pwd
119
120 function get_parent_path(path) result(parent)
121 character(len=*), intent(in) :: path
122 character(len=MAX_PATH) :: parent
123 integer :: pos
124
125 pos = index(path, "/", back=.true.)
126 if (pos > 1) then
127 parent = path(1:pos-1)
128 else if (pos == 1) then
129 parent = "/"
130 else
131 parent = "."
132 end if
133 end function get_parent_path
134
135 function join_path(base, name) result(full)
136 character(len=*), intent(in) :: base, name
137 character(len=MAX_PATH) :: full
138
139 if (base == "/") then
140 full = "/" // trim(name)
141 else
142 full = trim(base) // "/" // trim(name)
143 end if
144 end function join_path
145
146 function find_in_parent(dir, files, count) result(idx)
147 character(len=*), intent(in) :: dir
148 character(len=*), dimension(*), intent(in) :: files
149 integer, intent(in) :: count
150 integer :: idx, pos
151 character(len=256) :: basename
152
153 pos = index(dir, "/", back=.true.)
154 if (pos > 0) then
155 basename = dir(pos+1:)
156 else
157 basename = dir
158 end if
159
160 do idx = 1, count
161 if (trim(files(idx)) == trim(basename)) return
162 end do
163 idx = 1
164 end function find_in_parent
165
166 function find_file_in_list(target_path, files, count) result(idx)
167 character(len=*), intent(in) :: target_path
168 character(len=*), dimension(*), intent(in) :: files
169 integer, intent(in) :: count
170 integer :: idx, pos
171 character(len=MAX_PATH) :: basename
172
173 ! Extract basename from target_path
174 pos = index(target_path, "/", back=.true.)
175 if (pos > 0) then
176 basename = target_path(pos+1:)
177 else
178 basename = target_path
179 end if
180
181 ! Search for the file in the list
182 do idx = 1, count
183 if (trim(files(idx)) == trim(basename)) return
184 end do
185
186 ! Default to first item if not found
187 idx = 1
188 end function find_file_in_list
189
190 subroutine fzf_search(search_dir, result_path)
191 character(len=*), intent(in) :: search_dir
192 character(len=*), intent(out) :: result_path
193 character(len=MAX_PATH) :: temp_file, fzf_cmd
194 integer :: unit, ios, stat
195
196 result_path = ""
197
198 ! Create temp file for fzf output
199 call get_environment_variable("HOME", temp_file)
200 temp_file = trim(temp_file) // "/.fortress_fzf"
201
202 ! Restore terminal for fzf
203 call execute_command_line("stty icanon echo 2>/dev/null")
204
205 ! Build fzf command: find files, pipe to fzf, save selection
206 fzf_cmd = "cd '" // trim(search_dir) // "' && " // &
207 "find . -type f -o -type d | " // &
208 "sed 's|^\./||' | " // &
209 "fzf --height=40% --reverse --border --preview 'ls -lh {}' " // &
210 "> " // trim(temp_file) // " 2>/dev/null"
211
212 ! Run fzf
213 call execute_command_line(trim(fzf_cmd), exitstat=stat, wait=.true.)
214
215 ! Restore raw mode
216 call execute_command_line("stty -icanon -echo min 1 time 0 2>/dev/null")
217
218 ! Read result if fzf succeeded
219 if (stat == 0) then
220 open(newunit=unit, file=temp_file, status='old', iostat=ios)
221 if (ios == 0) then
222 read(unit, '(a)', iostat=ios) result_path
223 if (ios == 0) then
224 ! Convert relative path to absolute
225 result_path = join_path(search_dir, result_path)
226 end if
227 close(unit)
228 end if
229 end if
230
231 ! Cleanup
232 call execute_command_line("rm -f " // trim(temp_file) // " 2>/dev/null")
233 end subroutine fzf_search
234
235 subroutine write_exit_dir(dir)
236 character(len=*), intent(in) :: dir
237 character(len=MAX_PATH) :: temp_file
238 integer :: unit, ios
239
240 ! Create temp file in HOME directory
241 call get_environment_variable("HOME", temp_file)
242 temp_file = trim(temp_file) // "/.fortress_cd"
243
244 open(newunit=unit, file=temp_file, status='replace', action='write', iostat=ios)
245 if (ios == 0) then
246 write(unit, '(a)') trim(dir)
247 close(unit)
248 end if
249 end subroutine write_exit_dir
250
251 subroutine open_file_in_default_app(filepath)
252 character(len=*), intent(in) :: filepath
253 character(len=MAX_PATH) :: editor, visual, platform, temp_file
254 character(len=MAX_PATH*2) :: open_cmd
255 integer :: stat, unit, ios
256
257 ! Try $EDITOR first (preferred for text editing)
258 call get_environment_variable("EDITOR", editor, status=stat)
259 if (stat == 0 .and. len_trim(editor) > 0) then
260 ! Restore terminal to normal mode (like fuss does for pager)
261 call execute_command_line("stty sane < /dev/tty", exitstat=stat)
262
263 ! Open with $EDITOR - wait for it to finish
264 open_cmd = trim(editor) // " '" // trim(filepath) // "'"
265 call execute_command_line(trim(open_cmd), exitstat=stat, wait=.true.)
266
267 ! Restore raw mode
268 call execute_command_line("stty -icanon -echo min 1 time 0 < /dev/tty", exitstat=stat)
269 return
270 end if
271
272 ! Try $VISUAL as fallback
273 call get_environment_variable("VISUAL", visual, status=stat)
274 if (stat == 0 .and. len_trim(visual) > 0) then
275 ! Restore terminal to normal mode
276 call execute_command_line("stty sane < /dev/tty", exitstat=stat)
277
278 ! Open with $VISUAL - wait for it to finish
279 open_cmd = trim(visual) // " '" // trim(filepath) // "'"
280 call execute_command_line(trim(open_cmd), exitstat=stat, wait=.true.)
281
282 ! Restore raw mode
283 call execute_command_line("stty -icanon -echo min 1 time 0 < /dev/tty", exitstat=stat)
284 return
285 end if
286
287 ! Fall back to platform-specific default application opener
288 ! Detect platform using uname
289 call get_environment_variable("HOME", temp_file)
290 temp_file = trim(temp_file) // "/.fortress_platform"
291 call execute_command_line("uname > " // trim(temp_file) // " 2>/dev/null", wait=.true.)
292
293 open(newunit=unit, file=temp_file, status='old', iostat=ios)
294 if (ios == 0) then
295 read(unit, '(a)', iostat=ios) platform
296 close(unit)
297 else
298 platform = "unknown"
299 end if
300 call execute_command_line("rm -f " // trim(temp_file) // " 2>/dev/null")
301
302 ! Restore terminal before launching (in case default app is terminal-based)
303 call execute_command_line("stty sane < /dev/tty", exitstat=stat)
304
305 ! Use platform-specific opener (run in background, don't wait)
306 if (index(platform, "Darwin") > 0) then
307 ! macOS - open launches apps in new windows (usually GUI)
308 open_cmd = "open '" // trim(filepath) // "' 2>/dev/null &"
309 else if (index(platform, "Linux") > 0) then
310 ! Linux - xdg-open uses desktop environment defaults
311 open_cmd = "xdg-open '" // trim(filepath) // "' 2>/dev/null &"
312 else
313 ! Unknown platform - try xdg-open as a reasonable default
314 open_cmd = "xdg-open '" // trim(filepath) // "' 2>/dev/null &"
315 end if
316
317 call execute_command_line(trim(open_cmd), wait=.false.)
318
319 ! Restore raw mode immediately (since we're not waiting for the app)
320 call execute_command_line("stty -icanon -echo min 1 time 0 < /dev/tty", exitstat=stat)
321 end subroutine open_file_in_default_app
322
323 subroutine rename_file_prompt(dir, current_name, new_name, was_renamed)
324 use iso_fortran_env, only: output_unit
325 use terminal_control, only: BOLD, RESET, YELLOW, ESC
326 character(len=*), intent(in) :: dir, current_name
327 character(len=*), intent(out) :: new_name
328 logical, intent(out) :: was_renamed
329 character(len=MAX_PATH) :: input_buffer
330 integer :: input_len
331 character(len=1) :: key
332 integer :: ios
333 logical :: editing
334
335 ! Initialize - pre-fill with current name
336 was_renamed = .false.
337 input_buffer = current_name
338 input_len = len_trim(current_name)
339 editing = .true.
340
341 do while (editing)
342 ! Move cursor to bottom of screen and show rename prompt
343 write(output_unit, '(a)', advance='no') ESC // "[999;1H" ! Move to bottom left
344 write(output_unit, '(a)', advance='no') ESC // "[K" ! Clear line
345 write(output_unit, '(a)', advance='no') YELLOW // BOLD // "Rename: " // RESET // &
346 trim(input_buffer(1:input_len))
347 call flush(output_unit)
348
349 ! Read single character
350 read(*, '(a1)', advance='no', iostat=ios) key
351 if (ios /= 0) cycle
352
353 select case(ichar(key))
354 case(27) ! ESC - cancel
355 editing = .false.
356 was_renamed = .false.
357 case(13, 10) ! Enter - confirm
358 if (input_len > 0 .and. trim(input_buffer(1:input_len)) /= trim(current_name)) then
359 new_name = input_buffer(1:input_len)
360 was_renamed = .true.
361 end if
362 editing = .false.
363 case(127, 8) ! Backspace
364 if (input_len > 0) then
365 input_len = input_len - 1
366 end if
367 case(32:126) ! Printable characters
368 if (input_len < MAX_PATH) then
369 input_len = input_len + 1
370 input_buffer(input_len:input_len) = key
371 end if
372 end select
373 end do
374
375 ! Clear the prompt line
376 write(output_unit, '(a)', advance='no') ESC // "[999;1H" ! Move to bottom
377 write(output_unit, '(a)', advance='no') ESC // "[K" ! Clear line
378 call flush(output_unit)
379 end subroutine rename_file_prompt
380
381 end module filesystem_ops
382