Textualize / textual

The lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.
https://textual.textualize.io/
MIT License
25.41k stars 780 forks source link

Button not working if I type something in a custom text field #613

Closed Rohan-cod closed 1 year ago

Rohan-cod commented 2 years ago

Issue

I have two ways to go back. I mapped ctrl+b to the back function and the heading on each page (in purple) is also mapped to the back function. In the screen recording below I can press the heading or ctrl+b key to go back but when I type something in the input text field I am not able to back neither via the heading nor via the ctrl+b key. But, the ctrl+c key works fine using which I quit the app.

https://user-images.githubusercontent.com/47586886/179387565-fde4a211-7f92-44f3-9b16-b3fab0fdfaa6.mov

The custom text field is class InputText(Widget)

Code

#!/usr/bin/env python3
# 
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#

import os
import sys
import ast
import pickle
import re
import click
import six
import json
from pyfiglet import figlet_format
from pyfiglet import Figlet
from rich import print
from rich.console import Console
from rich.table import Table
from rich import box
try:
    import colorama
    colorama.init()
except ImportError:
    colorama = None
try:
    from termcolor import colored
except ImportError:
    colored = None

from unicorn import __version__ as uc_ver
from qiling import __version__ as ql_ver

from qiling import Qiling
from qiling.arch import utils as arch_utils
from qiling.debugger.qdb import QlQdb
from qiling.utils import arch_convert
from qiling.const import QL_VERBOSE, QL_ENDIAN, os_map, arch_map, verbose_map
from qiling.extensions.coverage import utils as cov_utils
from qiling.extensions import report

import asyncio

from rich.align import Align
from rich.box import DOUBLE
from rich.console import RenderableType, Console, ConsoleOptions, RenderResult
from rich.panel import Panel
from rich.style import Style
from rich.text import Text
from textual import events
from textual.app import App
from textual.reactive import Reactive
from textual.views import GridView, DockView
from textual.widget import Widget
from textual.widgets import Button, ButtonPressed, Header, Footer, ScrollView
from rich.padding import Padding
from rich.table import Table
from rich import box

PROG = os.path.basename(__file__)
BUTTON_STYLE = "bold green"
PAGE_TITLE_STYLE = "bold purple"
BACK_BUTTON_NAMES = ["menu_page_title", "version_page_title",
    "help_page_title", "examples_page_title",
    "run_options_page_title", "code_options_page_title"]

QLTOOL_EXAMPLES = f"""
    With code:
        {PROG} code --os linux --arch arm --format hex -f examples/shellcodes/linarm32_tcp_reverse_shell.hex
        {PROG} code --os linux --arch x86 --format asm -f examples/shellcodes/lin32_execve.asm

    With binary file:
        {PROG} run -f examples/rootfs/x8664_linux/bin/x8664_hello --rootfs examples/rootfs/x8664_linux
        {PROG} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux

    With binary file and Qdb:
        {PROG} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --qdb
        {PROG} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --qdb --rr

    With binary file and gdbserver:
        {PROG} run -f examples/rootfs/x8664_linux/bin/x8664_hello --gdb 127.0.0.1:9999 --rootfs examples/rootfs/x8664_linux

    With binary file and additional argv:
        {PROG} run -f examples/rootfs/x8664_linux/bin/x8664_args --rootfs examples/rootfs/x8664_linux --args test1 test2 test3

    With binary file and various output format:
        {PROG} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --verbose disasm
        {PROG} run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --filter ^open

    With UEFI file:
        {PROG} run -f examples/rootfs/x8664_efi/bin/TcgPlatformSetupPolicy --rootfs examples/rootfs/x8664_efi --env examples/rootfs/x8664_efi/rom2_nvar.pickel

    With binary file and json output:
        {PROG} run -f examples/rootfs/x86_windows/bin/x86_hello.exe --rootfs examples/rootfs/x86_windows --no-console --json
"""

def get_oad_table_columns(table):
    table.add_column("Option Name", justify="left", no_wrap=True, header_style="bold", style="red")
    table.add_column("Arguments", justify="left", header_style="bold")
    table.add_column("Description", justify="left", header_style="bold")

    return table

def run_help():
    table = Table(title="Run Help", show_lines=True, box=box.ROUNDED, title_style="bold", caption_justify="left", caption="Notes: - If filename is not specified, the last argument will be considered as program binary - If args is not speified, all trailing arguments will be considered as program command line arguments")

    table = get_oad_table_columns(table)

    table.add_row("filename", "filename", "Binary filename to emulate")
    table.add_row("rootfs", "dirname", "Emulation root directory; this is where all libraries reside")
    table.add_row("args", "...", "Emulated program command line arguments")
    table.add_row("run_args", "...", "run_args")

    return table

def code_help():
    table = Table(title="Code Help", show_lines=True, box=box.ROUNDED, title_style="bold", caption_justify="left", caption="Notes: - When format is set to hex, qltool will first look for data in input. If no input string specified, it will refer to the file specified in filename")

    table = get_oad_table_columns(table)

    table.add_row("filename", "filename", "Input filename")
    table.add_row("input", "hex", "Input hex string; only relevant when format is set to hex")
    table.add_row("format", "asm,  hex,  bin", "Specify file or input format: either an assembly, hex string or binary file")
    table.add_row("arch", "x86,  x8664,  arm,  arm_thumb,  arm64,  mips,  a8086,  evm", "Target architecture")
    table.add_row("endian", "little,  big", "Target endianess (default: little)")
    table.add_row("os", "linux,  freebsd,  macos,  windows,  uefi,  dos,  evm", "Target operating system")
    table.add_row("rootfs", "dirname", "Emulated root filesystem, that is where all libraries reside")
    table.add_row("thumb", "Boolean", "Specify thumb mode for ARM")

    return table

def additional_options_help():
    table = Table(title="Additional Options Help", show_lines=True, box=box.ROUNDED, title_style="bold")

    table = get_oad_table_columns(table)

    table.add_row("verbose", "off,  default,  debug,  disasm,  dump", "Set logging verbosity level")
    table.add_row("env", "filename", "Path of a Pickle file containing an environment dictionary, or a Python string that evaluates to a dictionary")
    table.add_row("gdb", "[server:port]", "Enable gdb server")
    table.add_row("qdb", "", "Attach qdb at entry point. Currently supporting only MIPS and ARM (thumb mode)")
    table.add_row("rr", "", "Enable qdb record and replay feature; requires `qdb`")
    table.add_row("profile", "filename", "Specify a profile file")
    table.add_row("no-console", "", "Do not emit program output to stdout")
    table.add_row("filter", "regexp", "Apply a filtering regexp on log output")
    table.add_row("log-file", "filename", "Emit log to file")
    table.add_row("log-plain", "", "Do not use colors in log output; useful when emitting log to a file")
    table.add_row("root", "", "Enable sudo required mode")
    table.add_row("debug-stop", "", "Stop emulation on first error; requires verbose to be set to either debug or dump")
    table.add_row("multithread", "", "Execute program in multithread mode")
    table.add_row("timeout", "microseconds", "Set emulation timeout in microseconds (1000000μs = 1s)")
    table.add_row("coverage-file", "filename", "Code coverage output file")
    table.add_row("coverage-format", "drcov,  drcov_exact", "Code coverage file format")
    table.add_row("json", "", "Emit an emulation report in JSON format")
    table.add_row("libcache", "boolean", "Enable dll caching for windows")

    return table

class FigletText:
    """A renderable to generate figlet text that adapts to fit the container."""

    def __init__(self, text: str, font: str) -> None:
        self.text = text
        self.font = font

    def __rich_console__(
        self, console: Console, options: ConsoleOptions
    ) -> RenderResult:
        """Build a Rich renderable to render the Figlet text."""
        size = min(options.max_width / 2, options.max_height)
        if size < 4:
            yield Text(self.text, style="bold")
        else:
            font = Figlet(font=self.font, width=options.max_width)
            yield Text(font.renderText(self.text).rstrip("\n"), style="bold")

class Word(Widget):

    label = Reactive("")
    foreground_color = Reactive("")
    background_color = Reactive("")
    bold = Reactive(False)
    figlet = Reactive(False)

    def render(self) -> RenderableType:
        if self.background_color:
           self.style=f"{self.foreground_color} on {self.background_color}"
        else:
            self.style=f"{self.foreground_color}" 

        if self.bold:
            self.style = "bold " + self.style

        text = Text(text=self.label)
        if self.figlet:
            text = FigletText(text=self.label, font="cyberlarge")
        return Padding(
            Align.center(text, vertical="middle"),
            (0, 1),
            style=self.style,
        )

class HelpTable(Widget):

    table = Reactive("")

    def render(self) -> RenderableType:
        if self.table == "run":
            help_table = run_help()
        if self.table == "code":
            help_table = code_help()
        if self.table == "additional_options":
            help_table = additional_options_help()
        return Padding(help_table)

class InputText(Widget):

    title: Reactive[RenderableType] = Reactive("")
    content: Reactive[RenderableType] = Reactive("")
    mouse_over: Reactive[RenderableType] = Reactive(False)

    def __init__(self, title: str, content: str = ""):
        super().__init__(title)
        self.title = title
        self.content = content

    def on_enter(self) -> None:
        self.mouse_over = True

    def on_leave(self) -> None:
        self.mouse_over = False

    def on_key(self, event: events.Key) -> None:
        if self.mouse_over == True:
            if event.key == "ctrl+h":
                self.content = self.content[:-1]
            else:
                self.content += event.key

    def validate_title(self, value) -> None:
        try:
            return value.lower()
        except (AttributeError, TypeError):
            raise AssertionError("title attribute should be a string.")

    def validate_content(self, value) -> None:
        try:
            return value.lower()
        except (AttributeError, TypeError):
            raise AssertionError("content attribute should be a string.")

    def render(self) -> RenderableType:
        renderable = Align.left(Text(self.content, style="bold"))
        return Panel(
            renderable,
            title=self.title,
            title_align="center",
            height=3,
            style="bold white on rgb(50,57,50)",
            border_style=Style(color=f'{"red" if self.mouse_over else "green"}'),
            box=DOUBLE,
        )

class HomePage(GridView):

    async def on_mount(self) -> None:
        title = Word()
        title.label = "Qiling"
        title.foreground_color = "purple"
        title.bold = True
        title.figlet = True

        sub_title = Word()
        sub_title.label = "Cross Platform and Multi Architecture Advanced Binary Emulation Framework"
        sub_title.foreground_color = "purple"

        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)

        self.grid.add_column("column", size=50)
        self.grid.add_row("row", size=6)
        self.grid.add_row("row", repeat=2, size=3)

        self.grid.add_widget(title)
        self.grid.add_widget(sub_title)
        self.grid.add_widget(
            Button(label="Enter", name="enter", style="bold white on green")
        )

class MenuPage(GridView):

    async def on_mount(self) -> None:
        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)

        self.grid.add_column("column", size=20)
        self.grid.add_row("row", repeat=6, size=1)

        self.grid.add_widget(
            Button(label="Select an Option", name="menu_page_title", style=PAGE_TITLE_STYLE)
        )
        self.grid.add_widget(
            Button(label="Version", name="version", style=BUTTON_STYLE)
        )
        self.grid.add_widget(
            Button(label="Help", name="help", style=BUTTON_STYLE)
        )
        self.grid.add_widget(
            Button(label="Examples", name="examples", style=BUTTON_STYLE)
        )
        self.grid.add_widget(
            Button(label="Run", name="run_options", style=BUTTON_STYLE)
        )
        self.grid.add_widget(
            Button(label="Code", name="code_options", style=BUTTON_STYLE)
        )

class VersionPage(GridView):

    async def on_mount(self) -> None:
        sub_title = Word()
        sub_title.label = f'{PROG} for Qiling {ql_ver}, using Unicorn {uc_ver}'
        sub_title.foreground_color = "purple"

        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)

        self.grid.add_column("column", size=40)
        self.grid.add_row("row", repeat=2, size=2)

        self.grid.add_widget(
            Button(label="Version", name="version_page_title", style=PAGE_TITLE_STYLE)
        )
        self.grid.add_widget(sub_title)

class HelpPage(GridView):

    async def on_mount(self) -> None:
        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)

        self.grid.add_column("column", size=20)
        self.grid.add_row("row", repeat=4, size=1)

        self.grid.add_widget(
            Button(label="Select an Option", name="help_page_title", style=PAGE_TITLE_STYLE)
        )
        self.grid.add_widget(
            Button(label="Run", name="run_help", style=BUTTON_STYLE)
        )
        self.grid.add_widget(
            Button(label="Code", name="code_help", style=BUTTON_STYLE)
        )
        self.grid.add_widget(
            Button(label="Additional Options", name="additional_options_help", style=BUTTON_STYLE)
        )

class RunHelpPage(GridView):

    async def on_mount(self) -> None:
        table = HelpTable()
        table.table = "run"

        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)

        self.grid.add_column("column", size=80)
        self.grid.add_row("row1", size=16)
        # self.grid.add_row("row2", size=2)

        self.grid.add_widget(table)

class CodeHelpPage(GridView):

    async def on_mount(self) -> None:
        table = HelpTable()
        table.table = "code"

        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)

        self.grid.add_column("column", size=100)
        self.grid.add_row("row1", size=28)
        # self.grid.add_row("row2", size=2)

        self.grid.add_widget(table)

class AdditionalOptionsHelpPage(GridView):

    async def on_mount(self) -> None:
        table = HelpTable()
        table.table = "additional_options"

        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)

        self.grid.add_column("column", size=100)
        self.grid.add_row("row1", size=50)
        # self.grid.add_row("row2", size=2)

        self.grid.add_widget(table)

class ExamplesPage(GridView):

    async def on_mount(self) -> None:
        sub_title = Word()
        sub_title.label = QLTOOL_EXAMPLES
        sub_title.foreground_color = "green"

        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)

        self.grid.add_column("column", size=150)
        self.grid.add_row("row1", size=2)
        self.grid.add_row("row2", size=30)

        self.grid.add_widget(
            Button(label="Examples", name="examples_page_title", style=PAGE_TITLE_STYLE)
        )
        self.grid.add_widget(sub_title)

class RunOptionsPage(GridView):
    filename: Reactive[RenderableType] = Reactive("")
    rootfs: Reactive[RenderableType] = Reactive("")
    args: Reactive[RenderableType] = Reactive("")
    run_args: Reactive[RenderableType] = Reactive("")

    async def on_mount(self) -> None:
        filename = InputText("filename")
        rootfs = InputText("rootfs")
        args = InputText("args")
        run_args = InputText("run_args")

        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)

        self.grid.add_column("column", size=50)
        self.grid.add_row("row1", size=2)
        self.grid.add_row("row1", repeat=4, size=4)

        self.grid.add_widget(
            Button(label="Run Options", name="run_options_page_title", style=PAGE_TITLE_STYLE)
        )
        self.grid.add_widget(filename)
        self.grid.add_widget(rootfs)
        self.grid.add_widget(args)
        self.grid.add_widget(run_args)

class CodeOptionsPage(GridView):
    filename: Reactive[RenderableType] = Reactive("")
    rootfs: Reactive[RenderableType] = Reactive("")
    args: Reactive[RenderableType] = Reactive("")
    run_args: Reactive[RenderableType] = Reactive("")

    async def on_mount(self) -> None:
        filename = InputText("filename")
        rootfs = InputText("rootfs")
        args = InputText("args")
        run_args = InputText("run_args")

        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)

        self.grid.add_column("column", size=50)
        self.grid.add_row("row1", size=2)
        self.grid.add_row("row1", repeat=4, size=4)

        self.grid.add_widget(
            Button(label="Code Options", name="code_options_page_title", style=PAGE_TITLE_STYLE)
        )
        self.grid.add_widget(filename)
        self.grid.add_widget(rootfs)
        self.grid.add_widget(args)
        self.grid.add_widget(run_args)

class MainApp(App):
    page: Reactive = Reactive(1)

    filename: Reactive[RenderableType] = Reactive("")
    rootfs: Reactive[RenderableType] = Reactive("")
    args: Reactive[RenderableType] = Reactive("")
    run_args: Reactive[RenderableType] = Reactive("")

    async def on_load(self, event):
        await self.bind(keys="ctrl + b", action="", description="Back")
        await self.bind(keys="ctrl + c", action="quit", description="Quit")

    async def on_key(self, event: events.Key) -> None:
        if event.key == "ctrl+b":
            self.clear_screen()
        if self.page in self.back_button_mapping:
            await self.back_button_mapping[self.page][0](self)
            self.page = self.back_button_mapping[self.page][1]

    async def handle_button_pressed(self, message: ButtonPressed) -> None:
        assert isinstance(message.sender, Button)
        button_name = message.sender.name

        if button_name in BACK_BUTTON_NAMES:
            self.clear_screen()
            if self.page in self.back_button_mapping:
                await self.back_button_mapping[self.page][0](self)
                self.page = self.back_button_mapping[self.page][1]

        if button_name == "enter":
            self.clear_screen()
            self.page = 2
            await self.mount_page_2()

        if button_name == "version":
            self.clear_screen()
            self.page = 3
            await self.mount_page_3()

        if button_name == "help":
            self.clear_screen()
            self.page = 4
            await self.mount_page_4()

        if button_name == "run_help":
            self.clear_screen()
            self.page = 5
            await self.mount_page_5()

        if button_name == "code_help":
            self.clear_screen()
            self.page = 6
            await self.mount_page_6()

        if button_name == "additional_options_help":
            self.clear_screen()
            self.page = 7
            await self.mount_page_7()

        if button_name == "examples":
            self.clear_screen()
            self.page = 8
            await self.mount_page_8()

        if button_name == "run_options":
            self.clear_screen()
            self.page = 9
            await self.mount_page_9()

        if button_name == "code_options":
            self.clear_screen()
            self.page = 10
            await self.mount_page_10()

    async def on_mount(self) -> None:
        await self.mount_page_1()

    async def mount_page_1(self) -> None:
        """
            Home Page - 1
        """
        await self.mount_header_and_footer()
        await self.view.dock(HomePage())

    async def mount_page_2(self) -> None:
        """
            Menu Page - 2
        """
        await self.mount_header_and_footer()
        await self.view.dock(MenuPage())

    async def mount_page_3(self) -> None:
        """
            Version Page - 3
        """
        await self.mount_header_and_footer()
        await self.view.dock(VersionPage())

    async def mount_page_4(self) -> None:
        """
            Help Page - 4
        """
        await self.mount_header_and_footer()
        await self.view.dock(HelpPage())

    async def mount_page_5(self) -> None:
        """
            Run Help Page - 5
        """
        await self.mount_header_and_footer()
        await self.view.dock(RunHelpPage())

    async def mount_page_6(self) -> None:
        """
            Code Help Page - 6
        """
        await self.mount_header_and_footer()
        await self.view.dock(CodeHelpPage())

    async def mount_page_7(self) -> None:
        """
            Additional Options Help Page - 7
        """
        await self.mount_header_and_footer()
        await self.view.dock(AdditionalOptionsHelpPage())

    async def mount_page_8(self) -> None:
        """
            Examples Page - 8
        """
        await self.mount_header_and_footer()
        await self.view.dock(ExamplesPage())

    async def mount_page_9(self) -> None:
        """
            Run Options Page - 9
        """
        await self.mount_header_and_footer()
        await self.view.dock(RunOptionsPage())

    async def mount_page_10(self) -> None:
        """
            Code Options Page - 10
        """
        await self.mount_header_and_footer()
        await self.view.dock(CodeOptionsPage())

    async def mount_header_and_footer(self) -> None:
        header = Header(tall=False)
        await self.view.dock(header)
        footer = Footer()
        await self.view.dock(footer, edge="bottom")

    def clear_screen(self) -> None:
        self.view.layout.docks.clear()
        self.view.widgets.clear()

    back_button_mapping = {
        2: (mount_page_1, 1),
        3: (mount_page_2, 2),
        4: (mount_page_2, 2),
        5: (mount_page_4, 4),
        6: (mount_page_4, 4),
        7: (mount_page_4, 4),
        8: (mount_page_2, 2),
        9: (mount_page_2, 2),
        10: (mount_page_2, 2)
    }

if __name__ == "__main__":
    MainApp.run(title="Qiling")
Rohan-cod commented 2 years ago

@willmcgugan

willmcgugan commented 1 year ago

https://github.com/Textualize/textual/wiki/Sorry-we-closed-your-issue

github-actions[bot] commented 1 year ago

Did we solve your problem?

Glad we could help!