Skip to content

Commit 0bb5e5f

Browse files
emcdclaude
andcommitted
Fix clipboard hang caused by xclip background process.
Add clipboard.py module to work around pyperclip bug where xclip operations hang indefinitely. The issue occurs because xclip forks into the background to serve clipboard requests, but pyperclip calls subprocess.communicate() which waits for the process to exit. Solution: On Linux with X11, call xclip directly without waiting for exit. Write to stdin and close it, then return immediately while xclip forks to background. Fall back to pyperclip on other platforms. Update create.py, apply.py, and prompt.py to use new clipboard module instead of calling pyperclip directly. Related: asweigart/pyperclip#247 Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 2e0242d commit 0bb5e5f

File tree

4 files changed

+90
-6
lines changed

4 files changed

+90
-6
lines changed

sources/mimeogram/apply.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ def stdin_is_tty( self ) -> bool:
134134
return __.sys.stdin.isatty( )
135135

136136
async def acquire_clipboard( self ) -> str:
137-
from pyperclip import paste
138-
return paste( )
137+
from . import clipboard
138+
return clipboard.copy_from_clipboard( )
139139

140140
async def acquire_file( self, path: str | __.Path ) -> str:
141141
return await __.appcore.io.acquire_text_file_async( path )

sources/mimeogram/clipboard.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# vim: set filetype=python fileencoding=utf-8:
2+
# -*- coding: utf-8 -*-
3+
4+
#============================================================================#
5+
# #
6+
# Licensed under the Apache License, Version 2.0 (the "License"); #
7+
# you may not use this file except in compliance with the License. #
8+
# You may obtain a copy of the License at #
9+
# #
10+
# http://www.apache.org/licenses/LICENSE-2.0 #
11+
# #
12+
# Unless required by applicable law or agreed to in writing, software #
13+
# distributed under the License is distributed on an "AS IS" BASIS, #
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
15+
# See the License for the specific language governing permissions and #
16+
# limitations under the License. #
17+
# #
18+
#============================================================================#
19+
20+
21+
# Clipboard operations module.
22+
#
23+
# This module exists to work around a bug in pyperclip where calling
24+
# subprocess.communicate() on xclip causes the process to hang indefinitely.
25+
# xclip forks into the background to serve clipboard paste requests and never
26+
# exits, but pyperclip waits for it to exit. The same bug exists in pyclip.
27+
#
28+
# See: https://github.com/asweigart/pyperclip/issues/247
29+
#
30+
# Our solution: Call xclip directly on Linux/X11 without waiting for exit.
31+
# Fall back to pyperclip on other platforms (may still hang on some systems).
32+
33+
34+
''' Clipboard operations. '''
35+
36+
37+
import subprocess as _subprocess
38+
39+
from . import __
40+
41+
42+
_scribe = __.produce_scribe( __name__ )
43+
44+
45+
def copy_to_clipboard( text: str ) -> None:
46+
''' Copies text to clipboard. '''
47+
# Try xclip first on Linux with X11
48+
if __.sys.platform == 'linux' and __.os.environ.get( 'DISPLAY' ):
49+
try:
50+
# Use xclip directly, don't wait for it to finish
51+
# xclip forks into background and stays running to serve pastes
52+
proc = _subprocess.Popen(
53+
[ 'xclip', '-selection', 'clipboard' ], # noqa: S607
54+
stdin = _subprocess.PIPE,
55+
stdout = _subprocess.DEVNULL,
56+
stderr = _subprocess.DEVNULL,
57+
close_fds = True,
58+
)
59+
except FileNotFoundError:
60+
_scribe.debug( "xclip not found, falling back to pyperclip" )
61+
except Exception as exc:
62+
_scribe.warning(
63+
f"xclip failed ({exc}), falling back to pyperclip" )
64+
else:
65+
assert proc.stdin is not None # noqa: S101
66+
proc.stdin.write( text.encode( 'utf-8' ) )
67+
proc.stdin.close( )
68+
# Don't call proc.wait() or proc.communicate() - let xclip fork
69+
_scribe.debug( "Copied to clipboard via xclip" )
70+
return
71+
# Fall back to pyperclip for other platforms or if xclip fails
72+
from pyperclip import copy
73+
try:
74+
copy( text )
75+
except Exception as exc:
76+
_scribe.error( f"Failed to copy to clipboard: {exc}" )
77+
raise
78+
_scribe.debug( "Copied to clipboard via pyperclip" )
79+
80+
81+
def copy_from_clipboard( ) -> str:
82+
''' Copies text from clipboard. '''
83+
from pyperclip import paste
84+
return paste( )

sources/mimeogram/create.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ async def _acquire_prompt(
153153

154154

155155
async def _copy_to_clipboard( mimeogram: str ) -> None:
156-
from pyperclip import copy
157-
copy( mimeogram )
156+
from . import clipboard
157+
clipboard.copy_to_clipboard( mimeogram )
158158
_scribe.info( "Copied mimeogram to clipboard." )
159159

160160

sources/mimeogram/prompt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ async def provide_prompt(
7979
prompt = await acquire_prompt( auxdata )
8080
options = auxdata.configuration.get( 'prompt', { } )
8181
if options.get( 'to-clipboard', False ):
82-
from pyperclip import copy
82+
from . import clipboard
8383
with _exceptions.report_exceptions(
8484
_scribe, "Could not copy prompt to clipboard."
85-
): copy( prompt )
85+
): clipboard.copy_to_clipboard( prompt )
8686
_scribe.info( "Copied prompt to clipboard." )
8787
else: print( prompt ) # TODO? Use output stream from configuration.
8888
raise SystemExit( 0 )

0 commit comments

Comments
 (0)