@@ -278,6 +278,16 @@ module readline |
| 278 | logical, save :: debug_selection = .false. | 278 | logical, save :: debug_selection = .false. |
| 279 | logical, save :: debug_selection_initialized = .false. | 279 | logical, save :: debug_selection_initialized = .false. |
| 280 | | 280 | |
| | 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 | + |
| 281 | contains | 291 | contains |
| 282 | | 292 | |
| 283 | !============================================================================ | 293 | !============================================================================ |
@@ -770,6 +780,135 @@ contains |
| 770 | ! END TEXT SELECTION HELPERS | 780 | ! END TEXT SELECTION HELPERS |
| 771 | !============================================================================ | 781 | !============================================================================ |
| 772 | | 782 | |
| | 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 | + |
| 773 | ! Initialize input_state_t with allocated strings | 912 | ! Initialize input_state_t with allocated strings |
| 774 | subroutine init_input_state(state) | 913 | subroutine init_input_state(state) |
| 775 | type(input_state_t), intent(inout) :: state | 914 | type(input_state_t), intent(inout) :: state |
@@ -1195,6 +1334,8 @@ contains |
| 1195 | call init_input_state(module_input_state) | 1334 | call init_input_state(module_input_state) |
| 1196 | ! Initialize syntax highlighting | 1335 | ! Initialize syntax highlighting |
| 1197 | call init_syntax_highlighting() | 1336 | call init_syntax_highlighting() |
| | 1337 | + ! Probe for system clipboard tool (Sprint 5 — pattern #19: once at init) |
| | 1338 | + call clipboard_detect() |
| 1198 | module_input_state_initialized = .true. | 1339 | module_input_state_initialized = .true. |
| 1199 | else | 1340 | else |
| 1200 | ! On subsequent calls, just reset the buffer and cursor | 1341 | ! On subsequent calls, just reset the buffer and cursor |