class TextInputDialog(object):
    def __init__(self, title='', label_text='', completer=None):
        self.future = Future()

        def accept_text():
            get_app().layout.focus(ok_button)
            self.text_area.buffer.complete_state = None

        def accept():
            self.future.set_result(self.text_area.text)

        def cancel():
            self.future.set_result(None)

        self.text_area = TextArea(
            completer=completer,
            multiline=False,
            width=D(preferred=40),
            accept_handler=accept_text)

        ok_button = Button(text='OK', handler=accept)
        cancel_button = Button(text='Cancel', handler=cancel)

        self.dialog = Dialog(
            title=title,
            body=HSplit([
                Label(text=label_text),
                self.text_area
            ]),
            buttons=[ok_button, cancel_button],
            width=D(preferred=80),
            modal=True)

    def __pt_container__(self):
        return self.dialog
Example #2
0
class InteractiveInputDialog(object):
    def __init__(self, title='', help_text='', evaluator=None, padding=10, completer=None):
        self.future = Future()

        def cancel():
            self.future.set_result(None)

        self.output_field = TextArea(
                text='',
                focusable=False,
                )
        self.input_field = TextArea(
            height=1, prompt='>>> ', multiline=False,
            focusable=True,
            wrap_lines=False)

        def accept(buff):
            # Evaluate "calculator" expression.
            try:
                output = 'In:  {}\nOut: {}\n\n'.format(
                    self.input_field.text,
                    evaluator(self.input_field.text))  
            except BaseException as e:
                output = '\n\n{}'.format(e)
            new_text = self.output_field.text + output

            # Add text to output buffer.
            self.output_field.buffer.text = new_text

        self.input_field.accept_handler = accept

        cancel_button = Button(text='Cancel', handler=cancel)

        self.dialog = Dialog(
            title=title,
            body=HSplit([
                Label(text=help_text),
                self.output_field,
                HorizontalLine(),
                self.input_field,
            ]),
            buttons=[cancel_button],
            width=D(preferred=shutil.get_terminal_size()[0]-padding),
            modal=True)

    def __pt_container__(self):
        return self.dialog
Example #3
0
class MessageDialog(object):
    def __init__(self, title, text):
        self.future = Future()

        def set_done():
            self.future.set_result(None)

        ok_button = Button(text='OK', handler=(lambda: set_done()))

        self.dialog = Dialog(title=title,
                             body=HSplit([
                                 Label(text=text),
                             ]),
                             buttons=[ok_button],
                             width=D(preferred=80),
                             modal=True)

    def __pt_container__(self):
        return self.dialog
Example #4
0
class YesNoDialog(object):
    def __init__(self,
                 title='',
                 text='',
                 yes_text='Yes',
                 no_text='No',
                 width=None,
                 wrap_lines=True,
                 scrollbar=False):
        self.future = Future()

        def yes_handler():
            self.future.set_result(True)

        def no_handler():
            self.future.set_result(False)

        text_width = len(max(text.split('\n'), key=len)) + 2

        self.text_area = TextArea(
            text=text,
            read_only=True,
            # focus_on_click = True,
            focusable=False,
            width=D(preferred=text_width),
            wrap_lines=wrap_lines,
            scrollbar=scrollbar)

        self.dialog = Dialog(title=title,
                             body=self.text_area,
                             buttons=[
                                 Button(text=yes_text,
                                        width=1,
                                        handler=yes_handler),
                                 Button(text=no_text,
                                        width=1,
                                        handler=no_handler)
                             ],
                             with_background=True,
                             modal=True)

    def __pt_container__(self):
        return self.dialog
Example #5
0
class MessageDialog(object):
    def __init__(self,
                 title='',
                 text='',
                 ok_text='Ok',
                 shadow=False,
                 width=None,
                 wrap_lines=True,
                 scrollbar=False):
        self.future = Future()

        def set_done():
            self.future.set_result(None)

        text_width = len(max(text.split('\n'), key=len)) + 2
        text_height = len(text.split('\n'))
        app_height = get_app(
        ).ctui.layout.output_field.window.render_info.window_height - 2

        def dynamic_scrollbar():
            if text_height > app_height:
                return True

        self.text_area = TextArea(
            text=text,
            read_only=True,
            # focus_on_click = True,
            focusable=False,
            width=D(preferred=text_width),
            wrap_lines=wrap_lines,
            scrollbar=dynamic_scrollbar())

        ok_button = Button(text='OK', handler=(lambda: set_done()))

        self.dialog = Dialog(title=title,
                             body=self.text_area,
                             buttons=[ok_button],
                             width=width,
                             modal=True)

    def __pt_container__(self):
        return self.dialog
Example #6
0
class Win32PipeConnection(PipeConnection):
    """
    A single active Win32 pipe connection on the server side.
    """
    def __init__(self, pipe_instance):
        assert isinstance(pipe_instance, PipeInstance)
        self.pipe_instance = pipe_instance
        self.done_f = Future()

    def read(self):
        """
        (coroutine)
        Read a single message from the pipe. (Return as text.)
        """
        if self.done_f.done():
            raise BrokenPipeError

        try:
            result = yield From(
                read_message_from_pipe(self.pipe_instance.pipe_handle))
            raise Return(result)
        except BrokenPipeError:
            self.done_f.set_result(None)
            raise

    def write(self, message):
        """
        (coroutine)
        Write a single message into the pipe.
        """
        if self.done_f.done():
            raise BrokenPipeError

        try:
            yield From(
                write_message_to_pipe(self.pipe_instance.pipe_handle, message))
        except BrokenPipeError:
            self.done_f.set_result(None)
            raise

    def close(self):
        pass
class MessageDialog(object):
    def __init__(self, title, text):
        self.future = Future()

        def set_done():
            self.future.set_result(None)

        ok_button = Button(text='OK', handler=(lambda: set_done()))

        self.dialog = Dialog(
            title=title,
            body=HSplit([
                Label(text=text),
            ]),
            buttons=[ok_button],
            width=D(preferred=80),
            modal=True)

    def __pt_container__(self):
        return self.dialog
Example #8
0
class Win32PipeConnection(PipeConnection):
    """
    A single active Win32 pipe connection on the server side.
    """
    def __init__(self, pipe_instance):
        assert isinstance(pipe_instance, PipeInstance)
        self.pipe_instance = pipe_instance
        self.done_f = Future()

    def read(self):
        """
        (coroutine)
        Read a single message from the pipe. (Return as text.)
        """
        if self.done_f.done():
            raise BrokenPipeError

        try:
            result = yield From(read_message_from_pipe(self.pipe_instance.pipe_handle))
            raise Return(result)
        except BrokenPipeError:
            self.done_f.set_result(None)
            raise

    def write(self, message):
        """
        (coroutine)
        Write a single message into the pipe.
        """
        if self.done_f.done():
            raise BrokenPipeError

        try:
            yield From(write_message_to_pipe(self.pipe_instance.pipe_handle, message))
        except BrokenPipeError:
            self.done_f.set_result(None)
            raise

    def close(self):
        pass
Example #9
0
class TextInputDialog(object):
    def __init__(self,
                 title='',
                 text='',
                 ok_text='Ok',
                 width=None,
                 wrap_lines=True,
                 scrollbar=False):
        self.future = Future()

        def accept_text(buf):
            get_app().layout.focus(ok_button)
            buf.complete_state = None
            return True

        def accept():
            self.future.set_result(self.text_area.text)

        def cancel():
            self.future.set_result(None)

        text_width = len(max(text.split('\n'), key=len)) + 2

        self.text_area = TextArea(completer=completer,
                                  multiline=False,
                                  width=D(preferred=text_width),
                                  accept_handler=accept_text)

        ok_button = Button(text='OK', handler=accept)
        cancel_button = Button(text='Cancel', handler=cancel)

        self.dialog = Dialog(title=title,
                             body=HSplit([Label(text=text), self.text_area]),
                             buttons=[ok_button, cancel_button],
                             width=width,
                             modal=True)

    def __pt_container__(self):
        return self.dialog
Example #10
0
class TextInputDialog(object):
    def __init__(self, title='', label_text='', default='', padding=10, completer=None):
        self.future = Future()

        def accept_text(buf):
            get_app().layout.focus(ok_button)
            buf.complete_state = None
            return True

        def accept():
            self.future.set_result(self.text_area.text)

        def cancel():
            self.future.set_result(None)

        self.text_area = TextArea(
            completer=completer,
            text=default,
            multiline=False,
            width=D(preferred=shutil.get_terminal_size()[0]-padding),
            accept_handler=accept_text)

        ok_button = Button(text='OK', handler=accept)
        cancel_button = Button(text='Cancel', handler=cancel)

        self.dialog = Dialog(
            title=title,
            body=HSplit([
                Label(text=label_text),
                self.text_area
            ]),
            buttons=[ok_button, cancel_button],
            # buttons=[ok_button],
            width=D(preferred=shutil.get_terminal_size()[0]-10),
            modal=True)

    def __pt_container__(self):
        return self.dialog
Example #11
0
class ConfirmDialog(object):
    def __init__(self, title="", text="", padding=10):
        self.future = Future()

        def set_yes():
            self.future.set_result(True)
        def set_no():
            self.future.set_result(False)

        yes_button = Button(text='Yes', handler=(lambda: set_yes()))
        no_button = Button(text='No', handler=(lambda: set_no()))

        self.dialog = Dialog(
            title=title,
            body=HSplit([
                Label(text=text),
            ]),
            buttons=[yes_button, no_button],
            width=D(preferred=shutil.get_terminal_size()[0]-padding),
            modal=True)

    def __pt_container__(self):
        return self.dialog
Example #12
0
class RadioListDialog(object):
    def __init__(self, title='', text='', label='', values=[], padding=4, completer=None):
        self.future = Future()

        self.radios = RadioList(values=values)
        # radios.current_value will contain the first component of the selected tuple 
        # title = "Delete"
        # values =[
        #     (0, 'this instance'),
        #     (1, 'this and all subsequent instances'),
        #     (2, 'this and all previous instances'),
        #     (3, 'all instances - the item itself'),
        # ]

        def accept():
            self.future.set_result(self.radios.current_value)

        def cancel():
            self.future.set_result(None)


        ok_button = Button(text='OK', handler=accept)
        cancel_button = Button(text='Cancel', handler=cancel)

        self.dialog = Dialog(
            title=title,
            body=HSplit([
                Label(text=text),
                Frame(title=label, body=self.radios)
            ]),
            # body= Frame(title=label, body=self.radios),
            buttons=[ok_button, cancel_button],
            width=D(preferred=shutil.get_terminal_size()[0]-10),
            modal=True)

    def __pt_container__(self):
        return self.dialog
Example #13
0
File: main.py Project: ashang/pymux
class Pymux(object):
    """
    The main Pymux application class.

    Usage:

        p = Pymux()
        p.listen_on_socket()
        p.run_server()

    Or:

        p = Pymux()
        p.run_standalone()
    """
    def __init__(self, source_file=None, startup_command=None):
        self._client_states = {}  # connection -> client_state

        # Options
        self.enable_mouse_support = True
        self.enable_status = True
        self.enable_pane_status = True  #False
        self.enable_bell = True
        self.remain_on_exit = False
        self.status_keys_vi_mode = False
        self.mode_keys_vi_mode = False
        self.history_limit = 2000
        self.status_interval = 4
        self.default_terminal = 'xterm-256color'
        self.status_left = '[#S] '
        self.status_left_length = 20
        self.status_right = ' %H:%M %d-%b-%y '
        self.status_right_length = 20
        self.window_status_current_format = '#I:#W#F'
        self.window_status_format = '#I:#W#F'
        self.session_name = '0'
        self.status_justify = Justify.LEFT
        self.default_shell = get_default_shell()

        self.options = ALL_OPTIONS
        self.window_options = ALL_WINDOW_OPTIONS

        # When no panes are available.
        self.original_cwd = os.getcwd()

        self.display_pane_numbers = False

        #: List of clients.
        self._runs_standalone = False
        self.connections = []
        self.done_f = Future()

        self._startup_done = False
        self.source_file = source_file
        self.startup_command = startup_command

        # Keep track of all the panes, by ID. (For quick lookup.)
        self.panes_by_id = weakref.WeakValueDictionary()

        # Socket information.
        self.socket = None
        self.socket_name = None

        # Key bindings manager.
        self.key_bindings_manager = PymuxKeyBindings(self)

        self.arrangement = Arrangement()

        self.style = ui_style

    def _start_auto_refresh_thread(self):
        """
        Start the background thread that auto refreshes all clients according to
        `self.status_interval`.
        """
        def run():
            while True:
                time.sleep(self.status_interval)
                self.invalidate()

        t = threading.Thread(target=run)
        t.daemon = True
        t.start()

    @property
    def apps(self):
        return [c.app for c in self._client_states.values()]

    def get_client_state(self):
        " Return the active ClientState instance. "
        app = get_app()
        for client_state in self._client_states.values():
            if client_state.app == app:
                return client_state

        raise ValueError('Client state for app %r not found' % (app, ))

    def get_connection(self):
        " Return the active Connection instance. "
        app = get_app()
        for connection, client_state in self._client_states.items():
            if client_state.app == app:
                return connection

        raise ValueError('Connection for app %r not found' % (app, ))

    def startup(self):
        # Handle start-up comands.
        # (Does initial key bindings.)
        if not self._startup_done:
            self._startup_done = True

            # Execute default config.
            for cmd in STARTUP_COMMANDS.splitlines():
                self.handle_command(cmd)

            # Source the given file.
            if self.source_file:
                call_command_handler('source-file', self, [self.source_file])

            # Make sure that there is one window created.
            self.create_window(command=self.startup_command)

    def get_title(self):
        """
        The title to be displayed in the titlebar of the terminal.
        """
        w = self.arrangement.get_active_window()

        if w and w.active_process:
            title = w.active_process.screen.title
        else:
            title = ''

        if title:
            return '%s - Pymux' % (title, )
        else:
            return 'Pymux'

    def get_window_size(self):
        """
        Get the size to be used for the DynamicBody.
        This will be the smallest size of all clients.
        """
        def active_window_for_app(app):
            with set_app(app):
                return self.arrangement.get_active_window()

        active_window = self.arrangement.get_active_window()

        # Get sizes for connections watching the same window.
        apps = [
            client_state.app for client_state in self._client_states.values()
            if active_window_for_app(client_state.app) == active_window
        ]
        sizes = [app.output.get_size() for app in apps]

        rows = [s.rows for s in sizes]
        columns = [s.columns for s in sizes]

        if rows and columns:
            return Size(rows=min(rows) - (1 if self.enable_status else 0),
                        columns=min(columns))
        else:
            return Size(rows=20, columns=80)

    def _create_pane(self, window=None, command=None, start_directory=None):
        """
        Create a new :class:`pymux.arrangement.Pane` instance. (Don't put it in
        a window yet.)

        :param window: If a window is given, take the CWD of the current
            process of that window as the start path for this pane.
        :param command: If given, run this command instead of `self.default_shell`.
        :param start_directory: If given, use this as the CWD.
        """
        assert window is None or isinstance(window, Window)
        assert command is None or isinstance(command, six.text_type)
        assert start_directory is None or isinstance(start_directory,
                                                     six.text_type)

        def done_callback():
            " When the process finishes. "
            if not self.remain_on_exit:
                # Remove pane from layout.
                self.arrangement.remove_pane(pane)

                # No panes left? -> Quit.
                if not self.arrangement.has_panes:
                    self.stop()

                # Make sure the right pane is focused for each client.
                for client_state in self._client_states.values():
                    client_state.sync_focus()

            self.invalidate()

        def bell():
            " Sound bell on all clients. "
            if self.enable_bell:
                for c in self.apps:
                    c.output.bell()

        # Start directory.
        if start_directory:
            path = start_directory
        elif window and window.active_process:
            # When the path of the active process is known,
            # start the new process at the same location.
            path = window.active_process.get_cwd()
        else:
            path = None

        def before_exec():
            " Called in the process fork (in the child process). "
            # Go to this directory.
            try:
                os.chdir(path or self.original_cwd)
            except OSError:
                pass  # No such file or directory.

            # Set terminal variable. (We emulate xterm.)
            os.environ['TERM'] = self.default_terminal

            # Make sure to set the PYMUX environment variable.
            if self.socket_name:
                os.environ['PYMUX'] = '%s,%i' % (self.socket_name,
                                                 pane.pane_id)

        if command:
            command = command.split()
        else:
            command = [self.default_shell]

        # Create new pane and terminal.
        terminal = Terminal(done_callback=done_callback,
                            bell_func=bell,
                            before_exec_func=before_exec)
        pane = Pane(terminal)

        # Keep track of panes. This is a WeakKeyDictionary, we only add, but
        # don't remove.
        self.panes_by_id[pane.pane_id] = pane

        logger.info('Created process %r.', command)

        return pane

    def invalidate(self):
        " Invalidate the UI for all clients. "
        logger.info('Invalidating %s applications', len(self.apps))

        for app in self.apps:
            app.invalidate()

    def stop(self):
        for app in self.apps:
            app.exit()
        self.done_f.set_result(None)

    def create_window(self, command=None, start_directory=None, name=None):
        """
        Create a new :class:`pymux.arrangement.Window` in the arrangement.
        """
        assert command is None or isinstance(command, six.text_type)
        assert start_directory is None or isinstance(start_directory,
                                                     six.text_type)

        pane = self._create_pane(None,
                                 command,
                                 start_directory=start_directory)

        self.arrangement.create_window(pane, name=name)
        pane.focus()
        self.invalidate()

    def add_process(self, command=None, vsplit=False, start_directory=None):
        """
        Add a new process to the current window. (vsplit/hsplit).
        """
        assert command is None or isinstance(command, six.text_type)
        assert start_directory is None or isinstance(start_directory,
                                                     six.text_type)

        window = self.arrangement.get_active_window()

        pane = self._create_pane(window,
                                 command,
                                 start_directory=start_directory)
        window.add_pane(pane, vsplit=vsplit)
        pane.focus()
        self.invalidate()

    def kill_pane(self, pane):
        """
        Kill the given pane, and remove it from the arrangement.
        """
        assert isinstance(pane, Pane)

        # Send kill signal.
        if not pane.process.is_terminated:
            pane.process.kill()

        # Remove from layout.
        self.arrangement.remove_pane(pane)

    def leave_command_mode(self, append_to_history=False):
        """
        Leave the command/prompt mode.
        """
        client_state = self.get_client_state()

        client_state.command_buffer.reset(append_to_history=append_to_history)
        client_state.prompt_buffer.reset(append_to_history=True)

        client_state.prompt_command = ''
        client_state.confirm_command = ''

        client_state.app.layout.focus_previous()

    def handle_command(self, command):
        """
        Handle command from the command line.
        """
        handle_command(self, command)

    def show_message(self, message):
        """
        Set a warning message. This will be shown at the bottom until a key has
        been pressed.

        :param message: String.
        """
        self.get_client_state().message = message

    def detach_client(self, app):
        """
        Detach the client that belongs to this CLI.
        """
        connection = self.get_connection()
        if connection:
            connection.detach_and_close()

        # Redraw all clients -> Maybe their size has to change.
        self.invalidate()

    def listen_on_socket(self, socket_name=None):
        """
        Listen for clients on a Unix socket.
        Returns the socket name.
        """
        def connection_cb(pipe_connection):
            # We have to create a new `context`, because this will be the scope for
            # a new prompt_toolkit.Application to become active.
            with context():
                connection = ServerConnection(self, pipe_connection)

            self.connections.append(connection)

        self.socket_name = bind_and_listen_on_socket(socket_name,
                                                     connection_cb)

        # Set session_name according to socket name.
        #        if '.' in self.socket_name:
        #            self.session_name = self.socket_name.rpartition('.')[-1]

        logger.info('Listening on %r.' % self.socket_name)
        return self.socket_name

    def run_server(self):
        # Ignore keyboard. (When people run "pymux server" and press Ctrl-C.)
        # Pymux has to be terminated by termining all the processes running in
        # its panes.
        def handle_sigint(*a):
            print('Ignoring keyboard interrupt.')

        signal.signal(signal.SIGINT, handle_sigint)

        # Start background threads.
        self._start_auto_refresh_thread()

        # Run eventloop.
        try:
            get_event_loop().run_until_complete(self.done_f)
        except:
            # When something bad happens, always dump the traceback.
            # (Otherwise, when running as a daemon, and stdout/stderr are not
            # available, it's hard to see what went wrong.)
            fd, path = tempfile.mkstemp(prefix='pymux.crash-')
            logger.fatal(
                'Pymux has crashed, dumping traceback to {0}'.format(path))
            os.write(fd, traceback.format_exc().encode('utf-8'))
            os.close(fd)
            raise

        finally:
            # Clean up socket.
            os.remove(self.socket_name)

    def run_standalone(self, color_depth):
        """
        Run pymux standalone, rather than using a client/server architecture.
        This is mainly useful for debugging.
        """
        self._runs_standalone = True
        self._start_auto_refresh_thread()

        client_state = self.add_client(input=create_input(),
                                       output=create_output(stdout=sys.stdout),
                                       color_depth=color_depth,
                                       connection=None)

        client_state.app.run()

    def add_client(self, output, input, color_depth, connection):
        client_state = ClientState(self,
                                   connection=None,
                                   input=input,
                                   output=output,
                                   color_depth=color_depth)

        self._client_states[connection] = client_state

        return client_state

    def remove_client(self, connection):
        if connection in self._client_states:
            del self._client_states[connection]
Example #14
0
class MessageDialog(object):
    def __init__(self,
                 title='',
                 text='',
                 ok_text='Ok',
                 lexer=None,
                 width=None,
                 wrap_lines=True,
                 scrollbar=False):
        self.future = Future()
        self.text = text

        def set_done():
            self.future.set_result(None)

        def get_text_width():
            if width is None:
                text_fragments = to_formatted_text(self.text)
                text = fragment_list_to_text(text_fragments)
                if text:
                    longest_line = max(
                        get_cwidth(line) for line in text.splitlines())
                else:
                    return D(preferred=0)
                return D(preferred=longest_line)
            else:
                return width

        # text_width = len(max(self.text.split('\n'), key=len)) + 2
        # text_height = len(text.split('\n'))

        # TODO: Add dynamic_h_scrollbar to TextArea and this Dialog
        # def dynamic_horizontal_scrollbar():
        #     max_text_width = get_app().renderer.output.get_size().columns - 2

        def dynamic_virtical_scrollbar():
            text_fragments = to_formatted_text(self.text)
            text = fragment_list_to_text(text_fragments)
            if text:
                text_height = len(self.text.splitlines())
                max_text_height = get_app().renderer.output.get_size().rows - 6
                if text_height > max_text_height:
                    return True

        self.text_area = TextArea(text=text,
                                  lexer=lexer,
                                  read_only=True,
                                  focusable=False,
                                  width=get_text_width(),
                                  wrap_lines=wrap_lines,
                                  scrollbar=dynamic_virtical_scrollbar())

        ok_button = Button(text='OK', handler=(lambda: set_done()))

        self.dialog = Dialog(title=title,
                             body=self.text_area,
                             buttons=[ok_button],
                             width=width,
                             modal=True)

    def __pt_container__(self):
        return self.dialog
Example #15
0
class Pymux(object):
    """
    The main Pymux application class.

    Usage:

        p = Pymux()
        p.listen_on_socket()
        p.run_server()

    Or:

        p = Pymux()
        p.run_standalone()
    """
    def __init__(self, source_file=None, startup_command=None):
        self._client_states = {}  # connection -> client_state

        # Options
        self.enable_mouse_support = True
        self.enable_status = True
        self.enable_pane_status = True#False
        self.enable_bell = True
        self.remain_on_exit = False
        self.status_keys_vi_mode = False
        self.mode_keys_vi_mode = False
        self.history_limit = 2000
        self.status_interval = 4
        self.default_terminal = 'xterm-256color'
        self.status_left = '[#S] '
        self.status_left_length = 20
        self.status_right = ' %H:%M %d-%b-%y '
        self.status_right_length = 20
        self.window_status_current_format = '#I:#W#F'
        self.window_status_format = '#I:#W#F'
        self.session_name = '0'
        self.status_justify = Justify.LEFT
        self.default_shell = get_default_shell()
        self.swap_dark_and_light = False

        self.options = ALL_OPTIONS
        self.window_options = ALL_WINDOW_OPTIONS

        # When no panes are available.
        self.original_cwd = os.getcwd()

        self.display_pane_numbers = False

        #: List of clients.
        self._runs_standalone = False
        self.connections = []
        self.done_f = Future()

        self._startup_done = False
        self.source_file = source_file
        self.startup_command = startup_command

        # Keep track of all the panes, by ID. (For quick lookup.)
        self.panes_by_id = weakref.WeakValueDictionary()

        # Socket information.
        self.socket = None
        self.socket_name = None

        # Key bindings manager.
        self.key_bindings_manager = PymuxKeyBindings(self)

        self.arrangement = Arrangement()

        self.style = ui_style

    def _start_auto_refresh_thread(self):
        """
        Start the background thread that auto refreshes all clients according to
        `self.status_interval`.
        """
        def run():
            while True:
                time.sleep(self.status_interval)
                self.invalidate()

        t = threading.Thread(target=run)
        t.daemon = True
        t.start()

    @property
    def apps(self):
        return [c.app for c in self._client_states.values()]

    def get_client_state(self):
        " Return the active ClientState instance. "
        app = get_app()
        for client_state in self._client_states.values():
            if client_state.app == app:
                return client_state

        raise ValueError('Client state for app %r not found' % (app, ))

    def get_connection(self):
        " Return the active Connection instance. "
        app = get_app()
        for connection, client_state in self._client_states.items():
            if client_state.app == app:
                return connection

        raise ValueError('Connection for app %r not found' % (app, ))

    def startup(self):
        # Handle start-up comands.
        # (Does initial key bindings.)
        if not self._startup_done:
            self._startup_done = True

            # Execute default config.
            for cmd in STARTUP_COMMANDS.splitlines():
                self.handle_command(cmd)

            # Source the given file.
            if self.source_file:
                call_command_handler('source-file', self, [self.source_file])

            # Make sure that there is one window created.
            self.create_window(command=self.startup_command)

    def get_title(self):
        """
        The title to be displayed in the titlebar of the terminal.
        """
        w = self.arrangement.get_active_window()

        if w and w.active_process:
            title = w.active_process.screen.title
        else:
            title = ''

        if title:
            return '%s - Pymux' % (title, )
        else:
            return 'Pymux'

    def get_window_size(self):
        """
        Get the size to be used for the DynamicBody.
        This will be the smallest size of all clients.
        """
        def active_window_for_app(app):
            with set_app(app):
                return self.arrangement.get_active_window()

        active_window = self.arrangement.get_active_window()

        # Get sizes for connections watching the same window.
        apps = [client_state.app for client_state in self._client_states.values()
                if active_window_for_app(client_state.app) == active_window]
        sizes = [app.output.get_size() for app in apps]

        rows = [s.rows for s in sizes]
        columns = [s.columns for s in sizes]

        if rows and columns:
            return Size(rows=min(rows) - (1 if self.enable_status else 0),
                        columns=min(columns))
        else:
            return Size(rows=20, columns=80)

    def _create_pane(self, window=None, command=None, start_directory=None):
        """
        Create a new :class:`pymux.arrangement.Pane` instance. (Don't put it in
        a window yet.)

        :param window: If a window is given, take the CWD of the current
            process of that window as the start path for this pane.
        :param command: If given, run this command instead of `self.default_shell`.
        :param start_directory: If given, use this as the CWD.
        """
        assert window is None or isinstance(window, Window)
        assert command is None or isinstance(command, six.text_type)
        assert start_directory is None or isinstance(start_directory, six.text_type)

        def done_callback():
            " When the process finishes. "
            if not self.remain_on_exit:
                # Remove pane from layout.
                self.arrangement.remove_pane(pane)

                # No panes left? -> Quit.
                if not self.arrangement.has_panes:
                    self.stop()

                # Make sure the right pane is focused for each client.
                for client_state in self._client_states.values():
                    client_state.sync_focus()

            self.invalidate()

        def bell():
            " Sound bell on all clients. "
            if self.enable_bell:
                for c in self.apps:
                    c.output.bell()

        # Start directory.
        if start_directory:
            path = start_directory
        elif window and window.active_process:
            # When the path of the active process is known,
            # start the new process at the same location.
            path = window.active_process.get_cwd()
        else:
            path = None

        def before_exec():
            " Called in the process fork (in the child process). "
            # Go to this directory.
            try:
                os.chdir(path or self.original_cwd)
            except OSError:
                pass  # No such file or directory.

            # Set terminal variable. (We emulate xterm.)
            os.environ['TERM'] = self.default_terminal

            # Make sure to set the PYMUX environment variable.
            if self.socket_name:
                os.environ['PYMUX'] = '%s,%i' % (
                    self.socket_name, pane.pane_id)

        if command:
            command = command.split()
        else:
            command = [self.default_shell]

        # Create new pane and terminal.
        terminal = Terminal(done_callback=done_callback, bell_func=bell,
                            before_exec_func=before_exec)
        pane = Pane(terminal)

        # Keep track of panes. This is a WeakKeyDictionary, we only add, but
        # don't remove.
        self.panes_by_id[pane.pane_id] = pane

        logger.info('Created process %r.', command)

        return pane

    def invalidate(self):
        " Invalidate the UI for all clients. "
        logger.info('Invalidating %s applications', len(self.apps))

        for app in self.apps:
            app.invalidate()

    def stop(self):
        for app in self.apps:
            app.exit()
        self.done_f.set_result(None)

    def create_window(self, command=None, start_directory=None, name=None):
        """
        Create a new :class:`pymux.arrangement.Window` in the arrangement.
        """
        assert command is None or isinstance(command, six.text_type)
        assert start_directory is None or isinstance(start_directory, six.text_type)

        pane = self._create_pane(None, command, start_directory=start_directory)

        self.arrangement.create_window(pane, name=name)
        pane.focus()
        self.invalidate()

    def add_process(self, command=None, vsplit=False, start_directory=None):
        """
        Add a new process to the current window. (vsplit/hsplit).
        """
        assert command is None or isinstance(command, six.text_type)
        assert start_directory is None or isinstance(start_directory, six.text_type)

        window = self.arrangement.get_active_window()

        pane = self._create_pane(window, command, start_directory=start_directory)
        window.add_pane(pane, vsplit=vsplit)
        pane.focus()
        self.invalidate()

    def kill_pane(self, pane):
        """
        Kill the given pane, and remove it from the arrangement.
        """
        assert isinstance(pane, Pane)

        # Send kill signal.
        if not pane.process.is_terminated:
            pane.process.kill()

        # Remove from layout.
        self.arrangement.remove_pane(pane)

    def leave_command_mode(self, append_to_history=False):
        """
        Leave the command/prompt mode.
        """
        client_state = self.get_client_state()

        client_state.command_buffer.reset(append_to_history=append_to_history)
        client_state.prompt_buffer.reset(append_to_history=True)

        client_state.prompt_command = ''
        client_state.confirm_command = ''

        client_state.app.layout.focus_previous()

    def handle_command(self, command):
        """
        Handle command from the command line.
        """
        handle_command(self, command)

    def show_message(self, message):
        """
        Set a warning message. This will be shown at the bottom until a key has
        been pressed.

        :param message: String.
        """
        self.get_client_state().message = message

    def detach_client(self, app):
        """
        Detach the client that belongs to this CLI.
        """
        connection = self.get_connection()
        if connection:
            connection.detach_and_close()

        # Redraw all clients -> Maybe their size has to change.
        self.invalidate()

    def listen_on_socket(self, socket_name=None):
        """
        Listen for clients on a Unix socket.
        Returns the socket name.
        """
        def connection_cb(pipe_connection):
            # We have to create a new `context`, because this will be the scope for
            # a new prompt_toolkit.Application to become active.
            with context():
                connection = ServerConnection(self, pipe_connection)

            self.connections.append(connection)

        self.socket_name = bind_and_listen_on_socket(socket_name, connection_cb)

        # Set session_name according to socket name.
#        if '.' in self.socket_name:
#            self.session_name = self.socket_name.rpartition('.')[-1]

        logger.info('Listening on %r.' % self.socket_name)
        return self.socket_name

    def run_server(self):
        # Ignore keyboard. (When people run "pymux server" and press Ctrl-C.)
        # Pymux has to be terminated by termining all the processes running in
        # its panes.
        def handle_sigint(*a):
            print('Ignoring keyboard interrupt.')

        signal.signal(signal.SIGINT, handle_sigint)

        # Start background threads.
        self._start_auto_refresh_thread()

        # Run eventloop.
        try:
            get_event_loop().run_until_complete(self.done_f)
        except:
            # When something bad happens, always dump the traceback.
            # (Otherwise, when running as a daemon, and stdout/stderr are not
            # available, it's hard to see what went wrong.)
            fd, path = tempfile.mkstemp(prefix='pymux.crash-')
            logger.fatal(
                'Pymux has crashed, dumping traceback to {0}'.format(path))
            os.write(fd, traceback.format_exc().encode('utf-8'))
            os.close(fd)
            raise

        finally:
            # Clean up socket.
            os.remove(self.socket_name)

    def run_standalone(self, color_depth):
        """
        Run pymux standalone, rather than using a client/server architecture.
        This is mainly useful for debugging.
        """
        self._runs_standalone = True
        self._start_auto_refresh_thread()

        client_state = self.add_client(
            input=create_input(),
            output=create_output(stdout=sys.stdout),
            color_depth=color_depth,
            connection=None)

        client_state.app.run()

    def add_client(self, output, input, color_depth, connection):
        client_state = ClientState(self,
            connection=None,
            input=input,
            output=output,
            color_depth=color_depth)

        self._client_states[connection] = client_state

        return client_state

    def remove_client(self, connection):
        if connection in self._client_states:
            del self._client_states[connection]
Example #16
0
File: posix.py Project: riag/ptterm
class PosixTerminal(Terminal):
    def __init__(self, exec_func):
        self.exec_func = exec_func

        # Create pseudo terminal for this pane.
        self.master, self.slave = os.openpty()

        # Master side -> attached to terminal emulator.
        self._reader = PosixStdinReader(self.master, errors='replace')
        self._reader_connected = False
        self._input_ready_callbacks = []

        self.ready_f = Future()
        self.loop = get_event_loop()
        self.pid = None

    def add_input_ready_callback(self, callback):
        self._input_ready_callbacks.append(callback)

    @classmethod
    def from_command(cls, command, before_exec_func=None):
        """
        Create Process from command,
        e.g. command=['python', '-c', 'print("test")']

        :param before_exec_func: Function that is called before `exec` in the
            process fork.
        """
        assert isinstance(command, list)
        assert before_exec_func is None or callable(before_exec_func)

        def execv():
            if before_exec_func:
                before_exec_func()

            for p in os.environ['PATH'].split(':'):
                path = os.path.join(p, command[0])
                if os.path.exists(path) and os.access(path, os.X_OK):
                    os.execv(path, command)

        return cls(execv)

    def connect_reader(self):
        if self.master is not None and not self._reader_connected:

            def ready():
                for cb in self._input_ready_callbacks:
                    cb()

            self.loop.add_reader(self.master, ready)
            self._reader_connected = True

    @property
    def closed(self):
        return self._reader.closed

    def disconnect_reader(self):
        if self.master is not None and self._reader_connected:
            self.loop.remove_reader(self.master)
            self._reader_connected = False

    def read_text(self, amount=4096):
        return self._reader.read(amount)

    def write_text(self, text):
        self.write_bytes(text.encode('utf-8'))

    def write_bytes(self, data):
        while self.master is not None:
            try:
                os.write(self.master, data)
            except OSError as e:
                # This happens when the window resizes and a SIGWINCH was received.
                # We get 'Error: [Errno 4] Interrupted system call'
                if e.errno == 4:
                    continue
            return

    def set_size(self, width, height):
        """
        Set terminal size.
        """
        assert isinstance(width, int)
        assert isinstance(height, int)

        if self.master is not None:
            set_terminal_size(self.master, height, width)

    def start(self):
        """
        Create fork and start the child process.
        """
        pid = os.fork()

        if pid == 0:
            self._in_child()
        elif pid > 0:
            # We wait a very short while, to be sure the child had the time to
            # call _exec. (Otherwise, we are still sharing signal handlers and
            # FDs.) Resizing the pty, when the child is still in our Python
            # code and has the signal handler from prompt_toolkit, but closed
            # the 'fd' for 'call_from_executor', will cause OSError.
            time.sleep(0.1)

            self.pid = pid

            # Wait for the process to finish.
            self._waitpid()

    def kill(self):
        " Terminate process. "
        self.send_signal(signal.SIGKILL)

    def send_signal(self, signal):
        " Send signal to running process. "
        assert isinstance(signal, int), type(signal)

        if self.pid and not self.closed:
            try:
                os.kill(self.pid, signal)
            except OSError:
                pass  # [Errno 3] No such process.

    def _in_child(self):
        " Will be executed in the forked child. "
        os.close(self.master)

        # Remove signal handler for SIGWINCH as early as possible.
        # (We don't want this to be triggered when execv has not been called
        # yet.)
        signal.signal(signal.SIGWINCH, 0)

        pty_make_controlling_tty(self.slave)

        # In the fork, set the stdin/out/err to our slave pty.
        os.dup2(self.slave, 0)
        os.dup2(self.slave, 1)
        os.dup2(self.slave, 2)

        # Execute in child.
        try:
            self._close_file_descriptors()
            self.exec_func()
        except Exception:
            traceback.print_exc()
            time.sleep(5)

            os._exit(1)
        os._exit(0)

    def _close_file_descriptors(self):
        # Do not allow child to inherit open file descriptors from parent.
        # (In case that we keep running Python code. We shouldn't close them.
        # because the garbage collector is still active, and he will close them
        # eventually.)
        max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[-1]

        try:
            os.closerange(3, max_fd)
        except OverflowError:
            # On OS X, max_fd can return very big values, than closerange
            # doesn't understand, e.g. 9223372036854775807. In this case, just
            # use 4096. This is what Linux systems report, and should be
            # sufficient. (I hope...)
            os.closerange(3, 4096)

    def _waitpid(self):
        """
        Create an executor that waits and handles process termination.
        """
        def wait_for_finished():
            " Wait for PID in executor. "
            os.waitpid(self.pid, 0)
            self.loop.call_from_executor(done)

        def done():
            " PID received. Back in the main thread. "
            # Close pty and remove reader.

            self.disconnect_reader()
            os.close(self.master)
            os.close(self.slave)

            self.master = None

            # Callback.
            self.ready_f.set_result(None)

        self.loop.run_in_executor(wait_for_finished)

    def get_name(self):
        " Return the process name. "
        result = '<unknown>'

        # Apparently, on a Linux system (like my Fedora box), I have to call
        # `tcgetpgrp` on the `master` fd. However, on te Window subsystem for
        # Linux, we have to use the `slave` fd.

        if self.master is not None:
            result = get_name_for_fd(self.master)

        if not result and self.slave is not None:
            result = get_name_for_fd(self.slave)

        return result

    def get_cwd(self):
        if self.pid:
            return get_cwd_for_pid(self.pid)