fortrangoingonforty/fortty / b5794f4

Browse files

Add PTY integration for shell communication

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
b5794f4c0e1bfb65268f9f398f10f231beb2f195
Parents
22425c0
Tree
399156a

6 changed files

StatusFile+-
M CMakeLists.txt 5 2
A c_src/pty_helpers.c 197 0
M src/fortty.f90 66 8
A src/pty/pty.f90 126 0
A src/pty/pty_bindings.f90 62 0
M src/window/window.f90 12 0
CMakeLists.txtmodified
@@ -15,15 +15,16 @@ find_package(glfw3 REQUIRED)
1515
 find_package(OpenGL REQUIRED)
1616
 find_package(Freetype REQUIRED)
1717
 
18
-# C helper libraries (GLAD + FreeType wrappers)
18
+# C helper libraries (GLAD + FreeType + PTY wrappers)
1919
 add_library(helpers STATIC
2020
     src/gl/glad.c
2121
     c_src/gl_loader.c
2222
     c_src/freetype_helpers.c
23
+    c_src/pty_helpers.c
2324
 )
2425
 target_include_directories(helpers PUBLIC ${CMAKE_SOURCE_DIR}/include)
2526
 target_include_directories(helpers PRIVATE ${FREETYPE_INCLUDE_DIRS})
26
-target_link_libraries(helpers PRIVATE glfw ${FREETYPE_LIBRARIES})
27
+target_link_libraries(helpers PRIVATE glfw ${FREETYPE_LIBRARIES} util)
2728
 
2829
 # Fortran module output directory
2930
 set(CMAKE_Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/modules)
@@ -37,6 +38,8 @@ set(FORTRAN_SOURCES
3738
     src/text/font.f90
3839
     src/text/atlas.f90
3940
     src/text/renderer.f90
41
+    src/pty/pty_bindings.f90
42
+    src/pty/pty.f90
4043
     src/window/glfw_bindings.f90
4144
     src/window/window.f90
4245
     src/fortty.f90
c_src/pty_helpers.cadded
@@ -0,0 +1,197 @@
1
+/*
2
+ * PTY helper functions for fortty
3
+ * Wraps POSIX PTY operations for Fortran binding
4
+ */
5
+
6
+#define _XOPEN_SOURCE 600
7
+#include <pty.h>
8
+#include <unistd.h>
9
+#include <fcntl.h>
10
+#include <sys/ioctl.h>
11
+#include <sys/wait.h>
12
+#include <signal.h>
13
+#include <errno.h>
14
+#include <stdlib.h>
15
+#include <string.h>
16
+#include <stdio.h>
17
+
18
+/* Store child PID for status checking */
19
+static pid_t child_pid = -1;
20
+
21
+/*
22
+ * Fork a shell process with PTY
23
+ * Returns master fd on success, -1 on error
24
+ */
25
+int fortty_pty_fork(const char *shell, int rows, int cols) {
26
+    int master_fd;
27
+    struct winsize ws;
28
+
29
+    /* Set initial window size */
30
+    ws.ws_row = rows;
31
+    ws.ws_col = cols;
32
+    ws.ws_xpixel = 0;
33
+    ws.ws_ypixel = 0;
34
+
35
+    /* forkpty does: openpty + fork + login_tty */
36
+    child_pid = forkpty(&master_fd, NULL, NULL, &ws);
37
+
38
+    if (child_pid < 0) {
39
+        perror("forkpty");
40
+        return -1;
41
+    }
42
+
43
+    if (child_pid == 0) {
44
+        /* Child process */
45
+
46
+        /* Set environment for terminal */
47
+        setenv("TERM", "xterm-256color", 1);
48
+        setenv("COLORTERM", "truecolor", 1);
49
+
50
+        /* Determine shell to use */
51
+        const char *sh = shell;
52
+        if (!sh || sh[0] == '\0') {
53
+            sh = getenv("SHELL");
54
+            if (!sh) {
55
+                sh = "/bin/sh";
56
+            }
57
+        }
58
+
59
+        /* Execute shell as login shell */
60
+        /* Using -l flag for login shell behavior */
61
+        execlp(sh, sh, "-l", (char *)NULL);
62
+
63
+        /* If exec fails */
64
+        perror("execlp");
65
+        _exit(127);
66
+    }
67
+
68
+    /* Parent process */
69
+
70
+    /* Set non-blocking mode on master */
71
+    int flags = fcntl(master_fd, F_GETFL, 0);
72
+    if (flags != -1) {
73
+        fcntl(master_fd, F_SETFL, flags | O_NONBLOCK);
74
+    }
75
+
76
+    return master_fd;
77
+}
78
+
79
+/*
80
+ * Set PTY window size
81
+ * Returns 0 on success, -1 on error
82
+ */
83
+int fortty_pty_set_size(int master_fd, int rows, int cols) {
84
+    struct winsize ws;
85
+    ws.ws_row = rows;
86
+    ws.ws_col = cols;
87
+    ws.ws_xpixel = 0;
88
+    ws.ws_ypixel = 0;
89
+
90
+    if (ioctl(master_fd, TIOCSWINSZ, &ws) < 0) {
91
+        perror("ioctl TIOCSWINSZ");
92
+        return -1;
93
+    }
94
+
95
+    return 0;
96
+}
97
+
98
+/*
99
+ * Read from PTY (non-blocking)
100
+ * Returns: bytes read (>0), 0 if would block, -1 on error/EOF
101
+ */
102
+int fortty_pty_read(int fd, char *buf, int count) {
103
+    ssize_t n = read(fd, buf, count);
104
+
105
+    if (n < 0) {
106
+        if (errno == EAGAIN || errno == EWOULDBLOCK) {
107
+            return 0;  /* No data available */
108
+        }
109
+        return -1;  /* Actual error */
110
+    }
111
+
112
+    if (n == 0) {
113
+        return -1;  /* EOF - child closed PTY */
114
+    }
115
+
116
+    return (int)n;
117
+}
118
+
119
+/*
120
+ * Write to PTY
121
+ * Returns: bytes written, -1 on error
122
+ */
123
+int fortty_pty_write(int fd, const char *buf, int count) {
124
+    ssize_t n = write(fd, buf, count);
125
+    if (n < 0) {
126
+        if (errno == EAGAIN || errno == EWOULDBLOCK) {
127
+            return 0;  /* Would block, try again later */
128
+        }
129
+        perror("write to pty");
130
+        return -1;
131
+    }
132
+    return (int)n;
133
+}
134
+
135
+/*
136
+ * Close PTY and wait for child
137
+ */
138
+void fortty_pty_close(int master_fd) {
139
+    if (master_fd >= 0) {
140
+        close(master_fd);
141
+    }
142
+
143
+    if (child_pid > 0) {
144
+        /* Send SIGHUP to child (standard terminal close behavior) */
145
+        kill(child_pid, SIGHUP);
146
+
147
+        /* Wait for child to exit (with timeout via WNOHANG loop) */
148
+        int status;
149
+        int attempts = 0;
150
+        while (waitpid(child_pid, &status, WNOHANG) == 0 && attempts < 100) {
151
+            usleep(10000);  /* 10ms */
152
+            attempts++;
153
+        }
154
+
155
+        /* Force kill if still running */
156
+        if (attempts >= 100) {
157
+            kill(child_pid, SIGKILL);
158
+            waitpid(child_pid, &status, 0);
159
+        }
160
+
161
+        child_pid = -1;
162
+    }
163
+}
164
+
165
+/*
166
+ * Check if child process is still alive
167
+ * Returns: 1 if alive, 0 if dead/not started
168
+ */
169
+int fortty_pty_child_alive(void) {
170
+    if (child_pid <= 0) {
171
+        return 0;
172
+    }
173
+
174
+    int status;
175
+    pid_t result = waitpid(child_pid, &status, WNOHANG);
176
+
177
+    if (result == 0) {
178
+        /* Child still running */
179
+        return 1;
180
+    }
181
+
182
+    if (result == child_pid) {
183
+        /* Child exited */
184
+        child_pid = -1;
185
+        return 0;
186
+    }
187
+
188
+    /* Error (treat as dead) */
189
+    return 0;
190
+}
191
+
192
+/*
193
+ * Get the child PID (for debugging/signals)
194
+ */
195
+int fortty_pty_get_child_pid(void) {
196
+    return (int)child_pid;
197
+}
src/fortty.f90modified
@@ -2,12 +2,21 @@ program fortty
22
   use window_mod
33
   use gl_bindings
44
   use renderer_mod
5
+  use pty_mod
56
   implicit none
67
 
78
   type(window_t) :: win
89
   type(renderer_t) :: ren
10
+  type(pty_t) :: pty
911
   integer :: win_width, win_height
12
+  integer :: prev_width, prev_height
13
+  integer :: term_rows, term_cols
14
+  integer :: new_rows, new_cols
1015
   character(len=256) :: font_path
16
+  character(len=4096) :: pty_buffer
17
+  integer :: nbytes
18
+  integer, parameter :: CELL_WIDTH = 10   ! Approximate char width
19
+  integer, parameter :: CELL_HEIGHT = 20  ! Approximate line height
1120
 
1221
   ! Window dimensions
1322
   win_width = 800
@@ -20,11 +29,11 @@ program fortty
2029
   font_path = "/usr/share/fonts/TTF/DejaVuSansMono.ttf"
2130
 
2231
   ! Create renderer with font
23
-  ren = renderer_create(trim(font_path), 24)
32
+  ren = renderer_create(trim(font_path), 16)
2433
   if (.not. ren%initialized) then
2534
     print *, "Warning: Could not load font, trying alternate path..."
2635
     font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
27
-    ren = renderer_create(trim(font_path), 24)
36
+    ren = renderer_create(trim(font_path), 16)
2837
   end if
2938
 
3039
   if (.not. ren%initialized) then
@@ -37,8 +46,53 @@ program fortty
3746
   ! Set up projection matrix
3847
   call renderer_set_projection(ren, win_width, win_height)
3948
 
49
+  ! Calculate terminal dimensions based on font metrics
50
+  term_cols = win_width / CELL_WIDTH
51
+  term_rows = win_height / CELL_HEIGHT
52
+  prev_width = win_width
53
+  prev_height = win_height
54
+
55
+  ! Open PTY with shell
56
+  pty = pty_open("", term_rows, term_cols)  ! Empty string = use $SHELL
57
+
58
+  if (.not. pty%active) then
59
+    print *, "Error: Could not open PTY"
60
+    call renderer_destroy(ren)
61
+    call window_destroy(win)
62
+    stop 1
63
+  end if
64
+
65
+  print *, "PTY opened successfully"
66
+  print *, "Terminal size:", term_cols, "x", term_rows
67
+
4068
   ! Main event loop
41
-  do while (.not. window_should_close(win))
69
+  do while (.not. window_should_close(win) .and. pty_is_alive(pty))
70
+    ! Check for window resize
71
+    call window_get_size(win, win_width, win_height)
72
+    if (win_width /= prev_width .or. win_height /= prev_height) then
73
+      prev_width = win_width
74
+      prev_height = win_height
75
+
76
+      ! Update projection matrix
77
+      call renderer_set_projection(ren, win_width, win_height)
78
+
79
+      ! Calculate new terminal size and notify PTY
80
+      new_cols = win_width / CELL_WIDTH
81
+      new_rows = win_height / CELL_HEIGHT
82
+      if (new_cols /= term_cols .or. new_rows /= term_rows) then
83
+        term_cols = new_cols
84
+        term_rows = new_rows
85
+        call pty_resize(pty, term_rows, term_cols)
86
+      end if
87
+    end if
88
+
89
+    ! Read from PTY (non-blocking)
90
+    nbytes = pty_read(pty, pty_buffer, 4096)
91
+    if (nbytes > 0) then
92
+      ! For now, just print to stdout (Phase 4 will render to screen)
93
+      write(*,'(A)', advance='no') pty_buffer(1:nbytes)
94
+    end if
95
+
4296
     ! Clear screen with dark gray background
4397
     call glClearColor(0.1, 0.1, 0.12, 1.0)
4498
     call glClear(GL_COLOR_BUFFER_BIT)
@@ -46,12 +100,12 @@ program fortty
46100
     ! Begin new frame
47101
     call renderer_begin(ren)
48102
 
49
-    ! Draw test text
50
-    call renderer_draw_string(ren, 50.0, 100.0, "Hello, Fortran!", &
51
-                              1.0, 1.0, 1.0, 1.0)
103
+    ! Draw status text
104
+    call renderer_draw_string(ren, 10.0, 30.0, "fortty - Shell connected", &
105
+                              0.0, 1.0, 0.0, 1.0)
52106
 
53
-    call renderer_draw_string(ren, 50.0, 150.0, "Terminal emulator in progress...", &
54
-                              0.7, 0.7, 0.7, 1.0)
107
+    call renderer_draw_string(ren, 10.0, 60.0, "(Shell output goes to stdout for now)", &
108
+                              0.5, 0.5, 0.5, 1.0)
55109
 
56110
     ! Flush to GPU
57111
     call renderer_flush(ren)
@@ -61,7 +115,11 @@ program fortty
61115
     call window_poll_events()
62116
   end do
63117
 
118
+  print *, ""
119
+  print *, "Shell exited or window closed"
120
+
64121
   ! Cleanup
122
+  call pty_close(pty)
65123
   call renderer_destroy(ren)
66124
   call window_destroy(win)
67125
 
src/pty/pty.f90added
@@ -0,0 +1,126 @@
1
+module pty_mod
2
+  use, intrinsic :: iso_c_binding
3
+  use pty_bindings
4
+  implicit none
5
+  private
6
+
7
+  public :: pty_t
8
+  public :: pty_open, pty_close, pty_resize
9
+  public :: pty_read, pty_write, pty_is_alive
10
+
11
+  type :: pty_t
12
+    integer :: master_fd = -1
13
+    integer :: rows = 24
14
+    integer :: cols = 80
15
+    logical :: active = .false.
16
+  end type pty_t
17
+
18
+contains
19
+
20
+  ! Open PTY and spawn shell
21
+  function pty_open(shell, rows, cols) result(p)
22
+    character(len=*), intent(in) :: shell
23
+    integer, intent(in) :: rows, cols
24
+    type(pty_t) :: p
25
+    integer(c_int) :: fd
26
+
27
+    p%rows = rows
28
+    p%cols = cols
29
+
30
+    ! Fork shell with PTY
31
+    fd = c_pty_fork(trim(shell) // c_null_char, int(rows, c_int), int(cols, c_int))
32
+
33
+    if (fd < 0) then
34
+      print *, "Error: Failed to create PTY"
35
+      p%active = .false.
36
+      return
37
+    end if
38
+
39
+    p%master_fd = fd
40
+    p%active = .true.
41
+
42
+  end function pty_open
43
+
44
+  ! Close PTY and cleanup
45
+  subroutine pty_close(p)
46
+    type(pty_t), intent(inout) :: p
47
+
48
+    if (p%master_fd >= 0) then
49
+      call c_pty_close(int(p%master_fd, c_int))
50
+      p%master_fd = -1
51
+    end if
52
+
53
+    p%active = .false.
54
+
55
+  end subroutine pty_close
56
+
57
+  ! Resize PTY
58
+  subroutine pty_resize(p, rows, cols)
59
+    type(pty_t), intent(inout) :: p
60
+    integer, intent(in) :: rows, cols
61
+    integer(c_int) :: result
62
+
63
+    if (.not. p%active) return
64
+
65
+    result = c_pty_set_size(int(p%master_fd, c_int), &
66
+                            int(rows, c_int), int(cols, c_int))
67
+
68
+    if (result == 0) then
69
+      p%rows = rows
70
+      p%cols = cols
71
+    end if
72
+
73
+  end subroutine pty_resize
74
+
75
+  ! Read from PTY (non-blocking)
76
+  ! Returns: bytes read (>0), 0 if no data, -1 on error/EOF
77
+  function pty_read(p, buffer, maxlen) result(nbytes)
78
+    type(pty_t), intent(in) :: p
79
+    character(len=*), intent(out) :: buffer
80
+    integer, intent(in) :: maxlen
81
+    integer :: nbytes
82
+
83
+    if (.not. p%active) then
84
+      nbytes = -1
85
+      return
86
+    end if
87
+
88
+    nbytes = c_pty_read(int(p%master_fd, c_int), buffer, int(maxlen, c_int))
89
+
90
+  end function pty_read
91
+
92
+  ! Write to PTY
93
+  subroutine pty_write(p, data, length)
94
+    type(pty_t), intent(in) :: p
95
+    character(len=*), intent(in) :: data
96
+    integer, intent(in) :: length
97
+    integer(c_int) :: result
98
+
99
+    if (.not. p%active) return
100
+
101
+    result = c_pty_write(int(p%master_fd, c_int), data, int(length, c_int))
102
+
103
+  end subroutine pty_write
104
+
105
+  ! Check if child shell is alive
106
+  function pty_is_alive(p) result(alive)
107
+    type(pty_t), intent(inout) :: p
108
+    logical :: alive
109
+    integer(c_int) :: status
110
+
111
+    if (.not. p%active) then
112
+      alive = .false.
113
+      return
114
+    end if
115
+
116
+    status = c_pty_child_alive()
117
+    alive = (status /= 0)
118
+
119
+    ! Update active flag if child died
120
+    if (.not. alive) then
121
+      p%active = .false.
122
+    end if
123
+
124
+  end function pty_is_alive
125
+
126
+end module pty_mod
src/pty/pty_bindings.f90added
@@ -0,0 +1,62 @@
1
+module pty_bindings
2
+  use, intrinsic :: iso_c_binding
3
+  implicit none
4
+  private
5
+
6
+  public :: c_pty_fork, c_pty_set_size, c_pty_read, c_pty_write
7
+  public :: c_pty_close, c_pty_child_alive, c_pty_get_child_pid
8
+
9
+  interface
10
+
11
+    ! Fork shell and return master fd (-1 on error)
12
+    integer(c_int) function c_pty_fork(shell, rows, cols) &
13
+        bind(C, name="fortty_pty_fork")
14
+      import :: c_int, c_char
15
+      character(kind=c_char), intent(in) :: shell(*)
16
+      integer(c_int), value :: rows, cols
17
+    end function c_pty_fork
18
+
19
+    ! Set PTY window size (0 on success, -1 on error)
20
+    integer(c_int) function c_pty_set_size(master_fd, rows, cols) &
21
+        bind(C, name="fortty_pty_set_size")
22
+      import :: c_int
23
+      integer(c_int), value :: master_fd, rows, cols
24
+    end function c_pty_set_size
25
+
26
+    ! Read from PTY (returns bytes read, 0 if would block, -1 on error/EOF)
27
+    integer(c_int) function c_pty_read(fd, buf, count) &
28
+        bind(C, name="fortty_pty_read")
29
+      import :: c_int, c_char
30
+      integer(c_int), value :: fd, count
31
+      character(kind=c_char), intent(out) :: buf(*)
32
+    end function c_pty_read
33
+
34
+    ! Write to PTY (returns bytes written, -1 on error)
35
+    integer(c_int) function c_pty_write(fd, buf, count) &
36
+        bind(C, name="fortty_pty_write")
37
+      import :: c_int, c_char
38
+      integer(c_int), value :: fd, count
39
+      character(kind=c_char), intent(in) :: buf(*)
40
+    end function c_pty_write
41
+
42
+    ! Close PTY and wait for child
43
+    subroutine c_pty_close(master_fd) bind(C, name="fortty_pty_close")
44
+      import :: c_int
45
+      integer(c_int), value :: master_fd
46
+    end subroutine c_pty_close
47
+
48
+    ! Check if child is alive (1 = alive, 0 = dead)
49
+    integer(c_int) function c_pty_child_alive() &
50
+        bind(C, name="fortty_pty_child_alive")
51
+      import :: c_int
52
+    end function c_pty_child_alive
53
+
54
+    ! Get child PID
55
+    integer(c_int) function c_pty_get_child_pid() &
56
+        bind(C, name="fortty_pty_get_child_pid")
57
+      import :: c_int
58
+    end function c_pty_get_child_pid
59
+
60
+  end interface
61
+
62
+end module pty_bindings
src/window/window.f90modified
@@ -9,6 +9,7 @@ module window_mod
99
   public :: window_t
1010
   public :: window_create, window_destroy
1111
   public :: window_should_close, window_swap_buffers, window_poll_events
12
+  public :: window_get_size
1213
 
1314
   type :: window_t
1415
     type(c_ptr) :: handle = c_null_ptr
@@ -120,6 +121,17 @@ contains
120121
     call glfwPollEvents()
121122
   end subroutine window_poll_events
122123
 
124
+  ! Get current framebuffer size
125
+  subroutine window_get_size(win, width, height)
126
+    type(window_t), intent(in) :: win
127
+    integer, intent(out) :: width, height
128
+    integer(c_int) :: w, h
129
+
130
+    call glfwGetFramebufferSize(win%handle, w, h)
131
+    width = int(w)
132
+    height = int(h)
133
+  end subroutine window_get_size
134
+
123135
   ! Callback: handle window resize
124136
   subroutine framebuffer_size_callback(window, width, height) bind(C)
125137
     type(c_ptr), value :: window