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