|  | 
|  | 1 | +# app.py | 
|  | 2 | +import rich.box | 
|  | 3 | +from rich.panel import Panel | 
|  | 4 | +from rich.style import Style | 
|  | 5 | +from rich.table import Table | 
|  | 6 | +from rich.text import Text | 
|  | 7 | +from textual import events | 
|  | 8 | +from textual.app import App | 
|  | 9 | +from textual.reactive import Reactive | 
|  | 10 | +from textual.widgets import Footer, Header, Static | 
|  | 11 | + | 
|  | 12 | +from textual_inputs import IntegerInput, TextInput | 
|  | 13 | + | 
|  | 14 | + | 
|  | 15 | +class CustomHeader(Header): | 
|  | 16 | +    """Override the default Header for Styling""" | 
|  | 17 | + | 
|  | 18 | +    def __init__(self) -> None: | 
|  | 19 | +        super().__init__() | 
|  | 20 | +        self.tall = False | 
|  | 21 | + | 
|  | 22 | +    def render(self) -> Table: | 
|  | 23 | +        header_table = Table.grid(padding=(0, 1), expand=True) | 
|  | 24 | +        header_table.style = Style(color="white", bgcolor="rgb(98,98,98)") | 
|  | 25 | +        header_table.add_column(justify="left", ratio=0, width=8) | 
|  | 26 | +        header_table.add_column("title", justify="center", ratio=1) | 
|  | 27 | +        header_table.add_column("clock", justify="right", width=8) | 
|  | 28 | +        header_table.add_row( | 
|  | 29 | +            "🔤", self.full_title, self.get_clock() if self.clock else "" | 
|  | 30 | +        ) | 
|  | 31 | +        return header_table | 
|  | 32 | + | 
|  | 33 | +    async def on_click(self, event: events.Click) -> None: | 
|  | 34 | +        return await super().on_click(event) | 
|  | 35 | + | 
|  | 36 | + | 
|  | 37 | +class CustomFooter(Footer): | 
|  | 38 | +    """Override the default Footer for Styling""" | 
|  | 39 | + | 
|  | 40 | +    def make_key_text(self) -> Text: | 
|  | 41 | +        """Create text containing all the keys.""" | 
|  | 42 | +        text = Text( | 
|  | 43 | +            style="white on rgb(98,98,98)", | 
|  | 44 | +            no_wrap=True, | 
|  | 45 | +            overflow="ellipsis", | 
|  | 46 | +            justify="left", | 
|  | 47 | +            end="", | 
|  | 48 | +        ) | 
|  | 49 | +        for binding in self.app.bindings.shown_keys: | 
|  | 50 | +            key_display = ( | 
|  | 51 | +                binding.key.upper() | 
|  | 52 | +                if binding.key_display is None | 
|  | 53 | +                else binding.key_display | 
|  | 54 | +            ) | 
|  | 55 | +            hovered = self.highlight_key == binding.key | 
|  | 56 | +            key_text = Text.assemble( | 
|  | 57 | +                (f" {key_display} ", "reverse" if hovered else "default on default"), | 
|  | 58 | +                f" {binding.description} ", | 
|  | 59 | +                meta={"@click": f"app.press('{binding.key}')", "key": binding.key}, | 
|  | 60 | +            ) | 
|  | 61 | +            text.append_text(key_text) | 
|  | 62 | +        return text | 
|  | 63 | + | 
|  | 64 | + | 
|  | 65 | +class Demo(App): | 
|  | 66 | + | 
|  | 67 | +    current_index: Reactive[int] = Reactive(-1) | 
|  | 68 | + | 
|  | 69 | +    def __init__(self, **kwargs) -> None: | 
|  | 70 | +        super().__init__(**kwargs) | 
|  | 71 | +        self.tab_index = ["username", "password", "age"] | 
|  | 72 | + | 
|  | 73 | +    async def on_load(self) -> None: | 
|  | 74 | +        await self.bind("q", "quit", "Quit") | 
|  | 75 | +        await self.bind("enter", "submit", "Submit") | 
|  | 76 | +        await self.bind("escape", "reset_focus", show=False) | 
|  | 77 | +        await self.bind("ctrl+i", "next_tab_index", show=False) | 
|  | 78 | +        await self.bind("shift+tab", "previous_tab_index", show=False) | 
|  | 79 | + | 
|  | 80 | +    async def on_mount(self) -> None: | 
|  | 81 | + | 
|  | 82 | +        self.header = CustomHeader() | 
|  | 83 | +        await self.view.dock(self.header, edge="top") | 
|  | 84 | +        await self.view.dock(CustomFooter(), edge="bottom") | 
|  | 85 | + | 
|  | 86 | +        self.username = TextInput( | 
|  | 87 | +            name="username", | 
|  | 88 | +            placeholder="enter your username...", | 
|  | 89 | +            title="Username", | 
|  | 90 | +        ) | 
|  | 91 | +        self.password = TextInput( | 
|  | 92 | +            name="password", | 
|  | 93 | +            placeholder="enter your password...", | 
|  | 94 | +            title="Password", | 
|  | 95 | +            password=True, | 
|  | 96 | +        ) | 
|  | 97 | +        self.age = IntegerInput( | 
|  | 98 | +            name="age", | 
|  | 99 | +            placeholder="enter your age...", | 
|  | 100 | +            title="Age", | 
|  | 101 | +        ) | 
|  | 102 | +        self.output = Static( | 
|  | 103 | +            renderable=Panel( | 
|  | 104 | +                "", | 
|  | 105 | +                title="Report", | 
|  | 106 | +                border_style="blue", | 
|  | 107 | +                box=rich.box.SQUARE | 
|  | 108 | +            ) | 
|  | 109 | +        ) | 
|  | 110 | + | 
|  | 111 | +        await self.view.dock(self.output, edge="left", size=40) | 
|  | 112 | +        await self.view.dock(self.username, self.password, self.age, edge="top") | 
|  | 113 | + | 
|  | 114 | +    async def action_next_tab_index(self) -> None: | 
|  | 115 | +        """Changes the focus to the next form field""" | 
|  | 116 | +        if self.current_index < len(self.tab_index) - 1: | 
|  | 117 | +            self.current_index += 1 | 
|  | 118 | +            await getattr(self, self.tab_index[self.current_index]).focus() | 
|  | 119 | + | 
|  | 120 | +    async def action_previous_tab_index(self) -> None: | 
|  | 121 | +        """Changes the focus to the previous form field""" | 
|  | 122 | +        self.log(f"PREVIOUS {self.current_index}") | 
|  | 123 | +        if self.current_index > 0: | 
|  | 124 | +            self.current_index -= 1 | 
|  | 125 | +            await getattr(self, self.tab_index[self.current_index]).focus() | 
|  | 126 | + | 
|  | 127 | +    async def action_submit(self) -> None: | 
|  | 128 | +        formatted = f""" | 
|  | 129 | +username: {self.username.value} | 
|  | 130 | +password: {"".join("•" for _ in self.password.value)} | 
|  | 131 | +     age: {self.age.value} | 
|  | 132 | +        """ | 
|  | 133 | +        await self.output.update( | 
|  | 134 | +            Panel( | 
|  | 135 | +                formatted, | 
|  | 136 | +                title="Report", | 
|  | 137 | +                border_style="blue", | 
|  | 138 | +                box=rich.box.SQUARE | 
|  | 139 | +            ) | 
|  | 140 | +        ) | 
|  | 141 | + | 
|  | 142 | +    async def action_reset_focus(self) -> None: | 
|  | 143 | +        self.current_index = -1 | 
|  | 144 | +        await self.header.focus() | 
|  | 145 | + | 
|  | 146 | +    async def message_input_on_change(self, message) -> None: | 
|  | 147 | +        self.log(f"Input: {message.sender.name} changed") | 
|  | 148 | + | 
|  | 149 | +    async def message_input_on_focus(self, message) -> None: | 
|  | 150 | +        self.current_index = self.tab_index.index(message.sender.name) | 
|  | 151 | + | 
|  | 152 | + | 
|  | 153 | +if __name__ == "__main__": | 
|  | 154 | +    Demo.run(title="Textual-Inputs Demo", log="textual.log") | 
0 commit comments