7373)
7474from prompt_toolkit .application import create_app_session , get_app
7575from prompt_toolkit .auto_suggest import AutoSuggestFromHistory
76+ from prompt_toolkit .clipboard import Clipboard
7677from prompt_toolkit .clipboard .pyperclip import PyperclipClipboard
7778from prompt_toolkit .completion import Completer , DummyCompleter
7879from prompt_toolkit .formatted_text import ANSI , AnyFormattedText
@@ -386,31 +387,32 @@ def __init__(
386387 ) -> None :
387388 """Easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
388389
389- :param completekey: name of a completion key, default to 'tab'. (If None or an empty string, 'tab' is used)
390+ :param completekey: name of a completion key, default to 'tab'. (If `` None`` or an empty string, 'tab' is used)
390391 :param stdin: alternate input file object, if not specified, sys.stdin is used
391392 :param stdout: alternate output file object, if not specified, sys.stdout is used
392393 :param allow_cli_args: if ``True``, then [cmd2.Cmd.__init__][] will process command
393394 line arguments as either commands to be run. This should be
394395 set to ``False`` if your application parses its own command line
395396 arguments.
396- :param allow_clipboard: If False, cmd2 will disable clipboard interactions
397+ :param allow_clipboard: If `` False`` , cmd2 will disable clipboard interactions
397398 :param allow_redirection: If ``False``, prevent output redirection and piping to shell
398399 commands. This parameter prevents redirection and piping, but
399400 does not alter parsing behavior. A user can still type
400401 redirection and piping tokens, and they will be parsed as such
401402 but they won't do anything.
402- :param auto_load_commands: If True, cmd2 will check for all subclasses of `CommandSet`
403+ :param auto_load_commands: If `` True`` , cmd2 will check for all subclasses of `CommandSet`
403404 that are currently loaded by Python and automatically
404- instantiate and register all commands. If False, CommandSets
405+ instantiate and register all commands. If `` False`` , CommandSets
405406 must be manually installed with `register_command_set`.
406- :param auto_suggest: If True, cmd2 will provide fish shell style auto-suggestions
407+ :param auto_suggest: If `` True`` , cmd2 will provide fish shell style auto-suggestions
407408 based on history. User can press right-arrow key to accept the
408409 provided suggestion.
409410 :param complete_in_thread: if ``True``, then completion will run in a separate thread.
410411 :param command_sets: Provide CommandSet instances to load during cmd2 initialization.
411412 This allows CommandSets with custom constructor parameters to be
412413 loaded. This also allows the a set of CommandSets to be provided
413- when `auto_load_commands` is set to False
414+ when `auto_load_commands` is set to ``False``
415+
414416 :param enable_bottom_toolbar: if ``True``, enables a bottom toolbar while at the main prompt.
415417 Override ``get_bottom_toolbar()`` to define its content.
416418 :param enable_rprompt: if ``True``, enables a right prompt while at the main prompt.
@@ -429,7 +431,7 @@ def __init__(
429431 updates in the bottom toolbar).
430432 :param shortcuts: Mapping containing shortcuts for commands. If not supplied,
431433 then defaults to constants.DEFAULT_SHORTCUTS. If you do not want
432- any shortcuts, pass None and an empty dictionary will be created.
434+ any shortcuts, pass `` None`` and an empty dictionary will be created.
433435 :param silence_startup_script: if ``True``, then the startup script's output will be
434436 suppressed. Anything written to stderr will still display.
435437 :param startup_script: file path to a script to execute at startup
@@ -440,7 +442,7 @@ def __init__(
440442 terminate single-line commands. If not supplied, the default
441443 is a semicolon. If your app only contains single-line commands
442444 and you want terminators to be treated as literals by the parser,
443- then set this to None.
445+ then set this to `` None`` .
444446 """
445447 # Check if py or ipy need to be disabled in this instance
446448 if not include_py :
@@ -790,9 +792,20 @@ def _(event: Any) -> None: # pragma: no cover
790792 "refresh_interval" : refresh_interval ,
791793 "rprompt" : self .get_rprompt if enable_rprompt else None ,
792794 "style" : DynamicStyle (get_pt_theme ),
793- "clipboard" : PyperclipClipboard (),
794795 }
795796
797+ # Only enable PyperclipClipboard if the system clipboard is accessible to Pyperclip.
798+ try :
799+ cb = PyperclipClipboard ()
800+ cb .get_data () # Check if the system clipboard is accessible to Pyperclip
801+ except Exception : # noqa: BLE001, S110
802+ # Prevent prompt_toolkit from crashing in headless environments and fallback
803+ # on prompt toolkit's default clipboard (InMemoryClipboard) by not providing
804+ # any argument for 'clipboard' in kwargs
805+ pass
806+ else :
807+ kwargs ["clipboard" ] = cb
808+
796809 if self .stdin .isatty () and self .stdout .isatty ():
797810 try :
798811 if self .stdin != sys .stdin :
@@ -1487,6 +1500,17 @@ def visible_prompt(self) -> str:
14871500 """
14881501 return su .strip_style (self .prompt )
14891502
1503+ @property
1504+ def clipboard (self ) -> Clipboard :
1505+ """The application clipboard.
1506+
1507+ The clipboard the be either a ``PyperclipClipboard`` or an ``InMemoryClipboard``
1508+ depending on weather the system clipboard is accessible to ``pyperclip``.
1509+
1510+ :return: the clipboard of the application's main session
1511+ """
1512+ return self .main_session .clipboard
1513+
14901514 def _get_core_print_console (
14911515 self ,
14921516 * ,
@@ -3325,12 +3349,9 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
33253349 if not self .allow_clipboard :
33263350 raise RedirectionError ("Clipboard access not allowed" )
33273351
3328- # attempt to get the paste buffer, this forces pyperclip to go figure
3329- # out if it can actually interact with the paste buffer, and will throw exceptions
3330- # if it's not gonna work. That way we throw the exception before we go
3331- # run the command and queue up all the output. if this is going to fail,
3332- # no point opening up the temporary file
3333- current_paste_buffer = self .main_session .clipboard .get_data ().text
3352+ # Get the current paste buffer from either the system clipboard if available
3353+ # or the in-memory clipboard only available to the main session
3354+ current_paste_buffer = self .clipboard .get_data ().text
33343355 # create a temporary file to store output
33353356 new_stdout = cast (TextIO , tempfile .TemporaryFile (mode = "w+" )) # noqa: SIM115
33363357 redir_saved_state .redirecting = True
@@ -3360,7 +3381,10 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec
33603381 and not statement .redirect_to
33613382 ):
33623383 self .stdout .seek (0 )
3363- self .main_session .clipboard .set_text (self .stdout .read ())
3384+ # Read stdout into the clipboard. Uses the system clipboard if available
3385+ # otherwise fall back to the in-memory clipboard only available to the main
3386+ # session
3387+ self .clipboard .set_text (self .stdout .read ())
33643388
33653389 with contextlib .suppress (BrokenPipeError ):
33663390 # Close the file or pipe that stdout was redirected to
0 commit comments