Fortran · 63927 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: system_interface
3 ! Purpose: C function interfaces and system call wrappers
4 ! ==============================================================================
5 module system_interface
6 use iso_c_binding
7 use shell_types
8 implicit none
9
10 ! Signal numbers (platform-specific)
11 #ifdef __APPLE__
12 ! macOS/Darwin signal numbers
13 integer(c_int), parameter :: SIGINT = 2
14 integer(c_int), parameter :: SIGPIPE = 13
15 integer(c_int), parameter :: SIGTSTP = 18
16 integer(c_int), parameter :: SIGCHLD = 20
17 integer(c_int), parameter :: SIGCONT = 19
18 integer(c_int), parameter :: SIGTTIN = 21
19 integer(c_int), parameter :: SIGTTOU = 22
20 #else
21 ! Linux signal numbers
22 integer(c_int), parameter :: SIGINT = 2
23 integer(c_int), parameter :: SIGPIPE = 13
24 integer(c_int), parameter :: SIGTSTP = 20
25 integer(c_int), parameter :: SIGCHLD = 17
26 integer(c_int), parameter :: SIGCONT = 18
27 integer(c_int), parameter :: SIGTTIN = 21
28 integer(c_int), parameter :: SIGTTOU = 22
29 #endif
30
31 ! Wait options
32 integer(c_int), parameter :: WNOHANG = 1
33 integer(c_int), parameter :: WUNTRACED = 2
34
35 ! Terminal control structures and constants
36 #ifdef __APPLE__
37 ! macOS has NCCS=20 and uses 8-byte tcflag_t
38 integer(c_int), parameter :: NCCS = 20
39 ! ioctl request for terminal size (macOS)
40 integer(c_long), parameter :: TIOCGWINSZ = int(z'40087468', c_long)
41 #else
42 ! Linux has NCCS=32 and uses 4-byte tcflag_t
43 integer(c_int), parameter :: NCCS = 32
44 ! ioctl request for terminal size (Linux)
45 integer(c_long), parameter :: TIOCGWINSZ = int(z'5413', c_long)
46 #endif
47
48 ! Window size structure for terminal dimensions
49 type, bind(c) :: winsize_t
50 integer(c_short) :: ws_row ! Number of rows
51 integer(c_short) :: ws_col ! Number of columns
52 integer(c_short) :: ws_xpixel ! Horizontal pixels
53 integer(c_short) :: ws_ypixel ! Vertical pixels
54 end type winsize_t
55
56 ! termios structure - must match C struct termios exactly
57 type, bind(c) :: termios_t
58 #ifdef __APPLE__
59 ! macOS: tcflag_t is unsigned long (8 bytes)
60 integer(c_long) :: c_iflag ! input flags (8 bytes)
61 integer(c_long) :: c_oflag ! output flags (8 bytes)
62 integer(c_long) :: c_cflag ! control flags (8 bytes)
63 integer(c_long) :: c_lflag ! local flags (8 bytes)
64 character(c_char) :: c_cc(20) ! control characters (20 bytes)
65 character(c_char) :: padding(4) ! padding for alignment (4 bytes)
66 integer(c_long) :: c_ispeed ! input speed (8 bytes)
67 integer(c_long) :: c_ospeed ! output speed (8 bytes)
68 ! Total: 72 bytes
69 #else
70 ! Linux: tcflag_t is unsigned int (4 bytes)
71 integer(c_int) :: c_iflag ! input flags (4 bytes)
72 integer(c_int) :: c_oflag ! output flags (4 bytes)
73 integer(c_int) :: c_cflag ! control flags (4 bytes)
74 integer(c_int) :: c_lflag ! local flags (4 bytes)
75 character(c_char) :: c_line ! line discipline (1 byte)
76 character(c_char) :: c_cc(NCCS) ! control characters (32 bytes)
77 character(c_char) :: padding(3) ! padding for alignment (3 bytes)
78 integer(c_int) :: c_ispeed ! input speed (4 bytes)
79 integer(c_int) :: c_ospeed ! output speed (4 bytes)
80 ! Total: 60 bytes (matches actual Linux struct termios)
81 #endif
82 end type termios_t
83
84 ! Terminal flags (platform-specific values)
85 #ifdef __APPLE__
86 ! macOS/Darwin values from sys/termios.h - use c_long to match tcflag_t
87 integer(c_long), parameter :: ICANON = int(z'00000100', c_long) ! canonical input
88 integer(c_long), parameter :: ECHO = int(z'00000008', c_long) ! enable echo
89 integer(c_long), parameter :: ECHOE = int(z'00000002', c_long) ! echo erase character
90 integer(c_long), parameter :: ECHOK = int(z'00000004', c_long) ! echo kill character
91 integer(c_long), parameter :: ECHONL = int(z'00000010', c_long) ! echo NL even if ECHO is off
92 integer(c_long), parameter :: IEXTEN = int(z'00000400', c_long) ! extended input processing
93 integer(c_long), parameter :: ISIG = int(z'00000080', c_long) ! enable signals
94
95 ! Control character indices (macOS)
96 integer(c_int), parameter :: VEOF = 0 ! EOF character (Ctrl-D)
97 integer(c_int), parameter :: VEOL = 1 ! EOL character
98 integer(c_int), parameter :: VEOL2 = 2 ! EOL2 character
99 integer(c_int), parameter :: VERASE = 3 ! ERASE character
100 integer(c_int), parameter :: VWERASE = 4 ! WERASE character
101 integer(c_int), parameter :: VKILL = 5 ! KILL character
102 integer(c_int), parameter :: VREPRINT = 6 ! REPRINT character
103 integer(c_int), parameter :: VINTR = 8 ! INTR character (Ctrl-C)
104 integer(c_int), parameter :: VQUIT = 9 ! QUIT character
105 integer(c_int), parameter :: VSUSP = 10 ! SUSP character (Ctrl-Z)
106 integer(c_int), parameter :: VDSUSP = 11 ! DSUSP character
107 integer(c_int), parameter :: VSTART = 12 ! START character (Ctrl-Q)
108 integer(c_int), parameter :: VSTOP = 13 ! STOP character (Ctrl-S)
109 integer(c_int), parameter :: VLNEXT = 14 ! LNEXT character
110 integer(c_int), parameter :: VDISCARD = 15 ! DISCARD character
111 integer(c_int), parameter :: VMIN = 16 ! minimum chars for noncanonical read
112 integer(c_int), parameter :: VTIME = 17 ! timeout for noncanonical read
113 #else
114 ! Linux values from bits/termios.h - use c_int to match tcflag_t
115 integer(c_int), parameter :: ICANON = int(z'00000002', c_int) ! canonical input
116 integer(c_int), parameter :: ECHO = int(z'00000008', c_int) ! enable echo
117 integer(c_int), parameter :: ECHOE = int(z'00000010', c_int) ! echo erase character
118 integer(c_int), parameter :: ECHOK = int(z'00000020', c_int) ! echo kill character
119 integer(c_int), parameter :: ECHONL = int(z'00000040', c_int) ! echo NL even if ECHO is off
120 integer(c_int), parameter :: IEXTEN = int(z'00008000', c_int) ! extended input processing
121 integer(c_int), parameter :: ISIG = int(z'00000001', c_int) ! enable signals
122
123 ! Control character indices (Linux)
124 integer(c_int), parameter :: VMIN = 6 ! minimum chars for noncanonical read
125 integer(c_int), parameter :: VTIME = 5 ! timeout for noncanonical read
126 #endif
127
128 ! tcsetattr options
129 integer(c_int), parameter :: TCSANOW = 0 ! change immediately
130 integer(c_int), parameter :: TCSADRAIN = 1 ! change after output drained
131 integer(c_int), parameter :: TCSAFLUSH = 2 ! change after output drained and input flushed
132
133 ! ANSI escape sequences for cursor control
134 character(len=*), parameter :: ESC_CLEAR_LINE = char(27) // '[K'
135 character(len=*), parameter :: ESC_MOVE_BOL = char(13) ! Carriage return
136 character(len=*), parameter :: ESC_CURSOR_LEFT = char(27) // '[D'
137 character(len=*), parameter :: ESC_CURSOR_RIGHT = char(27) // '[C'
138 character(len=*), parameter :: ESC_SAVE_CURSOR = char(27) // '[s'
139 character(len=*), parameter :: ESC_RESTORE_CURSOR = char(27) // '[u'
140 character(len=*), parameter :: ESC_HIDE_CURSOR = char(27) // '[?25l'
141 character(len=*), parameter :: ESC_SHOW_CURSOR = char(27) // '[?25h'
142
143 ! stat structure (must be defined before interface block)
144 ! Platform-specific layouts - field order matters!
145 type, bind(c) :: stat_t
146 #ifdef __APPLE__
147 ! macOS ARM64/x86_64 stat structure layout
148 integer(c_int) :: st_dev ! Device (4 bytes) - offset 0
149 integer(c_short) :: st_mode ! File type and mode (2 bytes) - offset 4
150 integer(c_short) :: st_nlink ! Number of hard links (2 bytes) - offset 6
151 integer(c_long) :: st_ino ! Inode (8 bytes) - offset 8
152 integer(c_int) :: st_uid ! User ID (4 bytes) - offset 16
153 integer(c_int) :: st_gid ! Group ID (4 bytes) - offset 20
154 integer(c_int) :: st_rdev ! Device type (4 bytes) - offset 24
155 ! Timespec structures (16 bytes each = 8 bytes + 8 bytes)
156 integer(c_long) :: st_atimespec_sec ! Access time seconds
157 integer(c_long) :: st_atimespec_nsec ! Access time nanoseconds
158 integer(c_long) :: st_mtimespec_sec ! Modification time seconds
159 integer(c_long) :: st_mtimespec_nsec ! Modification time nanoseconds
160 integer(c_long) :: st_ctimespec_sec ! Status change time seconds
161 integer(c_long) :: st_ctimespec_nsec ! Status change time nanoseconds
162 integer(c_long) :: st_birthtimespec_sec ! Birth time seconds
163 integer(c_long) :: st_birthtimespec_nsec ! Birth time nanoseconds
164 integer(c_long) :: st_size ! Total size in bytes (8 bytes)
165 integer(c_long) :: st_blocks ! Number of 512-byte blocks (8 bytes)
166 integer(c_int) :: st_blksize ! Optimal block size (4 bytes)
167 integer(c_int) :: st_flags ! User defined flags (4 bytes)
168 integer(c_int) :: st_gen ! File generation number (4 bytes)
169 integer(c_int) :: st_lspare ! Reserved (4 bytes)
170 integer(c_long) :: st_qspare(2) ! Reserved (16 bytes)
171 #elif defined(__aarch64__)
172 ! Linux aarch64 stat structure layout (glibc generic 64-bit)
173 ! Field order and sizes differ from x86_64: mode/nlink swapped, nlink is 4B not 8B
174 integer(c_long) :: st_dev ! Device (8 bytes) - offset 0
175 integer(c_long) :: st_ino ! Inode (8 bytes) - offset 8
176 integer(c_int) :: st_mode ! File type and mode (4 bytes) - offset 16
177 integer(c_int) :: st_nlink ! Number of hard links (4 bytes) - offset 20
178 integer(c_int) :: st_uid ! User ID (4 bytes) - offset 24
179 integer(c_int) :: st_gid ! Group ID (4 bytes) - offset 28
180 integer(c_long) :: st_rdev ! Device type (8 bytes) - offset 32
181 integer(c_long) :: pad1 ! __pad1 (8 bytes) - offset 40
182 integer(c_long) :: st_size ! Total size in bytes (8 bytes) - offset 48
183 integer(c_int) :: st_blksize ! Optimal block size (4 bytes) - offset 56
184 integer(c_int) :: pad2 ! __pad2 (4 bytes) - offset 60
185 integer(c_long) :: st_blocks ! Number of 512-byte blocks (8 bytes) - offset 64
186 ! Time fields (struct timespec = 16 bytes each)
187 integer(c_long) :: st_atime ! Access time seconds - offset 72
188 integer(c_long) :: st_atime_nsec ! Access time nanoseconds - offset 80
189 integer(c_long) :: st_mtime ! Modification time seconds - offset 88
190 integer(c_long) :: st_mtime_nsec ! Modification time nanoseconds - offset 96
191 integer(c_long) :: st_ctime ! Status change time seconds - offset 104
192 integer(c_long) :: st_ctime_nsec ! Status change time nanoseconds - offset 112
193 integer(c_int) :: glibc_reserved(2) ! Reserved (8 bytes) - offset 120
194 ! Total: 128 bytes
195 #else
196 ! Linux x86_64 stat structure layout
197 integer(c_long) :: st_dev ! Device (8 bytes)
198 integer(c_long) :: st_ino ! Inode (8 bytes)
199 integer(c_long) :: st_nlink ! Number of hard links (8 bytes)
200 integer(c_int) :: st_mode ! File type and mode (4 bytes)
201 integer(c_int) :: st_uid ! User ID (4 bytes)
202 integer(c_int) :: st_gid ! Group ID (4 bytes)
203 integer(c_int) :: pad0 ! Padding (4 bytes)
204 integer(c_long) :: st_rdev ! Device type (8 bytes)
205 integer(c_long) :: st_size ! Total size in bytes (8 bytes)
206 integer(c_long) :: st_blksize ! Optimal block size (8 bytes)
207 integer(c_long) :: st_blocks ! Number of 512-byte blocks (8 bytes)
208 ! Time fields
209 integer(c_long) :: st_atime ! Access time seconds
210 integer(c_long) :: st_atime_nsec ! Access time nanoseconds
211 integer(c_long) :: st_mtime ! Modification time seconds
212 integer(c_long) :: st_mtime_nsec ! Modification time nanoseconds
213 integer(c_long) :: st_ctime ! Status change time seconds
214 integer(c_long) :: st_ctime_nsec ! Status change time nanoseconds
215 integer(c_long) :: glibc_reserved(3) ! Reserved
216 #endif
217 end type stat_t
218
219 ! timeval structure for getrusage
220 type, bind(c) :: timeval_t
221 integer(c_long) :: tv_sec ! Seconds
222 integer(c_long) :: tv_usec ! Microseconds
223 end type timeval_t
224
225 ! rusage structure for getrusage
226 type, bind(c) :: rusage_t
227 type(timeval_t) :: ru_utime ! User CPU time used
228 type(timeval_t) :: ru_stime ! System CPU time used
229 integer(c_long) :: ru_maxrss ! Maximum resident set size
230 integer(c_long) :: ru_ixrss ! Integral shared memory size
231 integer(c_long) :: ru_idrss ! Integral unshared data size
232 integer(c_long) :: ru_isrss ! Integral unshared stack size
233 integer(c_long) :: ru_minflt ! Page reclaims (soft page faults)
234 integer(c_long) :: ru_majflt ! Page faults (hard page faults)
235 integer(c_long) :: ru_nswap ! Swaps
236 integer(c_long) :: ru_inblock ! Block input operations
237 integer(c_long) :: ru_oublock ! Block output operations
238 integer(c_long) :: ru_msgsnd ! IPC messages sent
239 integer(c_long) :: ru_msgrcv ! IPC messages received
240 integer(c_long) :: ru_nsignals ! Signals received
241 integer(c_long) :: ru_nvcsw ! Voluntary context switches
242 integer(c_long) :: ru_nivcsw ! Involuntary context switches
243 end type rusage_t
244
245 ! getrusage who parameter values
246 integer(c_int), parameter :: RUSAGE_SELF = 0
247 integer(c_int), parameter :: RUSAGE_CHILDREN = -1
248
249 ! rlimit structure for getrlimit/setrlimit
250 type, bind(c) :: rlimit_t
251 integer(c_long) :: rlim_cur ! Current (soft) limit
252 integer(c_long) :: rlim_max ! Maximum (hard) limit
253 end type rlimit_t
254
255 ! Resource limit constants
256 integer(c_int), parameter :: RLIMIT_CPU = 0 ! CPU time in seconds
257 integer(c_int), parameter :: RLIMIT_FSIZE = 1 ! Maximum file size
258 integer(c_int), parameter :: RLIMIT_DATA = 2 ! Maximum data segment size
259 integer(c_int), parameter :: RLIMIT_STACK = 3 ! Maximum stack size
260 integer(c_int), parameter :: RLIMIT_CORE = 4 ! Maximum core file size
261 integer(c_int), parameter :: RLIMIT_RSS = 5 ! Maximum resident set size
262 #ifdef __APPLE__
263 ! macOS uses different constant values than Linux for these
264 integer(c_int), parameter :: RLIMIT_MEMLOCK = 6
265 integer(c_int), parameter :: RLIMIT_NPROC = 7
266 integer(c_int), parameter :: RLIMIT_NOFILE = 8
267 integer(c_int), parameter :: RLIMIT_AS = 5 ! Same as RSS on macOS
268 #else
269 integer(c_int), parameter :: RLIMIT_NPROC = 6
270 integer(c_int), parameter :: RLIMIT_NOFILE = 7
271 integer(c_int), parameter :: RLIMIT_MEMLOCK = 8
272 integer(c_int), parameter :: RLIMIT_AS = 9
273 integer(c_int), parameter :: RLIMIT_LOCKS = 10
274 integer(c_int), parameter :: RLIMIT_SIGPENDING = 11
275 #endif
276 integer(c_int), parameter :: RLIMIT_MSGQUEUE = 12 ! Maximum bytes in POSIX message queues
277
278 ! Infinite limit value
279 #ifdef __APPLE__
280 integer(c_long), parameter :: RLIM_INFINITY = 9223372036854775807_c_long ! 0x7FFFFFFFFFFFFFFF on macOS
281 #else
282 integer(c_long), parameter :: RLIM_INFINITY = -1 ! (unsigned long)-1 on Linux
283 #endif
284
285 ! C function interfaces
286 interface
287 function c_fork() bind(C, name="fork")
288 import :: c_pid_t
289 integer(c_pid_t) :: c_fork
290 end function
291
292 function c_execvp(file, argv) bind(C, name="execvp")
293 import :: c_ptr, c_int
294 type(c_ptr), value :: file, argv
295 integer(c_int) :: c_execvp
296 end function
297
298 function c_waitpid(pid, status, options) bind(C, name="waitpid")
299 import :: c_pid_t, c_ptr, c_int
300 integer(c_pid_t), value :: pid
301 type(c_ptr), value :: status
302 integer(c_int), value :: options
303 integer(c_pid_t) :: c_waitpid
304 end function
305
306 function c_gethostname(name, len) bind(C, name="gethostname")
307 import :: c_ptr, c_size_t, c_int
308 type(c_ptr), value :: name
309 integer(c_size_t), value :: len
310 integer(c_int) :: c_gethostname
311 end function
312
313 function c_getenv(name) bind(C, name="getenv")
314 import :: c_ptr
315 type(c_ptr), value :: name
316 type(c_ptr) :: c_getenv
317 end function
318
319 function c_setenv(name, value, overwrite) bind(C, name="setenv")
320 import :: c_ptr, c_int
321 type(c_ptr), value :: name, value
322 integer(c_int), value :: overwrite
323 integer(c_int) :: c_setenv
324 end function
325
326 function c_unsetenv(name) bind(C, name="unsetenv")
327 import :: c_ptr, c_int
328 type(c_ptr), value :: name
329 integer(c_int) :: c_unsetenv
330 end function
331
332 function c_chdir(path) bind(C, name="chdir")
333 import :: c_ptr, c_int
334 type(c_ptr), value :: path
335 integer(c_int) :: c_chdir
336 end function
337
338 function c_getcwd(buf, size) bind(C, name="getcwd")
339 import :: c_ptr, c_size_t
340 type(c_ptr), value :: buf
341 integer(c_size_t), value :: size
342 type(c_ptr) :: c_getcwd
343 end function
344
345 function c_open(pathname, flags, mode) bind(C, name="open")
346 import :: c_ptr, c_int
347 type(c_ptr), value :: pathname
348 integer(c_int), value :: flags, mode
349 integer(c_int) :: c_open
350 end function
351
352 function c_close(fd) bind(C, name="close")
353 import :: c_int
354 integer(c_int), value :: fd
355 integer(c_int) :: c_close
356 end function
357
358 function c_dup(fd) bind(C, name="dup")
359 import :: c_int
360 integer(c_int), value :: fd
361 integer(c_int) :: c_dup
362 end function
363
364 function c_dup2(oldfd, newfd) bind(C, name="dup2")
365 import :: c_int
366 integer(c_int), value :: oldfd, newfd
367 integer(c_int) :: c_dup2
368 end function
369
370 function c_pipe(pipefd) bind(C, name="pipe")
371 import :: c_ptr, c_int
372 type(c_ptr), value :: pipefd
373 integer(c_int) :: c_pipe
374 end function
375
376 function c_getpid() bind(C, name="getpid")
377 import :: c_pid_t
378 integer(c_pid_t) :: c_getpid
379 end function
380
381 function c_getpgid(pid) bind(C, name="getpgid")
382 import :: c_pid_t
383 integer(c_pid_t), value :: pid
384 integer(c_pid_t) :: c_getpgid
385 end function
386
387 function c_setpgid(pid, pgid) bind(C, name="setpgid")
388 import :: c_pid_t, c_int
389 integer(c_pid_t), value :: pid, pgid
390 integer(c_int) :: c_setpgid
391 end function
392
393 function c_tcgetpgrp(fd) bind(C, name="tcgetpgrp")
394 import :: c_int, c_pid_t
395 integer(c_int), value :: fd
396 integer(c_pid_t) :: c_tcgetpgrp
397 end function
398
399 function c_tcsetpgrp(fd, pgrp) bind(C, name="tcsetpgrp")
400 import :: c_int, c_pid_t
401 integer(c_int), value :: fd
402 integer(c_pid_t), value :: pgrp
403 integer(c_int) :: c_tcsetpgrp
404 end function
405
406 function c_kill(pid, sig) bind(C, name="kill")
407 import :: c_pid_t, c_int
408 integer(c_pid_t), value :: pid
409 integer(c_int), value :: sig
410 integer(c_int) :: c_kill
411 end function
412
413 function c_signal(signum, handler) bind(C, name="signal")
414 import :: c_int, c_funptr
415 integer(c_int), value :: signum
416 type(c_funptr), value :: handler
417 type(c_funptr) :: c_signal
418 end function
419
420 function c_isatty(fd) bind(C, name="isatty")
421 import :: c_int
422 integer(c_int), value :: fd
423 integer(c_int) :: c_isatty
424 end function
425
426 function c_write(fd, buf, count) bind(C, name="write")
427 import :: c_int, c_ptr, c_size_t, c_intptr_t
428 integer(c_int), value :: fd
429 type(c_ptr), value :: buf
430 integer(c_size_t), value :: count
431 integer(c_intptr_t) :: c_write ! ssize_t is signed, returns -1 on error
432 end function
433
434 function c_popen(command, type) bind(C, name="popen")
435 import :: c_ptr
436 type(c_ptr), value :: command, type
437 type(c_ptr) :: c_popen
438 end function
439
440 function c_pclose(stream) bind(C, name="pclose")
441 import :: c_ptr, c_int
442 type(c_ptr), value :: stream
443 integer(c_int) :: c_pclose
444 end function
445
446 function c_fgets(s, size, stream) bind(C, name="fgets")
447 import :: c_ptr, c_int
448 type(c_ptr), value :: s
449 integer(c_int), value :: size
450 type(c_ptr), value :: stream
451 type(c_ptr) :: c_fgets
452 end function
453
454 ! Write a null-terminated string to a stream (shift phase Sprint 5:
455 ! used to pipe text into clipboard tools like pbcopy via popen("w")).
456 function c_fputs(s, stream) bind(C, name="fputs")
457 import :: c_ptr, c_int
458 type(c_ptr), value :: s
459 type(c_ptr), value :: stream
460 integer(c_int) :: c_fputs
461 end function
462
463 subroutine c_exit(status) bind(C, name="exit")
464 import :: c_int
465 integer(c_int), value :: status
466 end subroutine
467
468 subroutine c_perror(s) bind(C, name="perror")
469 import :: c_ptr
470 type(c_ptr), value :: s
471 end subroutine
472
473 ! Terminal control functions
474 function c_tcgetattr(fd, termios_p) bind(C, name="tcgetattr")
475 import :: c_int, termios_t
476 integer(c_int), value :: fd
477 type(termios_t), intent(out) :: termios_p
478 integer(c_int) :: c_tcgetattr
479 end function
480
481 function c_tcsetattr(fd, optional_actions, termios_p) bind(C, name="tcsetattr")
482 import :: c_int, termios_t
483 integer(c_int), value :: fd, optional_actions
484 type(termios_t), intent(in) :: termios_p
485 integer(c_int) :: c_tcsetattr
486 end function
487
488 function c_read(fd, buf, count) bind(C, name="read")
489 import :: c_int, c_ptr, c_size_t
490 integer(c_int), value :: fd
491 type(c_ptr), value :: buf
492 integer(c_size_t), value :: count
493 integer(c_size_t) :: c_read
494 end function
495
496 subroutine c_cfmakeraw(termios_p) bind(C, name="cfmakeraw")
497 import :: termios_t
498 type(termios_t), intent(inout) :: termios_p
499 end subroutine
500
501 function c_getppid() bind(C, name="getppid")
502 import :: c_pid_t
503 integer(c_pid_t) :: c_getppid
504 end function
505
506 function c_getuid() bind(C, name="getuid")
507 import :: c_int
508 integer(c_int) :: c_getuid
509 end function
510
511 function c_geteuid() bind(C, name="geteuid")
512 import :: c_int
513 integer(c_int) :: c_geteuid
514 end function
515
516 function c_stat(pathname, statbuf) bind(C, name="stat")
517 import :: c_ptr, c_int, stat_t
518 type(c_ptr), value :: pathname
519 type(stat_t), intent(out) :: statbuf
520 integer(c_int) :: c_stat
521 end function
522
523 function c_lstat(pathname, statbuf) bind(C, name="lstat")
524 import :: c_ptr, c_int, stat_t
525 type(c_ptr), value :: pathname
526 type(stat_t), intent(out) :: statbuf
527 integer(c_int) :: c_lstat
528 end function
529
530 ! Portable stat helpers — bypass Fortran struct stat layout across architectures
531 function fortsh_stat_mode(pathname) bind(C, name="fortsh_stat_mode")
532 import :: c_ptr, c_int
533 type(c_ptr), value :: pathname
534 integer(c_int) :: fortsh_stat_mode
535 end function
536
537 function fortsh_lstat_mode(pathname) bind(C, name="fortsh_lstat_mode")
538 import :: c_ptr, c_int
539 type(c_ptr), value :: pathname
540 integer(c_int) :: fortsh_lstat_mode
541 end function
542
543 function fortsh_stat_size(pathname) bind(C, name="fortsh_stat_size")
544 import :: c_ptr, c_long_long
545 type(c_ptr), value :: pathname
546 integer(c_long_long) :: fortsh_stat_size
547 end function
548
549 function fortsh_stat_uid(pathname) bind(C, name="fortsh_stat_uid")
550 import :: c_ptr, c_int
551 type(c_ptr), value :: pathname
552 integer(c_int) :: fortsh_stat_uid
553 end function
554
555 function fortsh_stat_mtime(pathname) bind(C, name="fortsh_stat_mtime")
556 import :: c_ptr, c_long_long
557 type(c_ptr), value :: pathname
558 integer(c_long_long) :: fortsh_stat_mtime
559 end function
560
561 function fortsh_stat_dev(pathname) bind(C, name="fortsh_stat_dev")
562 import :: c_ptr, c_long_long
563 type(c_ptr), value :: pathname
564 integer(c_long_long) :: fortsh_stat_dev
565 end function
566
567 function fortsh_stat_ino(pathname) bind(C, name="fortsh_stat_ino")
568 import :: c_ptr, c_long_long
569 type(c_ptr), value :: pathname
570 integer(c_long_long) :: fortsh_stat_ino
571 end function
572
573 function c_access(pathname, mode) bind(C, name="access")
574 import :: c_ptr, c_int
575 type(c_ptr), value :: pathname
576 integer(c_int), value :: mode
577 integer(c_int) :: c_access
578 end function
579
580 function c_umask(mask) bind(C, name="umask")
581 import :: c_int
582 integer(c_int), value :: mask
583 integer(c_int) :: c_umask
584 end function
585
586 function c_getrusage(who, usage) bind(C, name="getrusage")
587 import :: c_int, rusage_t
588 integer(c_int), value :: who
589 type(rusage_t), intent(out) :: usage
590 integer(c_int) :: c_getrusage
591 end function
592
593 function c_getrlimit(resource, rlim) bind(C, name="getrlimit")
594 import :: c_int, rlimit_t
595 integer(c_int), value :: resource
596 type(rlimit_t), intent(out) :: rlim
597 integer(c_int) :: c_getrlimit
598 end function
599
600 function c_setrlimit(resource, rlim) bind(C, name="setrlimit")
601 import :: c_int, rlimit_t
602 integer(c_int), value :: resource
603 type(rlimit_t), intent(in) :: rlim
604 integer(c_int) :: c_setrlimit
605 end function
606
607 function c_mkfifo(pathname, mode) bind(C, name="mkfifo")
608 import :: c_ptr, c_int
609 type(c_ptr), value :: pathname
610 integer(c_int), value :: mode
611 integer(c_int) :: c_mkfifo
612 end function
613
614 function c_unlink(pathname) bind(C, name="unlink")
615 import :: c_ptr, c_int
616 type(c_ptr), value :: pathname
617 integer(c_int) :: c_unlink
618 end function
619
620 function c_ioctl(fd, request, argp) bind(C, name="ioctl")
621 import :: c_int, c_long, c_ptr
622 integer(c_int), value :: fd
623 integer(c_long), value :: request
624 type(c_ptr), value :: argp
625 integer(c_int) :: c_ioctl
626 end function
627
628 ! C wrapper for getting terminal size
629 function get_term_size_c(rows, cols) bind(C, name="get_term_size_c")
630 import :: c_int
631 integer(c_int) :: rows, cols
632 integer(c_int) :: get_term_size_c
633 end function
634
635 ! Get environ pointer (array of environment strings)
636 function c_get_environ_ptr(idx) bind(C, name="get_environ_ptr")
637 import :: c_ptr, c_int
638 integer(c_int), value :: idx
639 type(c_ptr) :: c_get_environ_ptr
640 end function
641 end interface
642
643 ! Signal handler types (initialized in module initialization)
644 type(c_funptr) :: SIG_DFL, SIG_IGN
645
646 ! File flags for open() - platform-specific values
647 integer(c_int), parameter :: O_RDONLY = 0
648 integer(c_int), parameter :: O_WRONLY = 1
649 ! macOS/Darwin values (TODO: add Linux support)
650 integer(c_int), parameter :: O_CREAT = 512 ! 0x200 on macOS, 0x40 on Linux
651 integer(c_int), parameter :: O_TRUNC = 1024 ! 0x400 on macOS, 0x200 on Linux
652 integer(c_int), parameter :: O_APPEND = 8 ! 0x8 on macOS, 0x400 on Linux
653
654 ! File descriptors
655 integer(c_int), parameter :: STDIN_FD = 0
656 integer(c_int), parameter :: STDOUT_FD = 1
657 integer(c_int), parameter :: STDERR_FD = 2
658
659 ! File mode bits (for stat st_mode field)
660 integer(c_int), parameter :: S_IFMT = int(o'170000', c_int) ! File type mask
661 integer(c_int), parameter :: S_IFREG = int(o'100000', c_int) ! Regular file
662 integer(c_int), parameter :: S_IFDIR = int(o'040000', c_int) ! Directory
663 integer(c_int), parameter :: S_IFLNK = int(o'120000', c_int) ! Symbolic link
664 integer(c_int), parameter :: S_IFBLK = int(o'060000', c_int) ! Block device
665 integer(c_int), parameter :: S_IFCHR = int(o'020000', c_int) ! Character device
666 integer(c_int), parameter :: S_IFIFO = int(o'010000', c_int) ! FIFO (named pipe)
667 integer(c_int), parameter :: S_IFSOCK = int(o'140000', c_int) ! Socket
668
669 integer(c_int), parameter :: S_ISUID = int(o'004000', c_int) ! Set UID bit
670 integer(c_int), parameter :: S_ISGID = int(o'002000', c_int) ! Set GID bit
671 integer(c_int), parameter :: S_ISVTX = int(o'001000', c_int) ! Sticky bit
672
673 integer(c_int), parameter :: S_IRUSR = int(o'000400', c_int) ! Owner read
674 integer(c_int), parameter :: S_IWUSR = int(o'000200', c_int) ! Owner write
675 integer(c_int), parameter :: S_IXUSR = int(o'000100', c_int) ! Owner execute
676 integer(c_int), parameter :: S_IRGRP = int(o'000040', c_int) ! Group read
677 integer(c_int), parameter :: S_IWGRP = int(o'000020', c_int) ! Group write
678 integer(c_int), parameter :: S_IXGRP = int(o'000010', c_int) ! Group execute
679 integer(c_int), parameter :: S_IROTH = int(o'000004', c_int) ! Others read
680 integer(c_int), parameter :: S_IWOTH = int(o'000002', c_int) ! Others write
681 integer(c_int), parameter :: S_IXOTH = int(o'000001', c_int) ! Others execute
682
683 ! Access mode flags
684 integer(c_int), parameter :: F_OK = 0 ! File exists
685 integer(c_int), parameter :: R_OK = 4 ! Read permission
686 integer(c_int), parameter :: W_OK = 2 ! Write permission
687 integer(c_int), parameter :: X_OK = 1 ! Execute permission
688
689 contains
690
691 function get_environment_var(var_name) result(value)
692 character(len=*), intent(in) :: var_name
693 character(len=:), allocatable :: value
694 type(c_ptr) :: c_value_ptr
695 character(kind=c_char), pointer :: c_value(:)
696 integer :: i
697 character(len=256), target :: c_var_name
698
699 c_var_name = trim(var_name)//c_null_char
700 c_value_ptr = c_getenv(c_loc(c_var_name))
701
702 if (c_associated(c_value_ptr)) then
703 call c_f_pointer(c_value_ptr, c_value, [MAX_ENV_LEN])
704
705 do i = 1, MAX_ENV_LEN
706 if (c_value(i) == c_null_char) exit
707 end do
708
709 allocate(character(len=i-1) :: value)
710 do i = 1, len(value)
711 value(i:i) = c_value(i)
712 end do
713 else
714 allocate(character(len=0) :: value)
715 end if
716 end function
717
718 function set_environment_var(var_name, var_value) result(success)
719 character(len=*), intent(in) :: var_name, var_value
720 logical :: success
721 integer :: ret
722 character(len=MAX_TOKEN_LEN), target :: c_var_name
723 character(len=MAX_PATH_LEN), target :: c_var_value
724
725 c_var_name = trim(var_name)//c_null_char
726 c_var_value = trim(var_value)//c_null_char
727 ret = c_setenv(c_loc(c_var_name), c_loc(c_var_value), 1_c_int)
728 success = (ret == 0)
729 end function
730
731 subroutine unset_environment_var(var_name)
732 character(len=*), intent(in) :: var_name
733 integer :: ret
734 character(len=256), target :: c_var_name
735
736 c_var_name = trim(var_name)//c_null_char
737 ret = c_unsetenv(c_loc(c_var_name))
738 ! Ignore return value - unsetenv doesn't typically fail
739 end subroutine
740
741 ! Get environment entry by index (for iterating through all env vars)
742 ! Returns empty string when index is beyond end of environ array
743 function get_environ_entry(idx) result(entry)
744 integer, intent(in) :: idx
745 character(len=:), allocatable :: entry
746 type(c_ptr) :: c_entry_ptr
747 character(kind=c_char), pointer :: c_entry(:)
748 integer :: i
749
750 c_entry_ptr = c_get_environ_ptr(int(idx, c_int))
751
752 if (c_associated(c_entry_ptr)) then
753 call c_f_pointer(c_entry_ptr, c_entry, [MAX_ENV_LEN])
754
755 do i = 1, MAX_ENV_LEN
756 if (c_entry(i) == c_null_char) exit
757 end do
758
759 allocate(character(len=i-1) :: entry)
760 do i = 1, len(entry)
761 entry(i:i) = c_entry(i)
762 end do
763 else
764 allocate(character(len=0) :: entry)
765 end if
766 end function
767
768 function change_directory(path) result(success)
769 character(len=*), intent(in) :: path
770 logical :: success
771 integer :: ret
772 character(len=256), target :: c_path
773
774 c_path = trim(path)//c_null_char
775 ret = c_chdir(c_loc(c_path))
776 success = (ret == 0)
777 end function
778
779 function get_current_directory() result(path)
780 character(len=:), allocatable :: path
781 character(kind=c_char), target :: c_path(MAX_PATH_LEN)
782 type(c_ptr) :: ret_ptr
783 integer :: i
784
785 ret_ptr = c_getcwd(c_loc(c_path), int(MAX_PATH_LEN, c_size_t))
786
787 if (c_associated(ret_ptr)) then
788 do i = 1, MAX_PATH_LEN
789 if (c_path(i) == c_null_char) exit
790 end do
791
792 allocate(character(len=i-1) :: path)
793 do i = 1, len(path)
794 path(i:i) = c_path(i)
795 end do
796 else
797 allocate(character(len=0) :: path)
798 end if
799 end function
800
801 function create_pipe(read_fd, write_fd) result(success)
802 integer(c_int), intent(out) :: read_fd, write_fd
803 logical :: success
804 integer(c_int), target :: pipefd(2)
805 integer :: ret
806
807 ret = c_pipe(c_loc(pipefd))
808 if (ret == 0) then
809 read_fd = pipefd(1)
810 write_fd = pipefd(2)
811 success = .true.
812 else
813 success = .false.
814 end if
815 end function
816
817 ! Check process status macros
818 function WIFEXITED(status) result(exited)
819 integer(c_int), intent(in) :: status
820 logical :: exited
821 exited = (iand(status, 127) == 0)
822 end function
823
824 function WIFSTOPPED(status) result(stopped)
825 integer(c_int), intent(in) :: status
826 logical :: stopped
827 stopped = (iand(status, 255) == 127)
828 end function
829
830 function WEXITSTATUS(status) result(exit_status)
831 integer(c_int), intent(in) :: status
832 integer :: exit_status
833 exit_status = ishft(iand(status, 65280), -8)
834 end function
835
836 function WIFSIGNALED(status) result(signaled)
837 integer(c_int), intent(in) :: status
838 logical :: signaled
839 integer :: low7
840 low7 = iand(status, 127)
841 ! Process was killed by a signal if low 7 bits are non-zero and not 0x7f (stopped)
842 signaled = (low7 /= 0) .and. (low7 /= 127)
843 end function
844
845 function WTERMSIG(status) result(sig)
846 integer(c_int), intent(in) :: status
847 integer :: sig
848 sig = iand(status, 127)
849 end function
850
851 function WSTOPSIG(status) result(sig)
852 integer(c_int), intent(in) :: status
853 integer :: sig
854 sig = iand(ishft(status, -8), 255)
855 end function
856
857 function execute_and_capture(command) result(output)
858 character(len=*), intent(in) :: command
859 character(len=:), allocatable :: output
860
861 type(c_ptr) :: pipe_ptr
862 character(kind=c_char), target :: buffer(4096) ! Larger buffer per read
863 character(len=65536) :: temp_output ! 64KB buffer
864 type(c_ptr) :: ret_ptr
865 integer :: i, pos, bytes_read
866 character(len=256), target :: c_command
867 character(len=4), target :: c_mode
868
869 ! Convert strings to proper format
870 c_command = trim(command)//c_null_char
871 c_mode = 'r'//c_null_char
872
873 ! Open pipe to command
874 pipe_ptr = c_popen(c_loc(c_command), c_loc(c_mode))
875
876 if (.not. c_associated(pipe_ptr)) then
877 allocate(character(len=0) :: output)
878 return
879 end if
880
881 temp_output = ''
882 pos = 1
883
884 ! Read output with larger buffer
885 do
886 ! Initialize buffer for this read
887 buffer = c_null_char
888
889 ret_ptr = c_fgets(c_loc(buffer), int(4096, c_int), pipe_ptr)
890 if (.not. c_associated(ret_ptr)) exit
891
892 ! Convert to Fortran string
893 bytes_read = 0
894 do i = 1, 4096
895 if (buffer(i) == c_null_char) exit
896 bytes_read = bytes_read + 1
897 if (pos > len(temp_output)) exit ! Buffer full
898
899 if (buffer(i) /= char(10)) then ! Skip newlines
900 temp_output(pos:pos) = buffer(i)
901 pos = pos + 1
902 else if (pos > 1 .and. temp_output(pos-1:pos-1) /= ' ') then
903 temp_output(pos:pos) = ' '
904 pos = pos + 1
905 end if
906 end do
907
908 ! Debug: Check if we should continue reading
909 ! Exit if we read less than expected OR hit buffer limit
910 if (pos > len(temp_output)) exit
911 if (bytes_read == 0) exit
912 end do
913
914 ! Close pipe
915 i = c_pclose(pipe_ptr)
916
917 ! Return output (deallocate first if already allocated, which shouldn't happen but prevents crash)
918 if (allocated(output)) deallocate(output)
919 allocate(character(len=pos-1) :: output)
920 output = temp_output(:pos-1)
921 end function
922
923 ! Like execute_and_capture but converts newlines to tabs instead of spaces.
924 ! This preserves filenames with spaces for completion parsing.
925 function execute_and_capture_tabs(command) result(output)
926 character(len=*), intent(in) :: command
927 character(len=:), allocatable :: output
928
929 type(c_ptr) :: pipe_ptr
930 character(kind=c_char), target :: buffer(4096)
931 character(len=65536) :: temp_output
932 type(c_ptr) :: ret_ptr
933 integer :: i, pos, bytes_read
934 character(len=256), target :: c_command
935 character(len=4), target :: c_mode
936
937 c_command = trim(command)//c_null_char
938 c_mode = 'r'//c_null_char
939
940 pipe_ptr = c_popen(c_loc(c_command), c_loc(c_mode))
941 if (.not. c_associated(pipe_ptr)) then
942 allocate(character(len=0) :: output)
943 return
944 end if
945
946 temp_output = ''
947 pos = 1
948
949 do
950 buffer = c_null_char
951 ret_ptr = c_fgets(c_loc(buffer), int(4096, c_int), pipe_ptr)
952 if (.not. c_associated(ret_ptr)) exit
953
954 bytes_read = 0
955 do i = 1, 4096
956 if (buffer(i) == c_null_char) exit
957 bytes_read = bytes_read + 1
958 if (pos > len(temp_output)) exit
959
960 if (buffer(i) == char(10)) then
961 ! Convert newline to tab (preserves spaces in filenames)
962 if (pos > 1 .and. temp_output(pos-1:pos-1) /= char(9)) then
963 temp_output(pos:pos) = char(9)
964 pos = pos + 1
965 end if
966 else
967 temp_output(pos:pos) = buffer(i)
968 pos = pos + 1
969 end if
970 end do
971
972 if (pos > len(temp_output)) exit
973 if (bytes_read == 0) exit
974 end do
975
976 i = c_pclose(pipe_ptr)
977
978 if (allocated(output)) deallocate(output)
979 allocate(character(len=pos-1) :: output)
980 output = temp_output(:pos-1)
981 end function
982
983 ! Terminal control functions
984 function enable_raw_mode(original_termios) result(success)
985 use iso_fortran_env, only: output_unit, error_unit
986 type(termios_t), intent(out) :: original_termios
987 logical :: success
988 type(termios_t) :: raw_termios
989 integer :: ret
990 logical :: mode_ok ! Workaround for potential LLVM Flang compiler bug
991 integer, save :: call_count = 0
992
993 success = .false.
994 mode_ok = .false.
995
996 call_count = call_count + 1
997
998 ! Verify stdin is actually a TTY
999 if (c_isatty(STDIN_FD) == 0) then
1000 return
1001 end if
1002
1003 ! Get current terminal settings
1004 ret = c_tcgetattr(STDIN_FD, original_termios)
1005 if (ret /= 0) then
1006 write(*, '(a,i15)') '[ERROR: tcgetattr failed: ', ret, ']'
1007 return
1008 end if
1009
1010 ! Copy to modify for raw mode
1011 raw_termios = original_termios
1012
1013 ! DEBUG: Commented out - too noisy for normal use
1014
1015 ! Disable input processing that might consume control chars
1016 #ifdef __APPLE__
1017 ! macOS: Disable IXON (Ctrl-S/Q flow control), IXOFF, IXANY, BRKINT
1018 raw_termios%c_iflag = iand(raw_termios%c_iflag, not(int(z'00000600', c_long))) ! IXON | IXOFF
1019 raw_termios%c_iflag = iand(raw_termios%c_iflag, not(int(z'00000800', c_long))) ! IXANY
1020 raw_termios%c_iflag = iand(raw_termios%c_iflag, not(int(z'00000002', c_long))) ! BRKINT
1021 #else
1022 ! Linux: Disable flow control
1023 raw_termios%c_iflag = iand(raw_termios%c_iflag, not(int(z'00000400', c_int))) ! IXON
1024 raw_termios%c_iflag = iand(raw_termios%c_iflag, not(int(z'00001000', c_int))) ! IXOFF
1025 raw_termios%c_iflag = iand(raw_termios%c_iflag, not(int(z'00000800', c_int))) ! IXANY
1026 raw_termios%c_iflag = iand(raw_termios%c_iflag, not(int(z'00000002', c_int))) ! BRKINT
1027 #endif
1028
1029 ! Disable canonical mode, echo, and signals
1030 raw_termios%c_lflag = iand(raw_termios%c_lflag, not(ior(ior(ICANON, ECHO), ior(ECHOE, ECHOK))))
1031 raw_termios%c_lflag = iand(raw_termios%c_lflag, not(ior(ior(ECHONL, IEXTEN), ISIG)))
1032
1033 ! Also disable ECHOCTL which echoes control chars as ^C
1034 #ifdef __APPLE__
1035 ! macOS ECHOCTL flag (0x40)
1036 raw_termios%c_lflag = iand(raw_termios%c_lflag, not(int(z'00000040', c_long)))
1037 #else
1038 ! Linux ECHOCTL flag (typically 0x200)
1039 raw_termios%c_lflag = iand(raw_termios%c_lflag, not(int(z'00000200', c_int)))
1040 #endif
1041
1042 ! Set minimum characters and timeout for read
1043 raw_termios%c_cc(VMIN + 1) = char(1) ! Read at least 1 character
1044 raw_termios%c_cc(VTIME + 1) = char(0) ! No timeout
1045
1046 #ifdef __APPLE__
1047 ! Disable special character mappings that might intercept control chars
1048 ! With ISIG and ICANON disabled, most of these shouldn't matter, but
1049 ! explicitly clearing them ensures no control chars are intercepted
1050 raw_termios%c_cc(VINTR + 1) = char(0) ! Disable Ctrl-C (we handle it ourselves)
1051 raw_termios%c_cc(VQUIT + 1) = char(0) ! Disable Ctrl-\ quit
1052 raw_termios%c_cc(VSUSP + 1) = char(0) ! Disable Ctrl-Z suspend
1053 raw_termios%c_cc(VDSUSP + 1) = char(0) ! Disable delayed suspend
1054 raw_termios%c_cc(VSTART + 1) = char(0) ! Disable Ctrl-Q start (XON)
1055 raw_termios%c_cc(VSTOP + 1) = char(0) ! Disable Ctrl-S stop (XOFF)
1056 raw_termios%c_cc(VLNEXT + 1) = char(0) ! Disable literal next (Ctrl-V)
1057 raw_termios%c_cc(VDISCARD + 1) = char(0) ! Disable output discard
1058 raw_termios%c_cc(VWERASE + 1) = char(0) ! Disable word erase
1059 raw_termios%c_cc(VREPRINT + 1) = char(0) ! Disable reprint line
1060 ! Don't disable VEOF, VERASE, VKILL - we may want to check them
1061 #endif
1062
1063 ! Apply raw mode settings - use TCSAFLUSH to discard pending input
1064 ! TCSAFLUSH is critical on macOS to ensure settings actually take effect
1065 ret = c_tcsetattr(STDIN_FD, TCSAFLUSH, raw_termios)
1066 ! DEBUG: Commented out - too noisy
1067 mode_ok = (ret == 0)
1068 ! DEBUG: Commented out - too noisy
1069 ! if (mode_ok) then
1070 ! else
1071 ! end if
1072
1073 ! Verify flags were actually set
1074 ret = c_tcgetattr(STDIN_FD, raw_termios)
1075 if (ret == 0) then
1076 if (iand(raw_termios%c_lflag, ISIG) /= 0) then
1077 write(*, '(a)') '[BUG: ISIG still SET after tcsetattr!]'
1078 ! else
1079 end if
1080 if (iand(raw_termios%c_lflag, ICANON) /= 0) then
1081 write(*, '(a)') '[BUG: ICANON still SET!]'
1082 ! else
1083 end if
1084 ! DEBUG: Commented out - too noisy
1085
1086 #ifdef __APPLE__
1087 ! DEBUG: Commented out - too noisy
1088 #endif
1089 end if
1090
1091 ! Assign to result variable at the very end
1092 success = mode_ok
1093
1094 ! Enable bracketed paste mode (if raw mode succeeded).
1095 ! FORTSH_NO_BRACKETED_PASTE=1 disables the emit — a kill switch for
1096 ! bug triage on terminals that mishandle the mode (pattern #20).
1097 if (success) then
1098 block
1099 character(len=8) :: no_bp_env
1100 integer :: bp_stat
1101 logical :: bp_disabled
1102 call get_environment_variable('FORTSH_NO_BRACKETED_PASTE', no_bp_env, status=bp_stat)
1103 bp_disabled = (bp_stat == 0 .and. trim(no_bp_env) == '1')
1104 if (.not. bp_disabled) then
1105 ! ESC[?2004h = Enable bracketed paste
1106 ! Terminal will wrap pasted text in ESC[200~ ... ESC[201~
1107 write(output_unit, '(A)', advance='no') char(27) // '[?2004h'
1108 flush(output_unit)
1109 end if
1110 end block
1111
1112 ! Debug: Check if FORTSH_DEBUG_PASTE is set
1113 block
1114 character(len=16) :: debug_paste
1115 integer :: stat
1116 call get_environment_variable('FORTSH_DEBUG_PASTE', debug_paste, status=stat)
1117 if (stat == 0 .and. len_trim(debug_paste) > 0) then
1118 write(error_unit, '(A)') '[DEBUG: Bracketed paste mode ENABLED]'
1119 end if
1120 end block
1121 end if
1122
1123 ! DEBUG: Commented out - too noisy
1124 ! if (success) then
1125 ! else
1126 ! end if
1127 end function
1128
1129 function restore_terminal(original_termios) result(success)
1130 use iso_fortran_env, only: output_unit
1131 type(termios_t), intent(in) :: original_termios
1132 logical :: success
1133 integer :: ret
1134 character(len=8) :: no_bp_env
1135 integer :: bp_stat
1136 logical :: bp_disabled
1137
1138 ! Disable bracketed paste mode before restoring terminal,
1139 ! unless FORTSH_NO_BRACKETED_PASTE=1 (we never enabled it).
1140 call get_environment_variable('FORTSH_NO_BRACKETED_PASTE', no_bp_env, status=bp_stat)
1141 bp_disabled = (bp_stat == 0 .and. trim(no_bp_env) == '1')
1142 if (.not. bp_disabled) then
1143 ! ESC[?2004l = Disable bracketed paste
1144 write(output_unit, '(A)', advance='no') char(27) // '[?2004l'
1145 flush(output_unit)
1146 end if
1147
1148 ret = c_tcsetattr(STDIN_FD, TCSANOW, original_termios)
1149 success = (ret == 0)
1150 end function
1151
1152 function read_single_char(ch) result(success)
1153 use iso_fortran_env, only: error_unit
1154 character, intent(out) :: ch
1155 logical :: success
1156 character(c_char), target :: c_ch
1157 integer(c_size_t) :: bytes_read
1158
1159
1160 bytes_read = c_read(STDIN_FD, c_loc(c_ch), 1_c_size_t)
1161
1162 success = (bytes_read == 1)
1163 if (success) then
1164 ch = c_ch
1165 else
1166 ch = char(0)
1167 end if
1168 end function
1169
1170 ! Read a complete UTF-8 character (1-4 bytes)
1171 ! Returns the character in utf8_char and the number of bytes read
1172 function read_utf8_char(utf8_char, num_bytes) result(success)
1173 character(len=4), intent(out) :: utf8_char
1174 integer, intent(out) :: num_bytes
1175 logical :: success
1176 character(c_char), target :: bytes(4)
1177 integer(c_size_t) :: bytes_read
1178 integer :: lead_byte_val, i, expected_bytes
1179
1180 ! Initialize output
1181 utf8_char = ''
1182 num_bytes = 0
1183
1184 ! Read first byte
1185 bytes_read = c_read(STDIN_FD, c_loc(bytes(1)), 1_c_size_t)
1186 if (bytes_read /= 1) then
1187 success = .false.
1188 return
1189 end if
1190
1191 ! Get value of first byte (0-255)
1192 lead_byte_val = iand(iachar(bytes(1)), 255)
1193
1194 ! Determine how many bytes this UTF-8 character should have
1195 if (lead_byte_val < 128) then
1196 ! ASCII character (0x00-0x7F): 1 byte
1197 expected_bytes = 1
1198 else if (iand(lead_byte_val, 224) == 192) then
1199 ! 2-byte UTF-8 (0xC0-0xDF)
1200 expected_bytes = 2
1201 else if (iand(lead_byte_val, 240) == 224) then
1202 ! 3-byte UTF-8 (0xE0-0xEF)
1203 expected_bytes = 3
1204 else if (iand(lead_byte_val, 248) == 240) then
1205 ! 4-byte UTF-8 (0xF0-0xF7)
1206 expected_bytes = 4
1207 else
1208 ! Invalid UTF-8 lead byte - treat as single byte
1209 expected_bytes = 1
1210 end if
1211
1212 ! Read continuation bytes if needed
1213 if (expected_bytes > 1) then
1214 do i = 2, expected_bytes
1215 bytes_read = c_read(STDIN_FD, c_loc(bytes(i)), 1_c_size_t)
1216 if (bytes_read /= 1) then
1217 ! Failed to read continuation byte - return what we have
1218 success = .false.
1219 return
1220 end if
1221 end do
1222 end if
1223
1224 ! Copy bytes to output string
1225 do i = 1, expected_bytes
1226 utf8_char(i:i) = bytes(i)
1227 end do
1228
1229 num_bytes = expected_bytes
1230 success = .true.
1231 end function read_utf8_char
1232
1233 ! Get current process ID
1234 function get_pid() result(pid)
1235 integer(c_pid_t) :: pid
1236 pid = c_getpid()
1237 end function
1238
1239 ! Get parent process ID
1240 function get_ppid() result(ppid)
1241 integer(c_pid_t) :: ppid
1242 ppid = c_getppid()
1243 end function
1244
1245 function get_uid() result(uid)
1246 integer :: uid
1247 uid = int(c_getuid())
1248 end function
1249
1250 function get_euid() result(euid)
1251 integer :: euid
1252 euid = int(c_geteuid())
1253 end function
1254
1255 ! File test functions for test builtin support
1256 ! On aarch64 Linux, struct stat layout differs from x86_64 (mode/nlink swapped,
1257 ! different field sizes). USE_C_STAT routes through C helpers that use system headers.
1258 function file_exists(path) result(exists)
1259 character(len=*), intent(in) :: path
1260 logical :: exists
1261 character(len=256), target :: c_path
1262 #ifdef USE_C_STAT
1263 integer :: mode
1264 c_path = trim(path)//c_null_char
1265 mode = fortsh_stat_mode(c_loc(c_path))
1266 exists = (mode >= 0)
1267 #else
1268 integer :: ret
1269 type(stat_t) :: statbuf
1270 c_path = trim(path)//c_null_char
1271 ret = c_stat(c_loc(c_path), statbuf)
1272 exists = (ret == 0)
1273 #endif
1274 end function
1275
1276 function file_is_regular(path) result(is_reg)
1277 character(len=*), intent(in) :: path
1278 logical :: is_reg
1279 character(len=256), target :: c_path
1280 #ifdef USE_C_STAT
1281 integer :: mode
1282 c_path = trim(path)//c_null_char
1283 mode = fortsh_stat_mode(c_loc(c_path))
1284 is_reg = (mode >= 0 .and. iand(mode, S_IFMT) == S_IFREG)
1285 #else
1286 integer :: ret
1287 type(stat_t) :: statbuf
1288 c_path = trim(path)//c_null_char
1289 ret = c_stat(c_loc(c_path), statbuf)
1290 is_reg = (ret == 0 .and. iand(int(statbuf%st_mode, c_int), S_IFMT) == S_IFREG)
1291 #endif
1292 end function
1293
1294 function file_is_directory(path) result(is_dir)
1295 character(len=*), intent(in) :: path
1296 logical :: is_dir
1297 character(len=256), target :: c_path
1298 #ifdef USE_C_STAT
1299 integer :: mode
1300 c_path = trim(path)//c_null_char
1301 mode = fortsh_stat_mode(c_loc(c_path))
1302 is_dir = (mode >= 0 .and. iand(mode, S_IFMT) == S_IFDIR)
1303 #else
1304 integer :: ret
1305 type(stat_t) :: statbuf
1306 c_path = trim(path)//c_null_char
1307 ret = c_stat(c_loc(c_path), statbuf)
1308 is_dir = (ret == 0 .and. iand(int(statbuf%st_mode, c_int), S_IFMT) == S_IFDIR)
1309 #endif
1310 end function
1311
1312 function file_is_symlink(path) result(is_link)
1313 character(len=*), intent(in) :: path
1314 logical :: is_link
1315 character(len=256), target :: c_path
1316 #ifdef USE_C_STAT
1317 integer :: mode
1318 c_path = trim(path)//c_null_char
1319 mode = fortsh_lstat_mode(c_loc(c_path))
1320 is_link = (mode >= 0 .and. iand(mode, S_IFMT) == S_IFLNK)
1321 #else
1322 integer :: ret
1323 type(stat_t) :: statbuf
1324 c_path = trim(path)//c_null_char
1325 ret = c_lstat(c_loc(c_path), statbuf)
1326 is_link = (ret == 0 .and. iand(int(statbuf%st_mode, c_int), S_IFMT) == S_IFLNK)
1327 #endif
1328 end function
1329
1330 function file_is_block_device(path) result(is_blk)
1331 character(len=*), intent(in) :: path
1332 logical :: is_blk
1333 character(len=256), target :: c_path
1334 #ifdef USE_C_STAT
1335 integer :: mode
1336 c_path = trim(path)//c_null_char
1337 mode = fortsh_stat_mode(c_loc(c_path))
1338 is_blk = (mode >= 0 .and. iand(mode, S_IFMT) == S_IFBLK)
1339 #else
1340 integer :: ret
1341 type(stat_t) :: statbuf
1342 c_path = trim(path)//c_null_char
1343 ret = c_stat(c_loc(c_path), statbuf)
1344 is_blk = (ret == 0 .and. iand(int(statbuf%st_mode, c_int), S_IFMT) == S_IFBLK)
1345 #endif
1346 end function
1347
1348 function file_is_char_device(path) result(is_chr)
1349 character(len=*), intent(in) :: path
1350 logical :: is_chr
1351 character(len=256), target :: c_path
1352 #ifdef USE_C_STAT
1353 integer :: mode
1354 c_path = trim(path)//c_null_char
1355 mode = fortsh_stat_mode(c_loc(c_path))
1356 is_chr = (mode >= 0 .and. iand(mode, S_IFMT) == S_IFCHR)
1357 #else
1358 integer :: ret
1359 type(stat_t) :: statbuf
1360 c_path = trim(path)//c_null_char
1361 ret = c_stat(c_loc(c_path), statbuf)
1362 is_chr = (ret == 0 .and. iand(int(statbuf%st_mode, c_int), S_IFMT) == S_IFCHR)
1363 #endif
1364 end function
1365
1366 function file_is_fifo(path) result(is_fifo)
1367 character(len=*), intent(in) :: path
1368 logical :: is_fifo
1369 character(len=256), target :: c_path
1370 #ifdef USE_C_STAT
1371 integer :: mode
1372 c_path = trim(path)//c_null_char
1373 mode = fortsh_stat_mode(c_loc(c_path))
1374 is_fifo = (mode >= 0 .and. iand(mode, S_IFMT) == S_IFIFO)
1375 #else
1376 integer :: ret
1377 type(stat_t) :: statbuf
1378 c_path = trim(path)//c_null_char
1379 ret = c_stat(c_loc(c_path), statbuf)
1380 is_fifo = (ret == 0 .and. iand(int(statbuf%st_mode, c_int), S_IFMT) == S_IFIFO)
1381 #endif
1382 end function
1383
1384 function file_is_socket(path) result(is_sock)
1385 character(len=*), intent(in) :: path
1386 logical :: is_sock
1387 character(len=256), target :: c_path
1388 #ifdef USE_C_STAT
1389 integer :: mode
1390 c_path = trim(path)//c_null_char
1391 mode = fortsh_stat_mode(c_loc(c_path))
1392 is_sock = (mode >= 0 .and. iand(mode, S_IFMT) == S_IFSOCK)
1393 #else
1394 integer :: ret
1395 type(stat_t) :: statbuf
1396 c_path = trim(path)//c_null_char
1397 ret = c_stat(c_loc(c_path), statbuf)
1398 is_sock = (ret == 0 .and. iand(int(statbuf%st_mode, c_int), S_IFMT) == S_IFSOCK)
1399 #endif
1400 end function
1401
1402 function file_is_readable(path) result(is_readable)
1403 character(len=*), intent(in) :: path
1404 logical :: is_readable
1405 character(len=256), target :: c_path
1406 integer :: ret
1407
1408 c_path = trim(path)//c_null_char
1409 ret = c_access(c_loc(c_path), R_OK)
1410 is_readable = (ret == 0)
1411 end function
1412
1413 function file_is_writable(path) result(is_writable)
1414 character(len=*), intent(in) :: path
1415 logical :: is_writable
1416 character(len=256), target :: c_path
1417 integer :: ret
1418
1419 c_path = trim(path)//c_null_char
1420 ret = c_access(c_loc(c_path), W_OK)
1421 is_writable = (ret == 0)
1422 end function
1423
1424 function file_is_executable(path) result(is_exec)
1425 character(len=*), intent(in) :: path
1426 logical :: is_exec
1427 character(len=256), target :: c_path
1428 integer :: ret
1429
1430 c_path = trim(path)//c_null_char
1431 ret = c_access(c_loc(c_path), X_OK)
1432 is_exec = (ret == 0)
1433 end function
1434
1435 function file_has_suid(path) result(has_suid)
1436 character(len=*), intent(in) :: path
1437 logical :: has_suid
1438 character(len=256), target :: c_path
1439 #ifdef USE_C_STAT
1440 integer :: mode
1441 c_path = trim(path)//c_null_char
1442 mode = fortsh_stat_mode(c_loc(c_path))
1443 has_suid = (mode >= 0 .and. iand(mode, S_ISUID) /= 0)
1444 #else
1445 integer :: ret
1446 type(stat_t) :: statbuf
1447 c_path = trim(path)//c_null_char
1448 ret = c_stat(c_loc(c_path), statbuf)
1449 has_suid = (ret == 0 .and. iand(int(statbuf%st_mode, c_int), S_ISUID) /= 0)
1450 #endif
1451 end function
1452
1453 function file_has_sgid(path) result(has_sgid)
1454 character(len=*), intent(in) :: path
1455 logical :: has_sgid
1456 character(len=256), target :: c_path
1457 #ifdef USE_C_STAT
1458 integer :: mode
1459 c_path = trim(path)//c_null_char
1460 mode = fortsh_stat_mode(c_loc(c_path))
1461 has_sgid = (mode >= 0 .and. iand(mode, S_ISGID) /= 0)
1462 #else
1463 integer :: ret
1464 type(stat_t) :: statbuf
1465 c_path = trim(path)//c_null_char
1466 ret = c_stat(c_loc(c_path), statbuf)
1467 has_sgid = (ret == 0 .and. iand(int(statbuf%st_mode, c_int), S_ISGID) /= 0)
1468 #endif
1469 end function
1470
1471 function file_has_sticky(path) result(has_sticky)
1472 character(len=*), intent(in) :: path
1473 logical :: has_sticky
1474 character(len=256), target :: c_path
1475 #ifdef USE_C_STAT
1476 integer :: mode
1477 c_path = trim(path)//c_null_char
1478 mode = fortsh_stat_mode(c_loc(c_path))
1479 has_sticky = (mode >= 0 .and. iand(mode, S_ISVTX) /= 0)
1480 #else
1481 integer :: ret
1482 type(stat_t) :: statbuf
1483 c_path = trim(path)//c_null_char
1484 ret = c_stat(c_loc(c_path), statbuf)
1485 has_sticky = (ret == 0 .and. iand(int(statbuf%st_mode, c_int), S_ISVTX) /= 0)
1486 #endif
1487 end function
1488
1489 function file_has_size(path) result(has_size)
1490 character(len=*), intent(in) :: path
1491 logical :: has_size
1492 character(len=256), target :: c_path
1493 #ifdef USE_C_STAT
1494 integer(c_long_long) :: sz
1495 c_path = trim(path)//c_null_char
1496 sz = fortsh_stat_size(c_loc(c_path))
1497 has_size = (sz > 0)
1498 #else
1499 integer :: ret
1500 type(stat_t) :: statbuf
1501 c_path = trim(path)//c_null_char
1502 ret = c_stat(c_loc(c_path), statbuf)
1503 has_size = (ret == 0 .and. statbuf%st_size > 0)
1504 #endif
1505 end function
1506
1507 function file_owned_by_euid(path) result(is_owned)
1508 character(len=*), intent(in) :: path
1509 logical :: is_owned
1510 character(len=256), target :: c_path
1511 #ifdef USE_C_STAT
1512 integer :: uid
1513 c_path = trim(path)//c_null_char
1514 uid = fortsh_stat_uid(c_loc(c_path))
1515 is_owned = (uid >= 0 .and. uid == c_geteuid())
1516 #else
1517 integer :: ret
1518 type(stat_t) :: statbuf
1519 c_path = trim(path)//c_null_char
1520 ret = c_stat(c_loc(c_path), statbuf)
1521 is_owned = (ret == 0 .and. statbuf%st_uid == c_geteuid())
1522 #endif
1523 end function
1524
1525 function file_owned_by_egid(path) result(is_owned)
1526 character(len=*), intent(in) :: path
1527 logical :: is_owned
1528 ! Note: getegid() not declared yet, so we'll skip this check for now
1529 ! This should be added when getegid() is available
1530 is_owned = .false.
1531 if (.false.) print *, path ! Silence unused warning
1532 end function
1533
1534 function file_is_newer(file1, file2) result(is_newer)
1535 character(len=*), intent(in) :: file1, file2
1536 logical :: is_newer
1537 character(len=256), target :: c_path1, c_path2
1538 #ifdef USE_C_STAT
1539 integer(c_long_long) :: mt1, mt2
1540 c_path1 = trim(file1)//c_null_char
1541 c_path2 = trim(file2)//c_null_char
1542 mt1 = fortsh_stat_mtime(c_loc(c_path1))
1543 mt2 = fortsh_stat_mtime(c_loc(c_path2))
1544 is_newer = (mt1 >= 0 .and. mt2 >= 0 .and. mt1 > mt2)
1545 #else
1546 integer :: ret1, ret2
1547 type(stat_t) :: stat1, stat2
1548 c_path1 = trim(file1)//c_null_char
1549 c_path2 = trim(file2)//c_null_char
1550 ret1 = c_stat(c_loc(c_path1), stat1)
1551 ret2 = c_stat(c_loc(c_path2), stat2)
1552 #ifdef __APPLE__
1553 is_newer = (ret1 == 0 .and. ret2 == 0 .and. stat1%st_mtimespec_sec > stat2%st_mtimespec_sec)
1554 #else
1555 is_newer = (ret1 == 0 .and. ret2 == 0 .and. stat1%st_mtime > stat2%st_mtime)
1556 #endif
1557 #endif
1558 end function
1559
1560 function file_is_older(file1, file2) result(is_older)
1561 character(len=*), intent(in) :: file1, file2
1562 logical :: is_older
1563 character(len=256), target :: c_path1, c_path2
1564 #ifdef USE_C_STAT
1565 integer(c_long_long) :: mt1, mt2
1566 c_path1 = trim(file1)//c_null_char
1567 c_path2 = trim(file2)//c_null_char
1568 mt1 = fortsh_stat_mtime(c_loc(c_path1))
1569 mt2 = fortsh_stat_mtime(c_loc(c_path2))
1570 is_older = (mt1 >= 0 .and. mt2 >= 0 .and. mt1 < mt2)
1571 #else
1572 integer :: ret1, ret2
1573 type(stat_t) :: stat1, stat2
1574 c_path1 = trim(file1)//c_null_char
1575 c_path2 = trim(file2)//c_null_char
1576 ret1 = c_stat(c_loc(c_path1), stat1)
1577 ret2 = c_stat(c_loc(c_path2), stat2)
1578 #ifdef __APPLE__
1579 is_older = (ret1 == 0 .and. ret2 == 0 .and. stat1%st_mtimespec_sec < stat2%st_mtimespec_sec)
1580 #else
1581 is_older = (ret1 == 0 .and. ret2 == 0 .and. stat1%st_mtime < stat2%st_mtime)
1582 #endif
1583 #endif
1584 end function
1585
1586 function file_same_as(file1, file2) result(is_same)
1587 character(len=*), intent(in) :: file1, file2
1588 logical :: is_same
1589 character(len=256), target :: c_path1, c_path2
1590 #ifdef USE_C_STAT
1591 integer(c_long_long) :: dev1, dev2, ino1, ino2
1592 c_path1 = trim(file1)//c_null_char
1593 c_path2 = trim(file2)//c_null_char
1594 dev1 = fortsh_stat_dev(c_loc(c_path1))
1595 dev2 = fortsh_stat_dev(c_loc(c_path2))
1596 ino1 = fortsh_stat_ino(c_loc(c_path1))
1597 ino2 = fortsh_stat_ino(c_loc(c_path2))
1598 is_same = (dev1 >= 0 .and. dev2 >= 0 .and. dev1 == dev2 .and. ino1 == ino2)
1599 #else
1600 integer :: ret1, ret2
1601 type(stat_t) :: stat1, stat2
1602 c_path1 = trim(file1)//c_null_char
1603 c_path2 = trim(file2)//c_null_char
1604 ret1 = c_stat(c_loc(c_path1), stat1)
1605 ret2 = c_stat(c_loc(c_path2), stat2)
1606 is_same = (ret1 == 0 .and. ret2 == 0 .and. &
1607 stat1%st_dev == stat2%st_dev .and. &
1608 stat1%st_ino == stat2%st_ino)
1609 #endif
1610 end function
1611
1612 ! Create a named pipe (FIFO)
1613 function create_fifo(path, mode) result(success)
1614 character(len=*), intent(in) :: path
1615 integer, intent(in), optional :: mode
1616 logical :: success
1617 character(len=256), target :: c_path
1618 integer(c_int) :: ret
1619 integer(c_int) :: fifo_mode
1620
1621 ! Default mode is 0600 (read/write for owner only)
1622 if (present(mode)) then
1623 fifo_mode = int(mode, c_int)
1624 else
1625 fifo_mode = ior(S_IRUSR, S_IWUSR)
1626 end if
1627
1628 c_path = trim(path)//c_null_char
1629 ret = c_mkfifo(c_loc(c_path), fifo_mode)
1630 success = (ret == 0)
1631 end function
1632
1633 ! Remove a file or FIFO
1634 function remove_file(path) result(success)
1635 character(len=*), intent(in) :: path
1636 logical :: success
1637 character(len=256), target :: c_path
1638 integer :: ret
1639
1640 c_path = trim(path)//c_null_char
1641 ret = c_unlink(c_loc(c_path))
1642 success = (ret == 0)
1643 end function
1644
1645 ! Initialize signal handler constants
1646 ! Must be called before using SIG_DFL or SIG_IGN
1647 subroutine init_signal_constants()
1648 ! SIG_DFL is (void(*)())0 which is c_null_funptr
1649 SIG_DFL = c_null_funptr
1650 ! SIG_IGN is (void(*)())1, use transfer to convert integer to c_funptr
1651 SIG_IGN = transfer(1_c_intptr_t, SIG_IGN)
1652 end subroutine
1653
1654 ! Get terminal size (rows and columns)
1655 function get_terminal_size(rows, cols) result(success)
1656 use iso_fortran_env, only: error_unit
1657 integer, intent(out) :: rows, cols
1658 logical :: success
1659 type(winsize_t), target :: ws
1660 integer(c_int) :: ret, c_rows, c_cols
1661 character(len=16) :: debug_env
1662 integer :: stat
1663
1664 ! Try using C wrapper first (more reliable)
1665 c_rows = 0
1666 c_cols = 0
1667 ret = get_term_size_c(c_rows, c_cols)
1668
1669 call get_environment_variable('FORTSH_DEBUG_WINSIZE', debug_env, status=stat)
1670 if (stat == 0 .and. len_trim(debug_env) > 0) then
1671 write(error_unit, '(A,I0,A,I0,A,I0)') '[DEBUG: C wrapper ret=', ret, ' rows=', c_rows, ' cols=', c_cols
1672 end if
1673
1674 if (ret == 0 .and. c_rows > 0 .and. c_cols > 0) then
1675 rows = int(c_rows)
1676 cols = int(c_cols)
1677 success = .true.
1678 return
1679 end if
1680
1681 ! Fallback to direct ioctl if C wrapper fails
1682 ! Initialize structure to zero
1683 ws%ws_row = 0
1684 ws%ws_col = 0
1685 ws%ws_xpixel = 0
1686 ws%ws_ypixel = 0
1687
1688 ! Debug: Check if FDs are actually TTYs
1689 call get_environment_variable('FORTSH_DEBUG_WINSIZE', debug_env, status=stat)
1690 if (stat == 0 .and. len_trim(debug_env) > 0) then
1691 write(error_unit, '(A,I0,A,I0,A,I0)') '[DEBUG: isatty(0)=', c_isatty(STDIN_FD), &
1692 ' isatty(1)=', c_isatty(STDOUT_FD), ' isatty(2)=', c_isatty(STDERR_FD)
1693 end if
1694
1695 ! Try to get window size using ioctl
1696 ! Try stdout first, then stderr if stdout gives 0 dimensions
1697 ret = c_ioctl(STDOUT_FD, TIOCGWINSZ, c_loc(ws))
1698
1699 ! Debug output
1700 call get_environment_variable('FORTSH_DEBUG_WINSIZE', debug_env, status=stat)
1701 if (stat == 0 .and. len_trim(debug_env) > 0) then
1702 write(error_unit, '(A,I0,A,I0,A,I0)') '[DEBUG: ioctl(STDOUT) ret=', ret, ' rows=', ws%ws_row, ' cols=', ws%ws_col
1703 end if
1704
1705 ! If stdout doesn't give valid dimensions, try stderr
1706 if (ret /= 0 .or. ws%ws_row == 0 .or. ws%ws_col == 0) then
1707 ws%ws_row = 0
1708 ws%ws_col = 0
1709 ret = c_ioctl(STDERR_FD, TIOCGWINSZ, c_loc(ws))
1710
1711 call get_environment_variable('FORTSH_DEBUG_WINSIZE', debug_env, status=stat)
1712 if (stat == 0 .and. len_trim(debug_env) > 0) then
1713 write(error_unit, '(A,I0,A,I0,A,I0)') '[DEBUG: ioctl(STDERR) ret=', ret, ' rows=', ws%ws_row, ' cols=', ws%ws_col
1714 end if
1715 end if
1716
1717 ! If stderr also doesn't work, try stdin
1718 if (ret /= 0 .or. ws%ws_row == 0 .or. ws%ws_col == 0) then
1719 ws%ws_row = 0
1720 ws%ws_col = 0
1721 ret = c_ioctl(STDIN_FD, TIOCGWINSZ, c_loc(ws))
1722
1723 call get_environment_variable('FORTSH_DEBUG_WINSIZE', debug_env, status=stat)
1724 if (stat == 0 .and. len_trim(debug_env) > 0) then
1725 write(error_unit, '(A,I0,A,I0,A,I0)') '[DEBUG: ioctl(STDIN) ret=', ret, ' rows=', ws%ws_row, ' cols=', ws%ws_col
1726 end if
1727 end if
1728
1729 if (ret == 0 .and. ws%ws_row > 0 .and. ws%ws_col > 0) then
1730 rows = int(ws%ws_row)
1731 cols = int(ws%ws_col)
1732 success = .true.
1733 else
1734 ! Fallback to common defaults if ioctl fails
1735 rows = 24
1736 cols = 80
1737 success = .false.
1738 end if
1739 end function
1740
1741 ! Check if a path is a directory
1742 function test_is_directory(path) result(is_dir)
1743 character(len=*), intent(in) :: path
1744 logical :: is_dir
1745 character(len=len(path)+1), target :: c_path
1746 #ifdef USE_C_STAT
1747 integer :: mode
1748 is_dir = .false.
1749 c_path = trim(path) // c_null_char
1750 mode = fortsh_stat_mode(c_loc(c_path))
1751 is_dir = (mode >= 0 .and. iand(mode, S_IFDIR) /= 0)
1752 #else
1753 integer :: stat_result
1754 type(stat_t) :: file_stat
1755 is_dir = .false.
1756 c_path = trim(path) // c_null_char
1757 stat_result = c_stat(c_loc(c_path), file_stat)
1758 if (stat_result == 0) then
1759 is_dir = iand(int(file_stat%st_mode, c_int), S_IFDIR) /= 0
1760 end if
1761 #endif
1762 end function
1763
1764 ! Set terminal title using OSC sequences
1765 ! OSC 0 ; title BEL sets both icon and window title
1766 subroutine set_terminal_title(title)
1767 use iso_fortran_env, only: output_unit
1768 character(len=*), intent(in) :: title
1769 ! ESC ] 0 ; title BEL
1770 write(output_unit, '(A)', advance='no') char(27) // ']0;' // trim(title) // char(7)
1771 flush(output_unit)
1772 end subroutine
1773
1774 ! Check if terminal supports ANSI escape codes
1775 function terminal_supports_ansi() result(supports)
1776 logical :: supports
1777 character(len=256) :: term_type
1778 integer :: status
1779
1780 call get_environment_variable('TERM', term_type, status=status)
1781
1782 if (status /= 0 .or. len_trim(term_type) == 0) then
1783 ! No TERM set - assume dumb terminal
1784 supports = .false.
1785 return
1786 end if
1787
1788 ! Known dumb/non-ANSI terminals
1789 select case (trim(term_type))
1790 case ('dumb', 'unknown', 'cons25')
1791 supports = .false.
1792 case default
1793 supports = .true.
1794 end select
1795 end function terminal_supports_ansi
1796
1797 end module system_interface