Module cdev.utils.display_manager

Expand source code
from dataclasses import dataclass
from enum import Enum
import termios, sys, tty
from typing import Any, Dict, List, Tuple

from rich.layout import Layout
from rich.pretty import Pretty
from rich.live import Live
from rich.panel import Panel
from rich.console import Console
from rich.text import Text

from cdev.utils.git_safe.project_merger_info import (
    FINAL_PAGE_COMPLETE_TEMPLATE,
    FINAL_PAGE_INCOMPLETE_TEMPLATE,
)


class DidNotCompleteSelections(Exception):
    pass


class KEY_ACTIONS(str, Enum):
    UP = "UP"
    DOWN = "DOWN"
    LEFT = "LEFT"
    RIGHT = "RIGHT"
    EXPAND = "EXPAND"
    QUIT = "QUIT"
    ENTER = "ENTER"


DEFAULT_KEY_BINDINGS = {
    b"\x1b[A": KEY_ACTIONS.UP,
    b"\x1b[B": KEY_ACTIONS.DOWN,
    b"\x1b[C": KEY_ACTIONS.RIGHT,
    b"\x1b[D": KEY_ACTIONS.LEFT,
    b"e": KEY_ACTIONS.EXPAND,
    b"q": KEY_ACTIONS.QUIT,
    b"\x03": KEY_ACTIONS.QUIT,
    b"\r": KEY_ACTIONS.ENTER,
}


class PAGE_EXIT_CODES(str, Enum):
    BACKWARD = "BACKWARD"
    FORWARD = "FORWARD"
    ACTION = "ACTION"
    EXPAND = "EXPAND"
    QUIT = "QUIT"


class PAGE_NAV_EMOJIS(str, Enum):
    COMPLETE = ":heavy_check_mark:"
    INCOMPLETE = ":cross_mark:"
    INFORMATION = ":page_with_curl:"


@dataclass
class select_data:
    header: str
    content: Any
    rv: Any


@dataclass
class split_layout:
    base: Layout
    right: Layout
    left: Layout


@dataclass
class two_item_selection_data:
    left_data: select_data
    right_data: select_data


@dataclass
class single_item_selection_data:
    options: List[Tuple[str, Any]]
    content: Any


class Page:
    def __init__(self) -> None:
        self.base_layout = Layout()
        self.nav: Layout = None

    def blocking_selection_process(self) -> PAGE_EXIT_CODES:
        raise NotImplementedError

    def update_navigation_bar(self, nav_content: Panel) -> None:
        self.nav.update(nav_content)

    def expand_view(self) -> None:
        raise NotImplementedError


class InformationPage(Page):
    def __init__(self, header: str, content: Any) -> None:
        super().__init__()
        self._content = content
        self._header = header

        self.content = Layout(
            Panel(Text.from_markup(content, justify="center"), title=header),
            name="content",
            ratio=8,
        )

        self.nav = Layout(name="navbar", ratio=2)

        self.base_layout.split_column(
            self.content,
            self.nav,
        )

        self._key_strokes_mappings = DEFAULT_KEY_BINDINGS

    def update_content(self, content: Any):
        self._content = content

        self.content.update(
            Layout(
                Panel(Text.from_markup(content, justify="center"), title=self._header),
                name="content",
                ratio=8,
            )
        )

    def blocking_selection_process(self) -> PAGE_EXIT_CODES:
        with Live(self.base_layout, auto_refresh=False):
            while True:
                next_char = _blocking_get_keystroke(self._key_strokes_mappings)

                if next_char == KEY_ACTIONS.QUIT:
                    return PAGE_EXIT_CODES.QUIT

                if next_char == KEY_ACTIONS.EXPAND:
                    return PAGE_EXIT_CODES.EXPAND

                if next_char == KEY_ACTIONS.UP:
                    return PAGE_EXIT_CODES.BACKWARD

                if next_char == KEY_ACTIONS.DOWN:
                    return PAGE_EXIT_CODES.FORWARD

                if next_char == KEY_ACTIONS.ENTER:
                    return PAGE_EXIT_CODES.ACTION

    def expand_view(self) -> None:
        console = Console()
        with console.pager():
            console.print(Text.from_markup(self._content))


class FinalInformationPage(InformationPage):
    def __init__(self, content: Any) -> None:
        super().__init__("REVIEW PAGE", content)
        self._is_completable = True

    def blocking_selection_process(self) -> PAGE_EXIT_CODES:
        with Live(self.base_layout, auto_refresh=False):
            while True:
                next_char = _blocking_get_keystroke(self._key_strokes_mappings)

                if next_char == KEY_ACTIONS.QUIT:
                    return PAGE_EXIT_CODES.QUIT

                if next_char == KEY_ACTIONS.EXPAND:
                    return PAGE_EXIT_CODES.EXPAND

                if next_char == KEY_ACTIONS.UP:
                    return PAGE_EXIT_CODES.BACKWARD

                if next_char == KEY_ACTIONS.DOWN:
                    return PAGE_EXIT_CODES.FORWARD

                if next_char == KEY_ACTIONS.ENTER:
                    if self._is_completable:
                        return PAGE_EXIT_CODES.ACTION

    def set_is_completable(self, is_completed: bool) -> None:
        self._is_completable = is_completed


class SelectionPage(Page):
    def get_selected_item(self) -> Any:
        raise NotImplementedError

    def get_selected_label(self) -> Any:
        raise NotImplementedError


class SimpleSelectionListPage(Page):
    def __init__(self, options: List[str]):
        super().__init__()
        self.data = options
        self._selected_item_index = 0
        self._key_strokes_mappings = DEFAULT_KEY_BINDINGS
        self._update_selection()

    def blocking_selection_process(self) -> str:
        with Live(self.base_layout, auto_refresh=False) as live:

            while True:
                next_char = _blocking_get_keystroke(self._key_strokes_mappings)

                if next_char == KEY_ACTIONS.DOWN:
                    self._move_selection(1)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.UP:
                    self._move_selection(-1)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.ENTER:
                    self.base_layout = ""
                    return self.data[self._selected_item_index]

    def _move_selection(self, increment: int):
        _tmp_index = (
            0
            if self._selected_item_index is None
            else increment + self._selected_item_index
        )

        if _tmp_index >= len(self.data) or _tmp_index < 0:
            return

        self._selected_item_index = _tmp_index
        self._update_selection()

    def _update_selection(self):
        _options_list = "\n".join(
            [
                x if i != self._selected_item_index else f"> {x}"
                for i, x in enumerate(self.data)
            ]
        )
        self.base_layout = Text.from_markup(_options_list)


class SingleItemSelectionPage(SelectionPage):
    def __init__(self, header: str, selection_data: single_item_selection_data) -> None:
        super().__init__()

        self.selection_data = selection_data
        self._selected_item_index = None

        self.header = Layout(
            Panel(Text.from_markup(header)),
            name="header",
            ratio=5,
        )

        self.content = Layout(
            Panel(Pretty(self.selection_data.content), title="Data"),
            name="content",
            ratio=10,
        )
        self.selection = Layout(name="content", ratio=1)

        self.nav = Layout(name="bottom", ratio=4)

        self.base_layout.split_column(
            self.header,
            self.selection,
            self.content,
            self.nav,
        )

        self._update_selection()

        self._key_strokes_mappings = DEFAULT_KEY_BINDINGS

    def blocking_selection_process(self) -> PAGE_EXIT_CODES:
        with Live(self.base_layout, auto_refresh=False) as live:

            while True:
                next_char = _blocking_get_keystroke(self._key_strokes_mappings)

                if next_char == KEY_ACTIONS.RIGHT:
                    self._move_selection(1)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.LEFT:
                    self._move_selection(-1)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.QUIT:
                    return PAGE_EXIT_CODES.QUIT

                if next_char == KEY_ACTIONS.EXPAND:
                    return PAGE_EXIT_CODES.EXPAND

                if next_char == KEY_ACTIONS.UP:
                    return PAGE_EXIT_CODES.BACKWARD

                if next_char == KEY_ACTIONS.DOWN:
                    return PAGE_EXIT_CODES.FORWARD

                if next_char == KEY_ACTIONS.ENTER:
                    return PAGE_EXIT_CODES.FORWARD

    def expand_view(self) -> None:
        console = Console()
        with console.pager():
            console.print("++++++++++++++++DATA++++++++++++++++")
            console.print(Pretty(self.selection_data.content, expand_all=True))
            console.print("+++++++++++++++++++++++++++++++++++++++")

    def _move_selection(self, increment: int):
        _tmp_index = (
            0
            if self._selected_item_index is None
            else increment + self._selected_item_index
        )

        if _tmp_index >= len(self.selection_data.options) or _tmp_index < 0:
            return

        self._selected_item_index = _tmp_index
        self._update_selection()

    def _update_selection(self):

        _options_list = [x[0] for x in self.selection_data.options]

        if not self._selected_item_index is None:
            _options_list[
                self._selected_item_index
            ] = f"[bold reverse cyan]{_options_list[self._selected_item_index]}[/bold reverse cyan]"

        _options = " | ".join(_options_list)
        self.selection.update((f"{_options}"))

    def get_selected_item(self) -> Any:
        if self._selected_item_index is None:
            return None

        return self.selection_data.options[self._selected_item_index][1]

    def get_selected_label(self) -> Any:
        if self._selected_item_index is None:
            return None

        return self.selection_data.options[self._selected_item_index][0]


class TwoItemSelectionPage(SelectionPage):
    def __init__(self, header: str, selection_data: two_item_selection_data) -> None:
        super().__init__()

        self.selection_data = selection_data
        self.header = Layout(
            Panel(Text.from_markup(header)),
            name="header",
            ratio=5,
        )

        self.subheader = TwoItemSelectionPage._make_split_pane(
            name="subheader", ratio=1
        )
        self.content = TwoItemSelectionPage._make_split_pane(name="content", ratio=10)

        self.nav = Layout(name="nav", ratio=4)

        self.base_layout.split_column(
            self.header,
            self.subheader.base,
            self.content.base,
            self.nav,
        )

        self.subheader.left.update(
            Text.from_markup(self.selection_data.left_data.header)
        )
        self.subheader.right.update(
            Text.from_markup(self.selection_data.right_data.header)
        )

        self.content.left.update(
            self._create_content_panel(selection_data.left_data.content)
        )
        self.content.right.update(
            self._create_content_panel(selection_data.right_data.content)
        )

        self.is_left_selected = None

        self._key_strokes_mappings = DEFAULT_KEY_BINDINGS

    def blocking_selection_process(self) -> PAGE_EXIT_CODES:
        with Live(self.base_layout, auto_refresh=False) as live:
            while True:
                next_char = _blocking_get_keystroke(self._key_strokes_mappings)

                if next_char == KEY_ACTIONS.RIGHT:
                    self._select_side(select_left=False)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.LEFT:
                    self._select_side(select_left=True)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.QUIT:
                    return PAGE_EXIT_CODES.QUIT

                if next_char == KEY_ACTIONS.EXPAND:
                    return PAGE_EXIT_CODES.EXPAND

                if next_char == KEY_ACTIONS.UP:
                    return PAGE_EXIT_CODES.BACKWARD

                if next_char == KEY_ACTIONS.DOWN:
                    return PAGE_EXIT_CODES.FORWARD

                if next_char == KEY_ACTIONS.ENTER:
                    return PAGE_EXIT_CODES.FORWARD

    def expand_view(self) -> None:
        console = Console()
        with console.pager():
            console.print("+++++++++++++++++++++++++++++++++++++++")
            console.print(self.selection_data.left_data.header)
            console.print("+++++++++++++++++++++++++++++++++++++++")
            console.print(
                Pretty(self.selection_data.left_data.content, expand_all=True)
            )

            console.print("")
            console.print("+++++++++++++++++++++++++++++++++++++++")
            console.print(self.selection_data.right_data.header)
            console.print("+++++++++++++++++++++++++++++++++++++++")
            console.print(
                Pretty(self.selection_data.right_data.content, expand_all=True)
            )

    def _select_side(self, select_left: bool):
        if select_left == self.is_left_selected:
            return

        if self.is_left_selected is None:
            self.is_left_selected = not select_left

        if not select_left and self.is_left_selected:
            self.subheader.left.update(
                Text.from_markup(self.selection_data.left_data.header)
            )
            self.subheader.right.update(
                Text.from_markup(
                    f"[reverse cyan]{self.selection_data.right_data.header}[/reverse cyan]"
                )
            )

            self.content.left.update(
                self._create_content_panel(self.selection_data.left_data.content)
            )
            self.content.right.update(
                self._create_content_panel(
                    self.selection_data.right_data.content, is_selected=True
                )
            )

        if select_left and not self.is_left_selected:
            self.subheader.left.update(
                Text.from_markup(
                    f"[reverse cyan]{self.selection_data.left_data.header}[/reverse cyan]"
                )
            )
            self.subheader.right.update(
                Text.from_markup(self.selection_data.right_data.header)
            )

            self.content.left.update(
                self._create_content_panel(
                    self.selection_data.left_data.content, is_selected=True
                )
            )
            self.content.right.update(
                self._create_content_panel(self.selection_data.right_data.content)
            )

        self.is_left_selected = select_left

    def _make_split_pane(name: str, ratio: int = None) -> split_layout:
        base_layout = Layout(name=name, ratio=ratio) if ratio else Layout(name=name)

        right = Layout(name=f"{name}-right")
        left = Layout(name=f"{name}-left")

        base_layout.split_row(
            left,
            right,
        )

        return split_layout(base=base_layout, right=right, left=left)

    def _create_content_panel(self, data: Any, is_selected: bool = False) -> Panel:
        _border_style = "bold" if is_selected else "dim"
        return Panel(Pretty(data), border_style=_border_style)

    def get_selected_item(self) -> Any:
        if self.is_left_selected is None:
            return None

        return (
            self.selection_data.left_data.rv
            if self.is_left_selected
            else self.selection_data.right_data.rv
        )

    def get_selected_label(self) -> Any:
        if self.is_left_selected is None:
            return None

        return (
            self.selection_data.left_data.header
            if self.is_left_selected
            else self.selection_data.right_data.header
        )


class SelectionPageContainer:
    def __init__(
        self, quit_title: str, quit_content: str, quit_navbar: str = None
    ) -> None:
        self.pages: List[Page] = []
        self._current_index = 0
        self._final_page: FinalInformationPage = None
        self._quit_page = InformationPage(quit_title, quit_content)

        self._quit_page.update_navigation_bar(
            Panel(
                Text.from_markup(quit_navbar, justify="center"),
                title=f"NAVIGATION",
                title_align="center",
            )
        )

    def add_page(self, page: Page) -> None:
        self.pages.append(page)

    def _generate_nav_characters(self, page: Page) -> str:

        if isinstance(page, SelectionPage):
            return (
                PAGE_NAV_EMOJIS.INCOMPLETE.value
                if page.get_selected_item() is None
                else PAGE_NAV_EMOJIS.COMPLETE.value
            )

        else:
            return PAGE_NAV_EMOJIS.INFORMATION.value

    def _get_current_nav_panel(self) -> Panel:

        _base_nav_characters = [
            self._generate_nav_characters(x)
            if indx == self._current_index
            else f"[dim]{self._generate_nav_characters(x)}[/dim]"
            for indx, x in enumerate(self.pages)
        ]

        _nav_str = " ".join(_base_nav_characters)

        nav_content = f"""[bold yellow]up-arrow[/bold yellow]: Previous Page; [bold yellow]down-arrow[/bold yellow]: Next Page; [bold yellow]q[/bold yellow]: Quit; [bold yellow]e[/bold yellow]: Expand

{_nav_str} ({self._current_index+1}/{len(self.pages)})
        """

        return Panel(
            Text.from_markup(nav_content, justify="center"),
            title=f"NAVIGATION",
            title_align="center",
        )

    def _update_final_page(self) -> None:
        _results = self.get_results()
        is_completed = all(_results)

        self._final_page.set_is_completable(is_completed)

        _selection_str = "\n".join(
            f"page {i+1} -> {f'[blue]{x}[/blue]' if x else '[red]No Selection. Return to page to complete.[/red]'}"
            for i, x in enumerate(self.get_result_labels())
        )

        if is_completed:
            self._final_page.update_content(
                FINAL_PAGE_COMPLETE_TEMPLATE.format(selections=_selection_str)
            )
        else:
            self._final_page.update_content(
                FINAL_PAGE_INCOMPLETE_TEMPLATE.format(selections=_selection_str)
            )

    def _get_final_page(self) -> Page:
        if self._final_page:
            return self._final_page

        self._final_page = FinalInformationPage("Review your selections")

        return self._final_page

    def run_pages(self) -> None:
        self.add_page(self._get_final_page())
        while True:
            if self._current_index == len(self.pages) - 1:
                self._update_final_page()

            _current_page = self.pages[self._current_index]

            _current_page.update_navigation_bar(self._get_current_nav_panel())
            exit_code = _current_page.blocking_selection_process()

            if exit_code == PAGE_EXIT_CODES.QUIT:
                quit_rv = self._quit_page.blocking_selection_process()

                if quit_rv == PAGE_EXIT_CODES.QUIT:
                    raise DidNotCompleteSelections

            elif exit_code == PAGE_EXIT_CODES.BACKWARD:
                if self._current_index > 0:
                    self._current_index -= 1

            elif exit_code == PAGE_EXIT_CODES.FORWARD:
                if self._current_index < len(self.pages) - 1:
                    self._current_index += 1

            elif exit_code == PAGE_EXIT_CODES.EXPAND:
                _current_page.expand_view()

            elif exit_code == PAGE_EXIT_CODES.ACTION:
                if self._current_index == len(self.pages) - 1:
                    return

    def get_results(self) -> List[Any]:
        return [
            page.get_selected_item()
            for page in self.pages
            if isinstance(page, SelectionPage)
        ]

    def get_result_labels(self) -> List[Any]:
        return [
            page.get_selected_label()
            for page in self.pages
            if isinstance(page, SelectionPage)
        ]


def _blocking_get_keystroke(mapping: Dict[Any, KEY_ACTIONS] = None) -> KEY_ACTIONS:
    fd = sys.stdin.fileno()
    # save settings from the tty session
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(fd)
        ch = sys.stdin.buffer.read1(3)  # This number represents the length

    finally:
        # reset settings from the tty session
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

    return mapping.get(ch) if mapping else ch

Classes

class DidNotCompleteSelections (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class DidNotCompleteSelections(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException
class FinalInformationPage (content: Any)
Expand source code
class FinalInformationPage(InformationPage):
    def __init__(self, content: Any) -> None:
        super().__init__("REVIEW PAGE", content)
        self._is_completable = True

    def blocking_selection_process(self) -> PAGE_EXIT_CODES:
        with Live(self.base_layout, auto_refresh=False):
            while True:
                next_char = _blocking_get_keystroke(self._key_strokes_mappings)

                if next_char == KEY_ACTIONS.QUIT:
                    return PAGE_EXIT_CODES.QUIT

                if next_char == KEY_ACTIONS.EXPAND:
                    return PAGE_EXIT_CODES.EXPAND

                if next_char == KEY_ACTIONS.UP:
                    return PAGE_EXIT_CODES.BACKWARD

                if next_char == KEY_ACTIONS.DOWN:
                    return PAGE_EXIT_CODES.FORWARD

                if next_char == KEY_ACTIONS.ENTER:
                    if self._is_completable:
                        return PAGE_EXIT_CODES.ACTION

    def set_is_completable(self, is_completed: bool) -> None:
        self._is_completable = is_completed

Ancestors

Methods

def blocking_selection_process(self) ‑> PAGE_EXIT_CODES
Expand source code
def blocking_selection_process(self) -> PAGE_EXIT_CODES:
    with Live(self.base_layout, auto_refresh=False):
        while True:
            next_char = _blocking_get_keystroke(self._key_strokes_mappings)

            if next_char == KEY_ACTIONS.QUIT:
                return PAGE_EXIT_CODES.QUIT

            if next_char == KEY_ACTIONS.EXPAND:
                return PAGE_EXIT_CODES.EXPAND

            if next_char == KEY_ACTIONS.UP:
                return PAGE_EXIT_CODES.BACKWARD

            if next_char == KEY_ACTIONS.DOWN:
                return PAGE_EXIT_CODES.FORWARD

            if next_char == KEY_ACTIONS.ENTER:
                if self._is_completable:
                    return PAGE_EXIT_CODES.ACTION
def set_is_completable(self, is_completed: bool) ‑> None
Expand source code
def set_is_completable(self, is_completed: bool) -> None:
    self._is_completable = is_completed
class InformationPage (header: str, content: Any)
Expand source code
class InformationPage(Page):
    def __init__(self, header: str, content: Any) -> None:
        super().__init__()
        self._content = content
        self._header = header

        self.content = Layout(
            Panel(Text.from_markup(content, justify="center"), title=header),
            name="content",
            ratio=8,
        )

        self.nav = Layout(name="navbar", ratio=2)

        self.base_layout.split_column(
            self.content,
            self.nav,
        )

        self._key_strokes_mappings = DEFAULT_KEY_BINDINGS

    def update_content(self, content: Any):
        self._content = content

        self.content.update(
            Layout(
                Panel(Text.from_markup(content, justify="center"), title=self._header),
                name="content",
                ratio=8,
            )
        )

    def blocking_selection_process(self) -> PAGE_EXIT_CODES:
        with Live(self.base_layout, auto_refresh=False):
            while True:
                next_char = _blocking_get_keystroke(self._key_strokes_mappings)

                if next_char == KEY_ACTIONS.QUIT:
                    return PAGE_EXIT_CODES.QUIT

                if next_char == KEY_ACTIONS.EXPAND:
                    return PAGE_EXIT_CODES.EXPAND

                if next_char == KEY_ACTIONS.UP:
                    return PAGE_EXIT_CODES.BACKWARD

                if next_char == KEY_ACTIONS.DOWN:
                    return PAGE_EXIT_CODES.FORWARD

                if next_char == KEY_ACTIONS.ENTER:
                    return PAGE_EXIT_CODES.ACTION

    def expand_view(self) -> None:
        console = Console()
        with console.pager():
            console.print(Text.from_markup(self._content))

Ancestors

Subclasses

Methods

def blocking_selection_process(self) ‑> PAGE_EXIT_CODES
Expand source code
def blocking_selection_process(self) -> PAGE_EXIT_CODES:
    with Live(self.base_layout, auto_refresh=False):
        while True:
            next_char = _blocking_get_keystroke(self._key_strokes_mappings)

            if next_char == KEY_ACTIONS.QUIT:
                return PAGE_EXIT_CODES.QUIT

            if next_char == KEY_ACTIONS.EXPAND:
                return PAGE_EXIT_CODES.EXPAND

            if next_char == KEY_ACTIONS.UP:
                return PAGE_EXIT_CODES.BACKWARD

            if next_char == KEY_ACTIONS.DOWN:
                return PAGE_EXIT_CODES.FORWARD

            if next_char == KEY_ACTIONS.ENTER:
                return PAGE_EXIT_CODES.ACTION
def expand_view(self) ‑> None
Expand source code
def expand_view(self) -> None:
    console = Console()
    with console.pager():
        console.print(Text.from_markup(self._content))
def update_content(self, content: Any)
Expand source code
def update_content(self, content: Any):
    self._content = content

    self.content.update(
        Layout(
            Panel(Text.from_markup(content, justify="center"), title=self._header),
            name="content",
            ratio=8,
        )
    )
class KEY_ACTIONS (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code
class KEY_ACTIONS(str, Enum):
    UP = "UP"
    DOWN = "DOWN"
    LEFT = "LEFT"
    RIGHT = "RIGHT"
    EXPAND = "EXPAND"
    QUIT = "QUIT"
    ENTER = "ENTER"

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var DOWN
var ENTER
var EXPAND
var LEFT
var QUIT
var RIGHT
var UP
class PAGE_EXIT_CODES (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code
class PAGE_EXIT_CODES(str, Enum):
    BACKWARD = "BACKWARD"
    FORWARD = "FORWARD"
    ACTION = "ACTION"
    EXPAND = "EXPAND"
    QUIT = "QUIT"

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var ACTION
var BACKWARD
var EXPAND
var FORWARD
var QUIT
class PAGE_NAV_EMOJIS (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code
class PAGE_NAV_EMOJIS(str, Enum):
    COMPLETE = ":heavy_check_mark:"
    INCOMPLETE = ":cross_mark:"
    INFORMATION = ":page_with_curl:"

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var COMPLETE
var INCOMPLETE
var INFORMATION
class Page
Expand source code
class Page:
    def __init__(self) -> None:
        self.base_layout = Layout()
        self.nav: Layout = None

    def blocking_selection_process(self) -> PAGE_EXIT_CODES:
        raise NotImplementedError

    def update_navigation_bar(self, nav_content: Panel) -> None:
        self.nav.update(nav_content)

    def expand_view(self) -> None:
        raise NotImplementedError

Subclasses

Methods

def blocking_selection_process(self) ‑> PAGE_EXIT_CODES
Expand source code
def blocking_selection_process(self) -> PAGE_EXIT_CODES:
    raise NotImplementedError
def expand_view(self) ‑> None
Expand source code
def expand_view(self) -> None:
    raise NotImplementedError
def update_navigation_bar(self, nav_content: rich.panel.Panel) ‑> None
Expand source code
def update_navigation_bar(self, nav_content: Panel) -> None:
    self.nav.update(nav_content)
class SelectionPage
Expand source code
class SelectionPage(Page):
    def get_selected_item(self) -> Any:
        raise NotImplementedError

    def get_selected_label(self) -> Any:
        raise NotImplementedError

Ancestors

Subclasses

Methods

def get_selected_item(self) ‑> Any
Expand source code
def get_selected_item(self) -> Any:
    raise NotImplementedError
def get_selected_label(self) ‑> Any
Expand source code
def get_selected_label(self) -> Any:
    raise NotImplementedError
class SelectionPageContainer (quit_title: str, quit_content: str, quit_navbar: str = None)
Expand source code
class SelectionPageContainer:
    def __init__(
        self, quit_title: str, quit_content: str, quit_navbar: str = None
    ) -> None:
        self.pages: List[Page] = []
        self._current_index = 0
        self._final_page: FinalInformationPage = None
        self._quit_page = InformationPage(quit_title, quit_content)

        self._quit_page.update_navigation_bar(
            Panel(
                Text.from_markup(quit_navbar, justify="center"),
                title=f"NAVIGATION",
                title_align="center",
            )
        )

    def add_page(self, page: Page) -> None:
        self.pages.append(page)

    def _generate_nav_characters(self, page: Page) -> str:

        if isinstance(page, SelectionPage):
            return (
                PAGE_NAV_EMOJIS.INCOMPLETE.value
                if page.get_selected_item() is None
                else PAGE_NAV_EMOJIS.COMPLETE.value
            )

        else:
            return PAGE_NAV_EMOJIS.INFORMATION.value

    def _get_current_nav_panel(self) -> Panel:

        _base_nav_characters = [
            self._generate_nav_characters(x)
            if indx == self._current_index
            else f"[dim]{self._generate_nav_characters(x)}[/dim]"
            for indx, x in enumerate(self.pages)
        ]

        _nav_str = " ".join(_base_nav_characters)

        nav_content = f"""[bold yellow]up-arrow[/bold yellow]: Previous Page; [bold yellow]down-arrow[/bold yellow]: Next Page; [bold yellow]q[/bold yellow]: Quit; [bold yellow]e[/bold yellow]: Expand

{_nav_str} ({self._current_index+1}/{len(self.pages)})
        """

        return Panel(
            Text.from_markup(nav_content, justify="center"),
            title=f"NAVIGATION",
            title_align="center",
        )

    def _update_final_page(self) -> None:
        _results = self.get_results()
        is_completed = all(_results)

        self._final_page.set_is_completable(is_completed)

        _selection_str = "\n".join(
            f"page {i+1} -> {f'[blue]{x}[/blue]' if x else '[red]No Selection. Return to page to complete.[/red]'}"
            for i, x in enumerate(self.get_result_labels())
        )

        if is_completed:
            self._final_page.update_content(
                FINAL_PAGE_COMPLETE_TEMPLATE.format(selections=_selection_str)
            )
        else:
            self._final_page.update_content(
                FINAL_PAGE_INCOMPLETE_TEMPLATE.format(selections=_selection_str)
            )

    def _get_final_page(self) -> Page:
        if self._final_page:
            return self._final_page

        self._final_page = FinalInformationPage("Review your selections")

        return self._final_page

    def run_pages(self) -> None:
        self.add_page(self._get_final_page())
        while True:
            if self._current_index == len(self.pages) - 1:
                self._update_final_page()

            _current_page = self.pages[self._current_index]

            _current_page.update_navigation_bar(self._get_current_nav_panel())
            exit_code = _current_page.blocking_selection_process()

            if exit_code == PAGE_EXIT_CODES.QUIT:
                quit_rv = self._quit_page.blocking_selection_process()

                if quit_rv == PAGE_EXIT_CODES.QUIT:
                    raise DidNotCompleteSelections

            elif exit_code == PAGE_EXIT_CODES.BACKWARD:
                if self._current_index > 0:
                    self._current_index -= 1

            elif exit_code == PAGE_EXIT_CODES.FORWARD:
                if self._current_index < len(self.pages) - 1:
                    self._current_index += 1

            elif exit_code == PAGE_EXIT_CODES.EXPAND:
                _current_page.expand_view()

            elif exit_code == PAGE_EXIT_CODES.ACTION:
                if self._current_index == len(self.pages) - 1:
                    return

    def get_results(self) -> List[Any]:
        return [
            page.get_selected_item()
            for page in self.pages
            if isinstance(page, SelectionPage)
        ]

    def get_result_labels(self) -> List[Any]:
        return [
            page.get_selected_label()
            for page in self.pages
            if isinstance(page, SelectionPage)
        ]

Methods

def add_page(self, page: Page) ‑> None
Expand source code
def add_page(self, page: Page) -> None:
    self.pages.append(page)
def get_result_labels(self) ‑> List[Any]
Expand source code
def get_result_labels(self) -> List[Any]:
    return [
        page.get_selected_label()
        for page in self.pages
        if isinstance(page, SelectionPage)
    ]
def get_results(self) ‑> List[Any]
Expand source code
def get_results(self) -> List[Any]:
    return [
        page.get_selected_item()
        for page in self.pages
        if isinstance(page, SelectionPage)
    ]
def run_pages(self) ‑> None
Expand source code
def run_pages(self) -> None:
    self.add_page(self._get_final_page())
    while True:
        if self._current_index == len(self.pages) - 1:
            self._update_final_page()

        _current_page = self.pages[self._current_index]

        _current_page.update_navigation_bar(self._get_current_nav_panel())
        exit_code = _current_page.blocking_selection_process()

        if exit_code == PAGE_EXIT_CODES.QUIT:
            quit_rv = self._quit_page.blocking_selection_process()

            if quit_rv == PAGE_EXIT_CODES.QUIT:
                raise DidNotCompleteSelections

        elif exit_code == PAGE_EXIT_CODES.BACKWARD:
            if self._current_index > 0:
                self._current_index -= 1

        elif exit_code == PAGE_EXIT_CODES.FORWARD:
            if self._current_index < len(self.pages) - 1:
                self._current_index += 1

        elif exit_code == PAGE_EXIT_CODES.EXPAND:
            _current_page.expand_view()

        elif exit_code == PAGE_EXIT_CODES.ACTION:
            if self._current_index == len(self.pages) - 1:
                return
class SimpleSelectionListPage (options: List[str])
Expand source code
class SimpleSelectionListPage(Page):
    def __init__(self, options: List[str]):
        super().__init__()
        self.data = options
        self._selected_item_index = 0
        self._key_strokes_mappings = DEFAULT_KEY_BINDINGS
        self._update_selection()

    def blocking_selection_process(self) -> str:
        with Live(self.base_layout, auto_refresh=False) as live:

            while True:
                next_char = _blocking_get_keystroke(self._key_strokes_mappings)

                if next_char == KEY_ACTIONS.DOWN:
                    self._move_selection(1)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.UP:
                    self._move_selection(-1)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.ENTER:
                    self.base_layout = ""
                    return self.data[self._selected_item_index]

    def _move_selection(self, increment: int):
        _tmp_index = (
            0
            if self._selected_item_index is None
            else increment + self._selected_item_index
        )

        if _tmp_index >= len(self.data) or _tmp_index < 0:
            return

        self._selected_item_index = _tmp_index
        self._update_selection()

    def _update_selection(self):
        _options_list = "\n".join(
            [
                x if i != self._selected_item_index else f"> {x}"
                for i, x in enumerate(self.data)
            ]
        )
        self.base_layout = Text.from_markup(_options_list)

Ancestors

Methods

def blocking_selection_process(self) ‑> str
Expand source code
def blocking_selection_process(self) -> str:
    with Live(self.base_layout, auto_refresh=False) as live:

        while True:
            next_char = _blocking_get_keystroke(self._key_strokes_mappings)

            if next_char == KEY_ACTIONS.DOWN:
                self._move_selection(1)
                live.update(self.base_layout, refresh=True)

            if next_char == KEY_ACTIONS.UP:
                self._move_selection(-1)
                live.update(self.base_layout, refresh=True)

            if next_char == KEY_ACTIONS.ENTER:
                self.base_layout = ""
                return self.data[self._selected_item_index]
class SingleItemSelectionPage (header: str, selection_data: single_item_selection_data)
Expand source code
class SingleItemSelectionPage(SelectionPage):
    def __init__(self, header: str, selection_data: single_item_selection_data) -> None:
        super().__init__()

        self.selection_data = selection_data
        self._selected_item_index = None

        self.header = Layout(
            Panel(Text.from_markup(header)),
            name="header",
            ratio=5,
        )

        self.content = Layout(
            Panel(Pretty(self.selection_data.content), title="Data"),
            name="content",
            ratio=10,
        )
        self.selection = Layout(name="content", ratio=1)

        self.nav = Layout(name="bottom", ratio=4)

        self.base_layout.split_column(
            self.header,
            self.selection,
            self.content,
            self.nav,
        )

        self._update_selection()

        self._key_strokes_mappings = DEFAULT_KEY_BINDINGS

    def blocking_selection_process(self) -> PAGE_EXIT_CODES:
        with Live(self.base_layout, auto_refresh=False) as live:

            while True:
                next_char = _blocking_get_keystroke(self._key_strokes_mappings)

                if next_char == KEY_ACTIONS.RIGHT:
                    self._move_selection(1)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.LEFT:
                    self._move_selection(-1)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.QUIT:
                    return PAGE_EXIT_CODES.QUIT

                if next_char == KEY_ACTIONS.EXPAND:
                    return PAGE_EXIT_CODES.EXPAND

                if next_char == KEY_ACTIONS.UP:
                    return PAGE_EXIT_CODES.BACKWARD

                if next_char == KEY_ACTIONS.DOWN:
                    return PAGE_EXIT_CODES.FORWARD

                if next_char == KEY_ACTIONS.ENTER:
                    return PAGE_EXIT_CODES.FORWARD

    def expand_view(self) -> None:
        console = Console()
        with console.pager():
            console.print("++++++++++++++++DATA++++++++++++++++")
            console.print(Pretty(self.selection_data.content, expand_all=True))
            console.print("+++++++++++++++++++++++++++++++++++++++")

    def _move_selection(self, increment: int):
        _tmp_index = (
            0
            if self._selected_item_index is None
            else increment + self._selected_item_index
        )

        if _tmp_index >= len(self.selection_data.options) or _tmp_index < 0:
            return

        self._selected_item_index = _tmp_index
        self._update_selection()

    def _update_selection(self):

        _options_list = [x[0] for x in self.selection_data.options]

        if not self._selected_item_index is None:
            _options_list[
                self._selected_item_index
            ] = f"[bold reverse cyan]{_options_list[self._selected_item_index]}[/bold reverse cyan]"

        _options = " | ".join(_options_list)
        self.selection.update((f"{_options}"))

    def get_selected_item(self) -> Any:
        if self._selected_item_index is None:
            return None

        return self.selection_data.options[self._selected_item_index][1]

    def get_selected_label(self) -> Any:
        if self._selected_item_index is None:
            return None

        return self.selection_data.options[self._selected_item_index][0]

Ancestors

Methods

def blocking_selection_process(self) ‑> PAGE_EXIT_CODES
Expand source code
def blocking_selection_process(self) -> PAGE_EXIT_CODES:
    with Live(self.base_layout, auto_refresh=False) as live:

        while True:
            next_char = _blocking_get_keystroke(self._key_strokes_mappings)

            if next_char == KEY_ACTIONS.RIGHT:
                self._move_selection(1)
                live.update(self.base_layout, refresh=True)

            if next_char == KEY_ACTIONS.LEFT:
                self._move_selection(-1)
                live.update(self.base_layout, refresh=True)

            if next_char == KEY_ACTIONS.QUIT:
                return PAGE_EXIT_CODES.QUIT

            if next_char == KEY_ACTIONS.EXPAND:
                return PAGE_EXIT_CODES.EXPAND

            if next_char == KEY_ACTIONS.UP:
                return PAGE_EXIT_CODES.BACKWARD

            if next_char == KEY_ACTIONS.DOWN:
                return PAGE_EXIT_CODES.FORWARD

            if next_char == KEY_ACTIONS.ENTER:
                return PAGE_EXIT_CODES.FORWARD
def expand_view(self) ‑> None
Expand source code
def expand_view(self) -> None:
    console = Console()
    with console.pager():
        console.print("++++++++++++++++DATA++++++++++++++++")
        console.print(Pretty(self.selection_data.content, expand_all=True))
        console.print("+++++++++++++++++++++++++++++++++++++++")
def get_selected_item(self) ‑> Any
Expand source code
def get_selected_item(self) -> Any:
    if self._selected_item_index is None:
        return None

    return self.selection_data.options[self._selected_item_index][1]
def get_selected_label(self) ‑> Any
Expand source code
def get_selected_label(self) -> Any:
    if self._selected_item_index is None:
        return None

    return self.selection_data.options[self._selected_item_index][0]
class TwoItemSelectionPage (header: str, selection_data: two_item_selection_data)
Expand source code
class TwoItemSelectionPage(SelectionPage):
    def __init__(self, header: str, selection_data: two_item_selection_data) -> None:
        super().__init__()

        self.selection_data = selection_data
        self.header = Layout(
            Panel(Text.from_markup(header)),
            name="header",
            ratio=5,
        )

        self.subheader = TwoItemSelectionPage._make_split_pane(
            name="subheader", ratio=1
        )
        self.content = TwoItemSelectionPage._make_split_pane(name="content", ratio=10)

        self.nav = Layout(name="nav", ratio=4)

        self.base_layout.split_column(
            self.header,
            self.subheader.base,
            self.content.base,
            self.nav,
        )

        self.subheader.left.update(
            Text.from_markup(self.selection_data.left_data.header)
        )
        self.subheader.right.update(
            Text.from_markup(self.selection_data.right_data.header)
        )

        self.content.left.update(
            self._create_content_panel(selection_data.left_data.content)
        )
        self.content.right.update(
            self._create_content_panel(selection_data.right_data.content)
        )

        self.is_left_selected = None

        self._key_strokes_mappings = DEFAULT_KEY_BINDINGS

    def blocking_selection_process(self) -> PAGE_EXIT_CODES:
        with Live(self.base_layout, auto_refresh=False) as live:
            while True:
                next_char = _blocking_get_keystroke(self._key_strokes_mappings)

                if next_char == KEY_ACTIONS.RIGHT:
                    self._select_side(select_left=False)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.LEFT:
                    self._select_side(select_left=True)
                    live.update(self.base_layout, refresh=True)

                if next_char == KEY_ACTIONS.QUIT:
                    return PAGE_EXIT_CODES.QUIT

                if next_char == KEY_ACTIONS.EXPAND:
                    return PAGE_EXIT_CODES.EXPAND

                if next_char == KEY_ACTIONS.UP:
                    return PAGE_EXIT_CODES.BACKWARD

                if next_char == KEY_ACTIONS.DOWN:
                    return PAGE_EXIT_CODES.FORWARD

                if next_char == KEY_ACTIONS.ENTER:
                    return PAGE_EXIT_CODES.FORWARD

    def expand_view(self) -> None:
        console = Console()
        with console.pager():
            console.print("+++++++++++++++++++++++++++++++++++++++")
            console.print(self.selection_data.left_data.header)
            console.print("+++++++++++++++++++++++++++++++++++++++")
            console.print(
                Pretty(self.selection_data.left_data.content, expand_all=True)
            )

            console.print("")
            console.print("+++++++++++++++++++++++++++++++++++++++")
            console.print(self.selection_data.right_data.header)
            console.print("+++++++++++++++++++++++++++++++++++++++")
            console.print(
                Pretty(self.selection_data.right_data.content, expand_all=True)
            )

    def _select_side(self, select_left: bool):
        if select_left == self.is_left_selected:
            return

        if self.is_left_selected is None:
            self.is_left_selected = not select_left

        if not select_left and self.is_left_selected:
            self.subheader.left.update(
                Text.from_markup(self.selection_data.left_data.header)
            )
            self.subheader.right.update(
                Text.from_markup(
                    f"[reverse cyan]{self.selection_data.right_data.header}[/reverse cyan]"
                )
            )

            self.content.left.update(
                self._create_content_panel(self.selection_data.left_data.content)
            )
            self.content.right.update(
                self._create_content_panel(
                    self.selection_data.right_data.content, is_selected=True
                )
            )

        if select_left and not self.is_left_selected:
            self.subheader.left.update(
                Text.from_markup(
                    f"[reverse cyan]{self.selection_data.left_data.header}[/reverse cyan]"
                )
            )
            self.subheader.right.update(
                Text.from_markup(self.selection_data.right_data.header)
            )

            self.content.left.update(
                self._create_content_panel(
                    self.selection_data.left_data.content, is_selected=True
                )
            )
            self.content.right.update(
                self._create_content_panel(self.selection_data.right_data.content)
            )

        self.is_left_selected = select_left

    def _make_split_pane(name: str, ratio: int = None) -> split_layout:
        base_layout = Layout(name=name, ratio=ratio) if ratio else Layout(name=name)

        right = Layout(name=f"{name}-right")
        left = Layout(name=f"{name}-left")

        base_layout.split_row(
            left,
            right,
        )

        return split_layout(base=base_layout, right=right, left=left)

    def _create_content_panel(self, data: Any, is_selected: bool = False) -> Panel:
        _border_style = "bold" if is_selected else "dim"
        return Panel(Pretty(data), border_style=_border_style)

    def get_selected_item(self) -> Any:
        if self.is_left_selected is None:
            return None

        return (
            self.selection_data.left_data.rv
            if self.is_left_selected
            else self.selection_data.right_data.rv
        )

    def get_selected_label(self) -> Any:
        if self.is_left_selected is None:
            return None

        return (
            self.selection_data.left_data.header
            if self.is_left_selected
            else self.selection_data.right_data.header
        )

Ancestors

Methods

def blocking_selection_process(self) ‑> PAGE_EXIT_CODES
Expand source code
def blocking_selection_process(self) -> PAGE_EXIT_CODES:
    with Live(self.base_layout, auto_refresh=False) as live:
        while True:
            next_char = _blocking_get_keystroke(self._key_strokes_mappings)

            if next_char == KEY_ACTIONS.RIGHT:
                self._select_side(select_left=False)
                live.update(self.base_layout, refresh=True)

            if next_char == KEY_ACTIONS.LEFT:
                self._select_side(select_left=True)
                live.update(self.base_layout, refresh=True)

            if next_char == KEY_ACTIONS.QUIT:
                return PAGE_EXIT_CODES.QUIT

            if next_char == KEY_ACTIONS.EXPAND:
                return PAGE_EXIT_CODES.EXPAND

            if next_char == KEY_ACTIONS.UP:
                return PAGE_EXIT_CODES.BACKWARD

            if next_char == KEY_ACTIONS.DOWN:
                return PAGE_EXIT_CODES.FORWARD

            if next_char == KEY_ACTIONS.ENTER:
                return PAGE_EXIT_CODES.FORWARD
def expand_view(self) ‑> None
Expand source code
def expand_view(self) -> None:
    console = Console()
    with console.pager():
        console.print("+++++++++++++++++++++++++++++++++++++++")
        console.print(self.selection_data.left_data.header)
        console.print("+++++++++++++++++++++++++++++++++++++++")
        console.print(
            Pretty(self.selection_data.left_data.content, expand_all=True)
        )

        console.print("")
        console.print("+++++++++++++++++++++++++++++++++++++++")
        console.print(self.selection_data.right_data.header)
        console.print("+++++++++++++++++++++++++++++++++++++++")
        console.print(
            Pretty(self.selection_data.right_data.content, expand_all=True)
        )
def get_selected_item(self) ‑> Any
Expand source code
def get_selected_item(self) -> Any:
    if self.is_left_selected is None:
        return None

    return (
        self.selection_data.left_data.rv
        if self.is_left_selected
        else self.selection_data.right_data.rv
    )
def get_selected_label(self) ‑> Any
Expand source code
def get_selected_label(self) -> Any:
    if self.is_left_selected is None:
        return None

    return (
        self.selection_data.left_data.header
        if self.is_left_selected
        else self.selection_data.right_data.header
    )
class select_data (header: str, content: Any, rv: Any)

select_data(header: str, content: Any, rv: Any)

Expand source code
class select_data:
    header: str
    content: Any
    rv: Any

Class variables

var content : Any
var header : str
var rv : Any
class single_item_selection_data (options: List[Tuple[str, Any]], content: Any)

single_item_selection_data(options: List[Tuple[str, Any]], content: Any)

Expand source code
class single_item_selection_data:
    options: List[Tuple[str, Any]]
    content: Any

Class variables

var content : Any
var options : List[Tuple[str, Any]]
class split_layout (base: rich.layout.Layout, right: rich.layout.Layout, left: rich.layout.Layout)

split_layout(base: rich.layout.Layout, right: rich.layout.Layout, left: rich.layout.Layout)

Expand source code
class split_layout:
    base: Layout
    right: Layout
    left: Layout

Class variables

var base : rich.layout.Layout
var left : rich.layout.Layout
var right : rich.layout.Layout
class two_item_selection_data (left_data: select_data, right_data: select_data)

two_item_selection_data(left_data: cdev.utils.display_manager.select_data, right_data: cdev.utils.display_manager.select_data)

Expand source code
class two_item_selection_data:
    left_data: select_data
    right_data: select_data

Class variables

var left_dataselect_data
var right_dataselect_data