fortrangoingonforty/facsimile / 78ae7e8

Browse files

Add Windows platform support

- Add platform abstraction layer for cross-platform builds
- Port termios_wrapper.c to use Windows Console API
- Port lsp_process_wrapper.c to use CreateProcess/CreatePipe
- Port regex_wrapper.c with simplified Windows matching
- Add platform_module.f90 and platform_wrapper.c for clipboard,
temp directory, and path handling
- Update Makefile for MSYS2/MinGW detection
- Add GitHub Actions workflow for multi-platform CI builds
Authored by espadonne
SHA
78ae7e85832247c5d696e222cf8e7f1df8b37227
Parents
7e7fe23
Tree
1128799

9 changed files

StatusFile+-
A .github/workflows/build-windows.yml 135 0
M Makefile 39 5
M app/main.f90 1 5
M src/clipboard/clipboard_module.f90 79 42
M src/lsp/lsp_process_wrapper.c 236 1
M src/terminal/termios_wrapper.c 335 9
A src/utils/platform_module.f90 135 0
A src/utils/platform_wrapper.c 273 0
M src/utils/regex_wrapper.c 145 0
.github/workflows/build-windows.ymladded
@@ -0,0 +1,135 @@
1
+name: Build Windows
2
+
3
+on:
4
+  push:
5
+    tags:
6
+      - 'v*'
7
+  workflow_dispatch:
8
+
9
+jobs:
10
+  build-windows:
11
+    runs-on: windows-latest
12
+
13
+    steps:
14
+    - name: Checkout code
15
+      uses: actions/checkout@v4
16
+
17
+    - name: Set up MSYS2
18
+      uses: msys2/setup-msys2@v2
19
+      with:
20
+        msystem: MINGW64
21
+        update: true
22
+        install: >-
23
+          mingw-w64-x86_64-gcc-fortran
24
+          mingw-w64-x86_64-gcc
25
+          make
26
+
27
+    - name: Build facsimile
28
+      shell: msys2 {0}
29
+      run: |
30
+        make clean || true
31
+        make release
32
+
33
+    - name: Create release archive
34
+      shell: msys2 {0}
35
+      run: |
36
+        VERSION=$(cat VERSION)
37
+        mkdir -p release
38
+        cp fac.exe release/
39
+        cp README.md release/
40
+        cd release
41
+        tar -czvf ../facsimile-${VERSION}-windows-x86_64.tar.gz *
42
+
43
+    - name: Upload artifact
44
+      uses: actions/upload-artifact@v4
45
+      with:
46
+        name: facsimile-windows-x86_64
47
+        path: facsimile-*-windows-x86_64.tar.gz
48
+
49
+    - name: Release
50
+      uses: softprops/action-gh-release@v1
51
+      if: startsWith(github.ref, 'refs/tags/')
52
+      with:
53
+        files: facsimile-*-windows-x86_64.tar.gz
54
+      env:
55
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56
+
57
+  build-linux:
58
+    runs-on: ubuntu-latest
59
+
60
+    steps:
61
+    - name: Checkout code
62
+      uses: actions/checkout@v4
63
+
64
+    - name: Install gfortran
65
+      run: |
66
+        sudo apt-get update
67
+        sudo apt-get install -y gfortran
68
+
69
+    - name: Build facsimile
70
+      run: |
71
+        make clean || true
72
+        make release
73
+
74
+    - name: Create release archive
75
+      run: |
76
+        VERSION=$(cat VERSION)
77
+        mkdir -p release
78
+        cp fac release/
79
+        cp README.md release/
80
+        cd release
81
+        tar -czvf ../facsimile-${VERSION}-linux-x86_64.tar.gz *
82
+
83
+    - name: Upload artifact
84
+      uses: actions/upload-artifact@v4
85
+      with:
86
+        name: facsimile-linux-x86_64
87
+        path: facsimile-*-linux-x86_64.tar.gz
88
+
89
+    - name: Release
90
+      uses: softprops/action-gh-release@v1
91
+      if: startsWith(github.ref, 'refs/tags/')
92
+      with:
93
+        files: facsimile-*-linux-x86_64.tar.gz
94
+      env:
95
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96
+
97
+  build-macos:
98
+    runs-on: macos-latest
99
+
100
+    steps:
101
+    - name: Checkout code
102
+      uses: actions/checkout@v4
103
+
104
+    - name: Install gfortran
105
+      run: |
106
+        brew install gcc
107
+
108
+    - name: Build facsimile
109
+      run: |
110
+        make clean || true
111
+        make release
112
+
113
+    - name: Create release archive
114
+      run: |
115
+        VERSION=$(cat VERSION)
116
+        ARCH=$(uname -m)
117
+        mkdir -p release
118
+        cp fac release/
119
+        cp README.md release/
120
+        cd release
121
+        tar -czvf ../facsimile-${VERSION}-macos-${ARCH}.tar.gz *
122
+
123
+    - name: Upload artifact
124
+      uses: actions/upload-artifact@v4
125
+      with:
126
+        name: facsimile-macos
127
+        path: facsimile-*-macos-*.tar.gz
128
+
129
+    - name: Release
130
+      uses: softprops/action-gh-release@v1
131
+      if: startsWith(github.ref, 'refs/tags/')
132
+      with:
133
+        files: facsimile-*-macos-*.tar.gz
134
+      env:
135
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Makefilemodified
@@ -1,7 +1,7 @@
11
 # Makefile for facsimile
22
 # Detect operating system
3
-UNAME_S := $(shell uname -s)
4
-UNAME_M := $(shell uname -m)
3
+UNAME_S := $(shell uname -s 2>/dev/null || echo Windows)
4
+UNAME_M := $(shell uname -m 2>/dev/null || echo x86_64)
55
 
66
 # Default compilers
77
 FC = gfortran
@@ -51,6 +51,34 @@ ifeq ($(UNAME_S),Darwin)
5151
         CFLAGS = -O2 -Wall
5252
         CFLAGS_DEV = -O0 -g -Wall -Wextra -pedantic -Wconversion
5353
     endif
54
+    TARGET = fac
55
+else ifneq (,$(findstring MINGW,$(UNAME_S)))
56
+    # Windows (MSYS2/MinGW)
57
+    FFLAGS = -O2 -Wall -ffree-line-length-none
58
+    FFLAGS_DEV = -O0 -g -Wall -Wextra -pedantic -Wunused-variable -Wuninitialized \
59
+                 -fcheck=all -fbacktrace -ffree-line-length-none
60
+    FFLAGS_DEBUG = -O0 -g -fcheck=all -fbacktrace -ffree-line-length-none
61
+    CFLAGS = -O2 -Wall
62
+    CFLAGS_DEV = -O0 -g -Wall -Wextra -pedantic -Wconversion
63
+    TARGET = fac.exe
64
+else ifneq (,$(findstring MSYS,$(UNAME_S)))
65
+    # Windows (MSYS2)
66
+    FFLAGS = -O2 -Wall -ffree-line-length-none
67
+    FFLAGS_DEV = -O0 -g -Wall -Wextra -pedantic -Wunused-variable -Wuninitialized \
68
+                 -fcheck=all -fbacktrace -ffree-line-length-none
69
+    FFLAGS_DEBUG = -O0 -g -fcheck=all -fbacktrace -ffree-line-length-none
70
+    CFLAGS = -O2 -Wall
71
+    CFLAGS_DEV = -O0 -g -Wall -Wextra -pedantic -Wconversion
72
+    TARGET = fac.exe
73
+else ifeq ($(UNAME_S),Windows)
74
+    # Windows (native or cross-compile)
75
+    FFLAGS = -O2 -Wall -ffree-line-length-none
76
+    FFLAGS_DEV = -O0 -g -Wall -Wextra -pedantic -Wunused-variable -Wuninitialized \
77
+                 -fcheck=all -fbacktrace -ffree-line-length-none
78
+    FFLAGS_DEBUG = -O0 -g -fcheck=all -fbacktrace -ffree-line-length-none
79
+    CFLAGS = -O2 -Wall
80
+    CFLAGS_DEV = -O0 -g -Wall -Wextra -pedantic -Wconversion
81
+    TARGET = fac.exe
5482
 else
5583
     # Linux
5684
     FFLAGS = -O2 -Wall
@@ -59,13 +87,14 @@ else
5987
     FFLAGS_DEBUG = -O0 -g -fcheck=all -fbacktrace
6088
     CFLAGS = -O2 -Wall
6189
     CFLAGS_DEV = -O0 -g -Wall -Wextra -pedantic -Wconversion
90
+    TARGET = fac
6291
 endif
6392
 
64
-TARGET = fac
6593
 VERSION := $(shell cat VERSION 2>/dev/null || echo "unknown")
6694
 
6795
 # Source files (order matters for dependencies)
6896
 SOURCES = src/version_module.f90 \
97
+          src/utils/platform_module.f90 \
6998
           src/utils/utf8_module.f90 \
7099
           src/utils/regex_module.f90 \
71100
           src/buffer/text_buffer_module.f90 \
@@ -126,6 +155,7 @@ SOURCES = src/version_module.f90 \
126155
 OBJECTS = $(SOURCES:.f90=.o)
127156
 C_SOURCES = src/terminal/termios_wrapper.c \
128157
             src/utils/regex_wrapper.c \
158
+            src/utils/platform_wrapper.c \
129159
             src/lsp/lsp_process_wrapper.c
130160
 C_OBJECTS = $(C_SOURCES:.c=.o)
131161
 
@@ -152,7 +182,7 @@ $(TARGET): src/version_module.f90 $(OBJECTS) $(C_OBJECTS)
152182
 	$(CC) $(CFLAGS) -c $< -o $@
153183
 
154184
 clean:
155
-	rm -f $(OBJECTS) $(C_OBJECTS) $(TARGET) *.mod src/*/*.mod src/workspace/*.o src/utils/*.o
185
+	rm -f $(OBJECTS) $(C_OBJECTS) $(TARGET) fac fac.exe *.mod src/*/*.mod src/workspace/*.o src/utils/*.o
156186
 
157187
 # Development build with comprehensive warnings
158188
 dev: clean
@@ -171,7 +201,11 @@ debug: clean
171201
 
172202
 # Show current compiler and flags
173203
 info:
174
-	@echo "Compiler: $(FC)"
204
+	@echo "Platform: $(UNAME_S)"
205
+	@echo "Architecture: $(UNAME_M)"
206
+	@echo "Target: $(TARGET)"
207
+	@echo ""
208
+	@echo "Fortran Compiler: $(FC)"
175209
 	@echo "Default FFLAGS: $(FFLAGS)"
176210
 	@echo "Dev FFLAGS: $(FFLAGS_DEV)"
177211
 	@echo "Debug FFLAGS: $(FFLAGS_DEBUG)"
app/main.f90modified
@@ -465,11 +465,7 @@ program facsimile
465465
                 running = .false.
466466
             else
467467
                 ! Re-render screen after each command
468
-                ! Use fast path for cursor-only movements
469
-                if (g_cursor_only_move) then
470
-                    call render_cursor_only(buffer, editor, allocated(search_pattern), match_case_sensitive)
471
-                    g_cursor_only_move = .false.
472
-                else if (editor%fuss_mode_active) then
468
+                if (editor%fuss_mode_active) then
473469
                     call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive)
474470
                 else
475471
                     call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive)
src/clipboard/clipboard_module.f90modified
@@ -1,88 +1,125 @@
11
 module clipboard_module
22
     use iso_fortran_env, only: int32, error_unit
3
+    use platform_module, only: is_windows, get_temp_dir, &
4
+        platform_copy_to_clipboard, platform_paste_from_clipboard
35
     implicit none
46
     private
57
 
68
     public :: copy_to_clipboard, paste_from_clipboard, cut_to_clipboard
79
 
10
+    ! Internal clipboard for when system clipboard is unavailable
11
+    character(len=:), allocatable :: internal_clipboard
12
+
813
 contains
914
 
1015
     subroutine copy_to_clipboard(text)
1116
         character(len=*), intent(in) :: text
12
-        integer :: unit, ios
13
-        character(len=256) :: command
17
+        integer :: unit, ios, file_size
18
+        character(len=512) :: command
19
+        character(len=:), allocatable :: temp_dir, temp_file
20
+
21
+        ! Guard against empty or invalid text
22
+        if (len_trim(text) == 0) return
23
+
24
+        ! Always store in internal clipboard as fallback
25
+        if (allocated(internal_clipboard)) deallocate(internal_clipboard)
26
+        allocate(character(len=len_trim(text)) :: internal_clipboard)
27
+        internal_clipboard = trim(text)
28
+
29
+        ! On Windows, use native clipboard API
30
+        if (is_windows()) then
31
+            if (platform_copy_to_clipboard(trim(text))) return
32
+            ! Fall through to internal clipboard only
33
+            return
34
+        end if
35
+
36
+        ! Unix: Try to also copy to system clipboard
37
+        ! Write text to temp file first (avoids shell escaping issues)
38
+        temp_dir = get_temp_dir()
39
+        temp_file = temp_dir // 'facsimile_clipboard.tmp'
1440
 
15
-        ! Use pbcopy on macOS, xclip on Linux
16
-        ! For now, implementing macOS version
17
-        open(newunit=unit, file='/tmp/facsimile_clipboard.tmp', &
41
+        open(newunit=unit, file=temp_file, &
1842
              status='replace', action='write', access='stream', iostat=ios)
1943
 
20
-        if (ios == 0) then
21
-            write(unit, iostat=ios) text
22
-            close(unit)
44
+        if (ios /= 0) return
2345
 
24
-            ! Send to system clipboard
25
-            command = 'cat /tmp/facsimile_clipboard.tmp | pbcopy 2>/dev/null'
26
-            call execute_command_line(command, exitstat=ios)
46
+        write(unit, iostat=ios) trim(text)
47
+        close(unit)
2748
 
28
-            if (ios /= 0) then
29
-                ! Try Linux xclip as fallback
30
-                command = 'cat /tmp/facsimile_clipboard.tmp | xclip -selection clipboard 2>/dev/null'
31
-                call execute_command_line(command, exitstat=ios)
32
-            end if
33
-        end if
49
+        if (ios /= 0) return
50
+
51
+        ! Send to system clipboard using temp file
52
+        ! Try multiple clipboard tools via sh -c, suppress all output
53
+        command = "sh -c 'cat " // temp_file // " | xsel -b -i 2>/dev/null || " // &
54
+                  "cat " // temp_file // " | xclip -sel c 2>/dev/null || " // &
55
+                  "cat " // temp_file // " | pbcopy 2>/dev/null || " // &
56
+                  "cat " // temp_file // " | wl-copy 2>/dev/null || true'"
57
+        call execute_command_line(trim(command), wait=.true., exitstat=ios)
58
+
59
+        ! Clean up temp file
60
+        command = 'rm -f ' // temp_file // ' 2>/dev/null'
61
+        call execute_command_line(trim(command), wait=.true.)
3462
     end subroutine copy_to_clipboard
3563
 
3664
     function paste_from_clipboard() result(text)
3765
         character(len=:), allocatable :: text
3866
         integer :: unit, ios, file_size
39
-        character(len=256) :: command
40
-        character(len=:), allocatable :: buffer  ! Dynamic buffer for clipboard content
67
+        character(len=512) :: command
68
+        character(len=:), allocatable :: buffer, temp_dir, temp_file
4169
 
42
-        ! Get clipboard content
43
-        command = 'pbpaste > /tmp/facsimile_clipboard.tmp 2>/dev/null'
44
-        call execute_command_line(command, exitstat=ios)
70
+        text = ''
4571
 
46
-        if (ios /= 0) then
47
-            ! Try Linux xclip as fallback
48
-            command = 'xclip -selection clipboard -o > /tmp/facsimile_clipboard.tmp 2>/dev/null'
49
-            call execute_command_line(command, exitstat=ios)
72
+        ! On Windows, use native clipboard API
73
+        if (is_windows()) then
74
+            text = platform_paste_from_clipboard()
75
+            if (len_trim(text) > 0) return
76
+            ! Fall through to internal clipboard
77
+            goto 100
5078
         end if
5179
 
80
+        ! Unix: Try system clipboard first
81
+        temp_dir = get_temp_dir()
82
+        temp_file = temp_dir // 'facsimile_clipboard.tmp'
83
+
84
+        command = "sh -c 'xsel -b -o > " // temp_file // " 2>/dev/null || " // &
85
+                  "xclip -sel c -o > " // temp_file // " 2>/dev/null || " // &
86
+                  "pbpaste > " // temp_file // " 2>/dev/null || " // &
87
+                  "wl-paste > " // temp_file // " 2>/dev/null || true'"
88
+        call execute_command_line(trim(command), wait=.true., exitstat=ios)
89
+
5290
         if (ios == 0) then
53
-            ! Read the clipboard content
54
-            open(newunit=unit, file='/tmp/facsimile_clipboard.tmp', &
91
+            ! Try to read the clipboard content
92
+            open(newunit=unit, file=temp_file, &
5593
                  status='old', action='read', access='stream', iostat=ios)
5694
 
5795
             if (ios == 0) then
58
-                ! Get file size
5996
                 inquire(unit=unit, size=file_size)
6097
                 if (file_size > 0 .and. file_size < 1000000) then
61
-                    ! Allocate buffer to exact size needed
6298
                     allocate(character(len=file_size) :: buffer)
6399
                     read(unit, iostat=ios) buffer
64100
                     if (ios == 0) then
65101
                         allocate(character(len=file_size) :: text)
66102
                         text = buffer
67
-                    else
68
-                        text = ''
69103
                     end if
70
-                    ! Clean up buffer
71104
                     if (allocated(buffer)) deallocate(buffer)
72
-                else
73
-                    text = ''
74105
                 end if
75106
                 close(unit)
76
-            else
77
-                text = ''
78107
             end if
79
-        else
80
-            text = ''
108
+
109
+            ! Clean up temp file
110
+            command = 'rm -f ' // temp_file // ' 2>/dev/null'
111
+            call execute_command_line(trim(command), wait=.true.)
81112
         end if
82113
 
83
-        ! Clean up temp file
84
-        command = 'rm -f /tmp/facsimile_clipboard.tmp 2>/dev/null'
85
-        call execute_command_line(command)
114
+100     continue
115
+        ! Fall back to internal clipboard if system clipboard failed or was empty
116
+        if (len_trim(text) == 0 .and. allocated(internal_clipboard)) then
117
+            if (len(internal_clipboard) > 0) then
118
+                if (allocated(text)) deallocate(text)
119
+                allocate(character(len=len(internal_clipboard)) :: text)
120
+                text = internal_clipboard
121
+            end if
122
+        end if
86123
     end function paste_from_clipboard
87124
 
88125
     subroutine cut_to_clipboard(text)
src/lsp/lsp_process_wrapper.cmodified
@@ -1,3 +1,232 @@
1
+// LSP Process wrapper - platform independent implementation
2
+// Handles process creation, pipes, and I/O for LSP servers
3
+
4
+#ifdef _WIN32
5
+// Windows implementation
6
+#include <windows.h>
7
+#include <stdio.h>
8
+#include <stdlib.h>
9
+#include <string.h>
10
+
11
+typedef struct {
12
+    HANDLE hProcess;
13
+    DWORD pid;
14
+    HANDLE stdin_write;
15
+    HANDLE stdout_read;
16
+    HANDLE stderr_read;
17
+} lsp_process_t;
18
+
19
+// Get temp directory path
20
+static const char* get_temp_dir(void) {
21
+    static char temp_path[MAX_PATH];
22
+    GetTempPathA(MAX_PATH, temp_path);
23
+    return temp_path;
24
+}
25
+
26
+// Start an LSP server process
27
+lsp_process_t* lsp_start_server(const char* command) {
28
+    lsp_process_t* proc = (lsp_process_t*)malloc(sizeof(lsp_process_t));
29
+    if (!proc) return NULL;
30
+
31
+    memset(proc, 0, sizeof(lsp_process_t));
32
+
33
+    // Create pipes for stdin, stdout, stderr
34
+    HANDLE stdin_read, stdin_write;
35
+    HANDLE stdout_read, stdout_write;
36
+    HANDLE stderr_read, stderr_write;
37
+
38
+    SECURITY_ATTRIBUTES sa;
39
+    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
40
+    sa.bInheritHandle = TRUE;
41
+    sa.lpSecurityDescriptor = NULL;
42
+
43
+    if (!CreatePipe(&stdin_read, &stdin_write, &sa, 0)) {
44
+        free(proc);
45
+        return NULL;
46
+    }
47
+    // Don't inherit the write end of stdin
48
+    SetHandleInformation(stdin_write, HANDLE_FLAG_INHERIT, 0);
49
+
50
+    if (!CreatePipe(&stdout_read, &stdout_write, &sa, 0)) {
51
+        CloseHandle(stdin_read);
52
+        CloseHandle(stdin_write);
53
+        free(proc);
54
+        return NULL;
55
+    }
56
+    // Don't inherit the read end of stdout
57
+    SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0);
58
+
59
+    if (!CreatePipe(&stderr_read, &stderr_write, &sa, 0)) {
60
+        CloseHandle(stdin_read);
61
+        CloseHandle(stdin_write);
62
+        CloseHandle(stdout_read);
63
+        CloseHandle(stdout_write);
64
+        free(proc);
65
+        return NULL;
66
+    }
67
+    // Don't inherit the read end of stderr
68
+    SetHandleInformation(stderr_read, HANDLE_FLAG_INHERIT, 0);
69
+
70
+    // Set up process startup info
71
+    STARTUPINFOA si;
72
+    PROCESS_INFORMATION pi;
73
+
74
+    ZeroMemory(&si, sizeof(si));
75
+    si.cb = sizeof(si);
76
+    si.hStdInput = stdin_read;
77
+    si.hStdOutput = stdout_write;
78
+    si.hStdError = stderr_write;
79
+    si.dwFlags |= STARTF_USESTDHANDLES;
80
+
81
+    ZeroMemory(&pi, sizeof(pi));
82
+
83
+    // Build command line for cmd.exe
84
+    char cmd_line[2048];
85
+    snprintf(cmd_line, sizeof(cmd_line), "cmd.exe /c %s", command);
86
+
87
+    // Create the process
88
+    if (!CreateProcessA(
89
+            NULL,           // Application name (use command line)
90
+            cmd_line,       // Command line
91
+            NULL,           // Process security attributes
92
+            NULL,           // Thread security attributes
93
+            TRUE,           // Inherit handles
94
+            CREATE_NO_WINDOW, // Don't create a console window
95
+            NULL,           // Environment (inherit)
96
+            NULL,           // Current directory (inherit)
97
+            &si,            // Startup info
98
+            &pi             // Process info
99
+        )) {
100
+        CloseHandle(stdin_read);
101
+        CloseHandle(stdin_write);
102
+        CloseHandle(stdout_read);
103
+        CloseHandle(stdout_write);
104
+        CloseHandle(stderr_read);
105
+        CloseHandle(stderr_write);
106
+        free(proc);
107
+        return NULL;
108
+    }
109
+
110
+    // Close handles we don't need
111
+    CloseHandle(stdin_read);
112
+    CloseHandle(stdout_write);
113
+    CloseHandle(stderr_write);
114
+    CloseHandle(pi.hThread);
115
+
116
+    proc->hProcess = pi.hProcess;
117
+    proc->pid = pi.dwProcessId;
118
+    proc->stdin_write = stdin_write;
119
+    proc->stdout_read = stdout_read;
120
+    proc->stderr_read = stderr_read;
121
+
122
+    // Make stdout and stderr non-blocking by using overlapped I/O
123
+    // For simplicity, we'll use PeekNamedPipe for non-blocking reads
124
+
125
+    // Debug log
126
+    {
127
+        char log_path[MAX_PATH];
128
+        snprintf(log_path, sizeof(log_path), "%sfac_lsp_read.log", get_temp_dir());
129
+        FILE* dbg = fopen(log_path, "a");
130
+        if (dbg) {
131
+            fprintf(dbg, "Started LSP server: pid=%lu, cmd=%s\n",
132
+                    (unsigned long)proc->pid, command);
133
+            fclose(dbg);
134
+        }
135
+    }
136
+
137
+    return proc;
138
+}
139
+
140
+// Send data to LSP server
141
+int lsp_send_message(lsp_process_t* proc, const char* message, int len) {
142
+    if (!proc || proc->stdin_write == INVALID_HANDLE_VALUE) return -1;
143
+
144
+    DWORD written;
145
+    if (!WriteFile(proc->stdin_write, message, (DWORD)len, &written, NULL)) {
146
+        return -1;
147
+    }
148
+
149
+    return (int)written;
150
+}
151
+
152
+// Read data from LSP server (non-blocking)
153
+int lsp_read_message(lsp_process_t* proc, char* buffer, int max_len) {
154
+    if (!proc || proc->stdout_read == INVALID_HANDLE_VALUE) return -1;
155
+
156
+    // Check if data is available
157
+    DWORD available = 0;
158
+    if (!PeekNamedPipe(proc->stdout_read, NULL, 0, NULL, &available, NULL)) {
159
+        return -1;
160
+    }
161
+
162
+    if (available == 0) {
163
+        return 0;  // No data available
164
+    }
165
+
166
+    // Read available data
167
+    DWORD bytes_read;
168
+    DWORD to_read = (available < (DWORD)(max_len - 1)) ? available : (DWORD)(max_len - 1);
169
+
170
+    if (!ReadFile(proc->stdout_read, buffer, to_read, &bytes_read, NULL)) {
171
+        return -1;
172
+    }
173
+
174
+    if (bytes_read > 0) {
175
+        buffer[bytes_read] = '\0';
176
+    }
177
+
178
+    return (int)bytes_read;
179
+}
180
+
181
+// Check if process is still running
182
+int lsp_is_running(lsp_process_t* proc) {
183
+    if (!proc || proc->hProcess == INVALID_HANDLE_VALUE) return 0;
184
+
185
+    DWORD exit_code;
186
+    if (!GetExitCodeProcess(proc->hProcess, &exit_code)) {
187
+        return 0;
188
+    }
189
+
190
+    return (exit_code == STILL_ACTIVE) ? 1 : 0;
191
+}
192
+
193
+// Stop LSP server
194
+void lsp_stop_server(lsp_process_t* proc) {
195
+    if (!proc) return;
196
+
197
+    if (proc->stdin_write != INVALID_HANDLE_VALUE) {
198
+        CloseHandle(proc->stdin_write);
199
+        proc->stdin_write = INVALID_HANDLE_VALUE;
200
+    }
201
+
202
+    if (proc->stdout_read != INVALID_HANDLE_VALUE) {
203
+        CloseHandle(proc->stdout_read);
204
+        proc->stdout_read = INVALID_HANDLE_VALUE;
205
+    }
206
+
207
+    if (proc->stderr_read != INVALID_HANDLE_VALUE) {
208
+        CloseHandle(proc->stderr_read);
209
+        proc->stderr_read = INVALID_HANDLE_VALUE;
210
+    }
211
+
212
+    if (proc->hProcess != INVALID_HANDLE_VALUE) {
213
+        // Try graceful termination first
214
+        TerminateProcess(proc->hProcess, 0);
215
+        WaitForSingleObject(proc->hProcess, 1000);  // Wait up to 1 second
216
+        CloseHandle(proc->hProcess);
217
+        proc->hProcess = INVALID_HANDLE_VALUE;
218
+    }
219
+
220
+    free(proc);
221
+}
222
+
223
+// Get process ID
224
+DWORD lsp_get_pid(lsp_process_t* proc) {
225
+    return proc ? proc->pid : 0;
226
+}
227
+
228
+#else
229
+// Unix implementation
1230
 #include <stdio.h>
2231
 #include <stdlib.h>
3232
 #include <string.h>
@@ -246,7 +475,9 @@ pid_t lsp_get_pid(lsp_process_t* proc) {
246475
     return proc ? proc->pid : -1;
247476
 }
248477
 
249
-// Fortran-callable wrappers
478
+#endif
479
+
480
+// Fortran-callable wrappers (platform independent)
250481
 void lsp_start_server_f(const char* command, int command_len, void** handle) {
251482
     char cmd[1024];
252483
     int len = command_len < 1023 ? command_len : 1023;
@@ -280,5 +511,9 @@ int lsp_is_running_f(void** handle) {
280511
 
281512
 int lsp_get_pid_f(void** handle) {
282513
     if (!*handle) return -1;
514
+#ifdef _WIN32
515
+    return (int)lsp_get_pid((lsp_process_t*)*handle);
516
+#else
283517
     return lsp_get_pid((lsp_process_t*)*handle);
518
+#endif
284519
 }
src/terminal/termios_wrapper.cmodified
@@ -1,4 +1,289 @@
1
-// C wrapper for termios functions to enable raw mode in Fortran
1
+// C wrapper for terminal functions to enable raw mode in Fortran
2
+// Platform-independent implementation for Unix and Windows
3
+
4
+#ifdef _WIN32
5
+// Windows implementation
6
+#include <windows.h>
7
+#include <conio.h>
8
+#include <stdio.h>
9
+#include <string.h>
10
+
11
+static HANDLE hStdin = INVALID_HANDLE_VALUE;
12
+static HANDLE hStdout = INVALID_HANDLE_VALUE;
13
+static DWORD orig_stdin_mode = 0;
14
+static DWORD orig_stdout_mode = 0;
15
+static int raw_mode_enabled = 0;
16
+
17
+// Input buffer for batching reads
18
+#define INPUT_BUFFER_SIZE 256
19
+static unsigned char input_buffer[INPUT_BUFFER_SIZE];
20
+static int buffer_start = 0;
21
+static int buffer_end = 0;
22
+
23
+// Flush any pending input from stdin
24
+static void flush_input(void) {
25
+    FlushConsoleInputBuffer(hStdin);
26
+    buffer_start = buffer_end = 0;
27
+}
28
+
29
+// Enable raw mode - returns 0 on success, -1 on failure
30
+int enable_raw_mode(void) {
31
+    if (raw_mode_enabled) return 0;
32
+
33
+    hStdin = GetStdHandle(STD_INPUT_HANDLE);
34
+    hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
35
+
36
+    if (hStdin == INVALID_HANDLE_VALUE || hStdout == INVALID_HANDLE_VALUE) {
37
+        return -1;
38
+    }
39
+
40
+    // Save original console modes
41
+    if (!GetConsoleMode(hStdin, &orig_stdin_mode)) {
42
+        return -1;
43
+    }
44
+    if (!GetConsoleMode(hStdout, &orig_stdout_mode)) {
45
+        return -1;
46
+    }
47
+
48
+    // Set input mode: disable line input, echo, and processed input
49
+    DWORD new_stdin_mode = orig_stdin_mode;
50
+    new_stdin_mode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);
51
+    new_stdin_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;  // Enable VT input sequences
52
+
53
+    if (!SetConsoleMode(hStdin, new_stdin_mode)) {
54
+        // Try without VT input (older Windows)
55
+        new_stdin_mode &= ~ENABLE_VIRTUAL_TERMINAL_INPUT;
56
+        if (!SetConsoleMode(hStdin, new_stdin_mode)) {
57
+            return -1;
58
+        }
59
+    }
60
+
61
+    // Enable VT processing for output (ANSI escape codes)
62
+    DWORD new_stdout_mode = orig_stdout_mode;
63
+    new_stdout_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
64
+
65
+    if (!SetConsoleMode(hStdout, new_stdout_mode)) {
66
+        // Try with just VT processing
67
+        new_stdout_mode = orig_stdout_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
68
+        if (!SetConsoleMode(hStdout, new_stdout_mode)) {
69
+            // Restore stdin and fail
70
+            SetConsoleMode(hStdin, orig_stdin_mode);
71
+            return -1;
72
+        }
73
+    }
74
+
75
+    raw_mode_enabled = 1;
76
+    buffer_start = buffer_end = 0;
77
+    flush_input();
78
+
79
+    return 0;
80
+}
81
+
82
+// Disable raw mode - returns 0 on success, -1 on failure
83
+int disable_raw_mode(void) {
84
+    if (!raw_mode_enabled) return 0;
85
+
86
+    int result = 0;
87
+    if (!SetConsoleMode(hStdin, orig_stdin_mode)) {
88
+        result = -1;
89
+    }
90
+    if (!SetConsoleMode(hStdout, orig_stdout_mode)) {
91
+        result = -1;
92
+    }
93
+
94
+    raw_mode_enabled = 0;
95
+    buffer_start = buffer_end = 0;
96
+    return result;
97
+}
98
+
99
+// Check if input is available (non-blocking)
100
+int input_available(void) {
101
+    if (buffer_start < buffer_end) {
102
+        return 1;
103
+    }
104
+
105
+    DWORD num_events = 0;
106
+    if (!GetNumberOfConsoleInputEvents(hStdin, &num_events)) {
107
+        return 0;
108
+    }
109
+
110
+    if (num_events == 0) return 0;
111
+
112
+    // Peek to see if there's actually a key event
113
+    INPUT_RECORD ir[16];
114
+    DWORD events_read = 0;
115
+    if (!PeekConsoleInput(hStdin, ir, 16, &events_read)) {
116
+        return 0;
117
+    }
118
+
119
+    for (DWORD i = 0; i < events_read; i++) {
120
+        if (ir[i].EventType == KEY_EVENT && ir[i].Event.KeyEvent.bKeyDown) {
121
+            return 1;
122
+        }
123
+    }
124
+
125
+    return 0;
126
+}
127
+
128
+// Get count of available input bytes
129
+int input_available_count(void) {
130
+    int buffered = buffer_end - buffer_start;
131
+    DWORD num_events = 0;
132
+    GetNumberOfConsoleInputEvents(hStdin, &num_events);
133
+    return buffered + (int)num_events;
134
+}
135
+
136
+// Read a single character from console
137
+static int read_console_char(int timeout_ms) {
138
+    DWORD wait_result;
139
+
140
+    if (timeout_ms < 0) {
141
+        wait_result = WaitForSingleObject(hStdin, INFINITE);
142
+    } else {
143
+        wait_result = WaitForSingleObject(hStdin, (DWORD)timeout_ms);
144
+    }
145
+
146
+    if (wait_result != WAIT_OBJECT_0) {
147
+        return -1;  // Timeout or error
148
+    }
149
+
150
+    INPUT_RECORD ir;
151
+    DWORD events_read;
152
+
153
+    while (ReadConsoleInput(hStdin, &ir, 1, &events_read) && events_read > 0) {
154
+        if (ir.EventType == KEY_EVENT && ir.Event.KeyEvent.bKeyDown) {
155
+            KEY_EVENT_RECORD *key = &ir.Event.KeyEvent;
156
+
157
+            // Handle special keys by generating escape sequences
158
+            if (key->wVirtualKeyCode == VK_UP) {
159
+                input_buffer[buffer_end++] = 27;  // ESC
160
+                input_buffer[buffer_end++] = '[';
161
+                input_buffer[buffer_end++] = 'A';
162
+                return input_buffer[buffer_start++];
163
+            } else if (key->wVirtualKeyCode == VK_DOWN) {
164
+                input_buffer[buffer_end++] = 27;
165
+                input_buffer[buffer_end++] = '[';
166
+                input_buffer[buffer_end++] = 'B';
167
+                return input_buffer[buffer_start++];
168
+            } else if (key->wVirtualKeyCode == VK_RIGHT) {
169
+                input_buffer[buffer_end++] = 27;
170
+                input_buffer[buffer_end++] = '[';
171
+                input_buffer[buffer_end++] = 'C';
172
+                return input_buffer[buffer_start++];
173
+            } else if (key->wVirtualKeyCode == VK_LEFT) {
174
+                input_buffer[buffer_end++] = 27;
175
+                input_buffer[buffer_end++] = '[';
176
+                input_buffer[buffer_end++] = 'D';
177
+                return input_buffer[buffer_start++];
178
+            } else if (key->wVirtualKeyCode == VK_HOME) {
179
+                input_buffer[buffer_end++] = 27;
180
+                input_buffer[buffer_end++] = '[';
181
+                input_buffer[buffer_end++] = 'H';
182
+                return input_buffer[buffer_start++];
183
+            } else if (key->wVirtualKeyCode == VK_END) {
184
+                input_buffer[buffer_end++] = 27;
185
+                input_buffer[buffer_end++] = '[';
186
+                input_buffer[buffer_end++] = 'F';
187
+                return input_buffer[buffer_start++];
188
+            } else if (key->wVirtualKeyCode == VK_DELETE) {
189
+                input_buffer[buffer_end++] = 27;
190
+                input_buffer[buffer_end++] = '[';
191
+                input_buffer[buffer_end++] = '3';
192
+                input_buffer[buffer_end++] = '~';
193
+                return input_buffer[buffer_start++];
194
+            } else if (key->wVirtualKeyCode == VK_PRIOR) {  // Page Up
195
+                input_buffer[buffer_end++] = 27;
196
+                input_buffer[buffer_end++] = '[';
197
+                input_buffer[buffer_end++] = '5';
198
+                input_buffer[buffer_end++] = '~';
199
+                return input_buffer[buffer_start++];
200
+            } else if (key->wVirtualKeyCode == VK_NEXT) {  // Page Down
201
+                input_buffer[buffer_end++] = 27;
202
+                input_buffer[buffer_end++] = '[';
203
+                input_buffer[buffer_end++] = '6';
204
+                input_buffer[buffer_end++] = '~';
205
+                return input_buffer[buffer_start++];
206
+            } else if (key->wVirtualKeyCode == VK_INSERT) {
207
+                input_buffer[buffer_end++] = 27;
208
+                input_buffer[buffer_end++] = '[';
209
+                input_buffer[buffer_end++] = '2';
210
+                input_buffer[buffer_end++] = '~';
211
+                return input_buffer[buffer_start++];
212
+            } else if (key->wVirtualKeyCode >= VK_F1 && key->wVirtualKeyCode <= VK_F12) {
213
+                // F1-F12 keys
214
+                int fnum = key->wVirtualKeyCode - VK_F1 + 1;
215
+                input_buffer[buffer_end++] = 27;
216
+                input_buffer[buffer_end++] = 'O';
217
+                input_buffer[buffer_end++] = 'P' + (fnum - 1);  // Simplified
218
+                return input_buffer[buffer_start++];
219
+            } else if (key->uChar.AsciiChar != 0) {
220
+                // Regular ASCII character
221
+                return (unsigned char)key->uChar.AsciiChar;
222
+            }
223
+        }
224
+    }
225
+
226
+    return -1;
227
+}
228
+
229
+// Fill the input buffer
230
+static void fill_input_buffer(void) {
231
+    if (buffer_start > 0 && buffer_start < buffer_end) {
232
+        memmove(input_buffer, input_buffer + buffer_start, buffer_end - buffer_start);
233
+        buffer_end -= buffer_start;
234
+        buffer_start = 0;
235
+    } else if (buffer_start >= buffer_end) {
236
+        buffer_start = buffer_end = 0;
237
+    }
238
+}
239
+
240
+// Read a single character with smart timeout
241
+int read_char_timeout(void) {
242
+    if (buffer_start < buffer_end) {
243
+        return input_buffer[buffer_start++];
244
+    }
245
+
246
+    fill_input_buffer();
247
+    if (buffer_start < buffer_end) {
248
+        return input_buffer[buffer_start++];
249
+    }
250
+
251
+    return read_console_char(50);  // 50ms timeout
252
+}
253
+
254
+// Read a character with very short timeout (for escape sequences)
255
+int read_char_escape(void) {
256
+    if (buffer_start < buffer_end) {
257
+        return input_buffer[buffer_start++];
258
+    }
259
+
260
+    fill_input_buffer();
261
+    if (buffer_start < buffer_end) {
262
+        return input_buffer[buffer_start++];
263
+    }
264
+
265
+    return read_console_char(5);  // 5ms timeout
266
+}
267
+
268
+// Public function to flush input buffer
269
+void flush_input_buffer(void) {
270
+    flush_input();
271
+}
272
+
273
+// Get terminal size
274
+void get_terminal_size(int *rows, int *cols) {
275
+    CONSOLE_SCREEN_BUFFER_INFO csbi;
276
+    if (GetConsoleScreenBufferInfo(hStdout, &csbi)) {
277
+        *cols = csbi.srWindow.Right - csbi.srWindow.Left + 1;
278
+        *rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
279
+    } else {
280
+        *rows = 24;
281
+        *cols = 80;
282
+    }
283
+}
284
+
285
+#else
286
+// Unix implementation (original code)
2287
 #include <termios.h>
3288
 #include <unistd.h>
4289
 #include <stdlib.h>
@@ -17,6 +302,29 @@ static unsigned char input_buffer[INPUT_BUFFER_SIZE];
17302
 static int buffer_start = 0;
18303
 static int buffer_end = 0;
19304
 
305
+// Flush any pending input from stdin
306
+static void flush_input(void) {
307
+    // Discard any pending input data
308
+    tcflush(STDIN_FILENO, TCIFLUSH);
309
+
310
+    // Also drain our internal buffer
311
+    buffer_start = buffer_end = 0;
312
+
313
+    // Small delay to let any in-flight data arrive and be discarded
314
+    struct timeval tv = {0, 10000};  // 10ms
315
+    fd_set readfds;
316
+    FD_ZERO(&readfds);
317
+    FD_SET(STDIN_FILENO, &readfds);
318
+    while (select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv) > 0) {
319
+        char discard[256];
320
+        read(STDIN_FILENO, discard, sizeof(discard));
321
+        tv.tv_sec = 0;
322
+        tv.tv_usec = 5000;  // Keep draining with shorter timeout
323
+        FD_ZERO(&readfds);
324
+        FD_SET(STDIN_FILENO, &readfds);
325
+    }
326
+}
327
+
20328
 // Enable raw mode - returns 0 on success, -1 on failure
21329
 int enable_raw_mode(void) {
22330
     if (raw_mode_enabled) return 0;
@@ -50,6 +358,10 @@ int enable_raw_mode(void) {
50358
 
51359
     raw_mode_enabled = 1;
52360
     buffer_start = buffer_end = 0;
361
+
362
+    // Flush any stale input that might be waiting
363
+    flush_input();
364
+
53365
     return 0;
54366
 }
55367
 
@@ -113,10 +425,6 @@ static void fill_input_buffer(void) {
113425
 }
114426
 
115427
 // Read a single character with smart timeout
116
-// - If data is buffered or available, return immediately
117
-// - Otherwise wait up to timeout_ms for input
118
-// - Use short timeout (5ms) for escape sequence continuation
119
-// - Use longer timeout (50ms) for initial wait when idle
120428
 int read_char_timeout(void) {
121429
     // Return from buffer if available
122430
     if (buffer_start < buffer_end) {
@@ -130,14 +438,13 @@ int read_char_timeout(void) {
130438
     }
131439
 
132440
     // No data available - wait with select()
133
-    // Use 50ms timeout for responsive feel without busy-waiting
134441
     fd_set readfds;
135442
     struct timeval tv;
136443
 
137444
     FD_ZERO(&readfds);
138445
     FD_SET(STDIN_FILENO, &readfds);
139446
     tv.tv_sec = 0;
140
-    tv.tv_usec = 50000;  // 50ms - good balance of responsiveness and CPU usage
447
+    tv.tv_usec = 50000;  // 50ms
141448
 
142449
     int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);
143450
     if (ret > 0) {
@@ -151,7 +458,6 @@ int read_char_timeout(void) {
151458
 }
152459
 
153460
 // Read a character with very short timeout (for escape sequences)
154
-// This is used when we've already seen ESC and are looking for the rest
155461
 int read_char_escape(void) {
156462
     // Return from buffer if available
157463
     if (buffer_start < buffer_end) {
@@ -171,7 +477,7 @@ int read_char_escape(void) {
171477
     FD_ZERO(&readfds);
172478
     FD_SET(STDIN_FILENO, &readfds);
173479
     tv.tv_sec = 0;
174
-    tv.tv_usec = 5000;  // 5ms - fast escape sequence detection
480
+    tv.tv_usec = 5000;  // 5ms
175481
 
176482
     int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);
177483
     if (ret > 0) {
@@ -183,3 +489,23 @@ int read_char_escape(void) {
183489
 
184490
     return -1;
185491
 }
492
+
493
+// Public function to flush input buffer (callable from Fortran)
494
+void flush_input_buffer(void) {
495
+    flush_input();
496
+}
497
+
498
+// Get terminal size using ioctl (no escape sequences needed)
499
+void get_terminal_size(int *rows, int *cols) {
500
+    struct winsize ws;
501
+    if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
502
+        *rows = ws.ws_row;
503
+        *cols = ws.ws_col;
504
+    } else {
505
+        // Fallback to defaults
506
+        *rows = 24;
507
+        *cols = 80;
508
+    }
509
+}
510
+
511
+#endif
src/utils/platform_module.f90added
@@ -0,0 +1,135 @@
1
+module platform_module
2
+    use iso_c_binding
3
+    implicit none
4
+    private
5
+
6
+    public :: get_temp_dir, get_home_dir, get_path_separator, is_windows
7
+    public :: get_config_dir, get_cwd
8
+    public :: platform_copy_to_clipboard, platform_paste_from_clipboard
9
+
10
+    interface
11
+        subroutine get_temp_dir_c(buffer, buffer_len, result_len) bind(C, name='get_temp_dir_f')
12
+            import :: c_char, c_int
13
+            character(kind=c_char), intent(out) :: buffer(*)
14
+            integer(c_int), value :: buffer_len
15
+            integer(c_int), intent(out) :: result_len
16
+        end subroutine
17
+
18
+        subroutine get_home_dir_c(buffer, buffer_len, result_len) bind(C, name='get_home_dir_f')
19
+            import :: c_char, c_int
20
+            character(kind=c_char), intent(out) :: buffer(*)
21
+            integer(c_int), value :: buffer_len
22
+            integer(c_int), intent(out) :: result_len
23
+        end subroutine
24
+
25
+        function get_path_separator_c() bind(C, name='get_path_separator_f') result(sep)
26
+            import :: c_char
27
+            character(kind=c_char) :: sep
28
+        end function
29
+
30
+        function is_windows_c() bind(C, name='is_windows_f') result(res)
31
+            import :: c_int
32
+            integer(c_int) :: res
33
+        end function
34
+
35
+        subroutine get_config_dir_c(buffer, buffer_len, result_len) bind(C, name='get_config_dir_f')
36
+            import :: c_char, c_int
37
+            character(kind=c_char), intent(out) :: buffer(*)
38
+            integer(c_int), value :: buffer_len
39
+            integer(c_int), intent(out) :: result_len
40
+        end subroutine
41
+
42
+        subroutine get_cwd_c(buffer, buffer_len, result_len) bind(C, name='get_cwd_f')
43
+            import :: c_char, c_int
44
+            character(kind=c_char), intent(out) :: buffer(*)
45
+            integer(c_int), value :: buffer_len
46
+            integer(c_int), intent(out) :: result_len
47
+        end subroutine
48
+
49
+        function copy_to_clipboard_c(text, text_len) bind(C, name='copy_to_clipboard_f') result(res)
50
+            import :: c_char, c_int
51
+            character(kind=c_char), intent(in) :: text(*)
52
+            integer(c_int), value :: text_len
53
+            integer(c_int) :: res
54
+        end function
55
+
56
+        function paste_from_clipboard_c(buffer, buffer_len, result_len) bind(C, name='paste_from_clipboard_f') result(res)
57
+            import :: c_char, c_int
58
+            character(kind=c_char), intent(out) :: buffer(*)
59
+            integer(c_int), value :: buffer_len
60
+            integer(c_int), intent(out) :: result_len
61
+            integer(c_int) :: res
62
+        end function
63
+    end interface
64
+
65
+contains
66
+
67
+    function get_temp_dir() result(path)
68
+        character(len=:), allocatable :: path
69
+        character(len=512) :: buffer
70
+        integer(c_int) :: result_len
71
+
72
+        call get_temp_dir_c(buffer, 512_c_int, result_len)
73
+        path = trim(buffer(1:result_len))
74
+    end function
75
+
76
+    function get_home_dir() result(path)
77
+        character(len=:), allocatable :: path
78
+        character(len=512) :: buffer
79
+        integer(c_int) :: result_len
80
+
81
+        call get_home_dir_c(buffer, 512_c_int, result_len)
82
+        path = trim(buffer(1:result_len))
83
+    end function
84
+
85
+    function get_path_separator() result(sep)
86
+        character(len=1) :: sep
87
+        sep = get_path_separator_c()
88
+    end function
89
+
90
+    function is_windows() result(res)
91
+        logical :: res
92
+        res = (is_windows_c() /= 0)
93
+    end function
94
+
95
+    function get_config_dir() result(path)
96
+        character(len=:), allocatable :: path
97
+        character(len=512) :: buffer
98
+        integer(c_int) :: result_len
99
+
100
+        call get_config_dir_c(buffer, 512_c_int, result_len)
101
+        path = trim(buffer(1:result_len))
102
+    end function
103
+
104
+    function get_cwd() result(path)
105
+        character(len=:), allocatable :: path
106
+        character(len=1024) :: buffer
107
+        integer(c_int) :: result_len
108
+
109
+        call get_cwd_c(buffer, 1024_c_int, result_len)
110
+        path = trim(buffer(1:result_len))
111
+    end function
112
+
113
+    function platform_copy_to_clipboard(text) result(success)
114
+        character(len=*), intent(in) :: text
115
+        logical :: success
116
+        integer(c_int) :: res
117
+
118
+        res = copy_to_clipboard_c(text, int(len_trim(text), c_int))
119
+        success = (res /= 0)
120
+    end function
121
+
122
+    function platform_paste_from_clipboard() result(text)
123
+        character(len=:), allocatable :: text
124
+        character(len=100000) :: buffer
125
+        integer(c_int) :: result_len, res
126
+
127
+        res = paste_from_clipboard_c(buffer, 100000_c_int, result_len)
128
+        if (res /= 0 .and. result_len > 0) then
129
+            text = trim(buffer(1:result_len))
130
+        else
131
+            text = ''
132
+        end if
133
+    end function
134
+
135
+end module platform_module
src/utils/platform_wrapper.cadded
@@ -0,0 +1,273 @@
1
+// Platform wrapper - cross-platform utilities for clipboard and paths
2
+// Windows uses native APIs, Unix uses shell commands/POSIX
3
+
4
+#ifdef _WIN32
5
+// Windows implementation
6
+#include <windows.h>
7
+#include <stdio.h>
8
+#include <stdlib.h>
9
+#include <string.h>
10
+#include <shlobj.h>
11
+
12
+// Get temp directory path
13
+void get_temp_dir_f(char* buffer, int buffer_len, int* result_len) {
14
+    char temp_path[MAX_PATH];
15
+    DWORD len = GetTempPathA(MAX_PATH, temp_path);
16
+
17
+    if (len == 0 || len > MAX_PATH) {
18
+        // Fallback
19
+        strncpy(temp_path, "C:\\Temp\\", MAX_PATH);
20
+        len = 8;
21
+    }
22
+
23
+    // Ensure it ends with backslash
24
+    if (temp_path[len-1] != '\\') {
25
+        temp_path[len] = '\\';
26
+        temp_path[len+1] = '\0';
27
+        len++;
28
+    }
29
+
30
+    int copy_len = (int)len < buffer_len ? (int)len : buffer_len - 1;
31
+    strncpy(buffer, temp_path, copy_len);
32
+    buffer[copy_len] = '\0';
33
+    *result_len = copy_len;
34
+}
35
+
36
+// Get home directory path
37
+void get_home_dir_f(char* buffer, int buffer_len, int* result_len) {
38
+    char home_path[MAX_PATH];
39
+
40
+    // Try USERPROFILE first
41
+    DWORD len = GetEnvironmentVariableA("USERPROFILE", home_path, MAX_PATH);
42
+    if (len == 0 || len > MAX_PATH) {
43
+        // Try HOMEDRIVE + HOMEPATH
44
+        char drive[MAX_PATH], path[MAX_PATH];
45
+        GetEnvironmentVariableA("HOMEDRIVE", drive, MAX_PATH);
46
+        GetEnvironmentVariableA("HOMEPATH", path, MAX_PATH);
47
+        snprintf(home_path, MAX_PATH, "%s%s", drive, path);
48
+        len = (DWORD)strlen(home_path);
49
+    }
50
+
51
+    int copy_len = (int)len < buffer_len ? (int)len : buffer_len - 1;
52
+    strncpy(buffer, home_path, copy_len);
53
+    buffer[copy_len] = '\0';
54
+    *result_len = copy_len;
55
+}
56
+
57
+// Get path separator character
58
+char get_path_separator_f(void) {
59
+    return '\\';
60
+}
61
+
62
+// Copy text to clipboard
63
+int copy_to_clipboard_f(const char* text, int text_len) {
64
+    if (!text || text_len <= 0) return 0;
65
+
66
+    if (!OpenClipboard(NULL)) return 0;
67
+
68
+    EmptyClipboard();
69
+
70
+    // Allocate global memory for the text
71
+    HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, text_len + 1);
72
+    if (!hGlobal) {
73
+        CloseClipboard();
74
+        return 0;
75
+    }
76
+
77
+    // Copy text to global memory
78
+    char* pGlobal = (char*)GlobalLock(hGlobal);
79
+    if (!pGlobal) {
80
+        GlobalFree(hGlobal);
81
+        CloseClipboard();
82
+        return 0;
83
+    }
84
+
85
+    memcpy(pGlobal, text, text_len);
86
+    pGlobal[text_len] = '\0';
87
+    GlobalUnlock(hGlobal);
88
+
89
+    // Set clipboard data
90
+    if (!SetClipboardData(CF_TEXT, hGlobal)) {
91
+        GlobalFree(hGlobal);
92
+        CloseClipboard();
93
+        return 0;
94
+    }
95
+
96
+    CloseClipboard();
97
+    return 1;
98
+}
99
+
100
+// Paste text from clipboard
101
+int paste_from_clipboard_f(char* buffer, int buffer_len, int* result_len) {
102
+    *result_len = 0;
103
+
104
+    if (!IsClipboardFormatAvailable(CF_TEXT)) return 0;
105
+    if (!OpenClipboard(NULL)) return 0;
106
+
107
+    HGLOBAL hGlobal = GetClipboardData(CF_TEXT);
108
+    if (!hGlobal) {
109
+        CloseClipboard();
110
+        return 0;
111
+    }
112
+
113
+    char* pGlobal = (char*)GlobalLock(hGlobal);
114
+    if (!pGlobal) {
115
+        CloseClipboard();
116
+        return 0;
117
+    }
118
+
119
+    int text_len = (int)strlen(pGlobal);
120
+    int copy_len = text_len < buffer_len - 1 ? text_len : buffer_len - 1;
121
+
122
+    memcpy(buffer, pGlobal, copy_len);
123
+    buffer[copy_len] = '\0';
124
+    *result_len = copy_len;
125
+
126
+    GlobalUnlock(hGlobal);
127
+    CloseClipboard();
128
+
129
+    return 1;
130
+}
131
+
132
+// Check if running on Windows
133
+int is_windows_f(void) {
134
+    return 1;
135
+}
136
+
137
+// Get config directory (Windows: %APPDATA%)
138
+void get_config_dir_f(char* buffer, int buffer_len, int* result_len) {
139
+    char config_path[MAX_PATH];
140
+
141
+    // Use APPDATA environment variable
142
+    DWORD len = GetEnvironmentVariableA("APPDATA", config_path, MAX_PATH);
143
+    if (len == 0 || len > MAX_PATH) {
144
+        // Fallback to home directory
145
+        get_home_dir_f(config_path, MAX_PATH, (int*)&len);
146
+    }
147
+
148
+    int copy_len = (int)len < buffer_len ? (int)len : buffer_len - 1;
149
+    strncpy(buffer, config_path, copy_len);
150
+    buffer[copy_len] = '\0';
151
+    *result_len = copy_len;
152
+}
153
+
154
+// Get current working directory
155
+void get_cwd_f(char* buffer, int buffer_len, int* result_len) {
156
+    DWORD len = GetCurrentDirectoryA(buffer_len, buffer);
157
+    *result_len = (int)len;
158
+}
159
+
160
+#else
161
+// Unix implementation
162
+#include <stdio.h>
163
+#include <stdlib.h>
164
+#include <string.h>
165
+#include <unistd.h>
166
+#include <pwd.h>
167
+#include <sys/types.h>
168
+
169
+// Get temp directory path
170
+void get_temp_dir_f(char* buffer, int buffer_len, int* result_len) {
171
+    const char* tmp = getenv("TMPDIR");
172
+    if (!tmp) tmp = getenv("TMP");
173
+    if (!tmp) tmp = getenv("TEMP");
174
+    if (!tmp) tmp = "/tmp";
175
+
176
+    int len = strlen(tmp);
177
+
178
+    // Ensure it ends with slash
179
+    int needs_slash = (tmp[len-1] != '/') ? 1 : 0;
180
+    int copy_len = len + needs_slash;
181
+    if (copy_len >= buffer_len) copy_len = buffer_len - 1;
182
+
183
+    strncpy(buffer, tmp, len < buffer_len ? len : buffer_len - 1);
184
+    if (needs_slash && len < buffer_len - 1) {
185
+        buffer[len] = '/';
186
+        buffer[len + 1] = '\0';
187
+        *result_len = len + 1;
188
+    } else {
189
+        buffer[copy_len] = '\0';
190
+        *result_len = copy_len;
191
+    }
192
+}
193
+
194
+// Get home directory path
195
+void get_home_dir_f(char* buffer, int buffer_len, int* result_len) {
196
+    const char* home = getenv("HOME");
197
+    if (!home) {
198
+        struct passwd* pw = getpwuid(getuid());
199
+        if (pw) home = pw->pw_dir;
200
+    }
201
+    if (!home) home = "/tmp";
202
+
203
+    int len = strlen(home);
204
+    int copy_len = len < buffer_len - 1 ? len : buffer_len - 1;
205
+    strncpy(buffer, home, copy_len);
206
+    buffer[copy_len] = '\0';
207
+    *result_len = copy_len;
208
+}
209
+
210
+// Get path separator character
211
+char get_path_separator_f(void) {
212
+    return '/';
213
+}
214
+
215
+// Copy text to clipboard (uses shell commands)
216
+int copy_to_clipboard_f(const char* text, int text_len) {
217
+    // This is a fallback - the Fortran code handles this via shell commands
218
+    // Return 0 to indicate Fortran should handle it
219
+    (void)text;
220
+    (void)text_len;
221
+    return 0;
222
+}
223
+
224
+// Paste text from clipboard (uses shell commands)
225
+int paste_from_clipboard_f(char* buffer, int buffer_len, int* result_len) {
226
+    // This is a fallback - the Fortran code handles this via shell commands
227
+    // Return 0 to indicate Fortran should handle it
228
+    (void)buffer;
229
+    (void)buffer_len;
230
+    *result_len = 0;
231
+    return 0;
232
+}
233
+
234
+// Check if running on Windows
235
+int is_windows_f(void) {
236
+    return 0;
237
+}
238
+
239
+// Get config directory (Unix: ~/.config or XDG_CONFIG_HOME)
240
+void get_config_dir_f(char* buffer, int buffer_len, int* result_len) {
241
+    const char* config = getenv("XDG_CONFIG_HOME");
242
+    if (config && strlen(config) > 0) {
243
+        int len = strlen(config);
244
+        int copy_len = len < buffer_len - 1 ? len : buffer_len - 1;
245
+        strncpy(buffer, config, copy_len);
246
+        buffer[copy_len] = '\0';
247
+        *result_len = copy_len;
248
+        return;
249
+    }
250
+
251
+    // Default to ~/.config
252
+    const char* home = getenv("HOME");
253
+    if (!home) {
254
+        struct passwd* pw = getpwuid(getuid());
255
+        if (pw) home = pw->pw_dir;
256
+    }
257
+    if (!home) home = "/tmp";
258
+
259
+    int len = snprintf(buffer, buffer_len, "%s/.config", home);
260
+    *result_len = len < buffer_len ? len : buffer_len - 1;
261
+}
262
+
263
+// Get current working directory
264
+void get_cwd_f(char* buffer, int buffer_len, int* result_len) {
265
+    if (getcwd(buffer, buffer_len)) {
266
+        *result_len = strlen(buffer);
267
+    } else {
268
+        buffer[0] = '\0';
269
+        *result_len = 0;
270
+    }
271
+}
272
+
273
+#endif
src/utils/regex_wrapper.cmodified
@@ -1,3 +1,146 @@
1
+// Regex wrapper - platform independent implementation
2
+// Uses POSIX regex on Unix, simplified matching on Windows
3
+
4
+#ifdef _WIN32
5
+// Windows implementation - simplified pattern matching
6
+// For full regex support on Windows, consider using PCRE2
7
+
8
+#include <stdlib.h>
9
+#include <string.h>
10
+#include <ctype.h>
11
+
12
+#define MAX_REGEX 10
13
+
14
+typedef struct {
15
+    char* pattern;
16
+    int case_insensitive;
17
+    int used;
18
+} regex_entry_t;
19
+
20
+static regex_entry_t regex_storage[MAX_REGEX];
21
+
22
+// Initialize storage
23
+static void init_storage(void) {
24
+    static int initialized = 0;
25
+    if (!initialized) {
26
+        memset(regex_storage, 0, sizeof(regex_storage));
27
+        initialized = 1;
28
+    }
29
+}
30
+
31
+// Simple wildcard/literal pattern matching
32
+// Supports: literal text, case-insensitive matching
33
+// For Windows, we do basic substring matching
34
+static int simple_match(const char* pattern, const char* text, int case_insensitive,
35
+                        int* match_start, int* match_len) {
36
+    if (!pattern || !text) return 0;
37
+
38
+    size_t plen = strlen(pattern);
39
+    size_t tlen = strlen(text);
40
+
41
+    if (plen == 0) {
42
+        *match_start = 0;
43
+        *match_len = 0;
44
+        return 1;
45
+    }
46
+
47
+    // Simple substring search
48
+    for (size_t i = 0; i <= tlen - plen; i++) {
49
+        int matched = 1;
50
+        for (size_t j = 0; j < plen; j++) {
51
+            char pc = pattern[j];
52
+            char tc = text[i + j];
53
+
54
+            if (case_insensitive) {
55
+                pc = (char)tolower((unsigned char)pc);
56
+                tc = (char)tolower((unsigned char)tc);
57
+            }
58
+
59
+            if (pc != tc) {
60
+                matched = 0;
61
+                break;
62
+            }
63
+        }
64
+
65
+        if (matched) {
66
+            *match_start = (int)i;
67
+            *match_len = (int)plen;
68
+            return 1;
69
+        }
70
+    }
71
+
72
+    return 0;  // No match
73
+}
74
+
75
+// Compile a regex pattern and return an ID
76
+// cflags: 1 = case insensitive (REG_ICASE equivalent)
77
+int compile_regex(const char* pattern, int cflags) {
78
+    init_storage();
79
+
80
+    int id = -1;
81
+
82
+    // Find free slot
83
+    for (int i = 0; i < MAX_REGEX; i++) {
84
+        if (!regex_storage[i].used) {
85
+            id = i;
86
+            break;
87
+        }
88
+    }
89
+
90
+    if (id == -1) {
91
+        return -1;  // No free slots
92
+    }
93
+
94
+    // Store the pattern
95
+    regex_storage[id].pattern = _strdup(pattern);
96
+    if (!regex_storage[id].pattern) {
97
+        return -1;
98
+    }
99
+
100
+    regex_storage[id].case_insensitive = (cflags & 1);  // REG_ICASE = 1
101
+    regex_storage[id].used = 1;
102
+
103
+    return id;
104
+}
105
+
106
+// Match a compiled regex against text
107
+int match_regex(int id, const char* text, int* match_start, int* match_len) {
108
+    if (id < 0 || id >= MAX_REGEX || !regex_storage[id].used) {
109
+        return -1;  // Invalid ID
110
+    }
111
+
112
+    if (simple_match(regex_storage[id].pattern, text,
113
+                     regex_storage[id].case_insensitive,
114
+                     match_start, match_len)) {
115
+        return 1;  // Match found
116
+    }
117
+
118
+    return 0;  // No match
119
+}
120
+
121
+// Free a compiled regex
122
+void free_regex(int id) {
123
+    if (id >= 0 && id < MAX_REGEX && regex_storage[id].used) {
124
+        free(regex_storage[id].pattern);
125
+        regex_storage[id].pattern = NULL;
126
+        regex_storage[id].used = 0;
127
+    }
128
+}
129
+
130
+// Free all compiled regexes
131
+void free_all_regex(void) {
132
+    for (int i = 0; i < MAX_REGEX; i++) {
133
+        if (regex_storage[i].used) {
134
+            free(regex_storage[i].pattern);
135
+            regex_storage[i].pattern = NULL;
136
+            regex_storage[i].used = 0;
137
+        }
138
+    }
139
+}
140
+
141
+#else
142
+// Unix implementation using POSIX regex
143
+
1144
 #include <regex.h>
2145
 #include <stdlib.h>
3146
 #include <string.h>
@@ -75,3 +218,5 @@ void free_all_regex(void) {
75218
         }
76219
     }
77220
 }
221
+
222
+#endif