diff options
Diffstat (limited to 'pw_console/py/pw_console/console_app.py')
-rw-r--r-- | pw_console/py/pw_console/console_app.py | 120 |
1 files changed, 97 insertions, 23 deletions
diff --git a/pw_console/py/pw_console/console_app.py b/pw_console/py/pw_console/console_app.py index 38f0bfa38..d27ea7308 100644 --- a/pw_console/py/pw_console/console_app.py +++ b/pw_console/py/pw_console/console_app.py @@ -14,6 +14,7 @@ """ConsoleApp control class.""" import asyncio +import base64 import builtins import functools import socketserver @@ -21,13 +22,16 @@ import importlib.resources import logging import os from pathlib import Path +import subprocess import sys +import tempfile import time from threading import Thread from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union from jinja2 import Environment, DictLoader, make_logging_undefined from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard +from prompt_toolkit.clipboard import ClipboardData from prompt_toolkit.layout.menus import CompletionsMenu from prompt_toolkit.output import ColorDepth from prompt_toolkit.application import Application @@ -57,8 +61,9 @@ from ptpython.key_bindings import ( # type: ignore load_python_bindings, load_sidebar_bindings, ) +from pyperclip import PyperclipException # type: ignore -from pw_console.command_runner import CommandRunner +from pw_console.command_runner import CommandRunner, CommandRunnerItem from pw_console.console_log_server import ( ConsoleLogHTTPRequestHandler, pw_console_http_server, @@ -504,6 +509,58 @@ class ConsoleApp: ) return call_function + def set_system_clipboard_data(self, data: ClipboardData) -> str: + return self.set_system_clipboard(data.text) + + def set_system_clipboard(self, text: str) -> str: + """Set the host system clipboard. + + The following methods are attempted in order: + + - The pyperclip package which uses various cross platform methods. + - Teminal OSC 52 escape sequence which works on some terminal emulators + such as: iTerm2 (MacOS), Alacritty, xterm. + - Tmux paste buffer via the load-buffer command. This only happens if + pw-console is running inside tmux. You can paste in tmux by pressing: + ctrl-b = + """ + copied = False + copy_methods = [] + try: + self.application.clipboard.set_text(text) + + copied = True + copy_methods.append('system clipboard') + except PyperclipException: + pass + + # Set the clipboard via terminal escape sequence. + b64_data = base64.b64encode(text.encode('utf-8')) + sys.stdout.write(f"\x1B]52;c;{b64_data.decode('utf-8')}\x07") + _LOG.debug('Clipboard set via teminal escape sequence') + copy_methods.append('teminal') + copied = True + + if os.environ.get('TMUX'): + with tempfile.NamedTemporaryFile( + prefix='pw_console_clipboard_', + delete=True, + ) as clipboard_file: + clipboard_file.write(text.encode('utf-8')) + clipboard_file.flush() + subprocess.run( + ['tmux', 'load-buffer', '-w', clipboard_file.name] + ) + _LOG.debug('Clipboard set via tmux load-buffer') + copy_methods.append('tmux') + copied = True + + message = '' + if copied: + message = 'Copied to: ' + message += ', '.join(copy_methods) + return message + def update_menu_items(self): self.menu_items = self._create_menu_items() self.root_container.menu_items = self.menu_items @@ -521,11 +578,11 @@ class ConsoleApp: if not self.command_runner_is_open(): self.command_runner.open_dialog() - def _create_logger_completions(self) -> List[Tuple[str, Callable]]: - completions: List[Tuple[str, Callable]] = [ - ( - 'root', - functools.partial( + def _create_logger_completions(self) -> List[CommandRunnerItem]: + completions: List[CommandRunnerItem] = [ + CommandRunnerItem( + title='root', + handler=functools.partial( self.open_new_log_pane_for_logger, '', window_title='root' ), ), @@ -535,9 +592,9 @@ class ConsoleApp: for logger_name in all_logger_names: completions.append( - ( - logger_name, - functools.partial( + CommandRunnerItem( + title=logger_name, + handler=functools.partial( self.open_new_log_pane_for_logger, logger_name ), ) @@ -552,15 +609,15 @@ class ConsoleApp: if not self.command_runner_is_open(): self.command_runner.open_dialog() - def _create_history_completions(self) -> List[Tuple[str, Callable]]: + def _create_history_completions(self) -> List[CommandRunnerItem]: return [ - ( - description, - functools.partial( + CommandRunnerItem( + title=title, + handler=functools.partial( self.repl_pane.insert_text_into_input_buffer, text ), ) - for description, text in self.repl_pane.history_completions() + for title, text in self.repl_pane.history_completions() ] def open_command_runner_snippets(self) -> None: @@ -593,16 +650,24 @@ class ConsoleApp: ) server_thread.start() - def _create_snippet_completions(self) -> List[Tuple[str, Callable]]: - completions: List[Tuple[str, Callable]] = [ - ( - description, - functools.partial( - self.repl_pane.insert_text_into_input_buffer, text - ), + def _create_snippet_completions(self) -> List[CommandRunnerItem]: + completions: List[CommandRunnerItem] = [] + + for snippet in self.prefs.snippet_completions(): + fenced_code = f'```python\n{snippet.code.strip()}\n```' + description = '\n' + fenced_code + '\n' + if snippet.description: + description += '\n' + snippet.description.strip() + '\n' + completions.append( + CommandRunnerItem( + title=snippet.title, + handler=functools.partial( + self.repl_pane.insert_text_into_input_buffer, + snippet.code, + ), + description=description, + ) ) - for description, text in self.prefs.snippet_completions() - ] return completions @@ -1007,6 +1072,15 @@ class ConsoleApp: self.update_menu_items() self._update_help_window() + def all_log_stores(self) -> List[LogStore]: + log_stores: List[LogStore] = [] + for pane in self.window_manager.active_panes(): + if not isinstance(pane, LogPane): + continue + if pane.log_view.log_store not in log_stores: + log_stores.append(pane.log_view.log_store) + return log_stores + def add_log_handler( self, window_title: str, |