def test_layout_class():
    c1 = BufferControl()
    c2 = BufferControl()
    c3 = BufferControl()
    win1 = Window(content=c1)
    win2 = Window(content=c2)
    win3 = Window(content=c3)

    layout = Layout(container=VSplit([
        HSplit([
            win1,
            win2
        ]),
        win3
    ]))

    # Listing of windows/controls.
    assert list(layout.find_all_windows()) == [win1, win2, win3]
    assert list(layout.find_all_controls()) == [c1, c2, c3]

    # Focusing something.
    layout.focus(c1)
    assert layout.has_focus(c1)
    assert layout.has_focus(win1)
    assert layout.current_control == c1
    assert layout.previous_control == c1

    layout.focus(c2)
    assert layout.has_focus(c2)
    assert layout.has_focus(win2)
    assert layout.current_control == c2
    assert layout.previous_control == c1

    layout.focus(win3)
    assert layout.has_focus(c3)
    assert layout.has_focus(win3)
    assert layout.current_control == c3
    assert layout.previous_control == c2

    # Pop focus. This should focus the previous control again.
    layout.focus_last()
    assert layout.has_focus(c2)
    assert layout.has_focus(win2)
    assert layout.current_control == c2
    assert layout.previous_control == c1
def test_layout_class():
    c1 = BufferControl()
    c2 = BufferControl()
    c3 = BufferControl()
    win1 = Window(content=c1)
    win2 = Window(content=c2)
    win3 = Window(content=c3)

    layout = Layout(container=VSplit([HSplit([win1, win2]), win3]))

    # Listing of windows/controls.
    assert list(layout.find_all_windows()) == [win1, win2, win3]
    assert list(layout.find_all_controls()) == [c1, c2, c3]

    # Focusing something.
    layout.focus(c1)
    assert layout.has_focus(c1)
    assert layout.has_focus(win1)
    assert layout.current_control == c1
    assert layout.previous_control == c1

    layout.focus(c2)
    assert layout.has_focus(c2)
    assert layout.has_focus(win2)
    assert layout.current_control == c2
    assert layout.previous_control == c1

    layout.focus(win3)
    assert layout.has_focus(c3)
    assert layout.has_focus(win3)
    assert layout.current_control == c3
    assert layout.previous_control == c2

    # Pop focus. This should focus the previous control again.
    layout.focus_last()
    assert layout.has_focus(c2)
    assert layout.has_focus(win2)
    assert layout.current_control == c2
    assert layout.previous_control == c1
示例#3
0
class TUI(object):
    key_bindings = KeyBindings()

    def __init__(self):
        self.console = TextArea(
            scrollbar=True,
            focusable=False,
            line_numbers=False,
            lexer=PygmentsLexer(SerTermLexer),
        )

        self.cmd_line = TextArea(
            multiline=False,
            prompt=HTML('<orange>>>> </orange>'),
            style='bg: cyan',
            accept_handler=self.cmd_line_accept_handler,
            history=FileHistory('.ser-term-hist'),
            auto_suggest=AutoSuggestFromHistory(),
        )

        self.root = HSplit([
            self.console,
            self.cmd_line,
        ])

        self.menu = MenuContainer(
            self.root,
            menu_items=[
                MenuItem(text='[F2] Open port', handler=self.key_uart_open),
                MenuItem(text='[F3] Close port', handler=self.key_uart_close),
                MenuItem(
                    text='[F4] Baudrate',
                    children=[
                        MenuItem(
                            str(bd),
                            handler=(lambda bd: lambda: self.baudrate_update(
                                baudrate=int(bd)))(bd)) for bd in BAUDRATE
                    ],
                ),
                MenuItem(
                    text='[F5] End line',
                    children=[
                        MenuItem('LF   \\n',
                                 handler=lambda: self.end_line_update('\n')),
                        MenuItem('CR   \\r',
                                 handler=lambda: self.end_line_update('\r')),
                        MenuItem('CRLF \\r\\n',
                                 handler=lambda: self.end_line_update('\r\n')),
                    ],
                ),
                MenuItem(text='[F10] Quit', handler=self.key_application_quit),
            ],
        )

        if args.end_line == 'LF':
            self.end_line = '\n'
        elif args.end_line == 'CR':
            self.end_line = '\r'
        else:
            self.end_line = '\r\n'
        self.end_line_update()
        self.baudrate_update()

        self.layout = Layout(self.menu)
        self.layout.focus(self.root)

        self.key_bindings.add('s-tab')(focus_previous)
        self.key_bindings.add('tab')(focus_next)

        self.app = Application(
            layout=self.layout,
            key_bindings=self.key_bindings,
            full_screen=True,
            mouse_support=True,
        )

    @key_bindings.add('f10')
    @key_bindings.add('c-c')
    @key_bindings.add('c-d')
    @key_bindings.add('c-x')
    @key_bindings.add('c-q')
    @key_bindings.add('escape')
    def key_application_quit(self, event=None):
        get_app().exit()

    @key_bindings.add('f2')
    def key_uart_open(self, event=None):
        UART.run()

    @key_bindings.add('f3')
    def key_uart_close(self, event=None):
        UART.stop()

    @key_bindings.add('f4')
    def key_baudrate(self, event=None):
        tui.baudrate_update(shift=True)

    @key_bindings.add('f5')
    def key_end_line(self, event=None):
        tui.end_line_update(shift=True)

    def cmd_line_accept_handler(self, handler):
        line = ''.join(('Tx: ', handler.text))
        console_append(self.console, line)
        UART.queue_tx.put_nowait(handler.text + self.end_line)

    def end_line_update(self, symbol=None, shift=None):
        if symbol:
            self.end_line = symbol

        if shift:
            if self.end_line == '\n':
                self.end_line = '\r'
                self.menu.menu_items[-2].text = '[F5] End line CR'
            elif self.end_line == '\r':
                self.end_line = '\r\n'
                self.menu.menu_items[-2].text = '[F5] End line CRLF'
            else:
                self.end_line = '\n'
                self.menu.menu_items[-2].text = '[F5] End line LF'

        if self.end_line == '\n':
            self.menu.menu_items[-2].text = '[F5] End line LF'
        elif self.end_line == '\r':
            self.menu.menu_items[-2].text = '[F5] End line CR'
        else:
            self.menu.menu_items[-2].text = '[F5] End line CRLF'

    def baudrate_update(self, baudrate=None, shift=None):
        if baudrate in BAUDRATE:
            UART.baudrate = baudrate
            UART.stop()
            UART.run()

        if shift:
            i = BAUDRATE.index(UART.baudrate)
            i = 0 if i + 1 >= len(BAUDRATE) else i + 1
            UART.baudrate = BAUDRATE[i]
            UART.stop()
            UART.run()

        self.menu.menu_items[-3].text = '[F4] Baudrate ' + str(UART.baudrate)

    def run(self):
        prompt_toolkit.eventloop.use_asyncio_event_loop()
        asyncio.get_event_loop().run_until_complete(
            self.app.run_async().to_asyncio_future())
示例#4
0
class TUI:
    def __init__(self):
        # Map JobBase.name to SimpleNamespace with attributes:
        #   job       - JobBase instance
        #   widget    - JobWidgetBase instance
        #   container - Container instance
        self._jobs = collections.defaultdict(lambda: types.SimpleNamespace())
        self._app = self._make_app()
        self._app_terminated = False
        self._exception = None
        utils.get_aioloop().set_exception_handler(self._handle_exception)

    def _handle_exception(self, loop, context):
        exception = context.get('exception')
        if exception:
            _log.debug('Caught unhandled exception: %r', exception)
            if not self._exception:
                self._exception = exception
            self._exit()

    def _make_app(self):
        self._jobs_container = HSplit(
            # FIXME: Layout does not accept an empty list of children. We add an
            #        empty Window that doesn't display anything that gets
            #        removed automatically when we rebuild
            #        self._jobs_container.children.
            #        https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1257
            children=[Window()],
            style='class:default',
        )
        self._layout = Layout(self._jobs_container)

        kb = KeyBindings()

        @kb.add('escape')
        @kb.add('c-g')
        @kb.add('c-q')
        @kb.add('c-c')
        def _(event, self=self):
            if self._app.is_running:
                self._exit()

        app = Application(
            layout=self._layout,
            key_bindings=kb,
            style=style.style,
            full_screen=False,
            erase_when_done=False,
            mouse_support=False,
            on_invalidate=self._update_jobs_container,
        )
        # Make escape key work
        app.timeoutlen = 0.1
        app.ttimeoutlen = 0.1
        return app

    def add_jobs(self, *jobs):
        """Add :class:`~.jobs.base.JobBase` instances"""
        for job in jobs:
            if job.name in self._jobs:
                raise RuntimeError(f'Job was already added: {job.name}')
            else:
                self._jobs[job.name].job = job
                self._jobs[job.name].widget = jobwidgets.JobWidget(job, self._app)
                self._jobs[job.name].container = to_container(self._jobs[job.name].widget)

                # Terminate application if all jobs finished
                job.signal.register('finished', self._exit_if_all_jobs_finished)

                # Terminate application if any job finished with non-zero exit code
                job.signal.register('finished', self._exit_if_job_failed)

                if self._jobs[job.name].widget.is_interactive:
                    # Display next interactive job when currently focused job finishes
                    job.signal.register('finished', self._update_jobs_container)

        self._update_jobs_container()

    def _update_jobs_container(self, *_):
        job_containers = []

        # Start all enabled jobs that are supposed to start automatically
        for jobinfo in self._enabled_jobs:
            if not jobinfo.job.is_started and jobinfo.job.autostart:
                jobinfo.job.start()

        # List interactive jobs first
        for jobinfo in self._enabled_jobs:
            if jobinfo.widget.is_interactive:
                job_containers.append(jobinfo.container)

                # Focus the first unfinished job
                if not jobinfo.job.is_finished:
                    try:
                        self._layout.focus(jobinfo.container)
                    except ValueError:
                        pass
                    #     _log.debug('Failed to focus job: %r', jobinfo.job.name)
                    # else:
                    #     _log.debug('Focused job: %r', jobinfo.job.name)

                    # Don't display more than one unfinished interactive job
                    # unless any job has errors, in which case we are
                    # terminating the application and display all jobs.
                    if not any(jobinfo.job.errors for jobinfo in self._jobs.values()):
                        break

        # Add non-interactive jobs below interactive jobs so the interactive
        # widgets don't change position when non-interactive widgets change
        # size.
        for jobinfo in self._enabled_jobs:
            if not jobinfo.widget.is_interactive:
                job_containers.append(jobinfo.container)

        # Replace visible containers
        self._jobs_container.children[:] = job_containers

    @property
    def _enabled_jobs(self):
        return tuple(jobinfo for jobinfo in self._jobs.values()
                     if jobinfo.job.is_enabled)

    def run(self, jobs):
        """
        Block while running `jobs`

        :param jobs: Iterable of :class:`~.jobs.base.JobBase` instances

        :raise: Any exception that occured while running jobs

        :return: :attr:`~.JobBase.exit_code` from the first failed job or 0 for
            success
        """
        self.add_jobs(*jobs)

        # Block until _exit() is called
        self._app.run(set_exception_handler=False)

        exception = self._get_exception()
        if exception:
            _log.debug('Application exception: %r', exception)
            raise exception
        else:
            # First non-zero exit_code is the application exit_code
            for jobinfo in self._enabled_jobs:
                _log.debug('Checking exit_code of %r: %r', jobinfo.job.name, jobinfo.job.exit_code)
                if jobinfo.job.exit_code != 0:
                    return jobinfo.job.exit_code
            return 0

    def _exit_if_all_jobs_finished(self, *_):
        if all(jobinfo.job.is_finished for jobinfo in self._enabled_jobs):
            _log.debug('All jobs finished')
            self._exit()

    def _exit_if_job_failed(self, job):
        if job.is_finished and job.exit_code != 0:
            _log.debug('Terminating application because of failed job: %r', job.name)
            self._exit()

    def _exit(self):
        if not self._app_terminated:
            if not self._app.is_running and not self._app.is_done:
                utils.get_aioloop().call_soon(self._exit)
            else:
                def handle_jobs_terminated(task):
                    try:
                        task.result()
                    except BaseException as e:
                        _log.debug('Handling exception from %r', task)
                        self._exception = e
                    finally:
                        _log.debug('Calling %r', self._app.exit)
                        self._app.exit()
                        self._update_jobs_container()

                self._app_terminated = True
                task = self._app.create_background_task(self._terminate_jobs())
                task.add_done_callback(handle_jobs_terminated)

    async def _terminate_jobs(self, callback=None):
        _log.debug('Waiting for jobs before exiting')
        self._finish_jobs()
        for jobinfo in self._enabled_jobs:
            if jobinfo.job.is_started and not jobinfo.job.is_finished:
                _log.debug('Waiting for %r', jobinfo.job.name)
                await jobinfo.job.wait()
                _log.debug('Done waiting for %r', jobinfo.job.name)
        if callback:
            callback()

    def _finish_jobs(self):
        for jobinfo in self._enabled_jobs:
            if not jobinfo.job.is_finished:
                _log.debug('Finishing %s', jobinfo.job.name)
                jobinfo.job.finish()

    def _get_exception(self):
        if self._exception:
            # Exception from _handle_exception()
            return self._exception
        else:
            # First exception from jobs
            for jobinfo in self._enabled_jobs:
                if jobinfo.job.raised:
                    _log.debug('Exception from %s: %r', jobinfo.job.name, jobinfo.job.raised)
                    return jobinfo.job.raised
示例#5
0
class HspApp(Application):
    """prompt_toolkit application for controlling and displaying a high-speed playback

    Attributes
    ==========
    displayingHelpScreen : bool
        used to toggle between help screen and normal view
    disabled_bindings : bool
        used to toggle key_bindings
    save_location : str
        Name preamble used when saving playback files
    playback : hsp.Playback
        Playback object that is being controlled by the app
    command_cache : collections.deque
        Local reference to the most recent command objects from playback hist
    main_view : prompt_toolkit.layout.containers.HSplit
        main layout for the app

        

    Methods
    =======
    mainViewCondition()
    init_bindings(self, bindings)
        Adds custom key_bindings to the app
    toolbar_text(self)
        Returns bottom toolbar for app
    render_command(self, command)
        Return string of command object specific to this UI
    get_user_comment(self)
        Modifies the display to add an area to enter a comment for a command
    _set_user_comment(self, buff)
        Callback fuction from the BufferControl created for user comments
    update_display
        displays last N commands in the local cache


    Async Methods
    =============
    command_loop(self)
        Primary loop for receiving/displaying commands from playback
    redraw_timer(self)
        Async method to force a redraw of the app every hundreth second
    """
    def __init__(self, playback, save_location=None, *args, **kwargs):

        self.mainViewCondition = partial(self.mainView, self)
        self.mainViewCondition = Condition(self.mainViewCondition)
        self.disabled_bindings = False
        bindings = KeyBindings()
        self.init_bindings(bindings)

        super().__init__(full_screen=True,
                         key_bindings=bindings,
                         mouse_support=True,
                         *args,
                         **kwargs)

        self.displayingHelpScreen = (
            False)  # used to toggle between help screen on normal

        if save_location:
            self.save_location = save_location
        else:
            self.save_location = SAVE_LOCATION
        self.playback = playback
        self._savedLayout = Layout(Window())
        self.command_cache = deque([], maxlen=5)

        ##########################################
        ### Setting up views
        ##########################################

        self.old_command_window = FormattedTextControl(text="Output goes here",
                                                       focusable=True)
        self.new_command_window = FormattedTextControl(text="Output goes here",
                                                       focusable=True)

        self.body = Frame(
            HSplit([
                Frame(Window(self.old_command_window)),
                Frame(Window(self.new_command_window)),
            ]))
        self.toolbar = Window(
            FormattedTextControl(text=self.toolbar_text),
            height=Dimension(max=1, weight=10000),
            dont_extend_height=True,
        )

        self.main_view = HSplit([self.body, self.toolbar], padding_char="-")
        self.layout = Layout(self.main_view)

    @staticmethod
    def mainView(self):
        """Return if app is in main view.

        Returns
        =======
        _ : bool
            If app is not displaying the Help Screen or comment, it's in main view
        """
        disable = self.displayingHelpScreen or self.disabled_bindings
        return not disable

    @contextmanager
    def switched_layout(self):
        """Context manager to stores and restore the current layout

        Saves the current layout to an instance variable and then restores
        that layout on __exit__
        """
        self._saved_layout = self.layout
        yield
        self.layout = self._saved_layout
        self.invalidate()

    @contextmanager
    def paused_playback(self):
        """Context manager for pausing playback temporarily 

        Pauses the playback in the __enter__ section.  If the playback was
        originally paused, the __exit__ section will keep the playback paused,
        otherwise it will resume play of the playback
        """
        _originally_paused = self.playback.paused
        self.playback.pause()
        yield
        if not _originally_paused:
            self.playback.play()

    @contextmanager
    def bindings_off(self):
        """Context manager for disabling key_bindings for a given scope

        Sets disabled_bindings to True so that the mainView Condition
        will return False.  Upon __exit__, it returns the key_ binding state
        to what is was when it entered.
        """
        _originally_disabled = self.disabled_bindings
        self.disabled_bindings = True
        yield
        self.disabled_bindings = _originally_disabled

    def init_bindings(self, bindings):
        """Adds custom key_bindings to the app
        """
        @bindings.add("n", filter=self.mainViewCondition)
        @bindings.add("down", filter=self.mainViewCondition)
        @bindings.add("right", filter=self.mainViewCondition)
        def _(event):
            try:
                self.playback.loop_lock.release()
            except Exception as e:
                pass

        @bindings.add("p", filter=self.mainViewCondition)
        def _(event):
            if self.playback.paused:
                self.playback.play()
            else:
                self.playback.pause()

        @bindings.add("f", filter=self.mainViewCondition)
        def _(event):
            self.playback.speedup()

        @bindings.add("s", filter=self.mainViewCondition)
        def _(event):
            self.playback.slowdown()

        @bindings.add("q")
        @bindings.add("c-c")
        def _(event):
            event.app.exit()

        @bindings.add("c-m", filter=self.mainViewCondition)
        def _(event):
            self.playback.change_playback_mode()

        @bindings.add("c", filter=self.mainViewCondition)
        def _(event):
            self.get_user_comment()
            self.update_display()

        @bindings.add("c-f", filter=self.mainViewCondition)
        def _(event):
            # set the flag in both the self.playback and the local cache for display
            self.playback.flag_current_command()
            self.update_display()

        @bindings.add("c-s", filter=self.mainViewCondition)
        def _(event):
            time = datetime.datetime.now()
            with open(self.save_location + f"_{time.strftime('%Y%m%d%H%M')}",
                      "wb+") as outfi:
                pickle.dump(self.playback.hist, outfi)

        @bindings.add("g", filter=self.mainViewCondition)
        def _(event):
            # future: goto time
            pass

        @bindings.add("h")
        def _(event):
            # display help screen
            if event.app.displayingHelpScreen:
                # exit help screen
                event.app.displayingHelpScreen = False
                self.playback.pause()
                event.app.layout = event.app.savedLayout
                event.app.invalidate()

            else:
                # display help screen
                event.app.displayingHelpScreen = True
                event.app.savedLayout = event.app.layout
                event.app.layout = self.helpLayout
                event.app.invalidate()

    helpLayout = Layout(
        Frame(
            Window(
                FormattedTextControl(
                    "HELP SCREEN\n\n"
                    "h -        help screen\n"
                    "s -        slow down\n"
                    "p -        toggle play/pause\n"
                    "c -        add comment to current command\n"
                    "g -        goto specific time in history\n"
                    "ctrl-m     change self.playback mode\n"
                    "ctrl-f     flag event\n"
                    "ctrl-s     save playback object to file\n"
                    "n/dwn/rght next event\n"))))

    def toolbar_text(self):
        """Returns bottom toolbar for app

        Returns
        =======
        _ : prompt_toolkit.formatted_text.HTML
            Text for the bottom toolbar for the app
        """
        if self.playback.playback_mode == self.playback.EVENINTERVAL:
            return HTML(
                "<table><tr>"
                f"<th>PLAYBACK TIME: {self.playback.current_time.strftime('%b %d %Y %H:%M:%S')}</th>     "
                f"<th>PLAYBACK MODE: {self.playback.playback_mode}</th>    "
                f"<th>PAUSED: {self.playback.paused}</th>      "
                f"<th>PLAYBACK INTERVAL: {self.playback.playback_interval}s</th>"
                "</tr></table>")
        else:
            return HTML(
                "<table><tr>"
                f"<th>PLAYBACK TIME: {self.playback.current_time.strftime('%b %d %Y %H:%M:%S')}</th>     "
                f"<th>PLAYBACK MODE: {self.playback.playback_mode}</th>    "
                f"<th>PAUSED: {self.playback.paused}</th>      "
                f"<th>PLAYBACK RATE: {self.playback.playback_rate}</th>"
                "</tr></table>")

    def render_command(self, command):
        """Return string of command object specific to this UI

        Parameters
        ==========
        command : command.Command
            Command object to get string for

        Returns
        =======
        _ : str
            String representation of Command object
        """
        try:
            if command.flagged:
                color = "ansired"
            else:
                color = "ansiwhite"
        except:
            color = "ansiwhite"
        try:
            return [
                ("bg:ansiblue ansiwhite", f"{command.time.ctime()}\n"),
                (
                    color,
                    (f"{command.hostUUID}:{command.user} > {command.command}\n"
                     f"{command.result}"
                     f"{command.comment}"),
                ),
            ]
        except:
            # if this happens, we probaly didn't get an actual Command object
            # but we can have it rendered in the window anyway
            return [(color, str(command))]

    def get_user_comment(self):
        """Modifies the display to add an area to enter a comment for a command

        Creates a BufferControl in a Frame and replaces the toolbar with the Frame

        #bug: the new toolbar is unable to get focus right away; it requires the user to click 
                in the area
        """
        self._savedLayout = self.layout
        self.disabled_bindings = True
        commentControl = BufferControl(
            Buffer(accept_handler=self._set_user_comment), focus_on_click=True)
        user_in_area = Frame(
            Window(
                commentControl,
                height=Dimension(max=1, weight=10000),
                dont_extend_height=True,
            ),
            title="Enter Comment (alt-Enter to submit)",
        )

        self.toolbar = user_in_area
        self.main_view = HSplit([self.body, self.toolbar], padding_char="-")
        self.layout = Layout(self.main_view, focused_element=user_in_area.body)
        self.layout.focus(user_in_area.body)
        self.invalidate()

    def _set_user_comment(self, buff):
        """Callback fuction from the BufferControl created for user comments

        Takes the user comment and sets that as the current command's comment.
        Then replaces the original layout.
        """
        self.playback.hist[self.playback.playback_position -
                           1].comment = buff.text
        self.disabled_bindings = False
        self.layout = self._savedLayout
        self.update_display()
        self.invalidate()

    def update_display(self):
        """displays last N commands in the local cache

        This should only be called when the main display with command history is showing
        otherwise the requisite windows will not be focusable.

        #future: allow the number of commands displayed to grow to the size of the 
                available screen realastate
        """

        self.layout.focus(self.old_command_window)
        if len(self.command_cache) > 1:
            self.layout.current_control.text = self.render_command(
                self.command_cache[-2])
        else:
            self.layout.current_control.text = "COMMAND OUTPUT HERE"
        # self.layout.current_control.text = new_command_window.text
        self.layout.focus(self.new_command_window)
        if len(self.command_cache) > 0:
            self.layout.current_control.text = self.render_command(
                self.command_cache[-1])
        else:
            self.layout.current_control.text = "COMMAND OUTPUT HERE"
        self.invalidate()

    ###################################################
    # Setting Up Loop to async iter over history
    ###################################################

    async def command_loop(self):
        """Primary loop for receiving/displaying commands from playback

        Asynchronously iterates over the Command objects in the playback's history.
        Takes the command object and displays it to the screen
        """
        # give this thread control over playback for manual mode
        # lock is released by certain key bindings
        await self.playback.loop_lock.acquire()
        async for command in self.playback:
            if self.playback.playback_mode == "MANUAL":
                # regain the lock for MANUAL mode
                await self.playback.loop_lock.acquire()

            self.command_cache.append(command)
            # Update text in windows
            self.update_display()
        else:
            # future: fix; this will fail at the end of a playback history
            self.command_cache.append(command)
            self.update_display()

    async def redraw_timer(self):
        """Async method to force a redraw of the app every hundreth second

        Never terminates
        # future: do some more checking and error handling
        """
        while True:
            await asyncio.sleep(0.01)
            self.invalidate()
示例#6
0
from xradios.tui.buffers.display import buffer as display_buffer
from xradios.tui.buffers.popup import buffer as popup_buffer
from xradios.tui.buffers.prompt import buffer as prompt_buffer
from xradios.tui.buffers.listview import buffer as listview_buffer

layout = Layout(
    FloatContainer(
        content=HSplit([
            TopBar(message="Need help! Press `F1`."),
            Display(display_buffer),
            ListView(listview_buffer),
            Prompt(prompt_buffer),
        ]),
        modal=True,
        floats=[
            # Help text as a float.
            Float(
                top=3,
                bottom=2,
                left=2,
                right=2,
                content=ConditionalContainer(
                    content=PopupWindow(popup_buffer, title="Help"),
                    filter=has_focus(popup_buffer),
                ),
            )
        ],
    ))

layout.focus(prompt_buffer)