fortrangoingonforty/fortsh / 998f55b

Browse files

add clipboard bridge: pbcopy/wl-copy/xclip/xsel detection

Authored by espadonne
SHA
998f55b5b60ed9cfeb69a8c1e8fdccc73616a8c5
Parents
3132484
Tree
caf1b0c

1 changed file

StatusFile+-
M src/io/readline.f90 141 0
src/io/readline.f90modified
@@ -278,6 +278,16 @@ module readline
278278
   logical, save :: debug_selection = .false.
279279
   logical, save :: debug_selection_initialized = .false.
280280
 
281
+  ! Clipboard bridge state (shift phase, Sprint 5).
282
+  ! Probed once at init; the detected tool is cached. Pattern #19.
283
+  integer, parameter :: CLIP_NONE   = 0
284
+  integer, parameter :: CLIP_PBCOPY = 1  ! macOS
285
+  integer, parameter :: CLIP_WLCOPY = 2  ! Wayland
286
+  integer, parameter :: CLIP_XCLIP  = 3  ! X11
287
+  integer, parameter :: CLIP_XSEL   = 4  ! X11 fallback
288
+  integer, save :: clipboard_tool = CLIP_NONE
289
+  logical, save :: clipboard_initialized = .false.
290
+
281291
 contains
282292
 
283293
   !============================================================================
@@ -770,6 +780,135 @@ contains
770780
   ! END TEXT SELECTION HELPERS
771781
   !============================================================================
772782
 
783
+  !============================================================================
784
+  ! CLIPBOARD BRIDGE (shift phase, Sprint 5)
785
+  !============================================================================
786
+  ! Provides system-clipboard copy and paste via external tools.
787
+  ! Probe order: pbcopy (macOS), wl-copy (Wayland), xclip (X11), xsel (X11).
788
+  ! If no tool is found, operations no-op gracefully — the in-session
789
+  ! kill_buffer remains the source of truth. Pattern #19, #22.
790
+  !============================================================================
791
+
792
+  ! Detect the clipboard tool at startup (idempotent).
793
+  subroutine clipboard_detect()
794
+    character(len=:), allocatable :: result
795
+
796
+    if (clipboard_initialized) return
797
+    clipboard_initialized = .true.
798
+
799
+    ! Probe in preference order. execute_and_capture runs `which <tool>`
800
+    ! and returns the path if found, or an empty string if not.
801
+    result = execute_and_capture('which pbcopy 2>/dev/null')
802
+    if (len_trim(result) > 0) then
803
+      clipboard_tool = CLIP_PBCOPY
804
+      return
805
+    end if
806
+
807
+    result = execute_and_capture('which wl-copy 2>/dev/null')
808
+    if (len_trim(result) > 0) then
809
+      clipboard_tool = CLIP_WLCOPY
810
+      return
811
+    end if
812
+
813
+    result = execute_and_capture('which xclip 2>/dev/null')
814
+    if (len_trim(result) > 0) then
815
+      clipboard_tool = CLIP_XCLIP
816
+      return
817
+    end if
818
+
819
+    result = execute_and_capture('which xsel 2>/dev/null')
820
+    if (len_trim(result) > 0) then
821
+      clipboard_tool = CLIP_XSEL
822
+      return
823
+    end if
824
+
825
+    ! No tool found — clipboard_tool stays CLIP_NONE.
826
+  end subroutine clipboard_detect
827
+
828
+  ! Copy text to the system clipboard. No-op if no tool was detected.
829
+  subroutine clipboard_copy(text, text_len)
830
+    use iso_c_binding, only: c_ptr, c_null_char, c_loc, c_int, c_associated
831
+    character(len=*), intent(in) :: text
832
+    integer, intent(in) :: text_len
833
+    type(c_ptr) :: pipe_ptr
834
+    integer(c_int) :: rc
835
+    character(len=256), target :: c_command
836
+    character(len=4), target :: c_mode
837
+    ! Buffer for writing — must be null-terminated for c_fputs.
838
+    ! Use MAX_LINE_LEN+1 to accommodate the NUL terminator.
839
+    character(len=MAX_LINE_LEN+1), target :: c_text
840
+
841
+    if (.not. clipboard_initialized) call clipboard_detect()
842
+    if (clipboard_tool == CLIP_NONE) return
843
+    if (text_len <= 0) return
844
+
845
+    ! Build the popen command for the detected tool.
846
+    select case (clipboard_tool)
847
+    case (CLIP_PBCOPY)
848
+      c_command = 'pbcopy' // c_null_char
849
+    case (CLIP_WLCOPY)
850
+      c_command = 'wl-copy' // c_null_char
851
+    case (CLIP_XCLIP)
852
+      c_command = 'xclip -selection clipboard' // c_null_char
853
+    case (CLIP_XSEL)
854
+      c_command = 'xsel --clipboard --input' // c_null_char
855
+    case default
856
+      return
857
+    end select
858
+
859
+    c_mode = 'w' // c_null_char
860
+
861
+    pipe_ptr = c_popen(c_loc(c_command), c_loc(c_mode))
862
+    if (.not. c_associated(pipe_ptr)) return
863
+
864
+    ! Write the text, null-terminated, to the pipe.
865
+    c_text = text(1:text_len) // c_null_char
866
+    rc = c_fputs(c_loc(c_text), pipe_ptr)
867
+
868
+    rc = c_pclose(pipe_ptr)
869
+  end subroutine clipboard_copy
870
+
871
+  ! Paste text from the system clipboard into a buffer.
872
+  ! Returns the number of bytes read (0 if no tool or empty clipboard).
873
+  subroutine clipboard_paste(buffer, buffer_len, bytes_read)
874
+    character(len=*), intent(out) :: buffer
875
+    integer, intent(in) :: buffer_len
876
+    integer, intent(out) :: bytes_read
877
+    character(len=:), allocatable :: result
878
+    character(len=256) :: paste_cmd
879
+
880
+    bytes_read = 0
881
+
882
+    if (.not. clipboard_initialized) call clipboard_detect()
883
+    if (clipboard_tool == CLIP_NONE) return
884
+
885
+    ! Build the paste command.
886
+    select case (clipboard_tool)
887
+    case (CLIP_PBCOPY)
888
+      paste_cmd = 'pbpaste -Prefer txt 2>/dev/null'
889
+    case (CLIP_WLCOPY)
890
+      paste_cmd = 'wl-paste --no-newline 2>/dev/null'
891
+    case (CLIP_XCLIP)
892
+      paste_cmd = 'xclip -selection clipboard -o 2>/dev/null'
893
+    case (CLIP_XSEL)
894
+      paste_cmd = 'xsel --clipboard --output 2>/dev/null'
895
+    case default
896
+      return
897
+    end select
898
+
899
+    result = execute_and_capture(trim(paste_cmd))
900
+    if (.not. allocated(result)) return
901
+    if (len_trim(result) == 0) return
902
+
903
+    bytes_read = min(len_trim(result), buffer_len)
904
+    buffer = ''
905
+    buffer(1:bytes_read) = result(1:bytes_read)
906
+  end subroutine clipboard_paste
907
+
908
+  !============================================================================
909
+  ! END CLIPBOARD BRIDGE
910
+  !============================================================================
911
+
773912
   ! Initialize input_state_t with allocated strings
774913
   subroutine init_input_state(state)
775914
     type(input_state_t), intent(inout) :: state
@@ -1195,6 +1334,8 @@ contains
11951334
       call init_input_state(module_input_state)
11961335
       ! Initialize syntax highlighting
11971336
       call init_syntax_highlighting()
1337
+      ! Probe for system clipboard tool (Sprint 5 — pattern #19: once at init)
1338
+      call clipboard_detect()
11981339
       module_input_state_initialized = .true.
11991340
     else
12001341
       ! On subsequent calls, just reset the buffer and cursor